From 91d7e900bea019e3f5ae603d4773684274427b29 Mon Sep 17 00:00:00 2001 From: Michel Pelletier Date: Wed, 1 Nov 2023 10:07:01 -0700 Subject: [PATCH] Add config var to enable or disable event trigger function. (#94) * Add config var to enable or disable event trigger function. * add helper view to easily show labeled columns. * catch empty strings or bytes and throw error. --- sql/pgsodium--3.1.8--3.1.9.sql | 96 ++++++++++++++++++++++++++++++++++ src/pgsodium.c | 13 ++++- test/pgsodium_schema.sql | 10 ++-- test/tce.sql | 94 +++++++++++++++++++++++++++++---- 4 files changed, 198 insertions(+), 15 deletions(-) diff --git a/sql/pgsodium--3.1.8--3.1.9.sql b/sql/pgsodium--3.1.8--3.1.9.sql index 946e820..190c850 100644 --- a/sql/pgsodium--3.1.8--3.1.9.sql +++ b/sql/pgsodium--3.1.8--3.1.9.sql @@ -125,3 +125,99 @@ $$ LANGUAGE plpgsql SET search_path='' ; + +CREATE OR REPLACE FUNCTION pgsodium.trg_mask_update() +RETURNS EVENT_TRIGGER AS +$$ +DECLARE + r record; +BEGIN + IF (SELECT bool_or(in_extension) FROM pg_event_trigger_ddl_commands()) THEN + RAISE NOTICE 'skipping pgsodium mask regeneration in extension'; + RETURN; + ELSIF current_setting('pgsodium.enable_event_trigger') <> 'on' THEN + RAISE NOTICE 'skipping pgsodium mask regeneration due to false pgsodium.enable_event_trigger'; + RETURN; + END IF; + + FOR r IN + SELECT e.* + FROM pg_event_trigger_ddl_commands() e + WHERE EXISTS ( + SELECT FROM pg_catalog.pg_class c + JOIN pg_catalog.pg_seclabel s ON s.classoid = c.tableoid + AND s.objoid = c.oid + WHERE c.tableoid = e.classid + AND e.objid = c.oid + AND s.provider = 'pgsodium' + ) + LOOP + IF r.object_type in ('table', 'table column') + THEN + PERFORM pgsodium.update_mask(r.objid); + END IF; + END LOOP; +END +$$ + LANGUAGE plpgsql + SET search_path='' +; + +CREATE OR REPLACE FUNCTION pgsodium.encrypted_column(relid OID, m record) +RETURNS TEXT AS +$$ +DECLARE + expression TEXT; +BEGIN + expression := ''; + IF m.format_type = 'text' THEN + expression := expression || format($f$ + IF %1$s = '' THEN RAISE EXCEPTION 'Cannot encrypt empty string.'; END IF; + %1$s = CASE WHEN %1$s IS NULL THEN NULL ELSE + CASE WHEN %2$s IS NULL THEN NULL ELSE pg_catalog.encode( + pgsodium.crypto_aead_det_encrypt( + pg_catalog.convert_to(%1$s, 'utf8'), + pg_catalog.convert_to((%3$s)::text, 'utf8'), + %2$s::uuid, + %4$s + ), + 'base64') END END$f$, + 'new.' || quote_ident(m.attname), + COALESCE('new.' || quote_ident(m.key_id_column), quote_literal(m.key_id)), + COALESCE(pgsodium.quote_assoc(m.associated_columns, true), quote_literal('')), + COALESCE('new.' || quote_ident(m.nonce_column), 'NULL') + ); + ELSIF m.format_type = 'bytea' THEN + expression := expression || format($f$ + IF %1$s = ''::bytea THEN RAISE EXCEPTION 'Cannot encrypt empty bytes.'; END IF; + %1$s = CASE WHEN %1$s IS NULL THEN NULL ELSE + CASE WHEN %2$s IS NULL THEN NULL ELSE + pgsodium.crypto_aead_det_encrypt(%1$s::bytea, pg_catalog.convert_to((%3$s)::text, 'utf8'), + %2$s::uuid, + %4$s + ) END END$f$, + 'new.' || quote_ident(m.attname), + COALESCE('new.' || quote_ident(m.key_id_column), quote_literal(m.key_id)), + COALESCE(pgsodium.quote_assoc(m.associated_columns, true), quote_literal('')), + COALESCE('new.' || quote_ident(m.nonce_column), 'NULL') + ); + END IF; + RETURN expression; +END +$$ + LANGUAGE plpgsql + VOLATILE + SET search_path='' + ; + + +CREATE VIEW pgsodium.seclabel AS + SELECT nspname, relname, attname, label + FROM pg_seclabel sl, + pg_class c, + pg_attribute a, + pg_namespace n + WHERE sl.objoid = c.oid + AND c.oid = a.attrelid + AND a.attnum = sl.objsubid + AND n.oid = c.relnamespace; diff --git a/src/pgsodium.c b/src/pgsodium.c index b49b94b..ef4cc09 100644 --- a/src/pgsodium.c +++ b/src/pgsodium.c @@ -24,6 +24,7 @@ PG_MODULE_MAGIC; bytea *pgsodium_secret_key; static char *getkey_script = NULL; +static bool enable_event_trigger = true; /* * Checking the syntax of the masking rules @@ -120,10 +121,20 @@ _PG_init (void) /* Security label provider hook */ register_label_provider ("pgsodium", pgsodium_object_relabel); - // we're done if not preloaded, otherwise try to get internal shared key + // we're done if not preloaded if (!process_shared_preload_libraries_in_progress) return; + // Variable to enable/disable event trigger + DefineCustomBoolVariable("pgsodium.enable_event_trigger", + "Variable to enable/disable event trigger that regenerates triggers and views.", + NULL, + &enable_event_trigger, + true, + PGC_USERSET, 0, + NULL, NULL, NULL); + + // try to get internal shared key path = (char *) palloc0 (MAXPGPATH); get_share_path (my_exec_path, sharepath); snprintf (path, MAXPGPATH, "%s/extension/%s", sharepath, PG_GETKEY_EXEC); diff --git a/test/pgsodium_schema.sql b/test/pgsodium_schema.sql index 26ea97d..da827b4 100644 --- a/test/pgsodium_schema.sql +++ b/test/pgsodium_schema.sql @@ -175,7 +175,8 @@ SELECT bag_eq($$ ('view pgsodium.decrypted_key' ::text), ('view pgsodium.mask_columns' ::text), ('view pgsodium.masking_rule' ::text), - ('view pgsodium.valid_key' ::text) + ('view pgsodium.valid_key' ::text), + ('view pgsodium.seclabel' ::text) $$, 'Check extension object list'); @@ -460,7 +461,8 @@ SELECT views_are('pgsodium', ARRAY[ 'decrypted_key', 'mask_columns', 'masking_rule', - 'valid_key' + 'valid_key', + 'seclabel' ]); ---- VIEW decrypted_key @@ -4968,7 +4970,7 @@ SELECT function_privs_are('pgsodium'::name, proname, proargtypes::regtype[]::tex AND oidvectortypes(proargtypes) = ''; SELECT unnest(ARRAY[ - is(md5(prosrc), 'b58694d2602515d557e8637d43b6df1a', + is(md5(prosrc), 'faacedb8c19aba1c5f9c7556d18c2286', format('Function pgsodium.%s(%s) body should match checksum', proname, pg_get_function_identity_arguments(oid)) ), @@ -5604,7 +5606,7 @@ SELECT function_privs_are('pgsodium'::name, proname, proargtypes::regtype[]::tex AND oidvectortypes(proargtypes) = 'bytea'; SELECT unnest(ARRAY[ - is(md5(prosrc), 'b8b02682e0138dc894512f55587db8d4', + is(md5(prosrc), '7e6641f8c9f661514f123598b1ca2448', format('Function pgsodium.%s(%s) body should match checksum', proname, pg_get_function_identity_arguments(oid)) ), diff --git a/test/tce.sql b/test/tce.sql index 2b4687f..a34ebaf 100644 --- a/test/tce.sql +++ b/test/tce.sql @@ -20,7 +20,9 @@ SELECT throws_ok( 'quoted schema cannot be labled'); CREATE TABLE private.foo( + id integer, secret text, + secretbytes bytea, associated text default '' ); @@ -86,7 +88,14 @@ SELECT lives_ok( SECURITY LABEL FOR pgsodium ON COLUMN private.foo.secret IS 'ENCRYPT WITH KEY ID %s' $test$, :'secret_key_id'), - 'can label column for encryption'); + 'can label string column for encryption'); + +SELECT lives_ok( + format($test$ + SECURITY LABEL FOR pgsodium ON COLUMN private.foo.secretbytes + IS 'ENCRYPT WITH KEY ID %s' + $test$, :'secret_key_id'), + 'can label bytea column for encryption'); SELECT lives_ok( format($test$ @@ -139,16 +148,45 @@ select ok(has_table_privilege('bobo', 'private.bar', 'SELECT'), select ok(has_table_privilege('bobo', 'private.other_bar', 'SELECT'), 'user keeps view select privs after regeneration'); - + select ok(has_table_privilege('bobo', 'private.other_bar', 'INSERT'), 'user keeps view insert privs after regeneration'); - + select ok(has_table_privilege('bobo', 'private.other_bar', 'UPDATE'), 'user keeps view update privs after regeneration'); - + select ok(has_table_privilege('bobo', 'private.other_bar', 'DELETE'), 'user keeps view delete privs after regeneration'); +SET pgsodium.enable_event_trigger = 'off'; + +CREATE TABLE private.fooz( + secret text +); + +SELECT lives_ok( + format($test$ + SECURITY LABEL FOR pgsodium ON COLUMN private.fooz.secret + IS 'ENCRYPT WITH KEY ID %s' + $test$, :'secret_key_id'), + 'can label column for encryption with event trigger disabled'); + +SELECT hasnt_view('private', 'decrypted_fooz', 'Dynamic view was not created due to disabled event trigger.'); + +SELECT hasnt_trigger('private', 'fooz', 'fooz_encrypt_secret_trigger_secret', + 'Dynamic trigger was not created due to disabled event trigger.'); + +SELECT lives_ok( + $test$SELECT pgsodium.update_mask('private.fooz'::regclass);$test$, + 'can manually create trigger and view with event trigger disabled.'); + +SELECT has_view('private', 'decrypted_fooz', 'Dynamic view was created manually.'); + +SELECT has_trigger('private', 'fooz', 'fooz_encrypt_secret_trigger_secret', + 'Dynamic trigger was created manually.'); + +RESET pgsodium.enable_event_trigger; + SET SESSION AUTHORIZATION bobo; SET ROLE bobo; @@ -158,20 +196,56 @@ SELECT pgsodium.crypto_aead_det_noncegen() nonce2 \gset SELECT lives_ok( format( $test$ - INSERT INTO private.decrypted_foo (secret) VALUES ('s3kr3t'); + INSERT INTO private.decrypted_foo (id, secret) VALUES (1, 's3kr3t'); $test$), - 'can insert into decrypted view'); + 'can insert string into decrypted view'); + +SELECT lives_ok( + format( + $test$ + INSERT INTO private.decrypted_foo (id, secretbytes) VALUES (2, 's3kr3t'::bytea); + $test$), + 'can insert bytes into decrypted view'); + +SELECT throws_ok( + format( + $test$ + INSERT INTO private.decrypted_foo (id, secret) VALUES (3, ''); + $test$), + 'P0001', + 'Cannot encrypt empty string.', + 'cannot insert empty string into decrypted view'); + +SELECT throws_ok( + format( + $test$ + INSERT INTO private.decrypted_foo (id, secretbytes) VALUES (4, ''::bytea); + $test$), + 'P0001', + 'Cannot encrypt empty bytes.', + 'cannot insert empty bytes into decrypted view'); + +SELECT lives_ok( + format( + $test$ + UPDATE private.decrypted_foo SET secret = 'sp00n' WHERE id = 1; + $test$), + 'can update string into decrypted view'); + +SELECT results_eq($$SELECT decrypted_secret = 'sp00n' from private.decrypted_foo WHERE id = 1$$, + $$VALUES (true)$$, + 'can see updated string in decrypted view'); SELECT lives_ok( format( $test$ - UPDATE private.decrypted_foo SET secret = 'sp00n'; + UPDATE private.decrypted_foo SET secretbytes = 'sp00nb' WHERE id = 2; $test$), - 'can update into decrypted view'); + 'can update bytes into decrypted view'); -SELECT results_eq($$SELECT decrypted_secret = 'sp00n' from private.decrypted_foo$$, +SELECT results_eq($$SELECT decrypted_secretbytes = 'sp00nb' from private.decrypted_foo WHERE id = 2$$, $$VALUES (true)$$, - 'can see updated decrypted view'); + 'can see updated bytes in decrypted view'); SELECT lives_ok( $test$