On 10/31/22 8:56 PM, Michael Paquier wrote:
On Mon, Oct 31, 2022 at 04:27:08PM -0400, Jonathan S. Katz wrote:
1. password only -- this defers to the PG defaults for SCRAM
2. password + salt -- this is useful for the password history / dictionary
case to allow for a predictable way to check a password.

Well, one could pass a salt based on something generated by random()
to emulate what we currently do in the default case, as well.  The
salt length is an extra possibility, letting it be randomly generated
by pg_strong_random().

Sure, this is a good point. From a SQL level we can get that from pgcrypto "gen_random_bytes".

Per this and ilmari's feedback, I updated the 2nd argument to be a bytea. See the corresponding tests that then show using decode(..., 'base64') to handle this.

When I write the docs, I'll include that in the examples.

1. Location of the functions. I put them in
src/backend/utils/adt/cryptohashfuncs.c as I wasn't sure where it would make
sense to have them (and they could easily go into their own file).

As of adt/authfuncs.c?  cryptohashfuncs.c does not strike me as a good
fit.

I went with your suggested name.

Please let me know if you have any questions. I'll add a CF entry for this.

+{ oid => '8555', descr => 'Build a SCRAM secret',
+  proname => 'scram_build_secret_sha256', proleakproof => 't', prorettype => 
'text',
+  proargtypes => 'text', prosrc => 'scram_build_secret_sha256_from_password' },
+{ oid => '8556', descr => 'Build a SCRAM secret',
+  proname => 'scram_build_secret_sha256', proleakproof => 't',
+  provolatile => 'i', prorettype => 'text',
+  proargtypes => 'text text', prosrc => 
'scram_build_secret_sha256_from_password_and_salt' },
+{ oid => '8557', descr => 'Build a SCRAM secret',
+  proname => 'scram_build_secret_sha256', proleakproof => 't',
+  provolatile => 'i', prorettype => 'text',
+  proargtypes => 'text text int4', prosrc => 
'scram_build_secret_sha256_from_password_and_salt_and_iterations' },

Keeping this approach as-is, I don't think that you should consume 3
OIDs, but 1 (with scram_build_secret_sha256_from_password_and_..
as prosrc) that has two defaults for the second argument (salt string,
default as NULL) and third argument (salt, default at 0), with the
defaults set up in system_functions.sql via a redefinition.

Thanks for the suggestion. I went with this as well.

Note that you cannot pass down an expression for the password of
CREATE/ALTER USER, meaning that this would need a \gset at least if
done by a psql client for example, and passing down a password string
is not an encouraged practice, either.  Another approach is to also
provide a role OID in input and store the newly-computed password in
pg_authid (as of [1]), so as one can store it easily.

...unless you dynamically generate the CREATE/ALTER ROLE command ;) (and yes, lots of discussion on that).

Did you look at extending \password?  Being able to extend
PQencryptPasswordConn() with custom parameters has been discussed in
the past, but this has gone nowhere.  That's rather unrelated to what
you are looking for, just mentioning as we are playing with options to
have control the iteration number and the salt.

Not yet, but happy to do that as a follow-up patch.

Please see version 2. If folks are generally happy with this, I'll address any additional feedback and write up docs.

Thanks,

Jonathan
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/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..4b4bbd7b7f
--- /dev/null
+++ b/src/backend/utils/adt/authfuncs.c
@@ -0,0 +1,120 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "common/scram-common.h"
+#include "libpq/scram.h"
+#include "utils/builtins.h"
+
+static char *
+scram_build_secret_sha256_internal(const char *password, char *salt_str_enc,
+                                                               int iterations);
+
+
+/*
+ * Build a SCRAM secret that can be used for SCRAM-SHA-256 authentication.
+ */
+Datum
+scram_build_secret_sha256(PG_FUNCTION_ARGS)
+{
+       const char        *password;
+  char                 *salt_str = NULL;
+       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));
+
+       if (!PG_ARGISNULL(2))
+               iterations = PG_GETARG_INT32(2);
+
+       secret = scram_build_secret_sha256_internal(password, salt_str, 
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));
+}
+
+
+/*
+ * Workhorse function to that creates SCRAM secrets from user provided info.
+ * Returns the SCRAM secret in "text" form.
+ *
+ * This function can take three parameters:
+ *
+ * - 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.
+ */
+static char *
+scram_build_secret_sha256_internal(const char *password, char *salt_str,
+                                                               int iterations)
+{
+       char            salt_default[SCRAM_DEFAULT_SALT_LEN];
+       char       *secret;
+       const char *errstr = NULL;
+
+       Assert(password != NULL);
+
+       if (salt_str == NULL && iterations <= 0)
+       {
+               return pg_be_scram_build_secret(password);
+       }
+
+  /*
+   * If the salt is still null, generate a random salt based on the defaults.
+   * Otherwise, work to base64 decode the salt.
+   */
+       if (salt_str == NULL)
+       {
+               if (!pg_strong_random(salt_default, SCRAM_DEFAULT_SALT_LEN))
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_INTERNAL_ERROR),
+                                        errmsg("could not generate random 
salt")));  
+               salt_str = pnstrdup(salt_default, SCRAM_DEFAULT_SALT_LEN);
+       }
+
+       /* if iterations is <= 0, set to the default */
+       if (iterations <= 0)
+               iterations = SCRAM_DEFAULT_ITERATIONS;
+
+       /*
+        * As this is a backend function, the "errstr" will not be set.
+        * The current behavior is to elog an ERROR. We will at least assert 
that we
+        * don't return a NULL secret.
+        */
+       secret = scram_build_secret(salt_str, strlen(salt_str), iterations, 
password,
+                                                       &errstr);
+
+       Assert(secret != NULL);
+
+       return 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/test/regress/expected/scram.out 
b/src/test/regress/expected/scram.out
new file mode 100644
index 0000000000..4783310ca9
--- /dev/null
+++ b/src/test/regress/expected/scram.out
@@ -0,0 +1,81 @@
+-- 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:<salt>$<storedkey>:<serverkey>') AS scram_secret;
+                   scram_secret                    
+---------------------------------------------------
+ SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+(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:<salt>$<storedkey>:<serverkey>') AS scram_secret;
+                    scram_secret                    
+----------------------------------------------------
+ SCRAM-SHA-256$10000:<salt>$<storedkey>:<serverkey>
+(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)
+
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..53f87530f8
--- /dev/null
+++ b/src/test/regress/sql/scram.sql
@@ -0,0 +1,50 @@
+-- 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:<salt>$<storedkey>:<serverkey>') 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:<salt>$<storedkey>:<serverkey>') 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;

Attachment: OpenPGP_signature
Description: OpenPGP digital signature

Reply via email to