From 2b04b1aca0415b726fdacc7c0cc4903ee864257c Mon Sep 17 00:00:00 2001 From: "Jonathan S. Katz" Date: Mon, 31 Oct 2022 16:13:08 -0400 Subject: [PATCH] Add "scram_build_secret_sha_256" SQL function This function lets users build SCRAM secrets from SQL functions and provides the ability for the user to select the password, salt, and number of iterations for the password hashing algorithm. --- doc/src/sgml/func.sgml | 42 +++++++++++ src/backend/catalog/system_functions.sql | 6 ++ src/backend/libpq/auth-scram.c | 29 +++++--- src/backend/libpq/crypt.c | 2 +- src/backend/utils/adt/Makefile | 1 + src/backend/utils/adt/authfuncs.c | 69 ++++++++++++++++++ src/backend/utils/adt/meson.build | 1 + src/include/catalog/pg_proc.dat | 4 ++ src/include/libpq/scram.h | 3 +- src/test/regress/expected/scram.out | 91 ++++++++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/scram.sql | 56 +++++++++++++++ 12 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/backend/utils/adt/authfuncs.c create mode 100644 src/test/regress/expected/scram.out create mode 100644 src/test/regress/sql/scram.sql diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 6e0425cb3d..f582da138f 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -3485,6 +3485,48 @@ repeat('Pg', 4) PgPgPgPg + + + + scram_build_secret_sha256 + + scram_build_secret_sha256 ( password text + [, salt bytea + [, iterations integer ] ]) + text + + + Using the value provided in password, builds a + SCRAM secret equilvaent to what is stored in + pg_authid.rolpassword + and used with scram-sha-256 + authentication. If not provided or set to NULL, + salt is randomly generated and + iterations defaults to 4096. + + + SELECT scram_build_secret_sha256('secret password', decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64')); + + + SCRAM-SHA-256$4096:MTIzNDU2Nzg5MGFiY2RlZg==$D5BmucT796UQKargx2k3fdqjDYR7cH/L0viKKhGo6kA=:M33+iHFOESP8C3DKLDb2k5QAhkNVWEbp/YUIFd2CxN4= + + + + SELECT scram_build_secret_sha256('secret password', '\xabba5432'); + + + SCRAM-SHA-256$4096:q7pUMg==$05Nb9QHwHkMA0CRcYaEfwtgZ+3kStIefz8fLMjTEtio=:P126h1ycyP938E69yxktEfhoAILbiwL/UMsMk3Efb6o= + + + + SELECT scram_build_secret_sha256('secret password', decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64'), 10000); + + + SCRAM-SHA-256$10000:MTIzNDU2Nzg5MGFiY2RlZg==$9NkDu1TFpx3L30zMgHUqjRNSq3GRZRrdWU4TuGOnT3Q=:svuIH9L6HH8loyKWguT64XXoOLCrr4FkVViPd2JVR4M= + + + + diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 30a048f6b0..4aa76b81d9 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -594,6 +594,12 @@ LANGUAGE internal STRICT IMMUTABLE PARALLEL SAFE AS 'unicode_is_normalized'; +-- defaults for building a "scram-sha-256" secret +CREATE OR REPLACE FUNCTION + scram_build_secret_sha256(text, bytea DEFAULT NULL, int DEFAULT NULL) +RETURNS text +LANGUAGE INTERNAL +VOLATILE AS 'scram_build_secret_sha256'; -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index ee7f52218a..8d778f4346 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -456,10 +456,15 @@ scram_exchange(void *opaq, const char *input, int inputlen, /* * Construct a SCRAM secret, for storing in pg_authid.rolpassword. * + * "salt_str" can be NULL. If it is, this function will generate a random salt. + * + * If "iterations" is 0 or less, this function will set it to the default value. + * * The result is palloc'd, so caller is responsible for freeing it. */ char * -pg_be_scram_build_secret(const char *password) +pg_be_scram_build_secret(const char *password, char *salt_str, int salt_str_len, + int iterations) { char *prep_password; pg_saslprep_rc rc; @@ -476,14 +481,22 @@ pg_be_scram_build_secret(const char *password) if (rc == SASLPREP_SUCCESS) password = (const char *) prep_password; - /* Generate random salt */ - if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN)) - ereport(ERROR, - (errcode(ERRCODE_INTERNAL_ERROR), - errmsg("could not generate random salt"))); + /* If salt_str is NULL, generate random salt */ + if (salt_str == NULL) + { + if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generate random salt"))); + + salt_str = saltbuf; + salt_str_len = SCRAM_DEFAULT_SALT_LEN; + } + + if (iterations <= 0) + iterations = SCRAM_DEFAULT_ITERATIONS; - result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN, - SCRAM_DEFAULT_ITERATIONS, password, + result = scram_build_secret(salt_str, salt_str_len, iterations, password, &errstr); if (prep_password) diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 1ff8b0507d..649c51f990 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -138,7 +138,7 @@ encrypt_password(PasswordType target_type, const char *role, return encrypted_password; case PASSWORD_TYPE_SCRAM_SHA_256: - return pg_be_scram_build_secret(password); + return pg_be_scram_build_secret(password, NULL, -1, 0); case PASSWORD_TYPE_PLAINTEXT: elog(ERROR, "cannot encrypt password with 'plaintext'"); diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 0de0bbb1b8..7ddb186f96 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -22,6 +22,7 @@ OBJS = \ arraysubs.o \ arrayutils.o \ ascii.o \ + authfuncs.o \ bool.o \ cash.o \ char.o \ diff --git a/src/backend/utils/adt/authfuncs.c b/src/backend/utils/adt/authfuncs.c new file mode 100644 index 0000000000..a08489248e --- /dev/null +++ b/src/backend/utils/adt/authfuncs.c @@ -0,0 +1,69 @@ +/*------------------------------------------------------------------------- + * + * authfuncs.c + * Functions that assist with authentication management + * + * Portions Copyright (c) 2022, PostgreSQL Global Development Group + * + * + * IDENTIFICATION + * src/backend/utils/adt/authfuncs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "libpq/scram.h" +#include "utils/builtins.h" + +/* + * Build a SCRAM secret that can be used for SCRAM-SHA-256 authentication. + * + * This function can take three arguments: + * + * - password: a plaintext password. This argument is required. If none of the + * other arguments is set, the function short circuits to use a + * SCRAM secret generation function that relies on defaults. + * - salt_str_enc: a base64 encoded salt. If this is not provided, a salt using + * the defaults is generated. + * - iterations: the number of iterations to hash the password. If set to 0 + * or less, the default number of iterations is used. + */ +Datum +scram_build_secret_sha256(PG_FUNCTION_ARGS) +{ + const char *password; + char *salt_str = NULL; + int salt_str_len = -1; + int iterations = 0; + char *secret; + + if (PG_ARGISNULL(0)) + { + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("password must not be null"))); + } + + password = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + if (!PG_ARGISNULL(1)) + { + salt_str = text_to_cstring((text *) PG_GETARG_BYTEA_PP(1)); + salt_str_len = strlen(salt_str); + } + + if (!PG_ARGISNULL(2)) + iterations = PG_GETARG_INT32(2); + + secret = pg_be_scram_build_secret(password, salt_str, salt_str_len, + iterations); + + Assert(secret != NULL); + + /* + * convert the SCRAM secret to text which matches the type for + * pg_authid.rolpassword + */ + PG_RETURN_TEXT_P(cstring_to_text(secret)); +} diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build index ed9ceadfef..910f141e83 100644 --- a/src/backend/utils/adt/meson.build +++ b/src/backend/utils/adt/meson.build @@ -9,6 +9,7 @@ backend_sources += files( 'arraysubs.c', 'arrayutils.c', 'ascii.c', + 'authfuncs.c', 'bool.c', 'cash.c', 'char.c', diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 20f5aa56ea..04b662c348 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -7531,6 +7531,10 @@ { oid => '3422', descr => 'SHA-512 hash', proname => 'sha512', proleakproof => 't', prorettype => 'bytea', proargtypes => 'bytea', prosrc => 'sha512_bytea' }, +{ oid => '8557', descr => 'Build a SCRAM secret', + proname => 'scram_build_secret_sha256', prorettype => 'text', + proisstrict => 'f', proargtypes => 'text bytea int4', + prosrc => 'scram_build_secret_sha256' }, # crosstype operations for date vs. timestamp and timestamptz { oid => '2338', diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index c51e848c24..ba0e5f624f 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -21,7 +21,8 @@ extern PGDLLIMPORT const pg_be_sasl_mech pg_be_scram_mech; /* Routines to handle and check SCRAM-SHA-256 secret */ -extern char *pg_be_scram_build_secret(const char *password); +extern char *pg_be_scram_build_secret(const char *password, char *salt_str, + int salt_str_len, int iterations); extern bool parse_scram_secret(const char *secret, int *iterations, char **salt, uint8 *stored_key, uint8 *server_key); extern bool scram_verify_plain_password(const char *username, diff --git a/src/test/regress/expected/scram.out b/src/test/regress/expected/scram.out new file mode 100644 index 0000000000..6ff14d9979 --- /dev/null +++ b/src/test/regress/expected/scram.out @@ -0,0 +1,91 @@ +-- Test building SCRAM functions +-- test all nulls +-- fail +SELECT scram_build_secret_sha256(NULL); +ERROR: password must not be null +SELECT scram_build_secret_sha256(NULL, NULL); +ERROR: password must not be null +SELECT scram_build_secret_sha256(NULL, NULL, NULL); +ERROR: password must not be null +-- generated a SCRAM secret from a plaintext password +SELECT regexp_replace( + scram_build_secret_sha256('secret password'), + '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', + '\1$\2:$:') AS scram_secret; + scram_secret +--------------------------------------------------- + SCRAM-SHA-256$4096:$: +(1 row) + +-- test building a SCRAM secret with a predefined salt with a valid base64 +-- encoded string +SELECT scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64')); + scram_build_secret_sha256 +--------------------------------------------------------------------------------------------------------------------------------------- + SCRAM-SHA-256$4096:MTIzNDU2Nzg5MGFiY2RlZg==$D5BmucT796UQKargx2k3fdqjDYR7cH/L0viKKhGo6kA=:M33+iHFOESP8C3DKLDb2k5QAhkNVWEbp/YUIFd2CxN4= +(1 row) + +-- test building a SCRAM secret with a predefined salt that is not a valid +-- base64 string +-- fail +SELECT scram_build_secret_sha256('secret password', + decode('abc', 'base64')); +ERROR: invalid base64 end sequence +HINT: Input data is missing padding, is truncated, or is otherwise corrupted. +-- test building a SCRAM secret with a salt that looks like a string but is +-- cast to a bytea +SELECT scram_build_secret_sha256('secret password', 'abc'); + scram_build_secret_sha256 +------------------------------------------------------------------------------------------------------------------- + SCRAM-SHA-256$4096:YWJj$L27WlKwqjMDY5ZNsyaxGSMii2mhmoUB7xONbxjykmw4=:u1ofGUXUqTbMwfiH+ANWDCpwEjk3j1Xrocy3va/jaCU= +(1 row) + +-- test building a SCRAM secret with a bytea salt using the hex format +SELECT scram_build_secret_sha256('secret password', '\xabba5432'); + scram_build_secret_sha256 +----------------------------------------------------------------------------------------------------------------------- + SCRAM-SHA-256$4096:q7pUMg==$05Nb9QHwHkMA0CRcYaEfwtgZ+3kStIefz8fLMjTEtio=:P126h1ycyP938E69yxktEfhoAILbiwL/UMsMk3Efb6o= +(1 row) + +-- test building a SCRAM secret with a valid salt and a different set of +-- iterations +SELECT scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64'), 10000); + scram_build_secret_sha256 +---------------------------------------------------------------------------------------------------------------------------------------- + SCRAM-SHA-256$10000:MTIzNDU2Nzg5MGFiY2RlZg==$9NkDu1TFpx3L30zMgHUqjRNSq3GRZRrdWU4TuGOnT3Q=:svuIH9L6HH8loyKWguT64XXoOLCrr4FkVViPd2JVR4M= +(1 row) + +-- test what happens when the salt is a NULL value. +-- this should build a SCRAM secret using a random salt. +SELECT regexp_replace( + scram_build_secret_sha256('secret password', NULL, 10000), + '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]{24})\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', + '\1$\2:$:') AS scram_secret; + scram_secret +---------------------------------------------------- + SCRAM-SHA-256$10000:$: +(1 row) + +-- test what happens when iterations is a null value. this should set iterations +-- to be the default, currently 4096. +SELECT + scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64'), NULL) ~ + '^SCRAM-SHA-256\$4096' AS has_default_iterations; + has_default_iterations +------------------------ + t +(1 row) + +-- test SASLprep. This tests the case where a user supplies a non-ASCII space +-- character. +SELECT + scram_build_secret_sha256(U&'one\1680space', decode('h2y81+nUwWp5uIJc4PgyXA==', 'base64')) = + 'SCRAM-SHA-256$4096:h2y81+nUwWp5uIJc4PgyXA==$EiywEpO6rM3z3DGehubeoRpp8Orq0XuDUbdT9fQWwz8=:Wh7fq4C+bageihh3vTrkCr7YrlcDTG+JhfcFAuHn/6E='; + ?column? +---------- + t +(1 row) + diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 9a139f1e24..a02c9c0322 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -33,7 +33,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t # geometry depends on point, lseg, line, box, path, polygon, circle # horology depends on date, time, timetz, timestamp, timestamptz, interval # ---------- -test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc +test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc scram # ---------- # Load huge amounts of data diff --git a/src/test/regress/sql/scram.sql b/src/test/regress/sql/scram.sql new file mode 100644 index 0000000000..fc89d782a5 --- /dev/null +++ b/src/test/regress/sql/scram.sql @@ -0,0 +1,56 @@ +-- Test building SCRAM functions + +-- test all nulls +-- fail +SELECT scram_build_secret_sha256(NULL); +SELECT scram_build_secret_sha256(NULL, NULL); +SELECT scram_build_secret_sha256(NULL, NULL, NULL); + +-- generated a SCRAM secret from a plaintext password +SELECT regexp_replace( + scram_build_secret_sha256('secret password'), + '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', + '\1$\2:$:') AS scram_secret; + +-- test building a SCRAM secret with a predefined salt with a valid base64 +-- encoded string +SELECT scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64')); + +-- test building a SCRAM secret with a predefined salt that is not a valid +-- base64 string +-- fail +SELECT scram_build_secret_sha256('secret password', + decode('abc', 'base64')); + +-- test building a SCRAM secret with a salt that looks like a string but is +-- cast to a bytea +SELECT scram_build_secret_sha256('secret password', 'abc'); + +-- test building a SCRAM secret with a bytea salt using the hex format +SELECT scram_build_secret_sha256('secret password', '\xabba5432'); + +-- test building a SCRAM secret with a valid salt and a different set of +-- iterations +SELECT scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64'), 10000); + +-- test what happens when the salt is a NULL value. +-- this should build a SCRAM secret using a random salt. +SELECT regexp_replace( + scram_build_secret_sha256('secret password', NULL, 10000), + '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]{24})\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', + '\1$\2:$:') AS scram_secret; + +-- test what happens when iterations is a null value. this should set iterations +-- to be the default, currently 4096. +SELECT + scram_build_secret_sha256('secret password', + decode('MTIzNDU2Nzg5MGFiY2RlZg==', 'base64'), NULL) ~ + '^SCRAM-SHA-256\$4096' AS has_default_iterations; + +-- test SASLprep. This tests the case where a user supplies a non-ASCII space +-- character. +SELECT + scram_build_secret_sha256(U&'one\1680space', decode('h2y81+nUwWp5uIJc4PgyXA==', 'base64')) = + 'SCRAM-SHA-256$4096:h2y81+nUwWp5uIJc4PgyXA==$EiywEpO6rM3z3DGehubeoRpp8Orq0XuDUbdT9fQWwz8=:Wh7fq4C+bageihh3vTrkCr7YrlcDTG+JhfcFAuHn/6E='; -- 2.32.1 (Apple Git-133)