From eea84c48129a613e6385facb76d5a19978906b7a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 26 Apr 2017 12:55:35 +0300
Subject: [PATCH 2/3] Add PQencryptPasswordConn function to libpq.

The new function supports creating SCRAM verifiers, in addition to md5
hashes. The algorithm is chosen based on password_encryption, by default.

Michael Paquier and me

Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com
---
 doc/src/sgml/libpq.sgml              |  66 +++++++++++++++---
 src/interfaces/libpq/exports.txt     |   1 +
 src/interfaces/libpq/fe-auth-scram.c |  32 +++++++++
 src/interfaces/libpq/fe-auth.c       | 125 +++++++++++++++++++++++++++++++----
 src/interfaces/libpq/fe-auth.h       |   1 +
 src/interfaces/libpq/libpq-fe.h      |   1 +
 6 files changed, 203 insertions(+), 23 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4bc5bf3192..2228386eea 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -5875,11 +5875,11 @@ void PQconninfoFree(PQconninfoOption *connOptions);
     </listitem>
    </varlistentry>
 
-   <varlistentry id="libpq-pqencryptpassword">
+   <varlistentry id="libpq-pqencryptpasswordconn">
     <term>
-     <function>PQencryptPassword</function>
+     <function>PQencryptPasswordConn</function>
      <indexterm>
-      <primary>PQencryptPassword</primary>
+      <primary>PQencryptPasswordConn</primary>
      </indexterm>
     </term>
 
@@ -5887,20 +5887,64 @@ void PQconninfoFree(PQconninfoOption *connOptions);
      <para>
       Prepares the encrypted form of a <productname>PostgreSQL</> password.
 <synopsis>
-char * PQencryptPassword(const char *passwd, const char *user);
+char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
 </synopsis>
       This function is intended to be used by client applications that
       wish to send commands like <literal>ALTER USER joe PASSWORD
       'pwd'</>.  It is good practice not to send the original cleartext
       password in such a command, because it might be exposed in command
       logs, activity displays, and so on.  Instead, use this function to
-      convert the password to encrypted form before it is sent.  The
-      arguments are the cleartext password, and the SQL name of the user
-      it is for.  The return value is a string allocated by
-      <function>malloc</function>, or <symbol>NULL</symbol> if out of
-      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.
+      convert the password to encrypted form before it is sent.
+     </para>
+
+     <para>
+      The <parameter>passwd</> and <parameter>user</> arguments
+      are the cleartext password, and the SQL name of the user it is for.
+      <parameter>algorithm</> specifies the encryption algorithm
+      to use to encrypt the password. Currently supported algorithms are
+      <literal>md5</> and <literal>scram-sha-256</>. <literal>scram-sha-256</>
+      was introduced in <productname>PostgreSQL</> version 10, and will
+      not work correctly with older server versions. If <parameter>algorithm</>
+      is <symbol>NULL</>, this function will query the server for the current
+      value of the <xref linkend="guc-password-encryption"> setting. That can
+      block, and will fail if the current transaction is aborted, or if the
+      connection is busy executing another query. If you wish to use the
+      default algorithm for the server but want to avoid blocking, query
+      <varname>password_encryption</> yourself before calling
+      <function>PQencryptPasswordConn</>, and pass that value as the
+      <parameter>algorithm</>.
+     </para>
+
+     <para>
+      The return value is a string allocated by <function>malloc</>.
+      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. On error, returns <symbol>NULL</>, and
+      a suitable message is stored in the connection object.
+     </para>
+
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="libpq-pqencryptpassword">
+    <term>
+     <function>PQencryptPassword</function>
+     <indexterm>
+      <primary>PQencryptPassword</primary>
+     </indexterm>
+    </term>
+
+    <listitem>
+     <para>
+      Prepares the md5-encrypted form of a <productname>PostgreSQL</> password.
+ <synopsis>
+char *PQencryptPassword(const char *passwd, const char *user);
+ </synopsis>
+      <function>PQencryptPassword</> is an older, deprecated version of 
+      <function>PQencryptPasswodConn</>. The difference is that
+      <function>PQencryptPassword</> does not 
+      require a connection object, and <literal>md5</> is always used as the
+      encryption algorithm.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index 21dd772ca9..d6a38d0df8 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -171,3 +171,4 @@ PQsslAttributeNames       168
 PQsslAttribute            169
 PQsetErrorContextVisibility 170
 PQresultVerboseErrorMessage 171
+PQencryptPasswordConn     172
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index c56e91e0e0..6910c1e6db 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -610,6 +610,38 @@ verify_server_proof(fe_scram_state *state)
 	return true;
 }
 
+char *
+pg_fe_scram_build_verifier(const char *password)
+{
+	char	   *prep_password = NULL;
+	pg_saslprep_rc rc;
+	char		saltbuf[SCRAM_DEFAULT_SALT_LEN];
+	char	   *result;
+
+	/*
+	 * Normalize the password with SASLprep.  If that doesn't work, because
+	 * the password isn't valid UTF-8 or contains prohibited characters, just
+	 * proceed with the original password.  (See comments at top of file.)
+	 */
+	rc = pg_saslprep(password, &prep_password);
+	if (rc == SASLPREP_OOM)
+		return NULL;
+	if (rc == SASLPREP_SUCCESS)
+		password = (const char *) prep_password;
+
+	/* Generate salt */
+	if (!pg_frontend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
+		return NULL;
+
+	result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
+								  SCRAM_DEFAULT_ITERATIONS, password);
+
+	if (prep_password)
+		free(prep_password);
+
+	return result;
+}
+
 /*
  * Random number generator.
  */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index d81ee4f944..517854a305 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -1077,7 +1077,33 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
 
 
 /*
- * PQencryptPassword -- exported routine to encrypt a password
+ * PQencryptPassword -- exported routine to encrypt a password with MD5
+ *
+ * This function is equivalent to calling PQencryptPasswordConn with
+ * "md5" as the encryption method, except that this doesn't require
+ * a connection object. This function is deprecated, use
+ * PQencryptPasswordConn instead.
+ */
+char *
+PQencryptPassword(const char *passwd, const char *user)
+{
+	char	   *crypt_pwd;
+
+	crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+	if (!crypt_pwd)
+		return NULL;
+
+	if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+	{
+		free(crypt_pwd);
+		return NULL;
+	}
+
+	return crypt_pwd;
+}
+
+/*
+ * PQencryptPasswordConn -- exported routine to encrypt a password
  *
  * This is intended to be used by client applications that wish to send
  * commands like ALTER USER joe PASSWORD 'pwd'.  The password need not
@@ -1087,27 +1113,102 @@ 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 a connection object, the cleartext password, the SQL
+ * name of the user it is for, and a string indicating the algorithm to
+ * use for encrypting the password. If algorithm is NULL, this queries
+ * the server for the current 'password_encryption' value. If you wish
+ * to avoid that, e.g. to avoid blocking, you can execute
+ * 'show password_encryption' yourself before calling this function, and pass
+ * it as the algorithm.
  *
- * 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. The client may assume the string
+ * doesn't contain any special characters that would require escaping.
+ * On error, an error message is stored in the connection object, and
+ * returns NULL.
  */
 char *
-PQencryptPassword(const char *passwd, const char *user)
+PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
+					  const char *algorithm)
 {
-	char	   *crypt_pwd;
+#define MAX_ALGORITHM_NAME_LEN 50
+	char		algobuf[MAX_ALGORITHM_NAME_LEN + 1];
+	char	   *crypt_pwd = NULL;
 
-	crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
-	if (!crypt_pwd)
+	if (!conn)
 		return NULL;
 
-	if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+	/* If no algorithm was given, ask the server. */
+	if (algorithm == NULL)
 	{
-		free(crypt_pwd);
+		PGresult   *res;
+		char	   *val;
+
+		res = PQexec(conn, "show password_encryption");
+		if (res == NULL)
+		{
+			/* PQexec() should've set conn->errorMessage already */
+			return NULL;
+		}
+		if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		{
+			/* PQexec() should've set conn->errorMessage already */
+			PQclear(res);
+			return NULL;
+		}
+		if (PQntuples(res) != 1 || PQnfields(res) != 1)
+		{
+			PQclear(res);
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("unexpected shape of result set returned for SHOW\n"));
+			return NULL;
+		}
+		val = PQgetvalue(res, 0, 0);
+
+		if (strlen(val) > MAX_ALGORITHM_NAME_LEN)
+		{
+			PQclear(res);
+			printfPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("password_encryption value too long\n"));
+			return NULL;
+		}
+		strcpy(algobuf, val);
+		PQclear(res);
+
+		algorithm = algobuf;
+	}
+
+	/* Ok, now we know what algorithm to use */
+
+	if (strcmp(algorithm, "scram-sha-256") == 0)
+	{
+		crypt_pwd = pg_fe_scram_build_verifier(passwd);
+	}
+	else if (strcmp(algorithm, "md5") == 0)
+	{
+		crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
+		if (crypt_pwd)
+		{
+			if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
+			{
+				free(crypt_pwd);
+				crypt_pwd = NULL;
+			}
+		}
+	}
+	else if (strcmp(algorithm, "plain") == 0)
+	{
+		crypt_pwd = strdup(passwd);
+	}
+	else
+	{
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("unknown password encryption algorithm\n"));
 		return NULL;
 	}
 
+	if (!crypt_pwd)
+		printfPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("out of memory\n"));
+
 	return crypt_pwd;
 }
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index a5c739f01a..9f4c2a50d8 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -28,5 +28,6 @@ extern void pg_fe_scram_free(void *opaq);
 extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
 					 char **output, int *outputlen,
 					 bool *done, bool *success, PQExpBuffer errorMessage);
+extern char *pg_fe_scram_build_verifier(const char *password);
 
 #endif   /* FE_AUTH_H */
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 635af5b50e..c1497f00b6 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -597,6 +597,7 @@ extern int	PQenv2encoding(void);
 /* === in fe-auth.c === */
 
 extern char *PQencryptPassword(const char *passwd, const char *user);
+extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *method);
 
 /* === in encnames.c === */
 
-- 
2.11.0

