On Fri, Mar 24, 2017 at 10:12 PM, Heikki Linnakangas <hlinn...@iki.fi> wrote: > On 03/24/2017 03:02 PM, Michael Paquier wrote: >> >> In order to close this thread, I propose to reuse the patches I sent >> here to make scram_build_verifier() available to frontends: >> >> https://www.postgresql.org/message-id/CAB7nPqT4yc3u8wspYkWbG088Ndp6asMH3=zb___ck89ctvz...@mail.gmail.com >> >> And on top of it modify \password so as it generates a md5 verifier >> for pre-9.6 servers and a scram one for post-10 servers by looking at >> the backend version of the current connection. What do you think? > > Yep, sounds like a plan.
And attached is a set of rebased patches, with createuser and psql's \password extended to do that. -- Michael
From 21e73b1bc1cf1985b7ca0e638b0e752ffd4b89b1 Mon Sep 17 00:00:00 2001 From: Michael Paquier <mich...@paquier.xyz> Date: Sat, 25 Mar 2017 13:31:38 +0900 Subject: [PATCH 1/5] Use base64-based encoding for stored and server keys in SCRAM verifiers In order to be able to generate a SCRAM verifier even for frontends, let's simplify the tools used to generate it and switch all the elements of the verifiers to be base64-encoded using the routines already in place in src/common/. --- doc/src/sgml/catalogs.sgml | 2 +- src/backend/libpq/auth-scram.c | 30 +++++++++++++++++------------- src/test/regress/expected/password.out | 8 ++++---- src/test/regress/sql/password.sql | 8 ++++---- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index ac39c639ed..30a288ab91 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1371,7 +1371,7 @@ 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 + key and server key, respectively, in Base64 format. 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 bcc8d03ef5..e8068d3b7a 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -339,8 +339,8 @@ 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 *encoded_storedkey; + char *encoded_serverkey; char salt[SCRAM_SALT_LEN]; char *encoded_salt; int encoded_len; @@ -360,20 +360,25 @@ scram_build_verifier(const char *username, const char *password, encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt); encoded_salt[encoded_len] = '\0'; - /* Calculate StoredKey, and encode it in hex */ + /* Calculate StoredKey, and encode it in base64 */ + encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); scram_ClientOrServerKey(password, salt, 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_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, + encoded_storedkey); + encoded_storedkey[encoded_len] = '\0'; /* And same for ServerKey */ + encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); scram_ClientOrServerKey(password, salt, 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_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, + encoded_serverkey); + encoded_serverkey[encoded_len] = '\0'; - return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex); + return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, + encoded_storedkey, encoded_serverkey); } /* @@ -483,17 +488,16 @@ parse_scram_verifier(const char *verifier, char **salt, int *iterations, /* storedkey */ if ((p = strtok(NULL, ":")) == NULL) goto invalid_verifier; - if (strlen(p) != SCRAM_KEY_LEN * 2) + if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1) goto invalid_verifier; - - hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key); + pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) stored_key); /* serverkey */ if ((p = strtok(NULL, ":")) == NULL) goto invalid_verifier; - if (strlen(p) != SCRAM_KEY_LEN * 2) + if (strlen(p) != pg_b64_enc_len(SCRAM_KEY_LEN) - 1) goto invalid_verifier; - hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key); + pg_b64_decode(p, pg_b64_enc_len(SCRAM_KEY_LEN), (char *) server_key); pfree(v); return true; diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out index c503e43abe..0cdb6141e2 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:E4HxLGtnRzsYwg==:4096: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):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; @@ -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:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is SET password_encryption = 'scram'; 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):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql index f4b3a9ac3a..df1ff989cc 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:E4HxLGtnRzsYwg==:4096: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):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<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:VLK4RMaQLCvNtQ==:4096:6YtlR4t69SguDiwFvbVgVZtuz6gpJQQqUMZ7IQJK5yI=:ps75jrHeYU4lXCcXI4O8oIdJ3eO8o2jirjruw9phBTo='; -- client-supplied SCRAM verifier, use as it is SET password_encryption = 'scram'; 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):([a-zA-Z0-9+/]+==):(\d+):([a-zA-Z0-9+/]+=):([a-zA-Z0-9+/]+=)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked FROM pg_authid WHERE rolname LIKE 'regress_passwd%' ORDER BY rolname, rolpassword; -- 2.12.1
From ea4884552ed7646973c4be5f3357a6acb806f95c Mon Sep 17 00:00:00 2001 From: Michael Paquier <mich...@paquier.xyz> Date: Sat, 25 Mar 2017 13:42:45 +0900 Subject: [PATCH 3/5] Move routine to build SCRAM verifier into src/common/ This is aimed at being used by libpq to allow frontend-side creation of SCRAM verifiers. The result is not anymore allocated by the routine itself, the caller is responsible for that, similarly to md5. --- src/backend/libpq/auth-scram.c | 56 ++--------------------------- src/backend/libpq/crypt.c | 7 +++- src/common/scram-common.c | 74 +++++++++++++++++++++++++++++++++++++++ src/include/common/scram-common.h | 20 +++++++++++ src/include/libpq/scram.h | 5 +-- 5 files changed, 103 insertions(+), 59 deletions(-) diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index e8068d3b7a..9cec3c0f82 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -172,9 +172,9 @@ pg_be_scram_init(const char *username, const char *shadow_pass) * The stored password is in plain format. Generate a fresh SCRAM * verifier from it, and proceed with that. */ - char *verifier; + char *verifier = palloc(SCRAM_VERIFIER_LEN + 1); - verifier = scram_build_verifier(username, shadow_pass, 0); + (void) scram_build_verifier(username, shadow_pass, 0, verifier); (void) parse_scram_verifier(verifier, &state->salt, &state->iterations, state->StoredKey, state->ServerKey); @@ -328,58 +328,6 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen, return result; } -/* - * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword. - * - * If iterations is 0, default number of iterations is used. The result is - * palloc'd, so caller is responsible for freeing it. - */ -char * -scram_build_verifier(const char *username, const char *password, - int iterations) -{ - uint8 keybuf[SCRAM_KEY_LEN + 1]; - char *encoded_storedkey; - char *encoded_serverkey; - char salt[SCRAM_SALT_LEN]; - char *encoded_salt; - int encoded_len; - - if (iterations <= 0) - iterations = SCRAM_ITERATIONS_DEFAULT; - - if (!pg_backend_random(salt, SCRAM_SALT_LEN)) - { - ereport(LOG, - (errcode(ERRCODE_INTERNAL_ERROR), - errmsg("could not generate random salt"))); - return NULL; - } - - encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1); - encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt); - encoded_salt[encoded_len] = '\0'; - - /* Calculate StoredKey, and encode it in base64 */ - encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); - scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, - iterations, SCRAM_CLIENT_KEY_NAME, keybuf); - scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */ - encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, - encoded_storedkey); - encoded_storedkey[encoded_len] = '\0'; - - /* And same for ServerKey */ - encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); - scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations, - SCRAM_SERVER_KEY_NAME, keybuf); - encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, - encoded_serverkey); - encoded_serverkey[encoded_len] = '\0'; - - return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, - encoded_storedkey, encoded_serverkey); -} /* * Verify a plaintext password against a SCRAM verifier. This is used when diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 34beab5334..d9aa2d93d6 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -20,6 +20,7 @@ #include "catalog/pg_authid.h" #include "common/md5.h" +#include "common/scram-common.h" #include "libpq/crypt.h" #include "libpq/scram.h" #include "miscadmin.h" @@ -156,7 +157,11 @@ encrypt_password(PasswordType target_type, const char *role, switch (guessed_type) { case PASSWORD_TYPE_PLAINTEXT: - return scram_build_verifier(role, password, 0); + encrypted_password = palloc(SCRAM_VERIFIER_LEN + 1); + if (!scram_build_verifier(role, password, 0, + encrypted_password)) + elog(ERROR, "password encryption failed"); + return encrypted_password; case PASSWORD_TYPE_MD5: diff --git a/src/common/scram-common.c b/src/common/scram-common.c index e44f38f652..00e45a800a 100644 --- a/src/common/scram-common.c +++ b/src/common/scram-common.c @@ -24,6 +24,11 @@ #include <arpa/inet.h> #include "common/scram-common.h" +#ifndef FRONTEND +#include "utils/backend_random.h" +#else +#include "common/frontend_random.h" +#endif #define HMAC_IPAD 0x36 #define HMAC_OPAD 0x5C @@ -184,3 +189,72 @@ scram_ClientOrServerKey(const char *password, scram_HMAC_update(&ctx, keystr, strlen(keystr)); scram_HMAC_final(result, &ctx); } + +/* + * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword. + * + * If iterations is 0, default number of iterations is used. The result is + * stored in "verifier" that caller is responsible to allocate a buffer of + * size SCRAM_VERIFIER_LEN. Returns true if the verifier has been generated, + * false otherwise. It is important for this routine to do no memory + * allocations. + */ +bool +scram_build_verifier(const char *username, const char *password, + int iterations, char *verifier) +{ + uint8 keybuf[SCRAM_KEY_LEN + 1]; + char salt[SCRAM_SALT_LEN]; + char intbuf[SCRAM_ITERATION_LEN]; + char *p; + + if (iterations <= 0) + iterations = SCRAM_ITERATIONS_DEFAULT; + +#ifdef FRONTEND + if (!pg_frontend_random(salt, SCRAM_SALT_LEN)) + return false; +#else + if (!pg_backend_random(salt, SCRAM_SALT_LEN)) + { + ereport(LOG, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generate random salt"))); + return false; + } +#endif + + /* Fill in the data of the verifier */ + p = verifier; + memcpy(p, SCRAM_VERIFIER_PREFIX, strlen(SCRAM_VERIFIER_PREFIX)); + p += strlen(SCRAM_VERIFIER_PREFIX); + *p++ = ':'; + + /* salt */ + (void) pg_b64_encode(salt, SCRAM_SALT_LEN, p); + p += pg_b64_enc_len(SCRAM_SALT_LEN); + *p++ = ':'; + + /* iterations */ + sprintf(intbuf, "%d", iterations); + memcpy(p, intbuf, strlen(intbuf)); + p += strlen(intbuf); + *p++ = ':'; + + /* Calculate StoredKey, and encode it in base64 */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, + iterations, SCRAM_CLIENT_KEY_NAME, keybuf); + scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */ + (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p); + p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1; + *p++ = ':'; + + /* And same for ServerKey */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations, + SCRAM_SERVER_KEY_NAME, keybuf); + (void) pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN, p); + p += pg_b64_enc_len(SCRAM_KEY_LEN) - 1; + *p++ = '\0'; + + return true; +} diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h index 7c98cc74d6..3fdfca44d6 100644 --- a/src/include/common/scram-common.h +++ b/src/include/common/scram-common.h @@ -13,6 +13,7 @@ #ifndef SCRAM_COMMON_H #define SCRAM_COMMON_H +#include "common/base64.h" #include "common/sha2.h" /* Length of SCRAM keys (client and server) */ @@ -41,6 +42,23 @@ #define SCRAM_SERVER_KEY_NAME "Server Key" #define SCRAM_CLIENT_KEY_NAME "Client Key" +#define SCRAM_VERIFIER_PREFIX "scram-sha-256" + +/* + * Length of a SCRAM verifier, which is made of the following five fields + * separated by a colon: + * - prefix "scram-sha-256", made of 13 characters. + * - 4 colon separators. + * - 32-bit number of interations, up to 10 characters. + * - base64-encoded salt of length SCRAM_SALT_LEN + * - base64-encoded stored key of length SCRAM_KEY_LEN + * - base64-encoded server key of length SCRAM_KEY_LEN + */ +#define SCRAM_VERIFIER_LEN (strlen("scram-sha-256") + 4 + \ + SCRAM_ITERATION_LEN + \ + pg_b64_enc_len(SCRAM_SALT_LEN) + \ + pg_b64_enc_len(SCRAM_KEY_LEN) * 2) + /* * Context data for HMAC used in SCRAM authentication. */ @@ -58,5 +76,7 @@ extern void scram_H(const uint8 *str, int len, uint8 *result); extern void scram_ClientOrServerKey(const char *password, const char *salt, int saltlen, int iterations, const char *keystr, uint8 *result); +extern bool scram_build_verifier(const char *username, const char *password, + int iterations, char *verifier); #endif /* SCRAM_COMMON_H */ diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index e373f0c07e..803fc4ef7a 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -26,10 +26,7 @@ extern void *pg_be_scram_init(const char *username, const char *shadow_pass); extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen, char **output, int *outputlen, char **logdetail); -/* Routines to handle and check SCRAM-SHA-256 verifier */ -extern char *scram_build_verifier(const char *username, - const char *password, - int iterations); +/* Routines to check SCRAM-SHA-256 verifier */ extern bool is_scram_verifier(const char *verifier); extern bool scram_verify_plain_password(const char *username, const char *password, const char *verifier); -- 2.12.1
From 863c71ea02c1abe302aec622ed3ae7fb45c56425 Mon Sep 17 00:00:00 2001 From: Michael Paquier <mich...@paquier.xyz> Date: Sat, 25 Mar 2017 13:45:39 +0900 Subject: [PATCH 4/5] Extend PQencryptPassword with a hashing method This extra argument can use the following values when hashing the password: - scram, for SCRAM-SHA-256 hashing. - md5, for MD5 hashing. - plain, for cleartext. --- doc/src/sgml/libpq.sgml | 6 ++++- src/bin/psql/command.c | 2 +- src/bin/scripts/createuser.c | 3 ++- src/interfaces/libpq/fe-auth.c | 49 +++++++++++++++++++++++++++++++---------- src/interfaces/libpq/libpq-fe.h | 3 ++- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 4bc5bf3192..fc1aa4b5e5 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -5887,7 +5887,8 @@ void PQconninfoFree(PQconninfoOption *connOptions); <para> Prepares the encrypted form of a <productname>PostgreSQL</> password. <synopsis> -char * PQencryptPassword(const char *passwd, const char *user); +char * PQencryptPassword(const char *passwd, const char *user, + const char *method); </synopsis> This function is intended to be used by client applications that wish to send commands like <literal>ALTER USER joe PASSWORD @@ -5901,6 +5902,9 @@ char * PQencryptPassword(const char *passwd, const char *user); memory. The caller can assume the string doesn't contain any special characters that would require escaping. Use <function>PQfreemem</> to free the result when done with it. + The encryption method of the password can be specified as + <literal>md5</> for hashing with MD5, <literal>scram</> for + hashing with SCRAM-SHA-256 and <literal>plain</> for cleartext. </para> </listitem> </varlistentry> diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 4f4a0aa9bd..25205f589b 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1135,7 +1135,7 @@ exec_command(const char *cmd, else user = PQuser(pset.db); - encrypted_password = PQencryptPassword(pw1, user); + encrypted_password = PQencryptPassword(pw1, user, "md5"); if (!encrypted_password) { diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c index 3d74797a8f..5af263e34a 100644 --- a/src/bin/scripts/createuser.c +++ b/src/bin/scripts/createuser.c @@ -275,7 +275,8 @@ main(int argc, char *argv[]) char *encrypted_password; encrypted_password = PQencryptPassword(newpassword, - newuser); + newuser, + "md5"); if (!encrypted_password) { fprintf(stderr, _("Password encryption failed.\n")); diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 5fe7e565a0..c94fb6127f 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -39,6 +39,7 @@ #endif #include "common/md5.h" +#include "common/scram-common.h" #include "libpq-fe.h" #include "libpq/scram.h" #include "fe-auth.h" @@ -919,27 +920,51 @@ pg_fe_getauthname(PQExpBuffer errorMessage) * be dependent on low-level details like whether the encryption is MD5 * or something else. * - * Arguments are the cleartext password, and the SQL name of the user it - * is for. + * Arguments are the cleartext password, the SQL name of the user it + * is for, and the name of password hashing method: + * - "scram", to hash password using SCRAM-SHA-256. + * - "md5", to hash password using MD5. + * - "plain", to get a cleartext value of password. * - * Return value is a malloc'd string, or NULL if out-of-memory. The client - * may assume the string doesn't contain any special characters that would - * require escaping. + * Return value is a malloc'd string, or NULL if out-of-memory or in + * the event of an error. The client may assume the string doesn't + * contain any special characters that would require escaping. */ char * -PQencryptPassword(const char *passwd, const char *user) +PQencryptPassword(const char *passwd, const char *user, const char *method) { char *crypt_pwd; - crypt_pwd = malloc(MD5_PASSWD_LEN + 1); - if (!crypt_pwd) - return NULL; + if (strcmp(method, "md5") == 0) + { + crypt_pwd = malloc(MD5_PASSWD_LEN + 1); + if (!crypt_pwd) + return NULL; - if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd)) + if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd)) + { + free(crypt_pwd); + return NULL; + } + } + else if (strcmp(method, "scram") == 0) { - free(crypt_pwd); - return NULL; + crypt_pwd = malloc(SCRAM_VERIFIER_LEN + 1); + if (!crypt_pwd) + return NULL; + + if (!scram_build_verifier(user, passwd, 0, crypt_pwd)) + { + free(crypt_pwd); + return NULL; + } } + else if (strcmp(method, "plain") == 0) + { + crypt_pwd = strdup(passwd); + } + else + return NULL; return crypt_pwd; } diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 635af5b50e..c312dd0152 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -596,7 +596,8 @@ extern int PQenv2encoding(void); /* === in fe-auth.c === */ -extern char *PQencryptPassword(const char *passwd, const char *user); +extern char *PQencryptPassword(const char *passwd, const char *user, + const char *method); /* === in encnames.c === */ -- 2.12.1
From 0fb421993918b1f1ffddf2b92169f72c27b5f7ec Mon Sep 17 00:00:00 2001 From: Michael Paquier <mich...@paquier.xyz> Date: Sat, 25 Mar 2017 14:07:16 +0900 Subject: [PATCH 5/5] Extend psql's \password and createuser to handle SCRAM verifier creation Depending on the version of PostgreSQL those utilities are connected to, generate MD5 verifiers when connecting to a server older than 10, and MD5 otherwise. --- doc/src/sgml/ref/createuser.sgml | 6 ++++-- doc/src/sgml/ref/psql-ref.sgml | 4 +++- src/bin/psql/command.c | 9 ++++++++- src/bin/scripts/createuser.c | 16 +++++++++++++--- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml index 4332008c68..4454591d79 100644 --- a/doc/src/sgml/ref/createuser.sgml +++ b/doc/src/sgml/ref/createuser.sgml @@ -125,7 +125,9 @@ PostgreSQL documentation <listitem> <para> Encrypts the user's password stored in the database. If not - specified, the default password behavior is used. + specified, the default password behavior is used. The password + is hashed using SCRAM-SHA-256 when connecting to a version of + <productname>PostgreSQL</> newer than 10, and MD5 otherwise. </para> </listitem> </varlistentry> @@ -477,7 +479,7 @@ PostgreSQL documentation <prompt>$ </prompt><userinput>createuser -P -s -e joe</userinput> <computeroutput>Enter password for new role: </computeroutput><userinput>xyzzy</userinput> <computeroutput>Enter it again: </computeroutput><userinput>xyzzy</userinput> -<computeroutput>CREATE ROLE joe PASSWORD 'md5b5f5ba1a423792b526f799ae4eb3d59e' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput> +<computeroutput>CREATE ROLE joe PASSWORD 'scram-sha-256:aPheg4yCAFhStg==:4096:TOHLPF8w+NzqtcxD2oz9w5wPISutHLOXWMKoe4HCvuo=:lTG0OiB/ZH5/hsfUqnndRwfMziY5j5C6FS8IAIwL2nA=' SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;</computeroutput> </screen> In the above example, the new password isn't actually echoed when typed, but we show what was typed for clarity. As you see, the password is diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 2a9c412020..c162f59b5a 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2210,7 +2210,9 @@ lo_import 152801 user). This command prompts for the new password, encrypts it, and sends it to the server as an <command>ALTER ROLE</> command. This makes sure that the new password does not appear in cleartext in the - command history, the server log, or elsewhere. + command history, the server log, or elsewhere. The password is hashed + with SCRAM-SHA-256 when connecting to <productname>PostgreSQL</> 10 + and newer versions, and with MD5 otherwise. </para> </listitem> </varlistentry> diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 25205f589b..b504e4a6cd 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1135,7 +1135,14 @@ exec_command(const char *cmd, else user = PQuser(pset.db); - encrypted_password = PQencryptPassword(pw1, user, "md5"); + /* + * Hash password using SCRAM-SHA-256 when connecting to servers + * newer than Postgres 10, and hash with MD5 otherwise. + */ + if (pset.sversion < 100000) + encrypted_password = PQencryptPassword(pw1, user, "md5"); + else + encrypted_password = PQencryptPassword(pw1, user, "scram"); if (!encrypted_password) { diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c index 5af263e34a..0107497e23 100644 --- a/src/bin/scripts/createuser.c +++ b/src/bin/scripts/createuser.c @@ -274,9 +274,19 @@ main(int argc, char *argv[]) { char *encrypted_password; - encrypted_password = PQencryptPassword(newpassword, - newuser, - "md5"); + /* + * Hash password using SCRAM-SHA-256 when connecting to servers + * newer than Postgres 10, and hash with MD5 otherwise. + */ + if (PQserverVersion(conn) < 100000) + encrypted_password = PQencryptPassword(newpassword, + newuser, + "md5"); + else + encrypted_password = PQencryptPassword(newpassword, + newuser, + "scram"); + if (!encrypted_password) { fprintf(stderr, _("Password encryption failed.\n")); -- 2.12.1
From 7451c393920d85dc74858055571aeb8643d9a2a1 Mon Sep 17 00:00:00 2001 From: Michael Paquier <mich...@paquier.xyz> Date: Sat, 25 Mar 2017 13:36:42 +0900 Subject: [PATCH 2/5] Refactor frontend-side random number generation pg_frontend_random() is moved into its own file in src/common/ to give other portions of the code the ability to generate random numbers. This will be used for the SCRAM verifier generation from clients. --- src/common/Makefile | 3 +- src/common/frontend_random.c | 86 ++++++++++++++++++++++++++++++++++++ src/include/common/frontend_random.h | 17 +++++++ src/interfaces/libpq/.gitignore | 1 + src/interfaces/libpq/Makefile | 4 +- src/interfaces/libpq/fe-auth-scram.c | 59 +------------------------ src/tools/msvc/Mkvcbuild.pm | 2 +- 7 files changed, 110 insertions(+), 62 deletions(-) create mode 100644 src/common/frontend_random.c create mode 100644 src/include/common/frontend_random.h diff --git a/src/common/Makefile b/src/common/Makefile index 971ddd5ea7..b516ec43f1 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -50,7 +50,8 @@ else OBJS_COMMON += sha2.o endif -OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o restricted_token.o +OBJS_FRONTEND = $(OBJS_COMMON) fe_memutils.o file_utils.o frontend_random.c \ + restricted_token.o OBJS_SRV = $(OBJS_COMMON:%.o=%_srv.o) diff --git a/src/common/frontend_random.c b/src/common/frontend_random.c new file mode 100644 index 0000000000..7ecc7d5fdf --- /dev/null +++ b/src/common/frontend_random.c @@ -0,0 +1,86 @@ +/*------------------------------------------------------------------------- + * + * frontend_random.c + * Frontend random number generation routine. + * + * pg_frontend_random() function fills a buffer with random bytes. Normally, + * it is just a thin wrapper around pg_strong_random(), but when compiled + * with --disable-strong-random, there is a built-in implementation. + * + * The built-in implementation uses the standard erand48 algorithm, with + * a seed calculated using the process ID and a timestamp. + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/common/frontend_random.c + * + *------------------------------------------------------------------------- + */ + +#ifndef FRONTEND +#error "This file is not expected to be compiled for backend code" +#endif + +#include "postgres_fe.h" +#include "common/frontend_random.h" + +/* These are needed for getpid(), in the fallback implementation */ +#ifndef HAVE_STRONG_RANDOM +#include <sys/types.h> +#include <unistd.h> +#endif + +/* + * Random number generator. + */ +bool +pg_frontend_random(char *dst, int len) +{ +#ifdef HAVE_STRONG_RANDOM + return pg_strong_random(dst, len); +#else + int i; + char *end = dst + len; + + static unsigned short seed[3]; + static int mypid = 0; + + pglock_thread(); + + if (mypid != getpid()) + { + struct timeval now; + + gettimeofday(&now, NULL); + + seed[0] = now.tv_sec ^ getpid(); + seed[1] = (unsigned short) (now.tv_usec); + seed[2] = (unsigned short) (now.tv_usec >> 16); + } + + for (i = 0; dst < end; i++) + { + uint32 r; + int j; + + /* + * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from + * it. + */ + r = (uint32) pg_jrand48(seed); + + for (j = 0; j < 4 && dst < end; j++) + { + *(dst++) = (char) (r & 0xFF); + r >>= 8; + } + } + + pgunlock_thread(); + + return true; +#endif +} diff --git a/src/include/common/frontend_random.h b/src/include/common/frontend_random.h new file mode 100644 index 0000000000..600c55ace9 --- /dev/null +++ b/src/include/common/frontend_random.h @@ -0,0 +1,17 @@ +/*------------------------------------------------------------------------- + * + * frontend_random.h + * Declarations for frontend random number generation + * + * Portions Copyright (c) 1996-2017, PostgreSQL Global Development Group + * + * src/include/common/frontend_random.h + * + *------------------------------------------------------------------------- + */ +#ifndef FRONTEND_RANDOM_H +#define FRONTEND_RANDOM_H + +extern bool pg_frontend_random(char *dst, int len); + +#endif /* FRONTEND_RANDOM_H */ diff --git a/src/interfaces/libpq/.gitignore b/src/interfaces/libpq/.gitignore index 2224ada731..f84bc35706 100644 --- a/src/interfaces/libpq/.gitignore +++ b/src/interfaces/libpq/.gitignore @@ -2,6 +2,7 @@ /base64.c /chklocale.c /crypt.c +/frontend_random.c /getaddrinfo.c /getpeereid.c /inet_aton.c diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 36b57268a7..269f48ab1e 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -49,7 +49,7 @@ endif # src/backend/utils/mb OBJS += encnames.o wchar.o # src/common -OBJS += base64.o ip.o md5.o scram-common.o +OBJS += base64.o frontend_random.o ip.o md5.o scram-common.o ifeq ($(with_openssl),yes) OBJS += fe-secure-openssl.o sha2_openssl.o @@ -106,7 +106,7 @@ backend_src = $(top_srcdir)/src/backend chklocale.c crypt.c erand48.c getaddrinfo.c getpeereid.c inet_aton.c inet_net_ntop.c noblock.c open.c system.c pgsleep.c pg_strong_random.c pgstrcasecmp.c pqsignal.c snprintf.c strerror.c strlcpy.c thread.c win32error.c win32setlocale.c: % : $(top_srcdir)/src/port/% rm -f $@ && $(LN_S) $< . -ip.c md5.c base64.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/% +ip.c md5.c base64.c frontend_random.c scram-common.c sha2.c sha2_openssl.c: % : $(top_srcdir)/src/common/% rm -f $@ && $(LN_S) $< . encnames.c wchar.c: % : $(backend_src)/utils/mb/% diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index a7bb30a141..6390ccf30d 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -15,14 +15,10 @@ #include "postgres_fe.h" #include "common/base64.h" +#include "common/frontend_random.h" #include "common/scram-common.h" #include "fe-auth.h" -/* These are needed for getpid(), in the fallback implementation */ -#ifndef HAVE_STRONG_RANDOM -#include <sys/types.h> -#include <unistd.h> -#endif /* * Status of exchange messages used for SCRAM authentication via the @@ -73,7 +69,6 @@ static bool verify_server_proof(fe_scram_state *state); static void calculate_client_proof(fe_scram_state *state, const char *client_final_message_without_proof, uint8 *result); -static bool pg_frontend_random(char *dst, int len); /* * Initialize SCRAM exchange status. @@ -586,55 +581,3 @@ verify_server_proof(fe_scram_state *state) return true; } - -/* - * Random number generator. - */ -static bool -pg_frontend_random(char *dst, int len) -{ -#ifdef HAVE_STRONG_RANDOM - return pg_strong_random(dst, len); -#else - int i; - char *end = dst + len; - - static unsigned short seed[3]; - static int mypid = 0; - - pglock_thread(); - - if (mypid != getpid()) - { - struct timeval now; - - gettimeofday(&now, NULL); - - seed[0] = now.tv_sec ^ getpid(); - seed[1] = (unsigned short) (now.tv_usec); - seed[2] = (unsigned short) (now.tv_usec >> 16); - } - - for (i = 0; dst < end; i++) - { - uint32 r; - int j; - - /* - * pg_jrand48 returns a 32-bit integer. Fill the next 4 bytes from - * it. - */ - r = (uint32) pg_jrand48(seed); - - for (j = 0; j < 4 && dst < end; j++) - { - *(dst++) = (char) (r & 0xFF); - r >>= 8; - } - } - - pgunlock_thread(); - - return true; -#endif -} diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 12f73f344c..615c769483 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -125,7 +125,7 @@ sub mkvcbuild our @pgcommonfrontendfiles = ( @pgcommonallfiles, qw(fe_memutils.c file_utils.c - restricted_token.c)); + frontend_random.c restricted_token.c)); our @pgcommonbkndfiles = @pgcommonallfiles; -- 2.12.1
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers