From 6f2451e1f620e7e211804792a256ea9735bd0abf Mon Sep 17 00:00:00 2001 From: Joshua Brindle Date: Tue, 1 Feb 2022 14:32:55 -0800 Subject: [PATCH 2/3] multiple passwords work with scram and md5 also renaming roles, dropping roles, switching between password_encryption Signed-off-by: Joshua Brindle --- src/backend/commands/user.c | 199 +++++++++++++++++++++---- src/backend/libpq/auth-sasl.c | 4 +- src/backend/libpq/auth-scram.c | 184 +++++++++++++---------- src/backend/libpq/auth.c | 94 +++++------- src/backend/libpq/crypt.c | 50 ++++--- src/backend/parser/gram.y | 13 +- src/backend/utils/cache/catcache.c | 2 +- src/backend/utils/cache/syscache.c | 6 +- src/include/catalog/pg_auth_password.h | 9 +- src/include/commands/user.h | 2 +- src/include/libpq/crypt.h | 2 +- src/include/libpq/sasl.h | 4 +- src/include/libpq/scram.h | 2 +- src/include/parser/kwlist.h | 1 + src/include/utils/syscache.h | 2 +- src/test/regress/expected/password.out | 2 +- 16 files changed, 373 insertions(+), 203 deletions(-) diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 9ff99342eef..384816444da 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -18,6 +18,7 @@ #include "access/xact.h" #include "catalog/binary_upgrade.h" #include "catalog/catalog.h" + #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" @@ -31,11 +32,14 @@ #include "commands/defrem.h" #include "commands/seclabel.h" #include "commands/user.h" +#include "common/scram-common.h" #include "libpq/crypt.h" +#include "libpq/scram.h" #include "miscadmin.h" #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/syscache.h" #include "utils/timestamp.h" @@ -47,6 +51,9 @@ Oid binary_upgrade_next_pg_authid_oid = InvalidOid; /* GUC parameter */ int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256; +/* default password name */ +const char* default_passname = "__def__"; + /* Hook to check passwords in CreateRole() and AlterRole() */ check_password_hook_type check_password_hook = NULL; @@ -69,7 +76,7 @@ have_createrole_privilege(void) * Is the role able to log in */ bool -is_role_valid(const char *rolename, char **logdetail) +is_role_valid(const char *rolename, const char **logdetail) { HeapTuple roleTup; Datum datum; @@ -102,6 +109,64 @@ is_role_valid(const char *rolename, char **logdetail) } +static +bool +validate_and_get_salt(char *rolename, char **salt, const char **logdetail) +{ + char **current_secrets; + char *salt1, *salt2 = NULL; + int i, num; + PasswordType passtype; + + if (Password_encryption == PASSWORD_TYPE_MD5) + { + *salt = rolename; /* md5 always uses role name, no need to look through the passwords */ + return true; + } + else if (Password_encryption == PASSWORD_TYPE_PLAINTEXT) + { + *salt = NULL; /* Plaintext does not have a salt */ + return true; + } + + current_secrets = get_role_passwords(rolename, logdetail, &num); + if (num == 0) + { + *salt = NULL; /* No existing passwords, allow one to be generated */ + return true; + } + for (i = 0; i < num; i++) { + passtype = get_password_type(current_secrets[i]); + if (passtype == PASSWORD_TYPE_MD5 || passtype == PASSWORD_TYPE_PLAINTEXT) + continue; /* md5 uses rolename as salt so it is always the same and plaintext has no salt */ + else if (passtype == PASSWORD_TYPE_SCRAM_SHA_256) { + int iterations; + uint8 stored_key[SCRAM_KEY_LEN]; + uint8 server_key[SCRAM_KEY_LEN]; + + parse_scram_secret(current_secrets[i], &iterations, &salt1, + stored_key, server_key); + + if (salt2 != NULL) { + if (strcmp(salt1, salt2)) { + *logdetail = psprintf(_("inconsistent salts, clearing password")); + *salt = NULL; + return false; + } + } + else + salt2 = salt1; + } + + } + for (i = 0; i < num; i++) + pfree(current_secrets[i]); + pfree(current_secrets); + if (salt2) + *salt = pstrdup(salt2); + return true; +} + /* * CREATE ROLE */ @@ -117,6 +182,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) ListCell *item; ListCell *option; char *password = NULL; /* user password */ + char *passname = NULL; /* name of the password for managing multiple passwords */ bool issuper = false; /* Make the user a superuser? */ bool inherit = true; /* Auto inherit privileges? */ bool createrole = false; /* Can this user create roles? */ @@ -144,6 +210,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) DefElem *dadminmembers = NULL; DefElem *dvalidUntil = NULL; DefElem *dbypassRLS = NULL; + DefElem *dpassName = NULL; + /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) @@ -246,6 +314,13 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) errorConflictingDefElem(defel, pstate); dbypassRLS = defel; } + else if (strcmp(defel->defname, "passname") == 0) + { + if (dpassName) + errorConflictingDefElem(defel, pstate); + dpassName = defel; + } + else elog(ERROR, "option \"%s\" not recognized", defel->defname); @@ -283,6 +358,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) validUntil = strVal(dvalidUntil->arg); if (dbypassRLS) bypassrls = boolVal(dbypassRLS->arg); + if (dpassName) + passname = strVal(dpassName->arg); /* Check some permissions first */ if (issuper) @@ -424,8 +501,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) if (password) { - char *shadow_pass; - char *logdetail; + char *shadow_pass, *salt; + const char *logdetail; Datum new_password_record[Natts_pg_auth_password]; bool new_password_record_nulls[Natts_pg_auth_password]; Relation pg_auth_password_rel; @@ -453,12 +530,18 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) else { /* Encrypt the password to the requested format. */ - shadow_pass = encrypt_password(Password_encryption, stmt->role, + validate_and_get_salt(stmt->role, &salt, &logdetail); + shadow_pass = encrypt_password(Password_encryption, salt, password); MemSet(new_password_record, 0, sizeof(new_password_record)); MemSet(new_password_record_nulls, false, sizeof(new_password_record_nulls)); - + if (passname != NULL) + new_password_record[Anum_pg_auth_password_name - 1] = + DirectFunctionCall1(namein, CStringGetDatum(passname)); + else + new_password_record[Anum_pg_auth_password_name - 1] = + DirectFunctionCall1(namein, CStringGetDatum(default_passname)); /* open password table and insert it. */ pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock); pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel); @@ -560,6 +643,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) ListCell *option; char *rolename; char *password = NULL; /* user password */ + char *passname = NULL; /* password name for managing multiple passwords */ int connlimit = -1; /* maximum connections allowed */ char *validUntil = NULL; /* time the login is valid until */ Datum validUntil_datum; /* same, as timestamptz Datum */ @@ -575,6 +659,7 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; DefElem *dbypassRLS = NULL; + DefElem *dpassName = NULL; Oid roleid; check_rolespec_name(stmt->role, @@ -652,6 +737,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) errorConflictingDefElem(defel, pstate); dbypassRLS = defel; } + else if (strcmp(defel->defname, "passname") == 0) + { + if (dpassName) + errorConflictingDefElem(defel, pstate); + dpassName = defel; + } else elog(ERROR, "option \"%s\" not recognized", defel->defname); @@ -669,6 +760,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) } if (dvalidUntil) validUntil = strVal(dvalidUntil->arg); + if (dpassName) + passname = strVal(dpassName->arg); /* * Scan the pg_authid relation to be certain the user exists. @@ -805,8 +898,9 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) /* password */ if (password) { - char *shadow_pass; - const char *logdetail = NULL; + char *salt = NULL; + const char *logdetail = NULL; + char *shadow_pass; /* Like in CREATE USER, don't allow an empty password. */ if (password[0] == '\0' || @@ -818,13 +912,26 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) } else { - /* Encrypt the password to the requested format. */ - shadow_pass = encrypt_password(Password_encryption, rolename, - password); - new_password_record[Anum_pg_auth_password_password - 1] = - CStringGetTextDatum(shadow_pass); + if (!validate_and_get_salt(rolename, &salt, &logdetail)) + new_password_record_nulls[Anum_pg_auth_password_password - 1] = true; + else + { + /* Encrypt the password to the requested format. */ + shadow_pass = encrypt_password(Password_encryption, salt, + password); + new_password_record[Anum_pg_auth_password_password - 1] = + CStringGetTextDatum(shadow_pass); + + new_password_record_repl[Anum_pg_auth_password_password - 1] = true; + + if (passname) + new_password_record[Anum_pg_auth_password_name - 1] = + DirectFunctionCall1(namein, CStringGetDatum(passname)); + else + new_password_record[Anum_pg_auth_password_name - 1] = + DirectFunctionCall1(namein, CStringGetDatum(default_passname)); + } } - new_password_record_repl[Anum_pg_auth_password_password - 1] = true; } /* unset password */ @@ -859,7 +966,10 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock); pg_auth_password_dsc = RelationGetDescr(pg_auth_password_rel); - password_tuple = SearchSysCache1(AUTHPASSWORD, ObjectIdGetDatum(roleid)); + if (dpassName) + password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(passname)); + else + password_tuple = SearchSysCache2(AUTHPASSWORDNAME, ObjectIdGetDatum(roleid), CStringGetDatum(default_passname)); if (new_password_record_nulls[Anum_pg_auth_password_password - 1] == true) /* delete existing password */ { @@ -1157,14 +1267,23 @@ DropRole(DropRoleStmt *stmt) DeleteSharedSecurityLabel(roleid, AuthIdRelationId); /* - * Drop password + * Drop password(s) */ - tuple = SearchSysCache1(AUTHPASSWORD, roleid); - if (HeapTupleIsValid(tuple)) { - CatalogTupleDelete(pg_auth_password_rel, &tuple->t_self); - ReleaseSysCache(tuple); + ScanKeyInit(&scankey, + Anum_pg_auth_password_roleid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(roleid)); + + sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId, + true, NULL, 1, &scankey); + + while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan))) + { + CatalogTupleDelete(pg_auth_password_rel, &tmp_tuple->t_self); } + systable_endscan(sscan); + /* * Remove settings for this role. */ @@ -1201,6 +1320,9 @@ RenameRole(const char *oldname, const char *newname) newtuple, passtuple; TupleDesc dsc; + ScanKeyData scankey; + SysScanDesc sscan; + Relation rel; Datum datum; bool isnull = true; @@ -1211,6 +1333,7 @@ RenameRole(const char *oldname, const char *newname) Oid roleid; ObjectAddress address; Form_pg_authid authform; + Relation pg_auth_password_rel; rel = table_open(AuthIdRelationId, RowExclusiveLock); dsc = RelationGetDescr(rel); @@ -1301,28 +1424,38 @@ RenameRole(const char *oldname, const char *newname) CStringGetDatum(newname)); repl_null[Anum_pg_authid_rolname - 1] = false; - passtuple = SearchSysCache1(AUTHPASSWORD, roleid); + ScanKeyInit(&scankey, + Anum_pg_auth_password_roleid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(roleid)); + + pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock); - if (HeapTupleIsValid(passtuple)) - datum = SysCacheGetAttr(AUTHPASSWORD, passtuple, - Anum_pg_auth_password_password, &isnull); + sscan = systable_beginscan(pg_auth_password_rel, AuthPasswordRoleOidIndexId, + true, NULL, 1, &scankey); - if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5) + while (HeapTupleIsValid(passtuple = systable_getnext(sscan))) { - Relation pg_auth_password_rel; + datum = SysCacheGetAttr(AUTHPASSWORDNAME, passtuple, + Anum_pg_auth_password_password, &isnull); - /* MD5 uses the username as salt, so just clear it on a rename */ - pg_auth_password_rel = table_open(AuthPasswordRelationId, RowExclusiveLock); + if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5) + { - if (HeapTupleIsValid(passtuple)) { - CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self); - ReleaseSysCache(passtuple); + /* MD5 uses the username as salt, so just clear it on a rename */ + + if (HeapTupleIsValid(passtuple)) { + CatalogTupleDelete(pg_auth_password_rel, &passtuple->t_self); + ereport(NOTICE, + (errmsg("MD5 password cleared because of role rename"))); + + } } - table_close(pg_auth_password_rel, NoLock); - ereport(NOTICE, - (errmsg("MD5 password cleared because of role rename"))); } + systable_endscan(sscan); + table_close(pg_auth_password_rel, NoLock); + newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl); CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple); diff --git a/src/backend/libpq/auth-sasl.c b/src/backend/libpq/auth-sasl.c index 805b3695b78..652c18fa94a 100644 --- a/src/backend/libpq/auth-sasl.c +++ b/src/backend/libpq/auth-sasl.c @@ -49,7 +49,7 @@ * should just pass NULL. */ int -CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, +CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, const char **passwords, int num, const char **logdetail) { StringInfoData sasl_mechs; @@ -136,7 +136,7 @@ CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, * This is because we don't want to reveal to an attacker what * usernames are valid, nor which users have a valid password. */ - opaq = mech->init(port, selected_mech, shadow_pass); + opaq = mech->init(port, selected_mech, passwords, num); inputlen = pq_getmsgint(&buf, 4); if (inputlen == -1) diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index 795f1cba555..95db4aaf958 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -109,7 +109,7 @@ static void scram_get_mechanisms(Port *port, StringInfo buf); static void *scram_init(Port *port, const char *selected_mech, - const char *shadow_pass); + const char **secrets, const int num_secrets); static int scram_exchange(void *opaq, const char *input, int inputlen, char **output, int *outputlen, const char **logdetail); @@ -132,6 +132,13 @@ typedef enum SCRAM_AUTH_FINISHED } scram_state_enum; +typedef struct +{ + uint8 StoredKey[SCRAM_KEY_LEN]; + uint8 ServerKey[SCRAM_KEY_LEN]; +} scram_secret; + + typedef struct { scram_state_enum state; @@ -141,10 +148,16 @@ typedef struct Port *port; bool channel_binding_in_use; + /* + * The salt and iterations must be the same for all + * secrets since they are sent as part of the initial message + */ int iterations; char *salt; /* base64-encoded */ - uint8 StoredKey[SCRAM_KEY_LEN]; - uint8 ServerKey[SCRAM_KEY_LEN]; + /* Array of possible secrets */ + scram_secret *secrets; + int num_secrets; + int chosen_secret; /* secret chosen during final client message */ /* Fields of the first message from client */ char cbind_flag; @@ -220,17 +233,20 @@ scram_get_mechanisms(Port *port, StringInfo buf) * It should be one of the mechanisms that we support, as returned by * scram_get_mechanisms(). * - * 'shadow_pass' is the role's stored secret, from pg_auth_password.password. + * 'passwords' are the role's stored secrets, from pg_auth_password.password. * The username was provided by the client in the startup message, and is * available in port->user_name. If 'shadow_pass' is NULL, we still perform * an authentication exchange, but it will fail, as if an incorrect password * was given. */ static void * -scram_init(Port *port, const char *selected_mech, const char *shadow_pass) +scram_init(Port *port, const char *selected_mech, const char **secrets, const int num_secrets) { scram_state *state; - bool got_secret; + bool got_secret = false; + int i; + int iterations; + char *salt = NULL; /* base64-encoded */ state = (scram_state *) palloc0(sizeof(scram_state)); state->port = port; @@ -260,46 +276,48 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass) /* * Parse the stored secret. */ - if (shadow_pass) + if (secrets) { - int password_type = get_password_type(shadow_pass); - - if (password_type == PASSWORD_TYPE_SCRAM_SHA_256) + state->secrets = palloc0(sizeof(scram_secret) * num_secrets); + state->num_secrets = num_secrets; + for (i = 0; i < num_secrets; i++) { - if (parse_scram_secret(shadow_pass, &state->iterations, &state->salt, - state->StoredKey, state->ServerKey)) - got_secret = true; - else + int password_type = get_password_type(secrets[i]); + + if (password_type == PASSWORD_TYPE_SCRAM_SHA_256) { - /* - * The password looked like a SCRAM secret, but could not be - * parsed. - */ - ereport(LOG, - (errmsg("invalid SCRAM secret for user \"%s\"", - state->port->user_name))); - got_secret = false; + + if (parse_scram_secret(secrets[i], &state->iterations, &state->salt, + state->secrets[i].StoredKey, state->secrets[i].ServerKey)) + { + if (salt) { + /* The stored iterations and salt must match or we cannot proceed, allow failure via mock */ + if (strcmp(salt, state->salt) || iterations != state->iterations) { + ereport(WARNING, (errmsg("inconsistent salt or iterations for user \"%s\"", + state->port->user_name))); + got_secret = false; /* fail and allow mock creditials to be created */ + break; + } + } + else + { + salt = state->salt; + iterations = state->iterations; + got_secret = true; /* We got at least one good SCRAM secret */ + } + } + else + { + /* + * The password looked like a SCRAM secret, but could not be + * parsed. + */ + ereport(LOG, + (errmsg("invalid SCRAM secret for user \"%s\"", + state->port->user_name))); + } } } - else - { - /* - * The user doesn't have SCRAM secret. (You cannot do SCRAM - * authentication with an MD5 hash.) - */ - state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM secret."), - state->port->user_name); - got_secret = false; - } - } - else - { - /* - * The caller requested us to perform a dummy authentication. This is - * considered normal, since the caller requested it, so don't set log - * detail. - */ - got_secret = false; } /* @@ -310,8 +328,10 @@ scram_init(Port *port, const char *selected_mech, const char *shadow_pass) */ if (!got_secret) { + state->secrets = palloc0(sizeof(scram_secret)); + mock_scram_secret(state->port->user_name, &state->iterations, - &state->salt, state->StoredKey, state->ServerKey); + &state->salt, state->secrets[0].StoredKey, state->secrets[0].ServerKey); state->doomed = true; } @@ -459,7 +479,7 @@ scram_exchange(void *opaq, const char *input, int inputlen, * The result is palloc'd, so caller is responsible for freeing it. */ char * -pg_be_scram_build_secret(const char *password) +pg_be_scram_build_secret(const char *password, const char *salt) { char *prep_password; pg_saslprep_rc rc; @@ -476,12 +496,14 @@ pg_be_scram_build_secret(const char *password) if (rc == SASLPREP_SUCCESS) password = (const char *) prep_password; - /* Generate random salt */ - if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN)) + /* Use passed in salt or generate random salt */ + if (!salt && !pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN)) ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("could not generate random salt"))); - + else if (salt) + pg_b64_decode(salt, strlen(salt), saltbuf, SCRAM_DEFAULT_SALT_LEN); + result = scram_build_secret(saltbuf, SCRAM_DEFAULT_SALT_LEN, SCRAM_DEFAULT_ITERATIONS, password, &errstr); @@ -1114,47 +1136,57 @@ verify_client_proof(scram_state *state) uint8 ClientSignature[SCRAM_KEY_LEN]; uint8 ClientKey[SCRAM_KEY_LEN]; uint8 client_StoredKey[SCRAM_KEY_LEN]; - pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256); - int i; + pg_hmac_ctx *ctx; + int i, j; const char *errstr = NULL; - /* * Calculate ClientSignature. Note that we don't log directly a failure * here even when processing the calculations as this could involve a mock * authentication. */ - if (pg_hmac_init(ctx, state->StoredKey, SCRAM_KEY_LEN) < 0 || - pg_hmac_update(ctx, - (uint8 *) state->client_first_message_bare, - strlen(state->client_first_message_bare)) < 0 || - pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || - pg_hmac_update(ctx, - (uint8 *) state->server_first_message, - strlen(state->server_first_message)) < 0 || - pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || - pg_hmac_update(ctx, - (uint8 *) state->client_final_message_without_proof, - strlen(state->client_final_message_without_proof)) < 0 || - pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0) + for (j = 0; j < state->num_secrets; j++) { - elog(ERROR, "could not calculate client signature: %s", - pg_hmac_error(ctx)); - } + ctx = pg_hmac_create(PG_SHA256); + elog(LOG, "Trying to verify password %d", j); + + if (pg_hmac_init(ctx, state->secrets[j].StoredKey, SCRAM_KEY_LEN) < 0 || + pg_hmac_update(ctx, + (uint8 *) state->client_first_message_bare, + strlen(state->client_first_message_bare)) < 0 || + pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || + pg_hmac_update(ctx, + (uint8 *) state->server_first_message, + strlen(state->server_first_message)) < 0 || + pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || + pg_hmac_update(ctx, + (uint8 *) state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)) < 0 || + pg_hmac_final(ctx, ClientSignature, sizeof(ClientSignature)) < 0) + { + elog(LOG, "could not calculate client signature"); + pg_hmac_free(ctx); + continue; + } - pg_hmac_free(ctx); + elog(LOG, "success on %d", j); - /* Extract the ClientKey that the client calculated from the proof */ - for (i = 0; i < SCRAM_KEY_LEN; i++) - ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i]; + pg_hmac_free(ctx); - /* Hash it one more time, and compare with StoredKey */ - if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0) - elog(ERROR, "could not hash stored key: %s", errstr); + for (i = 0; i < SCRAM_KEY_LEN; i++) + ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i]; - if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) - return false; + /* Hash it one more time, and compare with StoredKey */ + if (scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey, &errstr) < 0) + elog(ERROR, "could not hash stored key: %s", errstr); - return true; + if (memcmp(client_StoredKey, state->secrets[j].StoredKey, SCRAM_KEY_LEN) == 0) { + elog(LOG, "Moving forward with Password %d", j); + state->chosen_secret = j; + return true; + } + } + + return false; } /* @@ -1380,7 +1412,7 @@ build_server_final_message(scram_state *state) pg_hmac_ctx *ctx = pg_hmac_create(PG_SHA256); /* calculate ServerSignature */ - if (pg_hmac_init(ctx, state->ServerKey, SCRAM_KEY_LEN) < 0 || + if (pg_hmac_init(ctx, state->secrets[state->chosen_secret].ServerKey, SCRAM_KEY_LEN) < 0 || pg_hmac_update(ctx, (uint8 *) state->client_first_message_bare, strlen(state->client_first_message_bare)) < 0 || diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 5cfde1eaa13..5569599ab31 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -58,8 +58,7 @@ static void set_authn_id(Port *port, const char *id); static int CheckPasswordAuth(Port *port, const char **logdetail); static int CheckPWChallengeAuth(Port *port, const char **logdetail); -static int CheckMD5Auth(Port *port, char *shadow_pass, - const char **logdetail); +static int CheckMD5Auth(Port *port, const char **passwords, int num, const char **logdetail); /*---------------------------------------------------------------- @@ -790,8 +789,9 @@ static int CheckPasswordAuth(Port *port, const char **logdetail) { char *passwd; - int result; - char *shadow_pass; + int result = STATUS_ERROR; + int i, num; + char **passwords; sendAuthRequest(port, AUTH_REQ_PASSWORD, NULL, 0); @@ -799,17 +799,21 @@ CheckPasswordAuth(Port *port, const char **logdetail) if (passwd == NULL) return STATUS_EOF; /* client wouldn't send password */ - shadow_pass = get_role_password(port->user_name, logdetail); - if (shadow_pass) - { - result = plain_crypt_verify(port->user_name, shadow_pass, passwd, - logdetail); - } - else - result = STATUS_ERROR; + passwords = get_role_passwords(port->user_name, logdetail, &num); + if (passwords != NULL) { + for (i = 0; i < num; i++) + { + result = plain_crypt_verify(port->user_name, passwords[i], passwd, + logdetail); + if (result == STATUS_OK) + break; /* Found a matching password, no need to try any others */ + } + for (i = 0; passwords[i] != NULL; i++) + pfree(passwords[i]); - if (shadow_pass) - pfree(shadow_pass); + pfree(passwords); + } + pfree(passwd); if (result == STATUS_OK) @@ -824,29 +828,15 @@ CheckPasswordAuth(Port *port, const char **logdetail) static int CheckPWChallengeAuth(Port *port, const char **logdetail) { - int auth_result; - char *shadow_pass; - PasswordType pwtype; + int auth_result = STATUS_ERROR; + int i, num; + char **passwords; Assert(port->hba->auth_method == uaSCRAM || port->hba->auth_method == uaMD5); - /* First look up the user's password. */ - shadow_pass = get_role_password(port->user_name, logdetail); - - /* - * If the user does not exist, or has no password or it's expired, we - * still go through the motions of authentication, to avoid revealing to - * the client that the user didn't exist. If 'md5' is allowed, we choose - * whether to use 'md5' or 'scram-sha-256' authentication based on current - * password_encryption setting. The idea is that most genuine users - * probably have a password of that type, and if we pretend that this user - * had a password of that type, too, it "blends in" best. - */ - if (!shadow_pass) - pwtype = Password_encryption; - else - pwtype = get_password_type(shadow_pass); + /* First look up the user's passwords. */ + passwords = get_role_passwords(port->user_name, logdetail, &num); /* * If 'md5' authentication is allowed, decide whether to perform 'md5' or @@ -858,23 +848,17 @@ CheckPWChallengeAuth(Port *port, const char **logdetail) * had an MD5 password, CheckSASLAuth() with the SCRAM mechanism will * fail. */ - if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5) - auth_result = CheckMD5Auth(port, shadow_pass, logdetail); - else - auth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass, - logdetail); + if (passwords != NULL) { + if (port->hba->auth_method == uaMD5) + auth_result = CheckMD5Auth(port, (const char **) passwords, num, logdetail); + else + auth_result = CheckSASLAuth(&pg_be_scram_mech, port, (const char **) passwords, num, + logdetail); - if (shadow_pass) - pfree(shadow_pass); + for (i = 0; i < num; i++) + pfree(passwords[i]); - /* - * If get_role_password() returned error, return error, even if the - * authentication succeeded. - */ - if (!shadow_pass) - { - Assert(auth_result != STATUS_OK); - return STATUS_ERROR; + pfree(passwords); } if (auth_result == STATUS_OK) @@ -884,11 +868,12 @@ CheckPWChallengeAuth(Port *port, const char **logdetail) } static int -CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail) +CheckMD5Auth(Port *port, const char **passwords, int num, const char **logdetail) { char md5Salt[4]; /* Password salt */ char *passwd; int result; + int i; if (Db_user_namespace) ereport(FATAL, @@ -909,12 +894,13 @@ CheckMD5Auth(Port *port, char *shadow_pass, const char **logdetail) if (passwd == NULL) return STATUS_EOF; /* client wouldn't send password */ - if (shadow_pass) - result = md5_crypt_verify(port->user_name, shadow_pass, passwd, + for (i = 0; i < num; i++) + { + result = md5_crypt_verify(port->user_name, passwords[i], passwd, md5Salt, 4, logdetail); - else - result = STATUS_ERROR; - + if (result == STATUS_OK) + break; + } pfree(passwd); return result; diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index 745e61034c2..46b458f7d8a 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -23,6 +23,7 @@ #include "libpq/scram.h" #include "miscadmin.h" #include "utils/builtins.h" +#include "utils/catcache.h" #include "utils/syscache.h" #include "utils/timestamp.h" @@ -34,13 +35,15 @@ * for the postmaster log, in *logdetail. The error reason should *not* be * sent to the client, to avoid giving away user information! */ -char * -get_role_password(const char *role, const char **logdetail) +char ** +get_role_passwords(const char *role, const char **logdetail, int *num) { - HeapTuple roleTup, passTup; + HeapTuple roleTup; Datum datum; bool isnull; - char *shadow_pass; + char **passwords; + CatCList *passlist; + int i; /* Get role info from pg_authid */ roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role)); @@ -52,30 +55,33 @@ get_role_password(const char *role, const char **logdetail) } datum = SysCacheGetAttr(AUTHNAME, roleTup, Anum_pg_authid_oid, &isnull); - - passTup = SearchSysCache1(AUTHPASSWORD, datum); + ReleaseSysCache(roleTup); + /* Find any existing password that is not the one being updated to get the salt */ + passlist = SearchSysCacheList1(AUTHPASSWORDNAME, datum); + *num = passlist->n_members; - if (!HeapTupleIsValid(passTup)) + if (passlist->n_members == 0) { *logdetail = psprintf(_("User \"%s\" has no password assigned."), role); + ReleaseCatCacheList(passlist); return NULL; /* user has no password */ } - datum = SysCacheGetAttr(AUTHPASSWORD, passTup, - Anum_pg_auth_password_password, &isnull); - ReleaseSysCache(roleTup); - if (isnull) /* this should not happen any more but just in case */ + passwords = palloc0(sizeof(char *) * passlist->n_members); + + for (i = 0; i < passlist->n_members; i++) { - ReleaseSysCache(passTup); - *logdetail = psprintf(_("User \"%s\" has no password assigned."), - role); - return NULL; /* user has no password */ + HeapTuple tup = &passlist->members[i]->tuple; + + datum = SysCacheGetAttr(AUTHPASSWORDNAME, tup, + Anum_pg_auth_password_password, &isnull); + passwords[i] = TextDatumGetCString(datum); } - shadow_pass = TextDatumGetCString(datum); - ReleaseSysCache(passTup); - return shadow_pass; + ReleaseCatCacheList(passlist); + + return passwords; } /* @@ -107,7 +113,7 @@ get_password_type(const char *shadow_pass) * hash, so it is stored as it is regardless of the requested type. */ char * -encrypt_password(PasswordType target_type, const char *role, +encrypt_password(PasswordType target_type, const char *salt, const char *password) { PasswordType guessed_type = get_password_type(password); @@ -128,13 +134,13 @@ encrypt_password(PasswordType target_type, const char *role, case PASSWORD_TYPE_MD5: encrypted_password = palloc(MD5_PASSWD_LEN + 1); - if (!pg_md5_encrypt(password, role, strlen(role), + if (!pg_md5_encrypt(password, salt, strlen(salt), encrypted_password, &errstr)) - elog(ERROR, "password encryption failed: %s", errstr); + elog(ERROR, "password encryption failed %s", errstr); return encrypted_password; case PASSWORD_TYPE_SCRAM_SHA_256: - return pg_be_scram_build_secret(password); + return pg_be_scram_build_secret(password, salt); case PASSWORD_TYPE_PLAINTEXT: elog(ERROR, "cannot encrypt password with 'plaintext'"); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a03b33b53bd..2a288ff13f9 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -716,7 +716,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY + PARALLEL PARSER PARTIAL PARTITION PASSING PASSNAME PASSWORD PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -1132,6 +1132,15 @@ AlterOptRoleElem: errhint("Remove UNENCRYPTED to store the password in encrypted form instead."), parser_errposition(@1))); } + | PASSNAME Sconst + { + $$ = makeDefElem("passname", + (Node *)makeString($2), @1); + } + | VALID FOR Sconst + { + $$ = makeDefElem("validFor", (Node *)makeString($3), @1); + } | INHERIT { $$ = makeDefElem("inherit", (Node *)makeBoolean(true), @1); @@ -15855,6 +15864,7 @@ unreserved_keyword: | PARTIAL | PARTITION | PASSING + | PASSNAME | PASSWORD | PLANS | POLICY @@ -16433,6 +16443,7 @@ bare_label_keyword: | PARTIAL | PARTITION | PASSING + | PASSNAME | PASSWORD | PLACING | PLANS diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 356e9cde6fb..c421ac0b19f 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey) case AUTHNAME: case AUTHOID: - case AUTHPASSWORD: + case AUTHPASSWORDNAME: case AUTHMEMMEMROLE: case DATABASEOID: diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index 49d49f13252..2c39cfb083f 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -255,12 +255,12 @@ static const struct cachedesc cacheinfo[] = { }, 8 }, - {AuthPasswordRelationId, /* AUTHPASSWORD */ + {AuthPasswordRelationId, /* AUTHPASSWORDNAME */ AuthPasswordRoleOidIndexId, - 1, + 2, { Anum_pg_auth_password_roleid, - 0, + Anum_pg_auth_password_name, 0, 0 }, diff --git a/src/include/catalog/pg_auth_password.h b/src/include/catalog/pg_auth_password.h index beaa2d40b90..4181caad30b 100644 --- a/src/include/catalog/pg_auth_password.h +++ b/src/include/catalog/pg_auth_password.h @@ -24,11 +24,13 @@ * typedef struct FormData_pg_auth_password * ---------------- */ -CATALOG(pg_auth_password,4548,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4549,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO +CATALOG(pg_auth_password,4551,AuthPasswordRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(4552,AuthPasswordRelation_Rowtype_Id) BKI_SCHEMA_MACRO { Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */ + NameData name; /* name of password for multiple password support */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ - text password; /* password */ + text password BKI_FORCE_NOT_NULL; /* password */ #endif } FormData_pg_auth_password; @@ -44,7 +46,6 @@ DECLARE_TOAST(pg_auth_password, 4175, 4176); typedef FormData_pg_auth_password *Form_pg_auth_password; -DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4550, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops)); - +DECLARE_UNIQUE_INDEX_PKEY(pg_auth_password_roleoid_index, 4553, AuthPasswordRoleOidIndexId, on pg_auth_password using btree(roleid oid_ops, name name_ops)); #endif /* PG_AUTH_PASSWORD_H */ diff --git a/src/include/commands/user.h b/src/include/commands/user.h index ce39db5a491..7223b1fdae4 100644 --- a/src/include/commands/user.h +++ b/src/include/commands/user.h @@ -24,7 +24,7 @@ typedef void (*check_password_hook_type) (const char *username, const char *shad extern PGDLLIMPORT check_password_hook_type check_password_hook; -extern bool is_role_valid(const char *rolename, char **logdetail); +extern bool is_role_valid(const char *rolename, const char **logdetail); extern Oid CreateRole(ParseState *pstate, CreateRoleStmt *stmt); extern Oid AlterRole(ParseState *pstate, AlterRoleStmt *stmt); extern Oid AlterRoleSet(AlterRoleSetStmt *stmt); diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h index b8ff8ccb417..84c9c4b4f48 100644 --- a/src/include/libpq/crypt.h +++ b/src/include/libpq/crypt.h @@ -35,7 +35,7 @@ extern PasswordType get_password_type(const char *shadow_pass); extern char *encrypt_password(PasswordType target_type, const char *role, const char *password); -extern char *get_role_password(const char *role, const char **logdetail); +extern char **get_role_passwords(const char *role, const char **logdetail, int *num); extern int md5_crypt_verify(const char *role, const char *shadow_pass, const char *client_pass, const char *md5_salt, diff --git a/src/include/libpq/sasl.h b/src/include/libpq/sasl.h index 71cc0dc2514..9892f9aad38 100644 --- a/src/include/libpq/sasl.h +++ b/src/include/libpq/sasl.h @@ -77,7 +77,7 @@ typedef struct pg_be_sasl_mech * disclosing valid user names. *--------- */ - void *(*init) (Port *port, const char *mech, const char *shadow_pass); + void *(*init) (Port *port, const char *mech, const char **secrets, const int num_secrets); /*--------- * exchange() @@ -131,6 +131,6 @@ typedef struct pg_be_sasl_mech /* Common implementation for auth.c */ extern int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, - char *shadow_pass, const char **logdetail); + const char **passwords, int num, const char **logdetail); #endif /* PG_SASL_H */ diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h index e60992a0d2d..8cc6f492eeb 100644 --- a/src/include/libpq/scram.h +++ b/src/include/libpq/scram.h @@ -21,7 +21,7 @@ extern const pg_be_sasl_mech pg_be_scram_mech; /* Routines to handle and check SCRAM-SHA-256 secret */ -extern char *pg_be_scram_build_secret(const char *password); +extern char *pg_be_scram_build_secret(const char *password, const char *salt); extern bool parse_scram_secret(const char *secret, int *iterations, char **salt, uint8 *stored_key, uint8 *server_key); extern bool scram_verify_plain_password(const char *username, diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index bcef7eed2f3..e7239b34885 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -311,6 +311,7 @@ PG_KEYWORD("parser", PARSER, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("passname", PASSNAME, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index ab846e8b7fa..869cc4e4e20 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -43,7 +43,7 @@ enum SysCacheIdentifier AUTHMEMROLEMEM, AUTHNAME, AUTHOID, - AUTHPASSWORD, + AUTHPASSWORDNAME, CASTSOURCETARGET, CLAAMNAMENSP, CLAOID, diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out index 4ffc41a5455..c6a84f86a84 100644 --- a/src/test/regress/expected/password.out +++ b/src/test/regress/expected/password.out @@ -120,7 +120,7 @@ CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941 -- should not contain the original salt. SELECT rolname, password not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed FROM pg_authid - LEFT JOIN pg_auth_password p + LEFT JOIN pg_auth_password p ON pg_authid.oid = p.roleid WHERE rolname LIKE 'regress_passwd_sha_len%' ORDER BY rolname; -- 2.31.1