Oracle-WRAP-equivalent procedural language for PostgreSQL.
Write stored procedures with LANGUAGE plpgsql_wrap; source is validated, then AES-256-GCM encrypted directly into pg_proc.prosrc.
Users cannot look at store procedures plain text source code any more unless they know the encryption key.
pg_dump will not expose store procedures plain text source code too and the pg_dump output is directly restorable. Pre-wrapped blobs are accepted at CREATE time.
# 1. Generate a key (keep it out of git with make-time override)
export WRAP_KEY_HEX=$(openssl rand -hex 32)
echo $WRAP_KEY_HEX # backup the key to be able to unwrap your stored procedure
make WRAP_KEY_HEX=$WRAP_KEY_HEX
sudo make install
# 2. In each database
CREATE EXTENSION plpgsql_wrap; -- requires plpgsqlIdentical to LANGUAGE plpgsql, only the language name differs: plpgsql_wrap.
CREATE OR REPLACE FUNCTION public.calculate_bonus(emp_id int, yr int)
RETURNS numeric
LANGUAGE plpgsql_wrap
AS $$
DECLARE
v_salary numeric;
BEGIN
SELECT salary INTO v_salary FROM employees WHERE id = emp_id;
RETURN v_salary * 0.15; -- confidential formula
END;
$$;
-- Calling, permissions, overloading — all identical to plpgsql:
SELECT calculate_bonus(42, 2024);In pg_proc the code is obfuscated:
SELECT substring(prosrc, 1, 50)||'...' AS wrapped_code
FROM pg_proc WHERE proname = 'calculate_bonus';
wrapped_code
-------------------------------------------------------
PLPGSQLWRAP:1:5752415001f5db365c6c185b66081ff0a23d...
(1 line)
This is also what will appears in the pg_dump output.
It is possible to recover plain text code of a wrapped stored procedure by using the plpgsql_wrap.unwrap_procedure function. It requires to be superuser and to provide the encryption key registered at compile time in the .so library.
-- Convert back to plain LANGUAGE plpgsql permanently:
SELECT plpgsql_wrap.unwrap_procedure('myhexkey', public', 'calculate_bonus');
-- After this:
SELECT prolang::regtype FROM pg_proc WHERE proname = 'calculate_bonus';
-- → plpgsql
SELECT prosrc FROM pg_proc WHERE proname = 'calculate_bonus';
-- → plain PL/pgSQL sourceIf you have overloaded functions, you need to distinguish them passing the parameters:
-- Convert back to plain LANGUAGE plpgsql permanently:
SELECT plpgsql_wrap.unwrap_procedure('myhexkey', public', 'calculate_bonus', 'emp_id int, yr int');The validator hook runs at the end of the CREATE FUNCTION command, inside the same transaction.
User writes plain PL/pgSQL in AS $$ ... $$
│
▼ pgwrap_validator() called by PostgreSQL
│
├─ 1. Read prosrc from pg_proc (plain PL/pgSQL)
│
├─ 2. Call plpgsql_validator(fn_oid)
│ Syntax error? → ereport → whole txn rolls back
│ User sees a normal PL/pgSQL syntax error
│
├─ 3. AES-256-GCM encrypt(source, compile_time_key)
│
└─ 4. UPDATE pg_proc SET prosrc = 'PLPGSQLWRAP:1:<hex>'
Plain source never persists on disk
AS $$ body starts with "PLPGSQLWRAP:1:"
│
▼ pgwrap_validator() called by PostgreSQL
│
├─ 1. Hex-decode the blob
│
├─ 2. AES-256-GCM authenticate (tag check, compile-time key)
│ Wrong key / tampered? → ereport → txn rolls back
│
└─ 3. prosrc is already correct — nothing to write
(pg_dump output accepted directly)
-- pg_dump output (excerpt):
CREATE OR REPLACE FUNCTION public.calculate_bonus(emp_id integer, yr integer)
RETURNS numeric
LANGUAGE plpgsql_wrap
AS $$
PLPGSQLWRAP:1:5752415001a3f1c9d2e6b70481...long hex string...9b2c
$$;The hex string is the raw EPGP blob stored in pg_proc.prosrc, verbatim.
# Plain SQL restore:
psql -d targetdb -f dump.sql
# pg_restore:
pg_restore -d targetdb dump.customThe validator sees PLPGSQLWRAP:1: → authenticates → accepts. No extra steps required as long as the same .so (same compile-time key) is installed.
If the target server has a different compile-time key, the GCM tag check fails at first call time (not at restore time — the validator only authenticates, it does not require the key to be valid for another server's key). To migrate:
- On the source server, unwrap all functions before dumping:
SELECT plpgsql_wrap.unwrap_procedure('myhexkey', schema, func_name) FROM plpgsql_wrap.list_wrapped();
pg_dumpthe database (functions are now plainplpgsql).- restore on the target by replacing
LANGUAGE plpgsqlwithLANGUAGE plpgsql_wrapin the CREATE FUNCTION code.
Any tool that generates wrapped blobs (e.g. a build pipeline that wraps source before deployment) can supply them directly to CREATE FUNCTION:
CREATE OR REPLACE FUNCTION public.my_func(x integer)
RETURNS integer
LANGUAGE plpgsql_wrap
AS $$
PLPGSQLWRAP:1:<hex-blob-generated-by-your-tool>
$$;This mirrors Oracle's CREATE OR REPLACE PROCEDURE ... WRAPPED <blob> syntax. The validator authenticates the blob and stores it — no re-encryption, no source visibility.
"PLPGSQLWRAP:1:" + lowercase_hex( raw_blob )
raw_blob layout:
Offset Len Field
------ --- -----
0 4 magic "WRAP"
4 1 version 0x01
5 12 nonce random AES-GCM nonce (new per wrap)
17 16 tag AES-GCM authentication tag
33 N ciphertext
Plain hex keeps prosrc valid ASCII in all PostgreSQL text contexts: pg_dump, streaming replication, logical replication, COPY, pg_upgrade.
| Threat | Result |
|---|---|
SELECT prosrc FROM pg_proc |
PLPGSQLWRAP:1:<hex> — no plaintext |
\sf / pg_get_functiondef() |
Returns opaque wrapped string |
pg_dump output |
Contains encrypted blob — useless without the .so |
pg_restore on matching server |
Works transparently |
pg_restore on server with wrong key |
Blob stored OK; first call fails with auth error |
| Tampered blob in dump | GCM tag rejects at first call |
| Syntax error in plain source | Rejected before encryption; CREATE rolls back |
- Gilles Darold < gilles AT darold DOT net >
This extension is free software distributed under the PostgreSQL Licence.
Copyright (c) 2026, Hexacluster Corp.