#include "lib/crypto/gnutls_helpers.h"
#include <gnutls/crypto.h>
+#include "kdc/db-glue.h"
+
#ifdef ENABLE_GPGME
#undef class
#include <gpgme.h>
bool is_krbtgt;
uint32_t restrictions;
struct dom_sid *account_sid;
+ bool store_nt_hash;
} u;
/* new credentials and old given credentials */
struct setup_password_fields_given {
const struct ldb_val *cleartext_utf8;
const struct ldb_val *cleartext_utf16;
+
struct samr_Password *nt_hash;
+
+ /*
+ * The AES256 kerberos key to confirm the previous password was
+ * not reused (for n) and to prove the old password was known
+ * (for og).
+ *
+ * We don't have any old salts, so we won't catch password reuse
+ * if said password was used prior to an account rename and
+ * another password change.
+ */
+ DATA_BLOB aes_256;
} n, og;
/* old credentials */
struct samr_Password *nt_history;
const struct ldb_val *supplemental;
struct supplementalCredentialsBlob scb;
+
+ /*
+ * The AES256 kerberos key as stored in the DB.
+ * Used to confirm the given password was correct
+ * and in case the previous password was reused.
+ */
+ DATA_BLOB aes_256;
+ DATA_BLOB salt;
+ uint32_t kvno;
} o;
/* generated credentials */
static int setup_nt_fields(struct setup_password_fields_io *io)
{
- struct ldb_context *ldb;
+ struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
uint32_t i;
-
- io->g.nt_hash = io->n.nt_hash;
- ldb = ldb_module_get_ctx(io->ac->module);
+ if (io->u.store_nt_hash) {
+ io->g.nt_hash = io->n.nt_hash;
+ }
if (io->ac->status->domain_data.pwdHistoryLength == 0) {
return LDB_SUCCESS;
}
/* We might not have an old NT password */
+
+ if (io->g.nt_hash == NULL) {
+ /*
+ * If there was not an NT hash specified, then don't
+ * store the NT password history.
+ *
+ * While the NTLM code on a Windows DC will cope with
+ * a missing unicodePwd, if it finds a last password
+ * in the ntPwdHistory, even if the bytes are zero ,
+ * it will (quite reasonably) treat it as a valid NT
+ * hash. NTLM logins with the previous password are
+ * allowed for a short time after the password is
+ * changed to allow for password propagation delays.
+ */
+ return LDB_SUCCESS;
+ }
+
io->g.nt_history = talloc_array(io->ac,
struct samr_Password,
io->ac->status->domain_data.pwdHistoryLength);
}
io->g.nt_history_len = i + 1;
- if (io->g.nt_hash) {
- io->g.nt_history[0] = *io->g.nt_hash;
- } else {
- /*
- * TODO: is this correct?
- * the simular behavior is correct for the lm history case
- */
- E_md4hash("", io->g.nt_history[0].hash);
- }
+ io->g.nt_history[0] = *io->g.nt_hash;
return LDB_SUCCESS;
}
return LDB_SUCCESS;
}
+static int setup_kerberos_key_hash(struct setup_password_fields_io *io,
+ struct setup_password_fields_given *g)
+{
+ struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
+ krb5_error_code krb5_ret;
+ krb5_data salt;
+ krb5_keyblock key;
+ krb5_data cleartext_data;
+
+ if (io->ac->search_res == NULL) {
+ /* No old data so nothing to do */
+ return LDB_SUCCESS;
+ }
+
+ if (io->o.salt.data == NULL) {
+ /* We didn't fetch the salt in setup_io(), so nothing to do */
+ return LDB_SUCCESS;
+ }
+
+ salt.data = (char *)io->o.salt.data;
+ salt.length = io->o.salt.length;
+
+ cleartext_data.data = (char *)g->cleartext_utf8->data;
+ cleartext_data.length = g->cleartext_utf8->length;
+
+ /*
+ * create ENCTYPE_AES256_CTS_HMAC_SHA1_96 key out of the salt
+ * and the cleartext password
+ */
+ krb5_ret = smb_krb5_create_key_from_string(io->smb_krb5_context->krb5_context,
+ NULL,
+ &salt,
+ &cleartext_data,
+ ENCTYPE_AES256_CTS_HMAC_SHA1_96,
+ &key);
+ if (krb5_ret) {
+ ldb_asprintf_errstring(ldb,
+ "setup_kerberos_key_hash: "
+ "generation of a aes256-cts-hmac-sha1-96 key failed: %s",
+ smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+ krb5_ret, io->ac));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ g->aes_256 = data_blob_talloc(io->ac,
+ KRB5_KEY_DATA(&key),
+ KRB5_KEY_LENGTH(&key));
+ krb5_free_keyblock_contents(io->smb_krb5_context->krb5_context, &key);
+ if (g->aes_256.data == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ talloc_keep_secret(g->aes_256.data);
+
+ return LDB_SUCCESS;
+}
+
static int setup_primary_kerberos(struct setup_password_fields_io *io,
const struct supplementalCredentialsBlob *old_scb,
struct package_PrimaryKerberosBlob *pkb)
* used in preference to the NT password hash
*/
if (io->g.nt_hash == NULL) {
- ldb_asprintf_errstring(ldb,
- "No NT Hash, unable to calculate userPassword hashes");
- return LDB_ERR_UNWILLING_TO_PERFORM;
+ /*
+ * When the NT hash is not available, we use this field to store
+ * the first 16 bytes of the AES256 key instead. This allows
+ * 'samba-tool user' to verify that the user's password is in
+ * sync with the userPassword package.
+ */
+ uint8_t hash_len = MIN(16, io->g.aes_256.length);
+
+ ZERO_STRUCT(p_userPassword_b->current_nt_hash);
+ memcpy(p_userPassword_b->current_nt_hash.hash,
+ io->g.aes_256.data,
+ hash_len);
+ } else {
+ p_userPassword_b->current_nt_hash = *io->g.nt_hash;
}
- p_userPassword_b->current_nt_hash = *io->g.nt_hash;
/*
* Determine the number of hashes
static int setup_given_passwords(struct setup_password_fields_io *io,
struct setup_password_fields_given *g)
{
- struct ldb_context *ldb;
-
- ldb = ldb_module_get_ctx(io->ac->module);
+ struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
if (g->cleartext_utf8) {
struct ldb_val *cleartext_utf16_blob;
g->cleartext_utf16->length);
}
+ /*
+ * We need to build one more hash, so we can compare with what might
+ * have been stored in the old password (for the LDAP password change)
+ *
+ * We don't have any old salts, so we won't catch password reuse if said
+ * password was used prior to an account rename and another password
+ * change.
+ *
+ * We don't have to store the 'opaque' (string2key iterations)
+ * as Heimdal doesn't allow that to be changed.
+ */
+ if (g->cleartext_utf8 != NULL) {
+ int ret = setup_kerberos_key_hash(io, g);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
return LDB_SUCCESS;
}
}
}
+ /*
+ * This relies on setup_kerberos_keys to make a NT-hash-like
+ * value for password history purposes
+ */
+
ret = setup_nt_fields(io);
if (ret != LDB_SUCCESS) {
return ret;
{
struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module);
int ret;
+ uint32_t i;
struct loadparm_context *lp_ctx =
talloc_get_type(ldb_get_opaque(ldb, "loadparm"),
struct loadparm_context);
*/
if (!io->ac->pwd_reset && !(io->ac->change
&& io->ac->change->old_password_checked == DSDB_PASSWORD_CHECKED_AND_CORRECT)) {
+ bool hash_checked = false;
/*
* we need the old nt hash given by the client (this
* is for the plaintext over LDAP password change,
* Kpasswd and SAMR supply the control)
*/
- if (!io->og.nt_hash) {
+ if (io->og.nt_hash == NULL && io->og.aes_256.length == 0) {
ldb_asprintf_errstring(ldb,
"check_password_restrictions: "
"You need to provide the old password in order "
return LDB_ERR_UNWILLING_TO_PERFORM;
}
+ /*
+ * First compare the ENCTYPE_AES256_CTS_HMAC_SHA1_96 password and see if we have a match
+ */
+
+ if (io->og.aes_256.length > 0 && io->o.aes_256.length) {
+ hash_checked = data_blob_equal_const_time(&io->og.aes_256, &io->o.aes_256);
+ }
+
/* The password modify through the NT hash is encouraged and
has no problems at all */
- if (!io->o.nt_hash || !mem_equal_const_time(io->og.nt_hash->hash, io->o.nt_hash->hash, 16)) {
+ if (!hash_checked && io->og.nt_hash && io->o.nt_hash) {
+ hash_checked = mem_equal_const_time(io->og.nt_hash->hash, io->o.nt_hash->hash, 16);
+ }
+
+ if (!hash_checked) {
return make_error_and_update_badPwdCount(io, werror);
}
}
return LDB_SUCCESS;
}
- if (io->n.nt_hash) {
- uint32_t i;
+ /*
+ * This check works by using the current Kerberos password to
+ * make up a password history. We already did the salted hash
+ * creation to pass the password change check.
+ *
+ * We check the pwdHistoryLength to ensure we honour the
+ * policy on if the history should be checked
+ */
+ if (io->ac->status->domain_data.pwdHistoryLength > 0
+ && io->g.aes_256.length && io->o.aes_256.length)
+ {
+ bool equal = data_blob_equal_const_time(&io->g.aes_256,
+ &io->o.aes_256);
+ if (equal) {
+ ret = LDB_ERR_CONSTRAINT_VIOLATION;
+ *werror = WERR_PASSWORD_RESTRICTION;
+ ldb_asprintf_errstring(ldb,
+ "%08X: %s - check_password_restrictions: "
+ "the password was already used (previous password)!",
+ W_ERROR_V(*werror),
+ ldb_strerror(ret));
+ io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
+ return ret;
+ }
+ }
- /* checks the NT hash password history */
+ if (io->n.nt_hash) {
+ /*
+ * checks the NT hash password history, against the
+ * generated NT hash
+ */
for (i = 0; i < io->o.nt_history_len; i++) {
bool pw_cmp = mem_equal_const_time(io->n.nt_hash, io->o.nt_history[i].hash, 16);
if (pw_cmp) {
}
}
+ /*
+ * This check works by using the old Kerberos passwords
+ * (old and older) to make up a password history.
+ *
+ * We check the pwdHistoryLength to ensure we honour the
+ * policy on if the history should be checked
+ */
+ for (i = 1;
+ i <= io->o.kvno && i < MIN(3, io->ac->status->domain_data.pwdHistoryLength);
+ i++)
+ {
+ krb5_error_code krb5_ret;
+ const uint32_t request_kvno = io->o.kvno - i;
+ DATA_BLOB db_key_blob;
+ bool pw_equal;
+
+ if (io->n.cleartext_utf8 == NULL) {
+ /*
+ * No point checking history if we don't have
+ * a cleartext password.
+ */
+ break;
+ }
+
+ if (io->ac->search_res == NULL) {
+ /*
+ * This is an ADD, no existing history to check
+ */
+ break;
+ }
+
+ /*
+ * If this account requires a smartcard for login, we don't
+ * attempt a comparison with the old password.
+ */
+ if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) {
+ break;
+ }
+
+ /*
+ * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96 value from
+ * the supplementalCredentials.
+ */
+ krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context,
+ io->ac,
+ io->ac->search_res->message,
+ io->u.userAccountControl,
+ &request_kvno, /* kvno */
+ NULL, /* kvno_out */
+ &db_key_blob,
+ NULL); /* salt */
+ if (krb5_ret == ENOENT) {
+ /*
+ * If there is no old AES hash (perhaps an imported DB with
+ * just unicodePwd) then we just wont have an old
+ * password to compare to if there is no NT hash
+ */
+ break;
+ } else if (krb5_ret) {
+ ldb_asprintf_errstring(ldb,
+ "check_password_restrictions: "
+ "extraction of old[%u - %d = %d] aes256-cts-hmac-sha1-96 key failed: %s",
+ io->o.kvno, i, io->o.kvno - i,
+ smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+ krb5_ret, io->ac));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ /* This is the actual history check */
+ pw_equal = data_blob_equal_const_time(&io->n.aes_256,
+ &db_key_blob);
+ if (pw_equal) {
+ ret = LDB_ERR_CONSTRAINT_VIOLATION;
+ *werror = WERR_PASSWORD_RESTRICTION;
+ ldb_asprintf_errstring(ldb,
+ "%08X: %s - check_password_restrictions: "
+ "the password was already used (in history)!",
+ W_ERROR_V(*werror),
+ ldb_strerror(ret));
+ io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY;
+ return ret;
+ }
+ }
+
/* are all password changes disallowed? */
if (io->ac->status->domain_data.pwdProperties & DOMAIN_REFUSE_PASSWORD_CHANGE) {
ret = LDB_ERR_CONSTRAINT_VIOLATION;
struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
struct loadparm_context *lp_ctx = talloc_get_type(
ldb_get_opaque(ldb, "loadparm"), struct loadparm_context);
+ enum store_nt_hash store_hash_setting =
+ lpcfg_nt_hash_store(lp_ctx);
int ret;
const struct ldb_message *info_msg = NULL;
struct dom_sid *account_sid = NULL;
MAX(io->ac->status->domain_data.pwdHistoryLength, 3);
}
+ /*
+ * Machine accounts need the NT hash to operate the NETLOGON
+ * ServerAuthenticate{,2,3} logic
+ */
+ if (!(io->u.userAccountControl & UF_NORMAL_ACCOUNT)) {
+ store_hash_setting = NT_HASH_STORE_ALWAYS;
+ }
+
+ switch (store_hash_setting) {
+ case NT_HASH_STORE_ALWAYS:
+ io->u.store_nt_hash = true;
+ break;
+ case NT_HASH_STORE_NEVER:
+ io->u.store_nt_hash = false;
+ break;
+ case NT_HASH_STORE_AUTO:
+ if (lpcfg_ntlm_auth(lp_ctx) == NTLM_AUTH_DISABLED) {
+ io->u.store_nt_hash = false;
+ break;
+ }
+ io->u.store_nt_hash = true;
+ break;
+ }
+
if (ac->userPassword) {
ret = msg_find_old_and_new_pwd_val(client_msg, "userPassword",
ac->req->operation,
if (existing_msg != NULL) {
NTSTATUS status;
+ krb5_error_code krb5_ret;
+ DATA_BLOB key_blob;
+ DATA_BLOB salt_blob;
+ uint32_t kvno;
if (ac->pwd_reset) {
/* Get the old password from the database */
return LDB_ERR_OPERATIONS_ERROR;
}
}
+
+ /*
+ * If this account requires a smartcard for login, we don't
+ * attempt a comparison with the old password.
+ */
+ if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) {
+ return LDB_SUCCESS;
+ }
+
+ /*
+ * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96
+ * value from the supplementalCredentials.
+ */
+ krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context,
+ io->ac,
+ existing_msg,
+ io->u.userAccountControl,
+ NULL, /* kvno */
+ &kvno, /* kvno_out */
+ &key_blob,
+ &salt_blob);
+ if (krb5_ret == ENOENT) {
+ /*
+ * If there is no old AES hash (perhaps an imported DB with
+ * just unicodePwd) then we just wont have an old
+ * password to compare to if there is no NT hash
+ */
+ return LDB_SUCCESS;
+ }
+ if (krb5_ret) {
+ ldb_asprintf_errstring(ldb,
+ "setup_io: "
+ "extraction of salt for old aes256-cts-hmac-sha1-96 key failed: %s",
+ smb_get_krb5_error_message(io->smb_krb5_context->krb5_context,
+ krb5_ret, io->ac));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ io->o.salt = salt_blob;
+ io->o.aes_256 = key_blob;
+ io->o.kvno = kvno;
}
return LDB_SUCCESS;
"badPasswordTime",
"badPwdCount",
"lockoutTime",
+ "msDS-KeyVersionNumber",
"msDS-SecondaryKrbTgtNumber",
NULL };
struct ldb_request *search_req;