From 6f66728f4173b8bfb6850ae936f60e81eae1657c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@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 5883673448..3d5999d829 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1382,7 +1382,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 5077ff33b1..75a261bd36 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -371,8 +371,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;
@@ -403,23 +403,28 @@ 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';
 
 	if (prep_password)
 		pfree(prep_password);
 
-	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);
 }
 
 /*
@@ -538,17 +543,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.2

