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

Reply via email to