The scope of Transparent Data Encryption (TDE) in PostgreSQL only applies to columns. It does not encrypt other aspects of the database, like table-level and database-level encryption; and those who deploy PostgreSQL may need to implement additional security measures to protect these database components.
You need to know two key elements before exploring TDE in PostgreSQL: Scheme inside a database and extensions. Unlike many databases, PostgreSQL schemas are not synonymous with a database. You may have multiple scheme (or, alternatively schemas) inside any PostgreSQL database.
Creating an extension is a one time event. Therefore, it’s easier to show you that first. You create a pgcrypto extension with the following command:
CREATE EXTENSION pgcrypto;
The public schema is the one most users deploy but for the purpose of hiding our AES encryption key this example creates a hidden schema. Unless you change the default find setting the hidden schema is not visible when connecting to the database.
You create the hidden schema with the following idimpotent (re-runnable) set of commands:
/* Drop dependent objects before dropping the schema. */
DROP TABLE IF EXISTS hidden.aes_key;
DROP FUNCTION IF EXISTS hidden.get_aes_key;
/*
* Drop function with cascade to remove the
* film_character_t trigger at same time.
*/
DROP FUNCTION IF EXISTS hidden.film_character_dml_f CASCADE;
/* Drop the schema conditionally. */
DROP SCHEMA IF EXISTS hidden;
/* Create the schema. */
CREATE SCHEMA hidden;
Next, we need to create a aes_key table and get_aes_key function in the hidden schema. The table will store the AES encryption key and the function lets us create an AES encryption key.
/* Create an aes encryption key table. */
CREATE TABLE hidden.aes_key
( aes_key text );
/* Create a hidden function to build an AES encryption key. */
CREATE OR REPLACE
FUNCTION hidden.get_aes_key() RETURNS text AS
$$
BEGIN
RETURN gen_random_bytes(16)::text;
END;
$$
LANGUAGE plpgsql;
After creating the public get_key() function, you insert a single row to the aes_key table by prefacing it with the hidden schema name, like this:
/* Insert the AES encryption key into a table. */
INSERT INTO hidden.aes_key
( aes_key )
VALUES
( hidden.get_aes_key());
Having built the plumbing for our AES encryption key, let’s show you how to encrypt and decrypt string values. This example lets you create an idimpotent film_character table in the public schema, like:
/* Drop the table conditionally. */
DROP TABLE IF EXISTS film_character;
/* Create the demonstration table for encrypting and decrypting strings. */
CREATE TABLE film_character
( character_id serial PRIMARY KEY
, plain_text text
, encrypted_text bytea );
After creating the AES encryption key table, function, and inserting a row of data, you need to create a public get_key() function, like:
/* Create a public function to retrieve the AES encryption key. */
CREATE OR REPLACE
FUNCTION get_key() RETURNS text AS
$$
DECLARE
retval text;
BEGIN
SELECT aes_key INTO retval FROM hidden.aes_key;
RETURN retval;
END;
$$
LANGUAGE plpgsql;
The following INSERT statement write a plain text column and encrypted text column into the film_character table. The get_key() function hides how the pgp_sym_encrypt function encrypts the string.
/* Insert plain and encrypted text into a table. */
INSERT INTO film_character
( plain_text
, encrypted_text )
VALUES
('Severus Snape'
, pgp_sym_encrypt('Slytherin',get_key()));
The following query displays the plain and encrypted text stored in a row of the film_character table.
/* Query plain and encrypted text from a table. */
SELECT character_id
, plain_text
, encrypted_text
FROM film_character;
It displays:
character_id | plain_text | encrypted_text
--------------+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------
1 | Severus Snape | \xc30d04070302fa1c4eebd90204cc7bd23901f1d4fa91b2455c3ef2987a305aebe01a4d94f9ebb467d6cb7a3846342ccd09cb55ac5e82a71cbaef93728fbeb4aaa9bf71b6fb93457758d1
(1 row)
Last, the following query displays the plain and decrypted text with the pgp_sym_decrypt function in a query:
/* Query the plain and decrypted text from a table. */
SELECT character_id
, plain_text
, pgp_sym_decrypt(encrypted_text,get_key()) AS encrypted_text
FROM film_character;
The query returns the plain and decrypted values:
character_id | plain_text | encrypted_text
--------------+---------------+-----------------
1 | Severus Snape | Slytherin
(1 row)
However, this approach exposes the method for encrypting the encrypted_text column’s string value. You can hide this by creating a film_character_dml_f function in the hidden schema and a film_character_t trigger in the public schema, like:
/* Create trigger function for insert or update. */
CREATE FUNCTION hidden.film_character_dml_f()
RETURNS trigger AS
$$
DECLARE
/* Declare local variable. */
unencrypted_input VARCHAR(30);
BEGIN
unencrypted_input := new.encrypted_text::text;
/* Encrypt the column. */
new.encrypted_text := pgp_sym_encrypt(unencrypted_input,get_key());
/* Return new record type. */
RETURN NEW;
END;
$$
LANGUAGE plpgsql;
CREATE TRIGGER film_character_t
BEFORE INSERT OR UPDATE ON film_character
FOR EACH ROW EXECUTE FUNCTION hidden.film_character_dml_f();
Now, you can insert the plain text data in an INSERT statement and the encryption occurs without disclosing how it happens. Here’s a sample statement:
INSERT INTO film_character
( plain_text
, encrypted_text )
VALUES
('Harry Potter'
,'Gryffindor');
A query of the table shows you that both rows have an encrypted value in the encrypted_text column.
/* Query plain and encrypted text from a table. */
SELECT character_id
, plain_text
, encrypted_text
FROM film_character;
Displayed like:
character_id | plain_text | encrypted_text
--------------+---------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | Severus Snape | \xc30d040703026716034f140d83e76cd23a01f99168afebe50d760b85c69373e3947c74473115a939843887db8e102cd0b2524378f4d684e0ba91c20afc436a056cd983fc47794eef7d4904
2 | Harry Potter | \xc30d040703020d8cc71d1f84e1ef6fd24701fd308f669e28a6135beac130fc51a6ccb5cef3c5005f4f557207fe5c84c4aedbb5b098dc9a882a9b7d801c61e34cd90517b4628b5a18b96b3fc61663b48391146b8c0fa2a858
(2 rows)
As always, I hope this code complete solution helps those trying to work with this technical stack.