From 8f8b9be51fd788bb11276df89606bc653163524e Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 3 May 2017 11:19:07 +0300 Subject: [PATCH] Add PQencryptPasswordConn function to libpq, use it in psql and createuser. 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 --- doc/src/sgml/libpq.sgml | 77 ++++++++++++---- src/backend/libpq/auth-scram.c | 51 ++--------- src/backend/libpq/crypt.c | 2 +- src/bin/psql/command.c | 4 +- src/bin/scripts/createuser.c | 9 +- src/common/scram-common.c | 64 +++++++++++++ src/include/common/scram-common.h | 3 + src/include/libpq/scram.h | 4 +- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-scram.c | 35 +++++++ src/interfaces/libpq/fe-auth.c | 131 ++++++++++++++++++++++++--- src/interfaces/libpq/fe-auth.h | 1 + src/interfaces/libpq/libpq-fe.h | 1 + 13 files changed, 299 insertions(+), 84 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 4bc5bf3192..4f60b203fb 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -5875,6 +5875,58 @@ void PQconninfoFree(PQconninfoOption *connOptions); + + + PQencryptPasswordConn + + PQencryptPasswordConn + + + + + + Prepares the encrypted form of a PostgreSQL password. + +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 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 @@ -5885,22 +5937,15 @@ void PQconninfoFree(PQconninfoOption *connOptions); - Prepares the encrypted form of a PostgreSQL password. - -char * PQencryptPassword(const char *passwd, const char *user); - - 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. + 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. diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index 5c85af943c..6e7a140582 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -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"); diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index d0030f2b6d..9fe79b4894 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -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: diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index 859ded71f6..b3263a9570 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -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 diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c index 3d74797a8f..35a53bf206 100644 --- a/src/bin/scripts/createuser.c +++ b/src/bin/scripts/createuser.c @@ -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); diff --git a/src/common/scram-common.c b/src/common/scram-common.c index a8ea44944c..77b54c8a5e 100644 --- a/src/common/scram-common.c +++ b/src/common/scram-common.c @@ -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; +} diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h index 656d9e1e6b..307f92b54a 100644 --- a/src/include/common/scram-common.h +++ b/src/include/common/scram-common.h @@ -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 */ diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index e373f0c07e..060b8af69e 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -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); 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 be271ce8ac..52dae49abf 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -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. */ diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index d81ee4f944..daa7cc9585 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -1077,22 +1077,12 @@ pg_fe_getauthname(PQExpBuffer errorMessage) /* - * PQencryptPassword -- exported routine to encrypt a password + * PQencryptPassword -- exported routine to encrypt a password with MD5 * - * This is intended to be used by client applications that wish to send - * commands like ALTER USER joe PASSWORD 'pwd'. The password need not - * be sent in cleartext if it is encrypted on the client side. This is - * good because it ensures the cleartext password won't end up in logs, - * pg_stat displays, etc. We export the function so that clients won't - * 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. - * - * 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. + * 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) @@ -1111,3 +1101,114 @@ PQencryptPassword(const char *passwd, const char *user) 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 + * be sent in cleartext if it is encrypted on the client side. This is + * good because it ensures the cleartext password won't end up in logs, + * pg_stat displays, etc. We export the function so that clients won't + * be dependent on low-level details like whether the encryption is MD5 + * or something else. + * + * 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. 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 * +PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, + const char *algorithm) +{ +#define MAX_ALGORITHM_NAME_LEN 50 + char algobuf[MAX_ALGORITHM_NAME_LEN + 1]; + char *crypt_pwd = NULL; + + if (!conn) + return NULL; + + /* If no algorithm was given, ask the server. */ + if (algorithm == NULL) + { + 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..093c4986d8 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 *algorithm); /* === in encnames.c === */