Skip to content

Commit

Permalink
Add config var to enable or disable event trigger function. (#94)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
michelp authored Nov 1, 2023
1 parent dc411ab commit 91d7e90
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 15 deletions.
96 changes: 96 additions & 0 deletions sql/pgsodium--3.1.8--3.1.9.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
13 changes: 12 additions & 1 deletion src/pgsodium.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 6 additions & 4 deletions test/pgsodium_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -460,7 +461,8 @@ SELECT views_are('pgsodium', ARRAY[
'decrypted_key',
'mask_columns',
'masking_rule',
'valid_key'
'valid_key',
'seclabel'
]);

---- VIEW decrypted_key
Expand Down Expand Up @@ -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))
),
Expand Down Expand Up @@ -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))
),
Expand Down
94 changes: 84 additions & 10 deletions test/tce.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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 ''
);

Expand Down Expand Up @@ -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$
Expand Down Expand Up @@ -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;

Expand All @@ -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$
Expand Down

0 comments on commit 91d7e90

Please sign in to comment.