Add PQencryptPasswordConn function to libpq, use it in psql and createuser.
authorHeikki Linnakangas
Wed, 3 May 2017 08:19:07 +0000 (11:19 +0300)
committerHeikki Linnakangas
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);
     
    
 
-   
+   conn">
     
-     PQencryptPassword
+     PQencryptPasswordConn
      
-      PQencryptPassword
+      PQencryptPasswordConn
      
     
 
@@ -5887,20 +5887,65 @@ void PQconninfoFree(PQconninfoOption *connOptions);
      
       Prepares the encrypted form of a PostgreSQL password.
 
-char * PQencryptPassword(const char *passwd, const char *user);
+char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
 
       This function is intended to be used by client applications that
       wish to send commands like 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
-      malloc, or NULL if out of
-      memory.  The caller can assume the string doesn't contain any
-      special characters that would require escaping.  Use
-      PQfreemem to free the result when done with it.
+      convert the password to encrypted form before it is sent.
+     
+
+     
+      The passwd and user arguments
+      are the cleartext password, and the SQL name of the user it is for.
+      algorithm specifies the encryption algorithm
+      to use to encrypt the password. Currently supported algorithms are
+      md5, scram-sha-256 and plain.
+      scram-sha-256 was introduced in PostgreSQL
+      version 10, and will not work correctly with older server versions. If
+      algorithm is NULL, this function will query
+      the server for the current value of the
+       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
+      password_encryption yourself before calling
+      PQencryptPasswordConn, and pass that value as the
+      algorithm.
+     
+
+     
+      The return value is a string allocated by malloc.
+      The caller can assume the string doesn't contain any special characters
+      that would require escaping.  Use PQfreemem to free the
+      result when done with it. On error, returns NULL, and
+      a suitable message is stored in the connection object.
+     
+
+    
+   
+
+   
+    
+     PQencryptPassword
+     
+      PQencryptPassword
+     
+    
+
+    
+     
+      Prepares the md5-encrypted form of a PostgreSQL password.
+char *PQencryptPassword(const char *passwd, const char *user);
+      PQencryptPassword is an older, deprecated version of 
+      PQencryptPasswodConn. The difference is that
+      PQencryptPassword does not 
+      require a connection object, and md5 is always used as the
+      encryption algorithm.
      
     
    
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 
 #include 
 
+#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$:$:
+    */
+   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 === */