Encryption Helpers

Use these helpers to encrypt/decrypt text columns with a symmetric key stored in encryption.root_key. They are written as DBMate migrations and can be chained after the Auth/Teams/RBAC steps.

What this covers

  • Runtime, application-layer encryption: data is encrypted/decrypted in your SQL.
  • Complements (does not replace) encryption at rest provided by your database/storage.
  • Useful to reduce blast radius from accidental dumps or “SELECT *” snooping while still relying on storage-level encryption for disks and backups.

Migration

-- migrate:up
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE OR REPLACE FUNCTION encrypt_text(data text) RETURNS text AS $$
DECLARE
    key text;
    encrypted text;
BEGIN
    key := current_setting('encryption.root_key', true);

    IF key IS NULL THEN
        RETURN data;
    ELSE
        BEGIN
            encrypted := pgp_sym_encrypt(data, key, 'compress-algo=1, cipher-algo=aes256');
            RETURN encrypted;
        EXCEPTION WHEN others THEN
            RETURN SQLERRM;
        END;
    END IF;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION decrypt_text(data text) RETURNS text AS $$
DECLARE
    key text;
    decrypted text;
BEGIN
    key := current_setting('encryption.root_key', true);

    IF key IS NULL THEN
        RETURN data;
    ELSE
        BEGIN
            decrypted := pgp_sym_decrypt(data::bytea, key);
            RETURN decrypted;
        EXCEPTION WHEN others THEN
            RETURN data;
        END;
    END IF;
END;
$$ LANGUAGE plpgsql;

-- migrate:down
DROP FUNCTION IF EXISTS decrypt_text(text);
DROP FUNCTION IF EXISTS encrypt_text(text);
DROP EXTENSION IF EXISTS pgcrypto;

Usage

  • Insert: INSERT INTO customers (email_encrypted) VALUES (encrypt_text('[email protected]'));
  • Update: UPDATE customers SET email_encrypted = encrypt_text('[email protected]') WHERE id = 1;
  • Select: SELECT decrypt_text(email_encrypted) AS email FROM customers;

Set the key per session/transaction so pgcrypto can decrypt:

SET LOCAL encryption.root_key = 'base64-or-env-derived-key';

If encryption.root_key is absent, the helpers return the input unchanged. This keeps local development simple while letting staging/production supply a real key through Postgres settings or ALTER SYSTEM.

Generate a key with OpenSSL

Create a 256-bit key and store it in your secret manager or environment:

openssl rand -base64 32

Set that value on the session before running queries (for example via SET LOCAL encryption.root_key = '...';) or configure it as a Postgres setting so the functions can encrypt/decrypt transparently.