diff options
-rw-r--r-- | doc/src/sgml/catalogs.sgml | 24 | ||||
-rw-r--r-- | src/backend/libpq/auth-scram.c | 142 | ||||
-rw-r--r-- | src/backend/libpq/crypt.c | 2 | ||||
-rw-r--r-- | src/include/catalog/catversion.h | 2 | ||||
-rw-r--r-- | src/test/regress/expected/password.out | 14 | ||||
-rw-r--r-- | src/test/regress/sql/password.sql | 8 |
6 files changed, 119 insertions, 73 deletions
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index ed74704b2ad..787fcbd51ac 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1376,14 +1376,22 @@ 32-character hexadecimal MD5 hash. The MD5 hash will be of the user's password concatenated to their user name. For example, if user <literal>joe</> has password <literal>xyzzy</>, <productname>PostgreSQL</> - will store the md5 hash of <literal>xyzzyjoe</>. If the password is - encrypted with SCRAM-SHA-256, it consists of 5 fields separated by colons. - The first field is the constant <literal>scram-sha-256</literal>, to - identify the password as a SCRAM-SHA-256 verifier. The second field is a - salt, Base64-encoded, and the third field is the number of iterations used - to generate the password. The fourth field and fifth field are the stored - key and server key, respectively, in hexadecimal format. A password that - does not follow either of those formats is assumed to be unencrypted. + will store the md5 hash of <literal>xyzzyjoe</>. + </para> + + <para> + If the password is encrypted with SCRAM-SHA-256, it has the format: +<synopsis> +SCRAM-SHA-256$<replaceable><iteration count></>:<replaceable><salt></>$<replaceable><StoredKey></>:<replaceable><ServerKey></> +</synopsis> + where <replaceable>salt</>, <replaceable>StoredKey</> and + <replaceable>ServerKey</> are in Base64 encoded format. This format is + the same as that specified by RFC 5803. + </para> + + <para> + A password that does not follow either of those formats is assumed to be + unencrypted. </para> </sect1> diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index 76c502d415d..16bea446e37 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -5,6 +5,7 @@ * * See the following RFCs for more details: * - RFC 5802: https://tools.ietf.org/html/rfc5802 + * - RFC 5803: https://tools.ietf.org/html/rfc5803 * - RFC 7677: https://tools.ietf.org/html/rfc7677 * * Here are some differences: @@ -19,7 +20,7 @@ * - Channel binding is not supported yet. * * - * The password stored in pg_authid consists of the salt, iteration count, + * The password stored in pg_authid consists of the iteration count, salt, * StoredKey and ServerKey. * * SASLprep usage @@ -111,8 +112,8 @@ typedef struct const char *username; /* username from startup packet */ - char *salt; /* base64-encoded */ int iterations; + char *salt; /* base64-encoded */ uint8 StoredKey[SCRAM_KEY_LEN]; uint8 ServerKey[SCRAM_KEY_LEN]; @@ -146,10 +147,10 @@ static char *build_server_first_message(scram_state *state); static char *build_server_final_message(scram_state *state); static bool verify_client_proof(scram_state *state); static bool verify_final_nonce(scram_state *state); -static bool parse_scram_verifier(const char *verifier, char **salt, - int *iterations, uint8 *stored_key, uint8 *server_key); -static void mock_scram_verifier(const char *username, char **salt, int *iterations, - uint8 *stored_key, uint8 *server_key); +static bool parse_scram_verifier(const char *verifier, int *iterations, + char **salt, uint8 *stored_key, uint8 *server_key); +static void mock_scram_verifier(const char *username, int *iterations, + char **salt, uint8 *stored_key, uint8 *server_key); static bool is_scram_printable(char *p); static char *sanitize_char(char c); static char *scram_MockSalt(const char *username); @@ -185,7 +186,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass) if (password_type == PASSWORD_TYPE_SCRAM_SHA_256) { - if (parse_scram_verifier(shadow_pass, &state->salt, &state->iterations, + if (parse_scram_verifier(shadow_pass, &state->iterations, &state->salt, state->StoredKey, state->ServerKey)) got_verifier = true; else @@ -208,7 +209,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass) verifier = scram_build_verifier(username, shadow_pass, 0); - (void) parse_scram_verifier(verifier, &state->salt, &state->iterations, + (void) parse_scram_verifier(verifier, &state->iterations, &state->salt, state->StoredKey, state->ServerKey); pfree(verifier); @@ -243,7 +244,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass) */ if (!got_verifier) { - mock_scram_verifier(username, &state->salt, &state->iterations, + mock_scram_verifier(username, &state->iterations, &state->salt, state->StoredKey, state->ServerKey); state->doomed = true; } @@ -393,14 +394,15 @@ char * scram_build_verifier(const char *username, const char *password, int iterations) { - uint8 keybuf[SCRAM_KEY_LEN + 1]; - char storedkey_hex[SCRAM_KEY_LEN * 2 + 1]; - char serverkey_hex[SCRAM_KEY_LEN * 2 + 1]; - char salt[SCRAM_SALT_LEN]; - char *encoded_salt; - int encoded_len; char *prep_password = NULL; pg_saslprep_rc rc; + char saltbuf[SCRAM_SALT_LEN]; + uint8 keybuf[SCRAM_KEY_LEN]; + char *encoded_salt; + char *encoded_storedkey; + char *encoded_serverkey; + int encoded_len; + char *result; /* * Normalize the password with SASLprep. If that doesn't work, because @@ -414,7 +416,8 @@ scram_build_verifier(const char *username, const char *password, if (iterations <= 0) iterations = SCRAM_ITERATIONS_DEFAULT; - if (!pg_backend_random(salt, SCRAM_SALT_LEN)) + /* Generate salt, and encode it in base64 */ + if (!pg_backend_random(saltbuf, SCRAM_SALT_LEN)) { ereport(LOG, (errcode(ERRCODE_INTERNAL_ERROR), @@ -423,26 +426,38 @@ scram_build_verifier(const char *username, const char *password, } encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1); - encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt); + encoded_len = pg_b64_encode(saltbuf, SCRAM_SALT_LEN, encoded_salt); encoded_salt[encoded_len] = '\0'; - /* Calculate StoredKey, and encode it in hex */ - scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, + /* Calculate StoredKey, and encode it in base64 */ + scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN, iterations, SCRAM_CLIENT_KEY_NAME, keybuf); scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */ - (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex); - storedkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); + encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, + encoded_storedkey); + encoded_storedkey[encoded_len] = '\0'; /* And same for ServerKey */ - scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations, + scram_ClientOrServerKey(password, saltbuf, SCRAM_SALT_LEN, iterations, SCRAM_SERVER_KEY_NAME, keybuf); - (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex); - serverkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); + encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, + encoded_serverkey); + encoded_serverkey[encoded_len] = '\0'; + + result = psprintf("SCRAM-SHA-256$%d:%s$%s:%s", iterations, encoded_salt, + encoded_storedkey, encoded_serverkey); if (prep_password) pfree(prep_password); + pfree(encoded_salt); + pfree(encoded_storedkey); + pfree(encoded_serverkey); - return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex); + return result; } /* @@ -464,7 +479,7 @@ scram_verify_plain_password(const char *username, const char *password, char *prep_password = NULL; pg_saslprep_rc rc; - if (!parse_scram_verifier(verifier, &encoded_salt, &iterations, + if (!parse_scram_verifier(verifier, &iterations, &encoded_salt, stored_key, server_key)) { /* @@ -509,13 +524,14 @@ scram_verify_plain_password(const char *username, const char *password, bool is_scram_verifier(const char *verifier) { - char *salt = NULL; int iterations; + char *salt = NULL; uint8 stored_key[SCRAM_KEY_LEN]; uint8 server_key[SCRAM_KEY_LEN]; bool result; - result = parse_scram_verifier(verifier, &salt, &iterations, stored_key, server_key); + result = parse_scram_verifier(verifier, &iterations, &salt, + stored_key, server_key); if (salt) pfree(salt); @@ -529,60 +545,82 @@ is_scram_verifier(const char *verifier) * Returns true if the SCRAM verifier has been parsed, and false otherwise. */ static bool -parse_scram_verifier(const char *verifier, char **salt, int *iterations, +parse_scram_verifier(const char *verifier, int *iterations, char **salt, uint8 *stored_key, uint8 *server_key) { char *v; char *p; + char *scheme_str; + char *salt_str; + char *iterations_str; + char *storedkey_str; + char *serverkey_str; + int decoded_len; + char *decoded_salt_buf; /* * The verifier is of form: * - * scram-sha-256:<salt>:<iterations>:<storedkey>:<serverkey> + * SCRAM-SHA-256$<iterations>:<salt>$<storedkey>:<serverkey> */ - if (strncmp(verifier, "scram-sha-256:", strlen("scram-sha-256:")) != 0) - return false; - - v = pstrdup(verifier + strlen("scram-sha-256:")); - - /* salt */ - if ((p = strtok(v, ":")) == NULL) + v = pstrdup(verifier); + if ((scheme_str = strtok(v, "$")) == NULL) + goto invalid_verifier; + if ((iterations_str = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if ((salt_str = strtok(NULL, "$")) == NULL) + goto invalid_verifier; + if ((storedkey_str = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if ((serverkey_str = strtok(NULL, "")) == NULL) goto invalid_verifier; - *salt = pstrdup(p); - /* iterations */ - if ((p = strtok(NULL, ":")) == NULL) + /* Parse the fields */ + if (strcmp(scheme_str, "SCRAM-SHA-256") != 0) goto invalid_verifier; + errno = 0; - *iterations = strtol(p, &p, 10); + *iterations = strtol(iterations_str, &p, 10); if (*p || errno != 0) goto invalid_verifier; - /* storedkey */ - if ((p = strtok(NULL, ":")) == NULL) - goto invalid_verifier; - if (strlen(p) != SCRAM_KEY_LEN * 2) + /* + * Verify that the salt is in Base64-encoded format, by decoding it, + * although we return the encoded version to the caller. + */ + decoded_salt_buf = palloc(pg_b64_dec_len(strlen(salt_str))); + decoded_len = pg_b64_decode(salt_str, strlen(salt_str), decoded_salt_buf); + if (decoded_len < 0) goto invalid_verifier; + *salt = pstrdup(salt_str); - hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key); + /* + * Decode StoredKey and ServerKey. + */ + if (pg_b64_dec_len(strlen(storedkey_str) != SCRAM_KEY_LEN)) + goto invalid_verifier; + decoded_len = pg_b64_decode(storedkey_str, strlen(storedkey_str), + (char *) stored_key); + if (decoded_len != SCRAM_KEY_LEN) + goto invalid_verifier; - /* serverkey */ - if ((p = strtok(NULL, ":")) == NULL) + if (pg_b64_dec_len(strlen(serverkey_str) != SCRAM_KEY_LEN)) goto invalid_verifier; - if (strlen(p) != SCRAM_KEY_LEN * 2) + decoded_len = pg_b64_decode(serverkey_str, strlen(serverkey_str), + (char *) server_key); + if (decoded_len != SCRAM_KEY_LEN) goto invalid_verifier; - hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key); - pfree(v); return true; invalid_verifier: pfree(v); + *salt = NULL; return false; } static void -mock_scram_verifier(const char *username, char **salt, int *iterations, +mock_scram_verifier(const char *username, int *iterations, char **salt, uint8 *stored_key, uint8 *server_key) { char *raw_salt; diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 03ef3cc6522..d0030f2b6d8 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -100,7 +100,7 @@ get_password_type(const char *shadow_pass) { if (strncmp(shadow_pass, "md5", 3) == 0 && strlen(shadow_pass) == MD5_PASSWD_LEN) return PASSWORD_TYPE_MD5; - if (strncmp(shadow_pass, "scram-sha-256:", strlen("scram-sha-256:")) == 0) + if (strncmp(shadow_pass, "SCRAM-SHA-256$", strlen("SCRAM-SHA-256$")) == 0) return PASSWORD_TYPE_SCRAM_SHA_256; return PASSWORD_TYPE_PLAINTEXT; } diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index ab92fd88ed8..6cc9e30ec24 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201704171 +#define CATALOG_VERSION_NO 201704211 #endif diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out index 676b3e6ff3f..9ec5a52bba4 100644 --- a/src/test/regress/expected/password.out +++ b/src/test/regress/expected/password.out @@ -23,11 +23,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL; -- check list of created entries -- -- The scram verifier will look something like: --- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee +-- SCRAM-SHA-256$4096:E4HxLGtnRzsYwg==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo= -- -- Since the salt is random, the exact value stored will be different on every test -- run. Use a regular expression to mask the changing parts. -SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/]+==)\$([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; @@ -36,7 +36,7 @@ SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==): regress_passwd1 | role_pwd1 regress_passwd2 | md54044304ba511dd062133eb5b4b84a2a3 regress_passwd3 | md50e5699b6911d87f17a08b8d76a21e8b8 - regress_passwd4 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey> + regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> regress_passwd5 | (5 rows) @@ -59,11 +59,11 @@ ALTER ROLE regress_passwd1 UNENCRYPTED PASSWORD 'foo'; -- unencrypted ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab9cdb'; -- encrypted with MD5 SET password_encryption = 'md5'; ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5 -ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is +ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'SCRAM-SHA-256$4096:VLK4RMaQLCvNtQ==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is SET password_encryption = 'scram-sha-256'; ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is -SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/]+==)\$([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; @@ -72,8 +72,8 @@ SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==): regress_passwd1 | foo regress_passwd2 | md5dfa155cadd5f4ad57860162f3fab9cdb regress_passwd3 | md5530de4c298af94b3b9f7d20305d2a1bf - regress_passwd4 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey> - regress_passwd5 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey> + regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> + regress_passwd5 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey> regress_passwd6 | md53725413363ab045e20521bf36b8d8d7f (6 rows) diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql index 95557e45660..1e022dbf2d6 100644 --- a/src/test/regress/sql/password.sql +++ b/src/test/regress/sql/password.sql @@ -24,11 +24,11 @@ CREATE ROLE regress_passwd5 PASSWORD NULL; -- check list of created entries -- -- The scram verifier will look something like: --- scram-sha-256:E4HxLGtnRzsYwg==:4096:5ebc825510cb7862efd87dfa638d8337179e6913a724441dc9e888a856fbc10c:e966b1c72fad89d69aaebb156eae04edc9581286f92207c044711e79cd461bee +-- SCRAM-SHA-256$4096:E4HxLGtnRzsYwg==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo= -- -- Since the salt is random, the exact value stored will be different on every test -- run. Use a regular expression to mask the changing parts. -SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/]+==)\$([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; @@ -48,13 +48,13 @@ ALTER ROLE regress_passwd2 UNENCRYPTED PASSWORD 'md5dfa155cadd5f4ad57860162f3fab SET password_encryption = 'md5'; ALTER ROLE regress_passwd3 ENCRYPTED PASSWORD 'foo'; -- encrypted with MD5 -ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:4096:3ded2376f7aafa93b1bdbd71bcc18b7d6ee50ed018029cc583d152ef3fc7d430:a6dd36dfc94c181956a6ae95f05e01b1864f0a22a2657d1de4ba84d2a24dc438'; -- client-supplied SCRAM verifier, use as it is +ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'SCRAM-SHA-256$4096:VLK4RMaQLCvNtQ==$6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is SET password_encryption = 'scram-sha-256'; ALTER ROLE regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is -SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked +SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/]+==)\$([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; |