Add PQencryptPasswordConn function to libpq, use it in psql and createuser.
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>
Wed, 3 May 2017 08:19:07 +0000 (11:19 +0300)
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>
Wed, 3 May 2017 08:19:07 +0000 (11:19 +0300)
The new function supports creating SCRAM verifiers, in addition to md5
hashes. The algorithm is chosen based on password_encryption, by default.

This fixes the issue reported by Jeff Janes, that there was previously
no way to create a SCRAM verifier with "\password".

Michael Paquier and me

Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com

13 files changed:
doc/src/sgml/libpq.sgml
src/backend/libpq/auth-scram.c
src/backend/libpq/crypt.c
src/bin/psql/command.c
src/bin/scripts/createuser.c
src/common/scram-common.c
src/include/common/scram-common.h
src/include/libpq/scram.h
src/interfaces/libpq/exports.txt
src/interfaces/libpq/fe-auth-scram.c
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-auth.h
src/interfaces/libpq/libpq-fe.h

index 4bc5bf31927ef1fe1055cf01f8394cf71a4fc261..4f60b203fbc502750963078cf3660b7907425ecf 100644 (file)
@@ -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,65 @@ 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</>, <literal>scram-sha-256</> and <literal>plain</>.
+      <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>
index 5c85af943cdcbd2e62a6d073fdf38b8081a969ff..6e7a1405826084d3d928f5fc1dd435328a57944c 100644 (file)
@@ -207,7 +207,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
                         */
                        char       *verifier;
 
-                       verifier = scram_build_verifier(username, shadow_pass, 0);
+                       verifier = pg_be_scram_build_verifier(shadow_pass);
 
                        (void) parse_scram_verifier(verifier, &state->iterations, &state->salt,
                                                                                state->StoredKey, state->ServerKey);
@@ -387,22 +387,14 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
 /*
  * 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.
+ * The result is palloc'd, so caller is responsible for freeing it.
  */
 char *
-scram_build_verifier(const char *username, const char *password,
-                                        int iterations)
+pg_be_scram_build_verifier(const char *password)
 {
        char       *prep_password = NULL;
        pg_saslprep_rc rc;
        char            saltbuf[SCRAM_DEFAULT_SALT_LEN];
-       uint8           salted_password[SCRAM_KEY_LEN];
-       uint8           keybuf[SCRAM_KEY_LEN];
-       char       *encoded_salt;
-       char       *encoded_storedkey;
-       char       *encoded_serverkey;
-       int                     encoded_len;
        char       *result;
 
        /*
@@ -414,10 +406,7 @@ scram_build_verifier(const char *username, const char *password,
        if (rc == SASLPREP_SUCCESS)
                password = (const char *) prep_password;
 
-       if (iterations <= 0)
-               iterations = SCRAM_DEFAULT_ITERATIONS;
-
-       /* Generate salt, and encode it in base64 */
+       /* Generate random salt */
        if (!pg_backend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
        {
                ereport(LOG,
@@ -426,37 +415,11 @@ scram_build_verifier(const char *username, const char *password,
                return NULL;
        }
 
-       encoded_salt = palloc(pg_b64_enc_len(SCRAM_DEFAULT_SALT_LEN) + 1);
-       encoded_len = pg_b64_encode(saltbuf, SCRAM_DEFAULT_SALT_LEN, encoded_salt);
-       encoded_salt[encoded_len] = '\0';
-
-       /* Calculate StoredKey, and encode it in base64 */
-       scram_SaltedPassword(password, saltbuf, SCRAM_DEFAULT_SALT_LEN,
-                                                iterations, salted_password);
-       scram_ClientKey(salted_password, keybuf);
-       scram_H(keybuf, SCRAM_KEY_LEN, keybuf);         /* StoredKey */
-
-       encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
-       encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
-                                                               encoded_storedkey);
-       encoded_storedkey[encoded_len] = '\0';
-
-       /* And same for ServerKey */
-       scram_ServerKey(salted_password, keybuf);
-
-       encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
-       encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
-                                                               encoded_serverkey);
-       encoded_serverkey[encoded_len] = '\0';
-
-       result = psprintf("SCRAM-SHA-256$%d:%s$%s:%s", iterations, encoded_salt,
-                                         encoded_storedkey, encoded_serverkey);
+       result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
+                                                                 SCRAM_DEFAULT_ITERATIONS, password);
 
        if (prep_password)
                pfree(prep_password);
-       pfree(encoded_salt);
-       pfree(encoded_storedkey);
-       pfree(encoded_serverkey);
 
        return result;
 }
@@ -1194,7 +1157,7 @@ scram_MockSalt(const char *username)
         * Generate salt using a SHA256 hash of the username and the cluster's
         * mock authentication nonce.  (This works as long as the salt length is
         * not larger the SHA256 digest length. If the salt is smaller, the caller
-        * will just ignore the extra data))
+        * will just ignore the extra data.)
         */
        StaticAssertStmt(PG_SHA256_DIGEST_LENGTH >= SCRAM_DEFAULT_SALT_LEN,
                                         "salt length greater than SHA256 digest length");
index d0030f2b6d80ae4953d0adebc6b8fcf06f8dbf07..9fe79b48946386bffca7a6fe27c914b5ec9599ef 100644 (file)
@@ -156,7 +156,7 @@ encrypt_password(PasswordType target_type, const char *role,
                        switch (guessed_type)
                        {
                                case PASSWORD_TYPE_PLAINTEXT:
-                                       return scram_build_verifier(role, password, 0);
+                                       return pg_be_scram_build_verifier(password);
 
                                case PASSWORD_TYPE_MD5:
 
index 859ded71f61e18f323249505df6de3444072617d..b3263a9570afc56dc96fc4885e19d8083b626496 100644 (file)
@@ -1878,11 +1878,11 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
                        else
                                user = PQuser(pset.db);
 
-                       encrypted_password = PQencryptPassword(pw1, user);
+                       encrypted_password = PQencryptPasswordConn(pset.db, pw1, user, NULL);
 
                        if (!encrypted_password)
                        {
-                               psql_error("Password encryption failed.\n");
+                               psql_error("%s", PQerrorMessage(pset.db));
                                success = false;
                        }
                        else
index 3d74797a8f5a3150159fea935a467eabc07ebcd4..35a53bf2064d9bde325d940e6c454322d7b347cb 100644 (file)
@@ -274,11 +274,14 @@ main(int argc, char *argv[])
                {
                        char       *encrypted_password;
 
-                       encrypted_password = PQencryptPassword(newpassword,
-                                                                                                  newuser);
+                       encrypted_password = PQencryptPasswordConn(conn,
+                                                                                                          newpassword,
+                                                                                                          newuser,
+                                                                                                          NULL);
                        if (!encrypted_password)
                        {
-                               fprintf(stderr, _("Password encryption failed.\n"));
+                               fprintf(stderr, _("%s: password encryption failed: %s"),
+                                               progname, PQerrorMessage(conn));
                                exit(1);
                        }
                        appendStringLiteralConn(&sql, encrypted_password, conn);
index a8ea44944c493749ce88b82b73fa3322efdc3b21..77b54c8a5e719b348ce0f3174481cdb9e2c82879 100644 (file)
@@ -23,6 +23,7 @@
 #include <netinet/in.h>
 #include <arpa/inet.h>
 
+#include "common/base64.h"
 #include "common/scram-common.h"
 
 #define HMAC_IPAD 0x36
@@ -180,3 +181,66 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result)
        scram_HMAC_update(&ctx, "Server Key", strlen("Server Key"));
        scram_HMAC_final(result, &ctx);
 }
+
+
+/*
+ * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
+ *
+ * The password should already have been processed with SASLprep, if necessary!
+ *
+ * If iterations is 0, default number of iterations is used.  The result is
+ * palloc'd or malloc'd, so caller is responsible for freeing it.
+ */
+char *
+scram_build_verifier(const char *salt, int saltlen, int iterations,
+                                        const char *password)
+{
+       uint8           salted_password[SCRAM_KEY_LEN];
+       uint8           stored_key[SCRAM_KEY_LEN];
+       uint8           server_key[SCRAM_KEY_LEN];
+       char       *result;
+       char       *p;
+       int                     maxlen;
+
+       if (iterations <= 0)
+               iterations = SCRAM_DEFAULT_ITERATIONS;
+
+       /* Calculate StoredKey and ServerKey */
+       scram_SaltedPassword(password, salt, saltlen, iterations,
+                                                salted_password);
+       scram_ClientKey(salted_password, stored_key);
+       scram_H(stored_key, SCRAM_KEY_LEN, stored_key);
+
+       scram_ServerKey(salted_password, server_key);
+
+       /*
+        * The format is:
+        * SCRAM-SHA-256$<iteration count>:<salt>$<StoredKey>:<ServerKey>
+        */
+       maxlen = strlen("SCRAM-SHA-256") + 1
+               + 10 + 1                                                                /* iteration count */
+               + pg_b64_enc_len(saltlen) + 1                   /* Base64-encoded salt */
+               + pg_b64_enc_len(SCRAM_KEY_LEN) + 1             /* Base64-encoded StoredKey */
+               + pg_b64_enc_len(SCRAM_KEY_LEN) + 1;    /* Base64-encoded ServerKey */
+
+#ifdef FRONTEND
+       result = malloc(maxlen);
+       if (!result)
+               return NULL;
+#else
+       result = palloc(maxlen);
+#endif
+
+       p = result + sprintf(result, "SCRAM-SHA-256$%d:", iterations);
+
+       p += pg_b64_encode(salt, saltlen, p);
+       *(p++) = '$';
+       p += pg_b64_encode((char *) stored_key, SCRAM_KEY_LEN, p);
+       *(p++) = ':';
+       p += pg_b64_encode((char *) server_key, SCRAM_KEY_LEN, p);
+       *(p++) = '\0';
+
+       Assert(p - result <= maxlen);
+
+       return result;
+}
index 656d9e1e6b1378e70d6bfb5f1c2d29c84282fa02..307f92b54a4578502d3c5a1620784f3df4b077cb 100644 (file)
@@ -53,4 +53,7 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
 extern void scram_ClientKey(const uint8 *salted_password, uint8 *result);
 extern void scram_ServerKey(const uint8 *salted_password, uint8 *result);
 
+extern char *scram_build_verifier(const char *salt, int saltlen, int iterations,
+                                        const char *password);
+
 #endif   /* SCRAM_COMMON_H */
index e373f0c07e80b032dd270d39a9014df81dbf0adc..060b8af69e306d3678a397367748f231af3ffbab 100644 (file)
@@ -27,9 +27,7 @@ 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);
+extern char *pg_be_scram_build_verifier(const char *password);
 extern bool is_scram_verifier(const char *verifier);
 extern bool scram_verify_plain_password(const char *username,
                                                        const char *password, const char *verifier);
index 21dd772ca919315b8dcdb22fb979107101bb81c6..d6a38d0df85b9c1bae9a5cff3ab5a9f9a5ca2a34 100644 (file)
@@ -171,3 +171,4 @@ PQsslAttributeNames       168
 PQsslAttribute            169
 PQsetErrorContextVisibility 170
 PQresultVerboseErrorMessage 171
+PQencryptPasswordConn     172
index be271ce8ac01d3544c8648effd97f141f8f889f1..52dae49abf6b4bbcf05d8bfa2f829fe4df2dd554 100644 (file)
@@ -614,6 +614,41 @@ verify_server_signature(fe_scram_state *state)
        return true;
 }
 
+/*
+ * Build a new SCRAM verifier.
+ */
+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 a random 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.
  */
index d81ee4f9447a65654ddb0bf1e1edf7c9aa554d1b..daa7cc95858b8fefeb1258a85fd09e71e89aff39 100644 (file)
@@ -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;
 }
index a5c739f01a31c9da6ec908f1a9485ca312b6e68f..9f4c2a50d8dd40932d2e6d9c99f1500954da75d7 100644 (file)
@@ -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 */
index 635af5b50e3bf3a8321672f8a910409950c09896..093c4986d8c8c8af69ae9caac1bc00db56fb633b 100644 (file)
@@ -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 *algorithm);
 
 /* === in encnames.c === */