Transparent Column Encryption

Transparent Column Encryption (TCE) lets you encrypt a column for storage to disk. This pattern is often called “Encryption at Rest”. The column is stored encrypted in the postgres database files, as well as log streams and database dumps. TCE uses Server Key Management managed keys by ID.

%load_ext sql
%config SqlMagic.feedback=False
%config SqlMagic.displaycon=False
%sql postgresql://postgres@/

To use TCE, first create the extension:

%%sql 
CREATE EXTENSION IF NOT EXISTS pgsodium;
[]

Encrypt Whole Column with One Key ID

To encrypt a column, the first step is to create a table. Here is a simple table with one text column that will be encrypted.

%%sql
DROP TABLE IF EXISTS my_secrets CASCADE;
CREATE TABLE my_secrets (
  secret text
);
[]

Create a new Key ID

The next step is create a single key id that will be used to encrypt the column with the pgsodium.create_key() function. This new key is used to create a label for the column with a SECURITY LABEL command that says which key id should be used to encrypt the table.

key = %sql SELECT * FROM pgsodium.create_key()
label = 'ENCRYPT WITH KEY ID ' + str(key[0].id)
print('The security label will be:', label)
The security label will be: ENCRYPT WITH KEY ID 3b72130d-32c1-42a2-8ed4-c35086ca60e8

Label a column

Now apply that label to the column:

%%sql 
SECURITY LABEL FOR pgsodium ON COLUMN my_secrets.secret IS :label;
[]

You can examine all labeled objects in the standard Postgre catalog table pg_catalog.pg_seclabel:

%%sql 
SELECT objoid::regclass, provider, label FROM pg_seclabel WHERE provider = 'pgsodium';
objoid provider label
pgsodium.key pgsodium ENCRYPT WITH KEY COLUMN parent_key ASSOCIATED (id, associated_data) NONCE raw_key_nonce
my_customer_secrets pgsodium DECRYPT WITH VIEW public.other_name_view
my_customer_secrets pgsodium ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (id, associated_data, owner) NONCE nonce
my_secrets pgsodium ENCRYPT WITH KEY ID 3b72130d-32c1-42a2-8ed4-c35086ca60e8

Insert test data

TCE works by dynamically creating an INSERT trigger for the labled table and a view that wraps the table and decrypts the column. To explain, here are some test rows for the table. Note that the inserted secret values are plaintext.

%%sql 
INSERT INTO my_secrets (secret) VALUES ('sekert1'), ('shhhhh'), ('0xABC_my_payment_processor_key');
[]

How Secrets are Stored

Now that there are some secrets in the table, selecting on the table will show that the data is stored in an authenticated encrypted form. The “signature” for authenticated the secret is appended to the value, which is why each value is 32 bytes longer.

%%sql 
SELECT * FROM my_secrets;
secret
ZpIusOfIP76sHzOTTWhPn6S0GNz14K2bmZFR2KRzHrYTRLCW8NO8
V96NMLHu6nhnJHw1AXA/+zsDpk8Hq7NY7DbbtBQjqG9TQ6t9dF4=
9z2kXtIYOgSNl/fcZEXqz44baRpVyFhV/jH/yp/jP/FbpR1Qle0oDIZrMF0iCyomTtGsKEKPioWZ
wf64xVs=

Accessing Decrypted Values

When a column is labled with TCE using SECURITY LABEL, pgsodium dynamically generate a view that can decrypt rows on the fly. By default this view is named decrypted_<table_name> for the table with any labeled columns.

%%sql 
SELECT * FROM decrypted_my_secrets;
secret decrypted_secret
ZpIusOfIP76sHzOTTWhPn6S0GNz14K2bmZFR2KRzHrYTRLCW8NO8 sekert1
V96NMLHu6nhnJHw1AXA/+zsDpk8Hq7NY7DbbtBQjqG9TQ6t9dF4= shhhhh
9z2kXtIYOgSNl/fcZEXqz44baRpVyFhV/jH/yp/jP/FbpR1Qle0oDIZrMF0iCyomTtGsKEKPioWZ
wf64xVs=
0xABC_my_payment_processor_key

Using per-Row Key Ids, Associated Data, and Nonces

The above approach is simple, there is one key to manage and it is used for the whole column. But in many cases you will want finer grained control over which key is applied to which row. For example if you host customer content, you may require a different key for different customers.

Furthermore authenticated encryption is useful, but you often have data associated with a secret that does not need to be encrypted but does need to be authenticated meaning that the associated data is included in the generation of the authentication signature. See the wikipedia page Authenticated Encryption with Assocaited Data for more information on this technique. pgsodium lets you specify one or more columns that can be associated with a secret as shown below.

Finally, a common pattern in encryption is to use a nonce value to deduplicate secrets and associated data. Without this nonce, duplicate secrets and associated data would create duplicate encrypted values, and this information can be used by attackers in some situations.

To put it all together, lets create another table for customer secrets:

%%sql
DROP TABLE IF EXISTS my_customer_secrets CASCADE;
CREATE TABLE my_customer_secrets (
    id bigserial,
    secret text,
    associated_data json,
    owner text NOT NULL,
    key_id uuid REFERENCES pgsodium.key(id) DEFAULT (pgsodium.create_key()).id,
    nonce bytea DEFAULT pgsodium.crypto_aead_det_noncegen());
[]
%%sql 
SECURITY LABEL FOR pgsodium ON TABLE my_customer_secrets IS 'DECRYPT WITH VIEW public.other_name_view';
[]

Notice that in this case there some new columns, an integer id, an owner, some “associated” data in JSON format, and a nonce. These columns will be used in the security label below. Notice also that the key_id and nonce columns have defaults, so that if you don’t provide a value for them, a new key_id and nonce will by automatically generated.

The new label has different syntax than the first example, instead of specifying a KEY ID the label specifies a KEY COLUMN and some optional ASSOCIATED data columns, of which there may be one or more, and a NONCE column.

%%sql 
SECURITY LABEL FOR pgsodium ON COLUMN my_customer_secrets.secret 
    IS 'ENCRYPT WITH KEY COLUMN key_id ASSOCIATED (id, associated_data, owner) NONCE nonce';
[]

Insert some test data:

%%sql 
INSERT INTO my_customer_secrets (secret, associated_data, owner) 
    VALUES  ('blue', '{"type":"color"}', 'bob'), 
            ('nuts', '{"type":"food"}',  'alice'),
            ('fast', '{"type":"car"}',   'mallory');
[]

As above, secret is now stored en anthenticated encrypted form. The id, associated_data, and owner columns are “mixed in” to the signature that is stored with the secret, so they cannot be forged. Any atttempt to decrypt the secret with inauthentic associated data will fail.

%%sql 
SELECT secret, associated_data, owner, key_id FROM my_customer_secrets;
secret associated_data owner key_id
GrDDDYZXzBvAjKWWeVrCBtt8ngVM3t+k4JFd37ll0EtGtREB {'type': 'color'} bob bb04c8f9-c9ff-4978-a39d-afe44ca98e9a
N7427oX6/ENTnuYuk4FfGdo1tldjiZI/wF4Hk9nT97Um+xSm {'type': 'food'} alice fa0050ed-2876-4f98-b677-0000cf5fffb4
AV2GSzBDgAO71TqCXUKJD4U3Bd7H55rfH3grNEElO6w3uajE {'type': 'car'} mallory eb9291a2-2cf2-4ce3-8e99-e002b47b9d57

Decrypted secret access the view by the name specified in the table label above.

%%sql 
SELECT decrypted_secret, associated_data, owner, key_id FROM other_name_view;
decrypted_secret associated_data owner key_id
blue {'type': 'color'} bob bb04c8f9-c9ff-4978-a39d-afe44ca98e9a
nuts {'type': 'food'} alice fa0050ed-2876-4f98-b677-0000cf5fffb4
fast {'type': 'car'} mallory eb9291a2-2cf2-4ce3-8e99-e002b47b9d57