/* ldb database module Copyright (C) Simo Sorce 2004-2008 Copyright (C) Andrew Bartlett 2005-2006 Copyright (C) Andrew Tridgell 2004 Copyright (C) Stefan Metzmacher 2007-2010 Copyright (C) Matthias Dieter Wallnöfer 2009-2010 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* * Name: ldb * * Component: ldb password_hash module * * Description: correctly handle AD password changes fields * * Author: Andrew Bartlett * Author: Stefan Metzmacher */ #include "includes.h" #include "ldb_module.h" #include "libcli/auth/libcli_auth.h" #include "libcli/security/dom_sid.h" #include "system/kerberos.h" #include "auth/kerberos/kerberos.h" #include "dsdb/samdb/samdb.h" #include "dsdb/samdb/ldb_modules/util.h" #include "dsdb/samdb/ldb_modules/password_modules.h" #include "librpc/gen_ndr/ndr_drsblobs.h" #include "lib/crypto/md4.h" #include "param/param.h" #include "lib/krb5_wrap/krb5_samba.h" #include "auth/auth_sam.h" #include "auth/common_auth.h" #include "lib/messaging/messaging.h" #include "lib/param/loadparm.h" #include "lib/crypto/gnutls_helpers.h" #include #include "kdc/db-glue.h" #ifdef ENABLE_GPGME #undef class #include /* * 1.2.0 is what dpkg-shlibdeps generates, based on used symbols and * libgpgme11.symbols * https://salsa.debian.org/debian/gpgme/blob/debian/master/debian/libgpgme11.symbols */ #define MINIMUM_GPGME_VERSION "1.2.0" #endif #undef strncasecmp #undef strcasecmp /* If we have decided there is a reason to work on this request, then * setup all the password hash types correctly. * * If we haven't the hashes yet but the password given as plain-text (attributes * 'unicodePwd', 'userPassword' and 'clearTextPassword') we have to check for * the constraints. Once this is done, we calculate the password hashes. * * Notice: unlike the real AD which only supports the UTF16 special based * 'unicodePwd' and the UTF8 based 'userPassword' plaintext attribute we * understand also a UTF16 based 'clearTextPassword' one. * The latter is also accessible through LDAP so it can also be set by external * tools and scripts. But be aware that this isn't portable on non SAMBA 4 ADs! * * Also when the module receives only the password hashes (possible through * specifying an internal LDB control - for security reasons) some checks are * performed depending on the operation mode (see below) (e.g. if the password * has been in use before if the password memory policy was activated). * * Attention: There is a difference between "modify" and "reset" operations * (see MS-ADTS 3.1.1.3.1.5). If the client sends a "add" and "remove" * operation for a password attribute we thread this as a "modify"; if it sends * only a "replace" one we have an (administrative) reset. * * Finally, if the administrator has requested that a password history * be maintained, then this should also be written out. * */ /* TODO: [consider always MS-ADTS 3.1.1.3.1.5] * - Check for right connection encryption */ /* Notice: Definition of "dsdb_control_password_change_status" moved into * "samdb.h" */ struct ph_context { struct ldb_module *module; struct ldb_request *req; struct ldb_request *dom_req; struct ldb_reply *dom_res; struct ldb_reply *pso_res; struct ldb_reply *search_res; struct ldb_message *update_msg; struct dsdb_control_password_change_status *status; struct dsdb_control_password_change *change; const char **gpg_key_ids; bool pwd_reset; bool change_status; bool hash_values; bool userPassword; bool update_password; bool update_lastset; bool pwd_last_set_bypass; bool pwd_last_set_default; bool smartcard_reset; const char **userPassword_schemes; }; struct setup_password_fields_io { struct ph_context *ac; struct smb_krb5_context *smb_krb5_context; /* info about the user account */ struct { uint32_t userAccountControl; NTTIME pwdLastSet; const char *sAMAccountName; const char *user_principal_name; const char *displayName; /* full name */ 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 { struct samr_Password *nt_hash; uint32_t nt_history_len; 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 */ struct { struct samr_Password *nt_hash; uint32_t nt_history_len; struct samr_Password *nt_history; const char *salt; DATA_BLOB aes_256; DATA_BLOB aes_128; DATA_BLOB des_md5; DATA_BLOB des_crc; struct ldb_val supplemental; NTTIME last_set; } g; }; static int msg_find_old_and_new_pwd_val(const struct ldb_message *msg, const char *name, enum ldb_request_type operation, const struct ldb_val **new_val, const struct ldb_val **old_val); static int password_hash_bypass(struct ldb_module *module, struct ldb_request *request) { struct ldb_context *ldb = ldb_module_get_ctx(module); const struct ldb_message *msg; struct ldb_message_element *nte; struct ldb_message_element *lme; struct ldb_message_element *nthe; struct ldb_message_element *lmhe; struct ldb_message_element *sce; int ret; switch (request->operation) { case LDB_ADD: msg = request->op.add.message; break; case LDB_MODIFY: msg = request->op.mod.message; break; default: return ldb_next_request(module, request); } /* nobody must touch password histories and 'supplementalCredentials' */ #define GET_VALUES(el, attr) do { \ ret = dsdb_get_expected_new_values(request, \ msg, \ attr, \ &el, \ request->operation); \ \ if (ret != LDB_SUCCESS) { \ return ret; \ } \ } while(0) GET_VALUES(nte, "unicodePwd"); /* * Even as Samba continues to ignore the LM hash, and reset it * when practical, we keep the constraint that it must be a 16 * byte value if specified. */ GET_VALUES(lme, "dBCSPwd"); GET_VALUES(nthe, "ntPwdHistory"); GET_VALUES(lmhe, "lmPwdHistory"); GET_VALUES(sce, "supplementalCredentials"); #undef GET_VALUES #define CHECK_HASH_ELEMENT(e, min, max) do {\ if (e && e->num_values) { \ unsigned int _count; \ if (e->num_values != 1) { \ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ "num_values != 1"); \ } \ if ((e->values[0].length % 16) != 0) { \ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ "length % 16 != 0"); \ } \ _count = e->values[0].length / 16; \ if (_count < min) { \ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ "count < min"); \ } \ if (_count > max) { \ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ "count > max"); \ } \ } \ } while (0) CHECK_HASH_ELEMENT(nte, 1, 1); CHECK_HASH_ELEMENT(lme, 1, 1); CHECK_HASH_ELEMENT(nthe, 1, INT32_MAX); CHECK_HASH_ELEMENT(lmhe, 1, INT32_MAX); if (sce && sce->num_values) { enum ndr_err_code ndr_err; struct supplementalCredentialsBlob *scb; struct supplementalCredentialsPackage *scpp = NULL; struct supplementalCredentialsPackage *scpk = NULL; struct supplementalCredentialsPackage *scpkn = NULL; struct supplementalCredentialsPackage *scpct = NULL; DATA_BLOB scpbp = data_blob_null; DATA_BLOB scpbk = data_blob_null; DATA_BLOB scpbkn = data_blob_null; DATA_BLOB scpbct = data_blob_null; DATA_BLOB blob; uint32_t i; if (sce->num_values != 1) { return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "num_values != 1"); } scb = talloc_zero(request, struct supplementalCredentialsBlob); if (!scb) { return ldb_module_oom(module); } ndr_err = ndr_pull_struct_blob_all(&sce->values[0], scb, scb, (ndr_pull_flags_fn_t)ndr_pull_supplementalCredentialsBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob_all"); } if (scb->sub.num_packages < 2) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "num_packages < 2"); } for (i=0; i < scb->sub.num_packages; i++) { DATA_BLOB subblob; subblob = strhex_to_data_blob(scb, scb->sub.packages[i].data); if (subblob.data == NULL) { talloc_free(scb); return ldb_module_oom(module); } if (strcmp(scb->sub.packages[i].name, "Packages") == 0) { if (scpp) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Packages twice"); } scpp = &scb->sub.packages[i]; scpbp = subblob; continue; } if (strcmp(scb->sub.packages[i].name, "Primary:Kerberos") == 0) { if (scpk) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Primary:Kerberos twice"); } scpk = &scb->sub.packages[i]; scpbk = subblob; continue; } if (strcmp(scb->sub.packages[i].name, "Primary:Kerberos-Newer-Keys") == 0) { if (scpkn) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Primary:Kerberos-Newer-Keys twice"); } scpkn = &scb->sub.packages[i]; scpbkn = subblob; continue; } if (strcmp(scb->sub.packages[i].name, "Primary:CLEARTEXT") == 0) { if (scpct) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Primary:CLEARTEXT twice"); } scpct = &scb->sub.packages[i]; scpbct = subblob; continue; } data_blob_free(&subblob); } if (scpp == NULL) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Primary:Packages missing"); } if (scpk == NULL) { /* * If Primary:Kerberos is missing w2k8r2 reboots * when a password is changed. */ talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Primary:Kerberos missing"); } if (scpp) { struct package_PackagesBlob *p; uint32_t n; p = talloc_zero(scb, struct package_PackagesBlob); if (p == NULL) { talloc_free(scb); return ldb_module_oom(module); } ndr_err = ndr_pull_struct_blob(&scpbp, p, p, (ndr_pull_flags_fn_t)ndr_pull_package_PackagesBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob Packages"); } if (p->names == NULL) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Packages names == NULL"); } for (n = 0; p->names[n]; n++) { /* noop */ } if (scb->sub.num_packages != (n + 1)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "Packages num_packages != num_names + 1"); } talloc_free(p); } if (scpk) { struct package_PrimaryKerberosBlob *k; k = talloc_zero(scb, struct package_PrimaryKerberosBlob); if (k == NULL) { talloc_free(scb); return ldb_module_oom(module); } ndr_err = ndr_pull_struct_blob(&scpbk, k, k, (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob PrimaryKerberos"); } if (k->version != 3) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos version != 3"); } if (k->ctr.ctr3.salt.string == NULL) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos salt == NULL"); } if (strlen(k->ctr.ctr3.salt.string) == 0) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos strlen(salt) == 0"); } if (k->ctr.ctr3.num_keys != 2) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos num_keys != 2"); } if (k->ctr.ctr3.num_old_keys > k->ctr.ctr3.num_keys) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos num_old_keys > num_keys"); } if (k->ctr.ctr3.keys[0].keytype != ENCTYPE_DES_CBC_MD5) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos key[0] != DES_CBC_MD5"); } if (k->ctr.ctr3.keys[1].keytype != ENCTYPE_DES_CBC_CRC) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos key[1] != DES_CBC_CRC"); } if (k->ctr.ctr3.keys[0].value_len != 8) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos key[0] value_len != 8"); } if (k->ctr.ctr3.keys[1].value_len != 8) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos key[1] value_len != 8"); } for (i = 0; i < k->ctr.ctr3.num_old_keys; i++) { if (k->ctr.ctr3.old_keys[i].keytype == k->ctr.ctr3.keys[i].keytype && k->ctr.ctr3.old_keys[i].value_len == k->ctr.ctr3.keys[i].value_len) { continue; } talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryKerberos old_keys type/value_len doesn't match"); } talloc_free(k); } if (scpkn) { struct package_PrimaryKerberosBlob *k; k = talloc_zero(scb, struct package_PrimaryKerberosBlob); if (k == NULL) { talloc_free(scb); return ldb_module_oom(module); } ndr_err = ndr_pull_struct_blob(&scpbkn, k, k, (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob PrimaryKerberosNeverKeys"); } if (k->version != 4) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNerverKeys version != 4"); } if (k->ctr.ctr4.salt.string == NULL) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys salt == NULL"); } if (strlen(k->ctr.ctr4.salt.string) == 0) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys strlen(salt) == 0"); } if (k->ctr.ctr4.num_keys != 4) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys num_keys != 4"); } if (k->ctr.ctr4.num_old_keys > k->ctr.ctr4.num_keys) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys num_old_keys > num_keys"); } if (k->ctr.ctr4.num_older_keys > k->ctr.ctr4.num_old_keys) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys num_older_keys > num_old_keys"); } if (k->ctr.ctr4.keys[0].keytype != ENCTYPE_AES256_CTS_HMAC_SHA1_96) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[0] != AES256"); } if (k->ctr.ctr4.keys[1].keytype != ENCTYPE_AES128_CTS_HMAC_SHA1_96) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[1] != AES128"); } if (k->ctr.ctr4.keys[2].keytype != ENCTYPE_DES_CBC_MD5) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[2] != DES_CBC_MD5"); } if (k->ctr.ctr4.keys[3].keytype != ENCTYPE_DES_CBC_CRC) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[3] != DES_CBC_CRC"); } if (k->ctr.ctr4.keys[0].value_len != 32) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[0] value_len != 32"); } if (k->ctr.ctr4.keys[1].value_len != 16) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[1] value_len != 16"); } if (k->ctr.ctr4.keys[2].value_len != 8) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[2] value_len != 8"); } if (k->ctr.ctr4.keys[3].value_len != 8) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "KerberosNewerKeys key[3] value_len != 8"); } /* * TODO: * Maybe we can check old and older keys here. * But we need to do some tests, if the old keys * can be taken from the PrimaryKerberos blob * (with only des keys), when the domain was upgraded * from w2k3 to w2k8. */ talloc_free(k); } if (scpct) { struct package_PrimaryCLEARTEXTBlob *ct; ct = talloc_zero(scb, struct package_PrimaryCLEARTEXTBlob); if (ct == NULL) { talloc_free(scb); return ldb_module_oom(module); } ndr_err = ndr_pull_struct_blob(&scpbct, ct, ct, (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryCLEARTEXTBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob PrimaryCLEARTEXT"); } if ((ct->cleartext.length % 2) != 0) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "PrimaryCLEARTEXT length % 2 != 0"); } talloc_free(ct); } ndr_err = ndr_push_struct_blob(&blob, scb, scb, (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "ndr_pull_struct_blob_all"); } if (sce->values[0].length != blob.length) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "supplementalCredentialsBlob length differ"); } if (!mem_equal_const_time(sce->values[0].data, blob.data, blob.length)) { talloc_free(scb); return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, "supplementalCredentialsBlob memcmp differ"); } talloc_free(scb); } ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_bypass - validated\n"); return ldb_next_request(module, request); } /* Get the NT hash, and fill it in as an entry in the password history, and specify it into io->g.nt_hash */ static int setup_nt_fields(struct setup_password_fields_io *io) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); uint32_t i; 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); if (!io->g.nt_history) { return ldb_oom(ldb); } for (i = 0; i < MIN(io->ac->status->domain_data.pwdHistoryLength-1, io->o.nt_history_len); i++) { io->g.nt_history[i+1] = io->o.nt_history[i]; } io->g.nt_history_len = i + 1; io->g.nt_history[0] = *io->g.nt_hash; return LDB_SUCCESS; } static int setup_kerberos_keys(struct setup_password_fields_io *io) { struct ldb_context *ldb; krb5_error_code krb5_ret; krb5_principal salt_principal = NULL; krb5_data salt_data; krb5_data salt; krb5_keyblock key; krb5_data cleartext_data; uint32_t uac_flags = 0; ldb = ldb_module_get_ctx(io->ac->module); cleartext_data.data = (char *)io->n.cleartext_utf8->data; cleartext_data.length = io->n.cleartext_utf8->length; uac_flags = io->u.userAccountControl & UF_ACCOUNT_TYPE_MASK; krb5_ret = smb_krb5_salt_principal(io->smb_krb5_context->krb5_context, io->ac->status->domain_data.realm, io->u.sAMAccountName, io->u.user_principal_name, uac_flags, &salt_principal); if (krb5_ret) { ldb_asprintf_errstring(ldb, "setup_kerberos_keys: " "generation of a salting principal failed: %s", smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, krb5_ret, io->ac)); return LDB_ERR_OPERATIONS_ERROR; } /* * create salt from salt_principal */ krb5_ret = smb_krb5_get_pw_salt(io->smb_krb5_context->krb5_context, salt_principal, &salt_data); krb5_free_principal(io->smb_krb5_context->krb5_context, salt_principal); if (krb5_ret) { ldb_asprintf_errstring(ldb, "setup_kerberos_keys: " "generation of krb5_salt failed: %s", smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, krb5_ret, io->ac)); return LDB_ERR_OPERATIONS_ERROR; } /* now use the talloced copy of the salt */ salt.data = talloc_strndup(io->ac, (char *)salt_data.data, salt_data.length); smb_krb5_free_data_contents(io->smb_krb5_context->krb5_context, &salt_data); if (salt.data == NULL) { return ldb_oom(ldb); } io->g.salt = salt.data; salt.length = strlen(io->g.salt); /* * 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_keys: " "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; } io->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 (!io->g.aes_256.data) { return ldb_oom(ldb); } /* * create ENCTYPE_AES128_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_AES128_CTS_HMAC_SHA1_96, &key); if (krb5_ret) { ldb_asprintf_errstring(ldb, "setup_kerberos_keys: " "generation of a aes128-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->g.aes_128 = 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 (!io->g.aes_128.data) { return ldb_oom(ldb); } /* * As per RFC-6649 single DES encryption types are no longer considered * secure to be used in Kerberos, we store random keys instead of the * ENCTYPE_DES_CBC_MD5 and ENCTYPE_DES_CBC_CRC keys. */ io->g.des_md5 = data_blob_talloc(io->ac, NULL, 8); if (!io->g.des_md5.data) { return ldb_oom(ldb); } generate_secret_buffer(io->g.des_md5.data, 8); io->g.des_crc = data_blob_talloc(io->ac, NULL, 8); if (!io->g.des_crc.data) { return ldb_oom(ldb); } generate_secret_buffer(io->g.des_crc.data, 8); 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) { struct ldb_context *ldb; struct package_PrimaryKerberosCtr3 *pkb3 = &pkb->ctr.ctr3; struct supplementalCredentialsPackage *old_scp = NULL; struct package_PrimaryKerberosBlob _old_pkb; struct package_PrimaryKerberosCtr3 *old_pkb3 = NULL; uint32_t i; enum ndr_err_code ndr_err; ldb = ldb_module_get_ctx(io->ac->module); /* * prepare generation of keys * * ENCTYPE_DES_CBC_MD5 * ENCTYPE_DES_CBC_CRC */ pkb->version = 3; pkb3->salt.string = io->g.salt; pkb3->num_keys = 2; pkb3->keys = talloc_array(io->ac, struct package_PrimaryKerberosKey3, pkb3->num_keys); if (!pkb3->keys) { return ldb_oom(ldb); } pkb3->keys[0].keytype = ENCTYPE_DES_CBC_MD5; pkb3->keys[0].value = &io->g.des_md5; pkb3->keys[1].keytype = ENCTYPE_DES_CBC_CRC; pkb3->keys[1].value = &io->g.des_crc; /* initialize the old keys to zero */ pkb3->num_old_keys = 0; pkb3->old_keys = NULL; /* if there're no old keys, then we're done */ if (!old_scb) { return LDB_SUCCESS; } for (i=0; i < old_scb->sub.num_packages; i++) { if (strcmp("Primary:Kerberos", old_scb->sub.packages[i].name) != 0) { continue; } if (!old_scb->sub.packages[i].data || !old_scb->sub.packages[i].data[0]) { continue; } old_scp = &old_scb->sub.packages[i]; break; } /* Primary:Kerberos element of supplementalCredentials */ if (old_scp) { DATA_BLOB blob; blob = strhex_to_data_blob(io->ac, old_scp->data); if (!blob.data) { return ldb_oom(ldb); } /* TODO: use ndr_pull_struct_blob_all(), when the ndr layer handles it correct with relative pointers */ ndr_err = ndr_pull_struct_blob(&blob, io->ac, &_old_pkb, (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring(ldb, "setup_primary_kerberos: " "failed to pull old package_PrimaryKerberosBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } if (_old_pkb.version != 3) { ldb_asprintf_errstring(ldb, "setup_primary_kerberos: " "package_PrimaryKerberosBlob version[%u] expected[3]", _old_pkb.version); return LDB_ERR_OPERATIONS_ERROR; } old_pkb3 = &_old_pkb.ctr.ctr3; } /* if we didn't found the old keys we're done */ if (!old_pkb3) { return LDB_SUCCESS; } /* fill in the old keys */ pkb3->num_old_keys = old_pkb3->num_keys; pkb3->old_keys = old_pkb3->keys; return LDB_SUCCESS; } static int setup_primary_kerberos_newer(struct setup_password_fields_io *io, const struct supplementalCredentialsBlob *old_scb, struct package_PrimaryKerberosBlob *pkb) { struct ldb_context *ldb; struct package_PrimaryKerberosCtr4 *pkb4 = &pkb->ctr.ctr4; struct supplementalCredentialsPackage *old_scp = NULL; struct package_PrimaryKerberosBlob _old_pkb; struct package_PrimaryKerberosCtr4 *old_pkb4 = NULL; uint32_t i; enum ndr_err_code ndr_err; ldb = ldb_module_get_ctx(io->ac->module); /* * prepare generation of keys * * ENCTYPE_AES256_CTS_HMAC_SHA1_96 * ENCTYPE_AES128_CTS_HMAC_SHA1_96 * ENCTYPE_DES_CBC_MD5 * ENCTYPE_DES_CBC_CRC */ pkb->version = 4; pkb4->salt.string = io->g.salt; pkb4->default_iteration_count = 4096; pkb4->num_keys = 4; pkb4->keys = talloc_array(io->ac, struct package_PrimaryKerberosKey4, pkb4->num_keys); if (!pkb4->keys) { return ldb_oom(ldb); } pkb4->keys[0].iteration_count = 4096; pkb4->keys[0].keytype = ENCTYPE_AES256_CTS_HMAC_SHA1_96; pkb4->keys[0].value = &io->g.aes_256; pkb4->keys[1].iteration_count = 4096; pkb4->keys[1].keytype = ENCTYPE_AES128_CTS_HMAC_SHA1_96; pkb4->keys[1].value = &io->g.aes_128; pkb4->keys[2].iteration_count = 4096; pkb4->keys[2].keytype = ENCTYPE_DES_CBC_MD5; pkb4->keys[2].value = &io->g.des_md5; pkb4->keys[3].iteration_count = 4096; pkb4->keys[3].keytype = ENCTYPE_DES_CBC_CRC; pkb4->keys[3].value = &io->g.des_crc; /* initialize the old keys to zero */ pkb4->num_old_keys = 0; pkb4->old_keys = NULL; pkb4->num_older_keys = 0; pkb4->older_keys = NULL; /* if there're no old keys, then we're done */ if (!old_scb) { return LDB_SUCCESS; } for (i=0; i < old_scb->sub.num_packages; i++) { if (strcmp("Primary:Kerberos-Newer-Keys", old_scb->sub.packages[i].name) != 0) { continue; } if (!old_scb->sub.packages[i].data || !old_scb->sub.packages[i].data[0]) { continue; } old_scp = &old_scb->sub.packages[i]; break; } /* Primary:Kerberos-Newer-Keys element of supplementalCredentials */ if (old_scp) { DATA_BLOB blob; blob = strhex_to_data_blob(io->ac, old_scp->data); if (!blob.data) { return ldb_oom(ldb); } /* TODO: use ndr_pull_struct_blob_all(), when the ndr layer handles it correct with relative pointers */ ndr_err = ndr_pull_struct_blob(&blob, io->ac, &_old_pkb, (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring(ldb, "setup_primary_kerberos_newer: " "failed to pull old package_PrimaryKerberosBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } if (_old_pkb.version != 4) { ldb_asprintf_errstring(ldb, "setup_primary_kerberos_newer: " "package_PrimaryKerberosBlob version[%u] expected[4]", _old_pkb.version); return LDB_ERR_OPERATIONS_ERROR; } old_pkb4 = &_old_pkb.ctr.ctr4; } /* if we didn't found the old keys we're done */ if (!old_pkb4) { return LDB_SUCCESS; } /* fill in the old keys */ pkb4->num_old_keys = old_pkb4->num_keys; pkb4->old_keys = old_pkb4->keys; pkb4->num_older_keys = old_pkb4->num_old_keys; pkb4->older_keys = old_pkb4->old_keys; return LDB_SUCCESS; } static int setup_primary_wdigest(struct setup_password_fields_io *io, const struct supplementalCredentialsBlob *old_scb, struct package_PrimaryWDigestBlob *pdb) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); DATA_BLOB sAMAccountName; DATA_BLOB sAMAccountName_l; DATA_BLOB sAMAccountName_u; const char *user_principal_name = io->u.user_principal_name; DATA_BLOB userPrincipalName; DATA_BLOB userPrincipalName_l; DATA_BLOB userPrincipalName_u; DATA_BLOB netbios_domain; DATA_BLOB netbios_domain_l; DATA_BLOB netbios_domain_u; DATA_BLOB dns_domain; DATA_BLOB dns_domain_l; DATA_BLOB dns_domain_u; DATA_BLOB digest; DATA_BLOB delim; DATA_BLOB backslash; uint8_t i; struct { DATA_BLOB *user; DATA_BLOB *realm; DATA_BLOB *nt4dom; } wdigest[] = { /* * See 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction * https://msdn.microsoft.com/en-us/library/cc245680.aspx * for what precalculated hashes are supposed to be stored... * * I can't reproduce all values which should contain "Digest" as realm, * am I doing something wrong or is w2k3 just broken...? * * W2K3 fills in following for a user: * * dn: CN=NewUser,OU=newtop,DC=sub1,DC=w2k3,DC=vmnet1,DC=vm,DC=base * sAMAccountName: NewUser2Sam * userPrincipalName: NewUser2Princ@sub1.w2k3.vmnet1.vm.base * * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 * b7ec9da91062199aee7d121e6710fe23 => newuser2sam:sub1:TestPwd2007 * 17d290bc5c9f463fac54c37a8cea134d => NEWUSER2SAM:SUB1:TestPwd2007 * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 * 5d57e7823938348127322e08cd81bcb5 => NewUser2Sam:sub1:TestPwd2007 * 07dd701bf8a011ece585de3d47237140 => NEWUSER2SAM:sub1:TestPwd2007 * e14fb0eb401498d2cb33c9aae1cc7f37 => newuser2sam:SUB1:TestPwd2007 * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * f52da1266a6bdd290ffd48b2c823dda7 => newuser2sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * d2b42f171248cec37a3c5c6b55404062 => NEWUSER2SAM:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * fff8d790ff6c152aaeb6ebe17b4021de => NewUser2Sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * 2a7563c3715bc418d626dabef378c008 => NEWUSER2SAM:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * c8e9557a87cd4200fda0c11d2fa03f96 => newuser2sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * 221c55284451ae9b3aacaa2a3c86f10f => NewUser2Princ@sub1.w2k3.vmnet1.vm.base::TestPwd2007 * 74e1be668853d4324d38c07e2acfb8ea => (w2k3 has a bug here!) newuser2princ@sub1.w2k3.vmnet1.vm.base::TestPwd2007 * e1e244ab7f098e3ae1761be7f9229bbb => NEWUSER2PRINC@SUB1.W2K3.VMNET1.VM.BASE::TestPwd2007 * 86db637df42513039920e605499c3af6 => SUB1\NewUser2Sam::TestPwd2007 * f5e43474dfaf067fee8197a253debaa2 => sub1\newuser2sam::TestPwd2007 * 2ecaa8382e2518e4b77a52422b279467 => SUB1\NEWUSER2SAM::TestPwd2007 * 31dc704d3640335b2123d4ee28aa1f11 => ??? changes with NewUser2Sam => NewUser1Sam * 36349f5cecd07320fb3bb0e119230c43 => ??? changes with NewUser2Sam => NewUser1Sam * 12adf019d037fb535c01fd0608e78d9d => ??? changes with NewUser2Sam => NewUser1Sam * 6feecf8e724906f3ee1105819c5105a1 => ??? changes with NewUser2Princ => NewUser1Princ * 6c6911f3de6333422640221b9c51ff1f => ??? changes with NewUser2Princ => NewUser1Princ * 4b279877e742895f9348ac67a8de2f69 => ??? changes with NewUser2Princ => NewUser1Princ * db0c6bff069513e3ebb9870d29b57490 => ??? changes with NewUser2Sam => NewUser1Sam * 45072621e56b1c113a4e04a8ff68cd0e => ??? changes with NewUser2Sam => NewUser1Sam * 11d1220abc44a9c10cf91ef4a9c1de02 => ??? changes with NewUser2Sam => NewUser1Sam * * dn: CN=NewUser,OU=newtop,DC=sub1,DC=w2k3,DC=vmnet1,DC=vm,DC=base * sAMAccountName: NewUser2Sam * * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 * b7ec9da91062199aee7d121e6710fe23 => newuser2sam:sub1:TestPwd2007 * 17d290bc5c9f463fac54c37a8cea134d => NEWUSER2SAM:SUB1:TestPwd2007 * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 * 5d57e7823938348127322e08cd81bcb5 => NewUser2Sam:sub1:TestPwd2007 * 07dd701bf8a011ece585de3d47237140 => NEWUSER2SAM:sub1:TestPwd2007 * e14fb0eb401498d2cb33c9aae1cc7f37 => newuser2sam:SUB1:TestPwd2007 * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * f52da1266a6bdd290ffd48b2c823dda7 => newuser2sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * d2b42f171248cec37a3c5c6b55404062 => NEWUSER2SAM:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * fff8d790ff6c152aaeb6ebe17b4021de => NewUser2Sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * 2a7563c3715bc418d626dabef378c008 => NEWUSER2SAM:sub1.w2k3.vmnet1.vm.base:TestPwd2007 * c8e9557a87cd4200fda0c11d2fa03f96 => newuser2sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 * 8a140d30b6f0a5912735dc1e3bc993b4 => NewUser2Sam@sub1.w2k3.vmnet1.vm.base::TestPwd2007 * 86d95b2faae6cae4ec261e7fbaccf093 => (here w2k3 is correct) newuser2sam@sub1.w2k3.vmnet1.vm.base::TestPwd2007 * dfeff1493110220efcdfc6362e5f5450 => NEWUSER2SAM@SUB1.W2K3.VMNET1.VM.BASE::TestPwd2007 * 86db637df42513039920e605499c3af6 => SUB1\NewUser2Sam::TestPwd2007 * f5e43474dfaf067fee8197a253debaa2 => sub1\newuser2sam::TestPwd2007 * 2ecaa8382e2518e4b77a52422b279467 => SUB1\NEWUSER2SAM::TestPwd2007 * 31dc704d3640335b2123d4ee28aa1f11 => ???M1 changes with NewUser2Sam => NewUser1Sam * 36349f5cecd07320fb3bb0e119230c43 => ???M1.L changes with newuser2sam => newuser1sam * 12adf019d037fb535c01fd0608e78d9d => ???M1.U changes with NEWUSER2SAM => NEWUSER1SAM * 569b4533f2d9e580211dd040e5e360a8 => ???M2 changes with NewUser2Princ => NewUser1Princ * 52528bddf310a587c5d7e6a9ae2cbb20 => ???M2.L changes with newuser2princ => newuser1princ * 4f629a4f0361289ca4255ab0f658fcd5 => ???M3 changes with NewUser2Princ => NewUser1Princ (doesn't depend on case of userPrincipal ) * db0c6bff069513e3ebb9870d29b57490 => ???M4 changes with NewUser2Sam => NewUser1Sam * 45072621e56b1c113a4e04a8ff68cd0e => ???M5 changes with NewUser2Sam => NewUser1Sam (doesn't depend on case of sAMAccountName) * 11d1220abc44a9c10cf91ef4a9c1de02 => ???M4.U changes with NEWUSER2SAM => NEWUSER1SAM */ /* * sAMAccountName, netbios_domain */ { .user = &sAMAccountName, .realm = &netbios_domain, }, { .user = &sAMAccountName_l, .realm = &netbios_domain_l, }, { .user = &sAMAccountName_u, .realm = &netbios_domain_u, }, { .user = &sAMAccountName, .realm = &netbios_domain_u, }, { .user = &sAMAccountName, .realm = &netbios_domain_l, }, { .user = &sAMAccountName_u, .realm = &netbios_domain_l, }, { .user = &sAMAccountName_l, .realm = &netbios_domain_u, }, /* * sAMAccountName, dns_domain * * TODO: * Windows preserves the case of the DNS domain, * Samba lower cases the domain at provision time * This means that for mixed case Domains, the WDigest08 hash * calculated by Samba differs from that calculated by Windows. * Until we get a real world use case this will remain a known * bug, as changing the case could have unforeseen impacts. * */ { .user = &sAMAccountName, .realm = &dns_domain, }, { .user = &sAMAccountName_l, .realm = &dns_domain_l, }, { .user = &sAMAccountName_u, .realm = &dns_domain_u, }, { .user = &sAMAccountName, .realm = &dns_domain_u, }, { .user = &sAMAccountName, .realm = &dns_domain_l, }, { .user = &sAMAccountName_u, .realm = &dns_domain_l, }, { .user = &sAMAccountName_l, .realm = &dns_domain_u, }, /* * userPrincipalName, no realm */ { .user = &userPrincipalName, }, { /* * NOTE: w2k3 messes this up, if the user has a real userPrincipalName, * the fallback to the sAMAccountName based userPrincipalName is correct */ .user = &userPrincipalName_l, }, { .user = &userPrincipalName_u, }, /* * nt4dom\sAMAccountName, no realm */ { .user = &sAMAccountName, .nt4dom = &netbios_domain }, { .user = &sAMAccountName_l, .nt4dom = &netbios_domain_l }, { .user = &sAMAccountName_u, .nt4dom = &netbios_domain_u }, /* * the following ones are guessed depending on the technet2 article * but not reproducible on a w2k3 server */ /* sAMAccountName with "Digest" realm */ { .user = &sAMAccountName, .realm = &digest }, { .user = &sAMAccountName_l, .realm = &digest }, { .user = &sAMAccountName_u, .realm = &digest }, /* userPrincipalName with "Digest" realm */ { .user = &userPrincipalName, .realm = &digest }, { .user = &userPrincipalName_l, .realm = &digest }, { .user = &userPrincipalName_u, .realm = &digest }, /* nt4dom\\sAMAccountName with "Digest" realm */ { .user = &sAMAccountName, .nt4dom = &netbios_domain, .realm = &digest }, { .user = &sAMAccountName_l, .nt4dom = &netbios_domain_l, .realm = &digest }, { .user = &sAMAccountName_u, .nt4dom = &netbios_domain_u, .realm = &digest }, }; int rc = LDB_ERR_OTHER; /* prepare DATA_BLOB's used in the combinations array */ sAMAccountName = data_blob_string_const(io->u.sAMAccountName); sAMAccountName_l = data_blob_string_const(strlower_talloc(io->ac, io->u.sAMAccountName)); if (!sAMAccountName_l.data) { return ldb_oom(ldb); } sAMAccountName_u = data_blob_string_const(strupper_talloc(io->ac, io->u.sAMAccountName)); if (!sAMAccountName_u.data) { return ldb_oom(ldb); } /* if the user doesn't have a userPrincipalName, create one (with lower case realm) */ if (!user_principal_name) { user_principal_name = talloc_asprintf(io->ac, "%s@%s", io->u.sAMAccountName, io->ac->status->domain_data.dns_domain); if (!user_principal_name) { return ldb_oom(ldb); } } userPrincipalName = data_blob_string_const(user_principal_name); userPrincipalName_l = data_blob_string_const(strlower_talloc(io->ac, user_principal_name)); if (!userPrincipalName_l.data) { return ldb_oom(ldb); } userPrincipalName_u = data_blob_string_const(strupper_talloc(io->ac, user_principal_name)); if (!userPrincipalName_u.data) { return ldb_oom(ldb); } netbios_domain = data_blob_string_const(io->ac->status->domain_data.netbios_domain); netbios_domain_l = data_blob_string_const(strlower_talloc(io->ac, io->ac->status->domain_data.netbios_domain)); if (!netbios_domain_l.data) { return ldb_oom(ldb); } netbios_domain_u = data_blob_string_const(strupper_talloc(io->ac, io->ac->status->domain_data.netbios_domain)); if (!netbios_domain_u.data) { return ldb_oom(ldb); } dns_domain = data_blob_string_const(io->ac->status->domain_data.dns_domain); dns_domain_l = data_blob_string_const(io->ac->status->domain_data.dns_domain); dns_domain_u = data_blob_string_const(io->ac->status->domain_data.realm); digest = data_blob_string_const("Digest"); delim = data_blob_string_const(":"); backslash = data_blob_string_const("\\"); pdb->num_hashes = ARRAY_SIZE(wdigest); pdb->hashes = talloc_array(io->ac, struct package_PrimaryWDigestHash, pdb->num_hashes); if (!pdb->hashes) { return ldb_oom(ldb); } for (i=0; i < ARRAY_SIZE(wdigest); i++) { gnutls_hash_hd_t hash_hnd = NULL; rc = gnutls_hash_init(&hash_hnd, GNUTLS_DIG_MD5); if (rc < 0) { rc = ldb_oom(ldb); goto out; } if (wdigest[i].nt4dom) { rc = gnutls_hash(hash_hnd, wdigest[i].nt4dom->data, wdigest[i].nt4dom->length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } rc = gnutls_hash(hash_hnd, backslash.data, backslash.length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } } rc = gnutls_hash(hash_hnd, wdigest[i].user->data, wdigest[i].user->length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } rc = gnutls_hash(hash_hnd, delim.data, delim.length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } if (wdigest[i].realm) { rc = gnutls_hash(hash_hnd, wdigest[i].realm->data, wdigest[i].realm->length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } } rc = gnutls_hash(hash_hnd, delim.data, delim.length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } rc = gnutls_hash(hash_hnd, io->n.cleartext_utf8->data, io->n.cleartext_utf8->length); if (rc < 0) { gnutls_hash_deinit(hash_hnd, NULL); rc = LDB_ERR_UNWILLING_TO_PERFORM; goto out; } gnutls_hash_deinit(hash_hnd, pdb->hashes[i].hash); } rc = LDB_SUCCESS; out: return rc; } #define SHA_SALT_PERMITTED_CHARS "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "0123456789./" #define SHA_SALT_SIZE 16 #define SHA_256_SCHEME "CryptSHA256" #define SHA_512_SCHEME "CryptSHA512" #define CRYPT "{CRYPT}" #define SHA_ID_LEN 3 #define SHA_256_ALGORITHM_ID 5 #define SHA_512_ALGORITHM_ID 6 #define ROUNDS_PARAMETER "rounds=" /* * Extract the crypt (3) algorithm number and number of hash rounds from the * supplied scheme string */ static bool parse_scheme(const char *scheme, int *algorithm, int *rounds) { const char *rp = NULL; /* Pointer to the 'rounds=' option */ char digits[21]; /* digits extracted from the rounds option */ int i = 0; /* loop index variable */ if (strncasecmp(SHA_256_SCHEME, scheme, strlen(SHA_256_SCHEME)) == 0) { *algorithm = SHA_256_ALGORITHM_ID; } else if (strncasecmp(SHA_512_SCHEME, scheme, strlen(SHA_256_SCHEME)) == 0) { *algorithm = SHA_512_ALGORITHM_ID; } else { return false; } rp = strcasestr(scheme, ROUNDS_PARAMETER); if (rp == NULL) { /* No options specified, use crypt default number of rounds */ *rounds = 0; return true; } rp += strlen(ROUNDS_PARAMETER); for (i = 0; isdigit(rp[i]) && i < (sizeof(digits) - 1); i++) { digits[i] = rp[i]; } digits[i] = '\0'; *rounds = atoi(digits); return true; } /* * Calculate the password hash specified by scheme, and return it in * hash_value */ static int setup_primary_userPassword_hash( TALLOC_CTX *ctx, struct setup_password_fields_io *io, const char* scheme, struct package_PrimaryUserPasswordValue *hash_value) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); const char *salt = NULL; /* Randomly generated salt */ const char *cmd = NULL; /* command passed to crypt */ const char *hash = NULL; /* password hash generated by crypt */ int algorithm = 0; /* crypt hash algorithm number */ int rounds = 0; /* The number of hash rounds */ DATA_BLOB *hash_blob = NULL; TALLOC_CTX *frame = talloc_stackframe(); #if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT_RN) struct crypt_data crypt_data = { .initialized = 0 /* working storage used by crypt */ }; #endif /* Generate a random password salt */ salt = generate_random_str_list(frame, SHA_SALT_SIZE, SHA_SALT_PERMITTED_CHARS); if (salt == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } /* determine the hashing algorithm and number of rounds*/ if (!parse_scheme(scheme, &algorithm, &rounds)) { ldb_asprintf_errstring( ldb, "setup_primary_userPassword: Invalid scheme of [%s] " "specified for 'password hash userPassword schemes' in " "samba.conf", scheme); TALLOC_FREE(frame); return LDB_ERR_OPERATIONS_ERROR; } hash_value->scheme = talloc_strdup(ctx, CRYPT); if (hash_value->scheme == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } hash_value->scheme_len = strlen(CRYPT) + 1; /* generate the id/salt parameter used by crypt */ if (rounds) { cmd = talloc_asprintf(frame, "$%d$rounds=%d$%s", algorithm, rounds, salt); if (cmd == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } } else { cmd = talloc_asprintf(frame, "$%d$%s", algorithm, salt); if (cmd == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } } /* * Relies on the assertion that cleartext_utf8->data is a zero * terminated UTF-8 string */ /* * crypt_r() and crypt() may return a null pointer upon error * depending on how libcrypt was configured, so we prefer * crypt_rn() from libcrypt / libxcrypt which always returns * NULL on error. * * POSIX specifies returning a null pointer and setting * errno. * * RHEL 7 (which does not use libcrypt / libxcrypt) returns a * non-NULL pointer from crypt_r() on success but (always?) * sets errno during internal processing in the NSS crypto * subsystem. * * By preferring crypt_rn we avoid the 'return non-NULL but * set-errno' that we otherwise cannot tell apart from the * RHEL 7 behaviour. */ errno = 0; #ifdef HAVE_CRYPT_RN hash = crypt_rn((char *)io->n.cleartext_utf8->data, cmd, &crypt_data, sizeof(crypt_data)); #elif HAVE_CRYPT_R hash = crypt_r((char *)io->n.cleartext_utf8->data, cmd, &crypt_data); #else /* * No crypt_r falling back to crypt, which is NOT thread safe * Thread safety MT-Unsafe race:crypt */ hash = crypt((char *)io->n.cleartext_utf8->data, cmd); #endif /* * On error, crypt() and crypt_r() may return a null pointer, * or a pointer to an invalid hash beginning with a '*'. */ if (hash == NULL || hash[0] == '*') { char buf[1024]; const char *reason = NULL; if (errno == ERANGE) { reason = "Password exceeds maximum length allowed for crypt() hashing"; } else { int err = strerror_r(errno, buf, sizeof(buf)); if (err == 0) { reason = buf; } else { reason = "Unknown error"; } } ldb_asprintf_errstring( ldb, "setup_primary_userPassword: generation of a %s " "password hash failed: (%s)", scheme, reason); TALLOC_FREE(frame); return LDB_ERR_OPERATIONS_ERROR; } hash_blob = talloc_zero(ctx, DATA_BLOB); if (hash_blob == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } *hash_blob = data_blob_talloc(hash_blob, (const uint8_t *)hash, strlen(hash)); if (hash_blob->data == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } hash_value->value = hash_blob; TALLOC_FREE(frame); return LDB_SUCCESS; } /* * Calculate the desired extra password hashes */ static int setup_primary_userPassword( struct setup_password_fields_io *io, const struct supplementalCredentialsBlob *old_scb, struct package_PrimaryUserPasswordBlob *p_userPassword_b) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); TALLOC_CTX *frame = talloc_stackframe(); int i; int ret; /* * Save the current nt_hash, use this to determine if the password * has been changed by windows. Which will invalidate the userPassword * hash. Note once NTLM-Strong-NOWTF becomes available it should be * used in preference to the NT password hash */ if (io->g.nt_hash == NULL) { /* * 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; } /* * Determine the number of hashes * Note: that currently there is no limit on the number of hashes * no checking is done on the number of schemes specified * or for uniqueness. */ p_userPassword_b->num_hashes = 0; for (i = 0; io->ac->userPassword_schemes[i]; i++) { p_userPassword_b->num_hashes++; } p_userPassword_b->hashes = talloc_array(io->ac, struct package_PrimaryUserPasswordValue, p_userPassword_b->num_hashes); if (p_userPassword_b->hashes == NULL) { TALLOC_FREE(frame); return ldb_oom(ldb); } for (i = 0; io->ac->userPassword_schemes[i]; i++) { ret = setup_primary_userPassword_hash( p_userPassword_b->hashes, io, io->ac->userPassword_schemes[i], &p_userPassword_b->hashes[i]); if (ret != LDB_SUCCESS) { TALLOC_FREE(frame); return ret; } } TALLOC_FREE(frame); return LDB_SUCCESS; } static int setup_primary_samba_gpg(struct setup_password_fields_io *io, struct package_PrimarySambaGPGBlob *pgb) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); #ifdef ENABLE_GPGME gpgme_error_t gret; gpgme_ctx_t ctx = NULL; size_t num_keys = str_list_length(io->ac->gpg_key_ids); gpgme_key_t keys[num_keys+1]; size_t ki = 0; size_t kr = 0; gpgme_data_t plain_data = NULL; gpgme_data_t crypt_data = NULL; size_t crypt_length = 0; char *crypt_mem = NULL; gret = gpgme_new(&ctx); if (gret != GPG_ERR_NO_ERROR) { ldb_debug(ldb, LDB_DEBUG_ERROR, "%s:%s: gret[%u] %s\n", __location__, __func__, gret, gpgme_strerror(gret)); return ldb_module_operr(io->ac->module); } gpgme_set_armor(ctx, 1); gret = gpgme_data_new_from_mem(&plain_data, (const char *)io->n.cleartext_utf16->data, io->n.cleartext_utf16->length, 0 /* no copy */); if (gret != GPG_ERR_NO_ERROR) { ldb_debug(ldb, LDB_DEBUG_ERROR, "%s:%s: gret[%u] %s\n", __location__, __func__, gret, gpgme_strerror(gret)); gpgme_release(ctx); return ldb_module_operr(io->ac->module); } gret = gpgme_data_new(&crypt_data); if (gret != GPG_ERR_NO_ERROR) { ldb_debug(ldb, LDB_DEBUG_ERROR, "%s:%s: gret[%u] %s\n", __location__, __func__, gret, gpgme_strerror(gret)); gpgme_data_release(plain_data); gpgme_release(ctx); return ldb_module_operr(io->ac->module); } for (ki = 0; ki < num_keys; ki++) { const char *key_id = io->ac->gpg_key_ids[ki]; size_t len = strlen(key_id); keys[ki] = NULL; if (len < 16) { ldb_debug(ldb, LDB_DEBUG_FATAL, "%s:%s: ki[%zu] key_id[%s] strlen < 16, " "please specify at least the 64bit key id\n", __location__, __func__, ki, key_id); for (kr = 0; keys[kr] != NULL; kr++) { gpgme_key_release(keys[kr]); } gpgme_data_release(crypt_data); gpgme_data_release(plain_data); gpgme_release(ctx); return ldb_module_operr(io->ac->module); } gret = gpgme_get_key(ctx, key_id, &keys[ki], 0 /* public key */); if (gret != GPG_ERR_NO_ERROR) { keys[ki] = NULL; if (gpg_err_source(gret) == GPG_ERR_SOURCE_GPGME && gpg_err_code(gret) == GPG_ERR_EOF) { ldb_debug(ldb, LDB_DEBUG_ERROR, "Invalid " "'password hash gpg key ids': " "Public Key ID [%s] " "not found in keyring\n", key_id); } else { ldb_debug(ldb, LDB_DEBUG_ERROR, "%s:%s: ki[%zu] key_id[%s] " "gret[%u] %s\n", __location__, __func__, ki, key_id, gret, gpgme_strerror(gret)); } for (kr = 0; keys[kr] != NULL; kr++) { gpgme_key_release(keys[kr]); } gpgme_data_release(crypt_data); gpgme_data_release(plain_data); gpgme_release(ctx); return ldb_module_operr(io->ac->module); } } keys[ki] = NULL; gret = gpgme_op_encrypt(ctx, keys, GPGME_ENCRYPT_ALWAYS_TRUST, plain_data, crypt_data); gpgme_data_release(plain_data); plain_data = NULL; for (kr = 0; keys[kr] != NULL; kr++) { gpgme_key_release(keys[kr]); keys[kr] = NULL; } gpgme_release(ctx); ctx = NULL; if (gret != GPG_ERR_NO_ERROR) { ldb_debug(ldb, LDB_DEBUG_ERROR, "%s:%s: gret[%u] %s\n", __location__, __func__, gret, gpgme_strerror(gret)); gpgme_data_release(crypt_data); return ldb_module_operr(io->ac->module); } crypt_mem = gpgme_data_release_and_get_mem(crypt_data, &crypt_length); crypt_data = NULL; if (crypt_mem == NULL) { return ldb_module_oom(io->ac->module); } pgb->gpg_blob = data_blob_talloc(io->ac, (const uint8_t *)crypt_mem, crypt_length); gpgme_free(crypt_mem); crypt_mem = NULL; crypt_length = 0; if (pgb->gpg_blob.data == NULL) { return ldb_module_oom(io->ac->module); } return LDB_SUCCESS; #else /* ENABLE_GPGME */ ldb_debug_set(ldb, LDB_DEBUG_FATAL, "You configured 'password hash gpg key ids', " "but GPGME support is missing. (%s:%d)", __FILE__, __LINE__); return LDB_ERR_UNWILLING_TO_PERFORM; #endif /* else ENABLE_GPGME */ } #define NUM_PACKAGES 6 static int setup_supplemental_field(struct setup_password_fields_io *io) { struct ldb_context *ldb; struct supplementalCredentialsBlob scb = {}; struct supplementalCredentialsBlob *old_scb = NULL; /* * Packages + * ( Kerberos-Newer-Keys, Kerberos, * WDigest, CLEARTEXT, userPassword, SambaGPG) */ uint32_t num_names = 0; const char *names[1+NUM_PACKAGES] = {}; uint32_t num_packages = 0; struct supplementalCredentialsPackage packages[1+NUM_PACKAGES] = {}; struct supplementalCredentialsPackage *pp = packages; int ret; enum ndr_err_code ndr_err; bool do_newer_keys = false; bool do_cleartext = false; bool do_samba_gpg = false; struct loadparm_context *lp_ctx = NULL; ldb = ldb_module_get_ctx(io->ac->module); lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), struct loadparm_context); if (!io->n.cleartext_utf8) { /* * when we don't have a cleartext password * we can't setup a supplementalCredentials value */ return LDB_SUCCESS; } /* if there's an old supplementalCredentials blob then use it */ if (io->o.supplemental) { if (io->o.scb.sub.signature == SUPPLEMENTAL_CREDENTIALS_SIGNATURE) { old_scb = &io->o.scb; } else { ldb_debug(ldb, LDB_DEBUG_ERROR, "setup_supplemental_field: " "supplementalCredentialsBlob " "signature[0x%04X] expected[0x%04X]", io->o.scb.sub.signature, SUPPLEMENTAL_CREDENTIALS_SIGNATURE); } } /* Per MS-SAMR 3.1.1.8.11.6 we create AES keys if our domain functionality level is 2008 or higher */ /* * The ordering is this * * Primary:Kerberos-Newer-Keys (optional) * Primary:Kerberos * Primary:WDigest * Primary:CLEARTEXT (optional) * Primary:userPassword * Primary:SambaGPG (optional) * * And the 'Packages' package is insert before the last * other package. * * Note: it's important that Primary:SambaGPG is added as * the last element. This is the indication that it matches * the current password. When a password change happens on * a Windows DC, it will keep the old Primary:SambaGPG value, * but as the first element. */ do_newer_keys = (dsdb_functional_level(ldb) >= DS_DOMAIN_FUNCTION_2008); if (do_newer_keys) { struct package_PrimaryKerberosBlob pknb; DATA_BLOB pknb_blob; char *pknb_hexstr; /* * setup 'Primary:Kerberos-Newer-Keys' element */ names[num_names++] = "Kerberos-Newer-Keys"; ret = setup_primary_kerberos_newer(io, old_scb, &pknb); if (ret != LDB_SUCCESS) { return ret; } ndr_err = ndr_push_struct_blob( &pknb_blob, io->ac, &pknb, (ndr_push_flags_fn_t)ndr_push_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push " "package_PrimaryKerberosNeverBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pknb_hexstr = data_blob_hex_string_upper(io->ac, &pknb_blob); if (!pknb_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:Kerberos-Newer-Keys"; pp->reserved = 1; pp->data = pknb_hexstr; pp++; num_packages++; } { /* * setup 'Primary:Kerberos' element */ /* Primary:Kerberos */ struct package_PrimaryKerberosBlob pkb; DATA_BLOB pkb_blob; char *pkb_hexstr; names[num_names++] = "Kerberos"; ret = setup_primary_kerberos(io, old_scb, &pkb); if (ret != LDB_SUCCESS) { return ret; } ndr_err = ndr_push_struct_blob( &pkb_blob, io->ac, &pkb, (ndr_push_flags_fn_t)ndr_push_package_PrimaryKerberosBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push package_PrimaryKerberosBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pkb_hexstr = data_blob_hex_string_upper(io->ac, &pkb_blob); if (!pkb_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:Kerberos"; pp->reserved = 1; pp->data = pkb_hexstr; pp++; num_packages++; } if (lpcfg_weak_crypto(lp_ctx) == SAMBA_WEAK_CRYPTO_ALLOWED) { /* * setup 'Primary:WDigest' element */ struct package_PrimaryWDigestBlob pdb; DATA_BLOB pdb_blob; char *pdb_hexstr; names[num_names++] = "WDigest"; ret = setup_primary_wdigest(io, old_scb, &pdb); if (ret != LDB_SUCCESS) { return ret; } ndr_err = ndr_push_struct_blob( &pdb_blob, io->ac, &pdb, (ndr_push_flags_fn_t)ndr_push_package_PrimaryWDigestBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push package_PrimaryWDigestBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pdb_hexstr = data_blob_hex_string_upper(io->ac, &pdb_blob); if (!pdb_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:WDigest"; pp->reserved = 1; pp->data = pdb_hexstr; pp++; num_packages++; } /* * setup 'Primary:CLEARTEXT' element */ if (io->ac->status->domain_data.store_cleartext && (io->u.userAccountControl & UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED)) { do_cleartext = true; } if (do_cleartext) { struct package_PrimaryCLEARTEXTBlob pcb; DATA_BLOB pcb_blob; char *pcb_hexstr; names[num_names++] = "CLEARTEXT"; pcb.cleartext = *io->n.cleartext_utf16; ndr_err = ndr_push_struct_blob( &pcb_blob, io->ac, &pcb, (ndr_push_flags_fn_t)ndr_push_package_PrimaryCLEARTEXTBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push package_PrimaryCLEARTEXTBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pcb_hexstr = data_blob_hex_string_upper(io->ac, &pcb_blob); if (!pcb_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:CLEARTEXT"; pp->reserved = 1; pp->data = pcb_hexstr; pp++; num_packages++; } /* * Don't generate crypt() or similar password for the krbtgt account. * It's unnecessary, and the length of the cleartext in UTF-8 form * exceeds the maximum (CRYPT_MAX_PASSPHRASE_SIZE) allowed by crypt(). */ if (io->ac->userPassword_schemes && !io->u.is_krbtgt) { /* * setup 'Primary:userPassword' element */ struct package_PrimaryUserPasswordBlob p_userPassword_b; DATA_BLOB p_userPassword_b_blob; char *p_userPassword_b_hexstr; names[num_names++] = "userPassword"; ret = setup_primary_userPassword(io, old_scb, &p_userPassword_b); if (ret != LDB_SUCCESS) { return ret; } ndr_err = ndr_push_struct_blob( &p_userPassword_b_blob, io->ac, &p_userPassword_b, (ndr_push_flags_fn_t) ndr_push_package_PrimaryUserPasswordBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: failed to push " "package_PrimaryUserPasswordBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } p_userPassword_b_hexstr = data_blob_hex_string_upper( io->ac, &p_userPassword_b_blob); if (!p_userPassword_b_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:userPassword"; pp->reserved = 1; pp->data = p_userPassword_b_hexstr; pp++; num_packages++; } /* * setup 'Primary:SambaGPG' element */ if (io->ac->gpg_key_ids != NULL) { do_samba_gpg = true; } if (do_samba_gpg) { struct package_PrimarySambaGPGBlob pgb; DATA_BLOB pgb_blob; char *pgb_hexstr; names[num_names++] = "SambaGPG"; ret = setup_primary_samba_gpg(io, &pgb); if (ret != LDB_SUCCESS) { return ret; } ndr_err = ndr_push_struct_blob(&pgb_blob, io->ac, &pgb, (ndr_push_flags_fn_t)ndr_push_package_PrimarySambaGPGBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring(ldb, "setup_supplemental_field: failed to " "push package_PrimarySambaGPGBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pgb_hexstr = data_blob_hex_string_upper(io->ac, &pgb_blob); if (!pgb_hexstr) { return ldb_oom(ldb); } pp->name = "Primary:SambaGPG"; pp->reserved = 1; pp->data = pgb_hexstr; pp++; num_packages++; } /* * setup 'Packages' element */ { struct package_PackagesBlob pb; DATA_BLOB pb_blob; char *pb_hexstr; pb.names = names; ndr_err = ndr_push_struct_blob( &pb_blob, io->ac, &pb, (ndr_push_flags_fn_t)ndr_push_package_PackagesBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push package_PackagesBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } pb_hexstr = data_blob_hex_string_upper(io->ac, &pb_blob); if (!pb_hexstr) { return ldb_oom(ldb); } pp->name = "Packages"; pp->reserved = 2; pp->data = pb_hexstr; num_packages++; /* * We don't increment pp so it's pointing to the last package */ } /* * setup 'supplementalCredentials' value */ { /* * The 'Packages' element needs to be the second last element * in supplementalCredentials */ struct supplementalCredentialsPackage temp; struct supplementalCredentialsPackage *prev; prev = pp-1; temp = *prev; *prev = *pp; *pp = temp; scb.sub.signature = SUPPLEMENTAL_CREDENTIALS_SIGNATURE; scb.sub.num_packages = num_packages; scb.sub.packages = packages; ndr_err = ndr_push_struct_blob( &io->g.supplemental, io->ac, &scb, (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring( ldb, "setup_supplemental_field: " "failed to push supplementalCredentialsBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } } return LDB_SUCCESS; } static int setup_last_set_field(struct setup_password_fields_io *io) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); const struct ldb_message *msg = NULL; struct timeval tv = { .tv_sec = 0 }; const struct ldb_val *old_val = NULL; const struct ldb_val *new_val = NULL; int ret; switch (io->ac->req->operation) { case LDB_ADD: msg = io->ac->req->op.add.message; break; case LDB_MODIFY: msg = io->ac->req->op.mod.message; break; default: return LDB_ERR_OPERATIONS_ERROR; break; } if (io->ac->pwd_last_set_bypass) { struct ldb_message_element *el = NULL; size_t i; size_t count = 0; /* * This is a message from pdb_samba_dsdb_replace_by_sam() * * We want to ensure there is only one pwdLastSet element, and * it isn't deleting. */ if (msg == NULL) { return LDB_ERR_CONSTRAINT_VIOLATION; } for (i = 0; i < msg->num_elements; i++) { if (ldb_attr_cmp(msg->elements[i].name, "pwdLastSet") == 0) { count++; el = &msg->elements[i]; } } if (count != 1) { return LDB_ERR_CONSTRAINT_VIOLATION; } if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE) { return LDB_ERR_CONSTRAINT_VIOLATION; } io->g.last_set = samdb_result_nttime(msg, "pwdLastSet", 0); return LDB_SUCCESS; } ret = msg_find_old_and_new_pwd_val(msg, "pwdLastSet", io->ac->req->operation, &new_val, &old_val); if (ret != LDB_SUCCESS) { return ret; } if (old_val != NULL && new_val == NULL) { ldb_set_errstring(ldb, "'pwdLastSet' deletion is not allowed!"); return LDB_ERR_UNWILLING_TO_PERFORM; } io->g.last_set = UINT64_MAX; if (new_val != NULL) { struct ldb_message *tmp_msg = NULL; tmp_msg = ldb_msg_new(io->ac); if (tmp_msg == NULL) { return ldb_module_oom(io->ac->module); } if (old_val != NULL) { NTTIME old_last_set = 0; ret = ldb_msg_add_value(tmp_msg, "oldval", old_val, NULL); if (ret != LDB_SUCCESS) { return ret; } old_last_set = samdb_result_nttime(tmp_msg, "oldval", 1); if (io->u.pwdLastSet != old_last_set) { return dsdb_module_werror(io->ac->module, LDB_ERR_NO_SUCH_ATTRIBUTE, WERR_DS_CANT_REM_MISSING_ATT_VAL, "setup_last_set_field: old pwdLastSet " "value not found!"); } } ret = ldb_msg_add_value(tmp_msg, "newval", new_val, NULL); if (ret != LDB_SUCCESS) { return ret; } io->g.last_set = samdb_result_nttime(tmp_msg, "newval", 1); } else if (ldb_msg_find_element(msg, "pwdLastSet")) { ldb_set_errstring(ldb, "'pwdLastSet' deletion is not allowed!"); return LDB_ERR_UNWILLING_TO_PERFORM; } else if (io->ac->smartcard_reset) { /* * adding UF_SMARTCARD_REQUIRED doesn't update * pwdLastSet implicitly. */ io->ac->update_lastset = false; } /* only 0 or -1 (0xFFFFFFFFFFFFFFFF) are allowed */ switch (io->g.last_set) { case 0: if (!io->ac->pwd_last_set_default) { break; } if (!io->ac->update_password) { break; } FALL_THROUGH; case UINT64_MAX: if (!io->ac->update_password && io->u.pwdLastSet != 0 && io->u.pwdLastSet != UINT64_MAX) { /* * Just setting pwdLastSet to -1, while not changing * any password field has no effect if pwdLastSet * is already non-zero. */ io->ac->update_lastset = false; break; } /* -1 means set it as now */ GetTimeOfDay(&tv); io->g.last_set = timeval_to_nttime(&tv); break; default: return dsdb_module_werror(io->ac->module, LDB_ERR_OTHER, WERR_INVALID_PARAMETER, "setup_last_set_field: " "pwdLastSet must be 0 or -1 only!"); } if (io->ac->req->operation == LDB_ADD) { /* * We always need to store the value on add * operations. */ return LDB_SUCCESS; } if (io->g.last_set == io->u.pwdLastSet) { /* * Just setting pwdLastSet to 0, is no-op if it's already 0. */ io->ac->update_lastset = false; } return LDB_SUCCESS; } static int setup_given_passwords(struct setup_password_fields_io *io, struct setup_password_fields_given *g) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); if (g->cleartext_utf8) { struct ldb_val *cleartext_utf16_blob; cleartext_utf16_blob = talloc(io->ac, struct ldb_val); if (!cleartext_utf16_blob) { return ldb_oom(ldb); } if (!convert_string_talloc(io->ac, CH_UTF8, CH_UTF16, g->cleartext_utf8->data, g->cleartext_utf8->length, &cleartext_utf16_blob->data, &cleartext_utf16_blob->length)) { if (g->cleartext_utf8->length != 0) { talloc_free(cleartext_utf16_blob); ldb_asprintf_errstring(ldb, "setup_password_fields: " "failed to generate UTF16 password from cleartext UTF8 one for user '%s'!", io->u.sAMAccountName); return LDB_ERR_CONSTRAINT_VIOLATION; } else { /* passwords with length "0" are valid! */ cleartext_utf16_blob->data = NULL; cleartext_utf16_blob->length = 0; } } g->cleartext_utf16 = cleartext_utf16_blob; } else if (g->cleartext_utf16) { struct ldb_val *cleartext_utf8_blob; cleartext_utf8_blob = talloc(io->ac, struct ldb_val); if (!cleartext_utf8_blob) { return ldb_oom(ldb); } if (!convert_string_talloc(io->ac, CH_UTF16MUNGED, CH_UTF8, g->cleartext_utf16->data, g->cleartext_utf16->length, &cleartext_utf8_blob->data, &cleartext_utf8_blob->length)) { if (g->cleartext_utf16->length != 0) { /* We must bail out here, the input wasn't even * a multiple of 2 bytes */ talloc_free(cleartext_utf8_blob); ldb_asprintf_errstring(ldb, "setup_password_fields: " "failed to generate UTF8 password from cleartext UTF 16 one for user '%s' - the latter had odd length (length must be a multiple of 2)!", io->u.sAMAccountName); return LDB_ERR_CONSTRAINT_VIOLATION; } else { /* passwords with length "0" are valid! */ cleartext_utf8_blob->data = NULL; cleartext_utf8_blob->length = 0; } } g->cleartext_utf8 = cleartext_utf8_blob; } if (g->cleartext_utf16) { struct samr_Password *nt_hash; nt_hash = talloc(io->ac, struct samr_Password); if (!nt_hash) { return ldb_oom(ldb); } g->nt_hash = nt_hash; /* compute the new nt hash */ mdfour(nt_hash->hash, g->cleartext_utf16->data, 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; } static int setup_password_fields(struct setup_password_fields_io *io) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); int ret; ret = setup_last_set_field(io); if (ret != LDB_SUCCESS) { return ret; } if (!io->ac->update_password) { return LDB_SUCCESS; } if (io->u.is_krbtgt) { size_t min = 196; size_t max = 255; size_t diff = max - min; size_t len = max; struct ldb_val *krbtgt_utf16 = NULL; if (!io->ac->pwd_reset) { return dsdb_module_werror(io->ac->module, LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS, WERR_DS_ATT_ALREADY_EXISTS, "Password change on krbtgt not permitted!"); } if (io->n.cleartext_utf16 == NULL) { return dsdb_module_werror(io->ac->module, LDB_ERR_UNWILLING_TO_PERFORM, WERR_DS_INVALID_ATTRIBUTE_SYNTAX, "Password reset on krbtgt requires UTF16!"); } /* * Instead of taking the callers value, * we just generate a new random value here. * * Include null termination in the array. */ if (diff > 0) { size_t tmp; generate_random_buffer((uint8_t *)&tmp, sizeof(tmp)); tmp %= diff; len = min + tmp; } krbtgt_utf16 = talloc_zero(io->ac, struct ldb_val); if (krbtgt_utf16 == NULL) { return ldb_oom(ldb); } *krbtgt_utf16 = data_blob_talloc_zero(krbtgt_utf16, (len+1)*2); if (krbtgt_utf16->data == NULL) { return ldb_oom(ldb); } krbtgt_utf16->length = len * 2; generate_secret_buffer(krbtgt_utf16->data, krbtgt_utf16->length); io->n.cleartext_utf16 = krbtgt_utf16; } /* transform the old password (for password changes) */ ret = setup_given_passwords(io, &io->og); if (ret != LDB_SUCCESS) { return ret; } /* transform the new password */ ret = setup_given_passwords(io, &io->n); if (ret != LDB_SUCCESS) { return ret; } if (io->n.cleartext_utf8) { ret = setup_kerberos_keys(io); if (ret != LDB_SUCCESS) { return ret; } } /* * 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; } ret = setup_supplemental_field(io); if (ret != LDB_SUCCESS) { return ret; } return LDB_SUCCESS; } static int setup_smartcard_reset(struct setup_password_fields_io *io) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); struct supplementalCredentialsBlob scb = { .__ndr_size = 0 }; enum ndr_err_code ndr_err; if (!io->ac->smartcard_reset) { return LDB_SUCCESS; } io->g.nt_hash = talloc(io->ac, struct samr_Password); if (io->g.nt_hash == NULL) { return ldb_module_oom(io->ac->module); } generate_secret_buffer(io->g.nt_hash->hash, sizeof(io->g.nt_hash->hash)); io->g.nt_history_len = 0; /* * We take the "old" value and store it * with num_packages = 0. * * On "add" we have scb.sub.signature == 0, which * results in: * * [0000] 00 00 00 00 00 00 00 00 00 00 00 00 00 * * On modify it's likely to be scb.sub.signature == * SUPPLEMENTAL_CREDENTIALS_SIGNATURE (0x0050), which results in * something like: * * [0000] 00 00 00 00 62 00 00 00 00 00 00 00 20 00 20 00 * [0010] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 * [0020] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 * [0030] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 * [0040] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 * [0050] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 * [0060] 20 00 20 00 20 00 20 00 20 00 20 00 50 00 00 * * See https://bugzilla.samba.org/show_bug.cgi?id=11441 * and ndr_{push,pull}_supplementalCredentialsSubBlob(). */ scb = io->o.scb; scb.sub.num_packages = 0; /* * setup 'supplementalCredentials' value without packages */ ndr_err = ndr_push_struct_blob(&io->g.supplemental, io->ac, &scb, (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { NTSTATUS status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring(ldb, "setup_smartcard_reset: " "failed to push supplementalCredentialsBlob: %s", nt_errstr(status)); return LDB_ERR_OPERATIONS_ERROR; } io->ac->update_password = true; return LDB_SUCCESS; } static int make_error_and_update_badPwdCount(struct setup_password_fields_io *io, WERROR *werror) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); struct ldb_message *mod_msg = NULL; struct ldb_message *pso_msg = NULL; struct ldb_message *current = NULL; NTSTATUS status = NT_STATUS_OK; int ret; /* The errors we will actually return */ int dbg_ret; /* The errors we can only complain about in logs */ /* * OK, horrible semantics ahead. * * - We need to abort any existing transaction * - create a transaction around the badPwdCount update * - re-open the transaction so the upper layer * doesn't know what happened. * * This is needed because returning an error to the upper * layer will cancel the transaction and undo the badPwdCount * update. */ /* * Checking errors here is a bit pointless. * What can we do if we can't end the transaction? */ dbg_ret = ldb_next_del_trans(io->ac->module); if (dbg_ret != LDB_SUCCESS) { ldb_debug(ldb, LDB_DEBUG_FATAL, "Failed to abort transaction prior to update of badPwdCount of %s: %s", ldb_dn_get_linearized(io->ac->search_res->message->dn), ldb_errstring(ldb)); /* * just return the original error */ goto done; } /* Likewise, what should we do if we can't open a new transaction? */ dbg_ret = ldb_next_start_trans(io->ac->module); if (dbg_ret != LDB_SUCCESS) { ldb_debug(ldb, LDB_DEBUG_ERROR, "Failed to open transaction to update badPwdCount of %s: %s", ldb_dn_get_linearized(io->ac->search_res->message->dn), ldb_errstring(ldb)); /* * just return the original error */ goto done; } /* * Re-read the account details, using the GUID in case the DN * is being changed. */ status = authsam_reread_user_logon_data( ldb, io->ac, io->ac->search_res->message, ¤t); if (!NT_STATUS_IS_OK(status)) { /* The re-read can return account locked out, as well * as an internal error */ goto end_transaction; } /* PSO search result is optional (NULL if no PSO applies) */ if (io->ac->pso_res != NULL) { pso_msg = io->ac->pso_res->message; } status = dsdb_update_bad_pwd_count(io->ac, ldb, current, io->ac->dom_res->message, pso_msg, &mod_msg); if (!NT_STATUS_IS_OK(status)) { goto end_transaction; } if (mod_msg == NULL) { goto end_transaction; } dbg_ret = dsdb_module_modify(io->ac->module, mod_msg, DSDB_FLAG_NEXT_MODULE, io->ac->req); if (dbg_ret != LDB_SUCCESS) { ldb_debug(ldb, LDB_DEBUG_ERROR, "Failed to update badPwdCount of %s: %s", ldb_dn_get_linearized(io->ac->search_res->message->dn), ldb_errstring(ldb)); /* * We can only ignore this... */ } end_transaction: dbg_ret = ldb_next_end_trans(io->ac->module); if (dbg_ret != LDB_SUCCESS) { ldb_debug(ldb, LDB_DEBUG_ERROR, "Failed to close transaction to update badPwdCount of %s: %s", ldb_dn_get_linearized(io->ac->search_res->message->dn), ldb_errstring(ldb)); /* * We can only ignore this... */ } dbg_ret = ldb_next_start_trans(io->ac->module); if (dbg_ret != LDB_SUCCESS) { ldb_debug(ldb, LDB_DEBUG_ERROR, "Failed to open transaction after update of badPwdCount of %s: %s", ldb_dn_get_linearized(io->ac->search_res->message->dn), ldb_errstring(ldb)); /* * We can only ignore this... */ } done: ret = LDB_ERR_CONSTRAINT_VIOLATION; if (NT_STATUS_EQUAL(status, NT_STATUS_ACCOUNT_LOCKED_OUT)) { *werror = WERR_ACCOUNT_LOCKED_OUT; } else { *werror = WERR_INVALID_PASSWORD; } ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "The old password specified doesn't match!", W_ERROR_V(*werror), ldb_strerror(ret)); return ret; } static int check_password_restrictions(struct setup_password_fields_io *io, WERROR *werror) { 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); struct dsdb_encrypted_connection_state *opaque_connection_state = ldb_get_opaque(ldb,DSDB_OPAQUE_ENCRYPTED_CONNECTION_STATE_NAME); *werror = WERR_INVALID_PARAMETER; if (!io->ac->update_password) { return LDB_SUCCESS; } /* * Prevent update password on an insecure connection. * The opaque is added in the ldap backend init. */ if (opaque_connection_state != NULL && !opaque_connection_state->using_encrypted_connection) { ret = LDB_ERR_UNWILLING_TO_PERFORM; *werror = WERR_GEN_FAILURE; ldb_asprintf_errstring(ldb, "%08X: SvcErr: DSID-031A126C, " "problem 5003 (WILL_NOT_PERFORM), " "data 0\n" "Password modification over LDAP " "must be over an encrypted connection", W_ERROR_V(*werror)); return ret; } /* * First check the old password is correct, for password * changes when this hasn't already been checked by a * trustworthy layer above */ 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 == NULL && io->og.aes_256.length == 0) { ldb_asprintf_errstring(ldb, "check_password_restrictions: " "You need to provide the old password in order " "to change it!"); 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 (!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); } } if (io->u.restrictions == 0) { /* FIXME: Is this right? */ return LDB_SUCCESS; } /* Password minimum age: yes, this is a minus. The ages are in negative 100nsec units! */ if ((io->u.pwdLastSet - io->ac->status->domain_data.minPwdAge > io->g.last_set) && !io->ac->pwd_reset) { ret = LDB_ERR_CONSTRAINT_VIOLATION; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "password is too young to change!", W_ERROR_V(*werror), ldb_strerror(ret)); return ret; } /* * Fundamental password checks done by the call * "samdb_check_password". * It is also in use by "dcesrv_samr_ValidatePassword". */ if (io->n.cleartext_utf8 != NULL) { enum samr_ValidationStatus vstat; vstat = samdb_check_password(io->ac, lp_ctx, io->u.sAMAccountName, io->u.user_principal_name, io->u.displayName, io->n.cleartext_utf8, io->ac->status->domain_data.pwdProperties, io->ac->status->domain_data.minPwdLength); switch (vstat) { case SAMR_VALIDATION_STATUS_SUCCESS: /* perfect -> proceed! */ break; case SAMR_VALIDATION_STATUS_PWD_TOO_SHORT: ret = LDB_ERR_CONSTRAINT_VIOLATION; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "the password is too short. It should be equal to or longer than %u characters!", W_ERROR_V(*werror), ldb_strerror(ret), io->ac->status->domain_data.minPwdLength); io->ac->status->reject_reason = SAM_PWD_CHANGE_PASSWORD_TOO_SHORT; return ret; case SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH: ret = LDB_ERR_CONSTRAINT_VIOLATION; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "the password does not meet the complexity criteria!", W_ERROR_V(*werror), ldb_strerror(ret)); io->ac->status->reject_reason = SAM_PWD_CHANGE_NOT_COMPLEX; return ret; default: ret = LDB_ERR_CONSTRAINT_VIOLATION; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "the password doesn't fit due to a miscellaneous restriction!", W_ERROR_V(*werror), ldb_strerror(ret)); return ret; } } if (io->ac->pwd_reset) { *werror = WERR_OK; return LDB_SUCCESS; } /* * 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; } } 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) { 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; } } } /* * 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 won't 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; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "password changes disabled!", W_ERROR_V(*werror), ldb_strerror(ret)); return ret; } /* can this user change the password? */ if (io->u.userAccountControl & UF_PASSWD_CANT_CHANGE) { ret = LDB_ERR_CONSTRAINT_VIOLATION; *werror = WERR_PASSWORD_RESTRICTION; ldb_asprintf_errstring(ldb, "%08X: %s - check_password_restrictions: " "password can't be changed on this account!", W_ERROR_V(*werror), ldb_strerror(ret)); return ret; } return LDB_SUCCESS; } static int check_password_restrictions_and_log(struct setup_password_fields_io *io) { WERROR werror; int ret = check_password_restrictions(io, &werror); struct ph_context *ac = io->ac; /* * Password resets are not authentication events, and if the * upper layer checked the password and supplied the hash * values as proof, then this is also not an authentication * even at this layer (already logged). This is to log LDAP * password changes. */ /* Do not record a failure in the auth log below in the success case */ if (ret == LDB_SUCCESS) { werror = WERR_OK; } if (ac->pwd_reset == false && ac->change == NULL) { struct ldb_context *ldb = ldb_module_get_ctx(ac->module); struct imessaging_context *msg_ctx; struct loadparm_context *lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), struct loadparm_context); NTSTATUS status = werror_to_ntstatus(werror); const char *domain_name = lpcfg_sam_name(lp_ctx); void *opaque_remote_address = NULL; /* * Forcing this via the NTLM auth structure is not ideal, but * it is the most practical option right now, and ensures the * logs are consistent, even if some elements are always NULL. */ struct auth_usersupplied_info ui = { .was_mapped = true, .client = { .account_name = io->u.sAMAccountName, .domain_name = domain_name, }, .mapped = { .account_name = io->u.sAMAccountName, .domain_name = domain_name, }, .service_description = "LDAP Password Change", .auth_description = "LDAP Modify", .password_type = "plaintext" }; opaque_remote_address = ldb_get_opaque(ldb, "remoteAddress"); if (opaque_remote_address == NULL) { ldb_asprintf_errstring(ldb, "Failed to obtain remote address for " "the LDAP client while changing the " "password"); return LDB_ERR_OPERATIONS_ERROR; } ui.remote_host = talloc_get_type(opaque_remote_address, struct tsocket_address); msg_ctx = imessaging_client_init(ac, lp_ctx, ldb_get_event_context(ldb)); if (!msg_ctx) { ldb_asprintf_errstring(ldb, "Failed to generate client messaging context in %s", lpcfg_imessaging_path(ac, lp_ctx)); return LDB_ERR_OPERATIONS_ERROR; } log_authentication_event(msg_ctx, lp_ctx, NULL, &ui, status, domain_name, io->u.sAMAccountName, io->u.account_sid, NULL /* client_audit_info */, NULL /* server_audit_info */); } return ret; } static int update_final_msg(struct setup_password_fields_io *io) { struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); int ret; int el_flags = 0; bool update_password = io->ac->update_password; bool update_scb = io->ac->update_password; /* * If we add a user without initial password, * we need to add replication meta data for * following attributes: * - unicodePwd * - dBCSPwd * - ntPwdHistory * - lmPwdHistory * * If we add a user with initial password or a * password is changed of an existing user, * we need to replace the following attributes * with a forced meta data update, e.g. also * when updating an empty attribute with an empty value: * - unicodePwd * - dBCSPwd * - ntPwdHistory * - lmPwdHistory * - supplementalCredentials */ switch (io->ac->req->operation) { case LDB_ADD: update_password = true; el_flags |= DSDB_FLAG_INTERNAL_FORCE_META_DATA; break; case LDB_MODIFY: el_flags |= LDB_FLAG_MOD_REPLACE; el_flags |= DSDB_FLAG_INTERNAL_FORCE_META_DATA; break; default: return ldb_module_operr(io->ac->module); } if (update_password) { ret = ldb_msg_add_empty(io->ac->update_msg, "unicodePwd", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } /* * This wipes any old LM password after any password * update operation. * * This is the same as the previous default behaviour * of 'lanman auth = no' */ ret = ldb_msg_add_empty(io->ac->update_msg, "dBCSPwd", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } ret = ldb_msg_add_empty(io->ac->update_msg, "ntPwdHistory", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } /* * This wipes any LM password history after any password * update operation. * * This is the same as the previous default behaviour * of 'lanman auth = no' */ ret = ldb_msg_add_empty(io->ac->update_msg, "lmPwdHistory", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } } if (update_scb) { ret = ldb_msg_add_empty(io->ac->update_msg, "supplementalCredentials", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } } if (io->ac->update_lastset) { ret = ldb_msg_add_empty(io->ac->update_msg, "pwdLastSet", el_flags, NULL); if (ret != LDB_SUCCESS) { return ret; } } if (io->g.nt_hash != NULL) { ret = samdb_msg_add_hash(ldb, io->ac, io->ac->update_msg, "unicodePwd", io->g.nt_hash); if (ret != LDB_SUCCESS) { return ret; } } if (io->g.nt_history_len > 0) { ret = samdb_msg_add_hashes(ldb, io->ac, io->ac->update_msg, "ntPwdHistory", io->g.nt_history, io->g.nt_history_len); if (ret != LDB_SUCCESS) { return ret; } } if (io->g.supplemental.length > 0) { ret = ldb_msg_add_value(io->ac->update_msg, "supplementalCredentials", &io->g.supplemental, NULL); if (ret != LDB_SUCCESS) { return ret; } } if (io->ac->update_lastset) { ret = samdb_msg_add_uint64(ldb, io->ac, io->ac->update_msg, "pwdLastSet", io->g.last_set); if (ret != LDB_SUCCESS) { return ret; } } return LDB_SUCCESS; } /* * This is intended for use by the "password_hash" module since there * password changes can be specified through one message element with the * new password (to set) and another one with the old password (to unset). * * The first which sets a password (new value) can have flags * (LDB_FLAG_MOD_ADD, LDB_FLAG_MOD_REPLACE) but also none (on "add" operations * for entries). The latter (old value) has always specified * LDB_FLAG_MOD_DELETE. * * Returns LDB_ERR_CONSTRAINT_VIOLATION and LDB_ERR_UNWILLING_TO_PERFORM if * matching message elements are malformed in respect to the set/change rules. * Otherwise it returns LDB_SUCCESS. */ static int msg_find_old_and_new_pwd_val(const struct ldb_message *msg, const char *name, enum ldb_request_type operation, const struct ldb_val **new_val, const struct ldb_val **old_val) { unsigned int i; *new_val = NULL; *old_val = NULL; if (msg == NULL) { return LDB_SUCCESS; } for (i = 0; i < msg->num_elements; i++) { if (ldb_attr_cmp(msg->elements[i].name, name) != 0) { continue; } if ((operation == LDB_MODIFY) && (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) == LDB_FLAG_MOD_DELETE)) { /* 0 values are allowed */ if (msg->elements[i].num_values == 1) { *old_val = &msg->elements[i].values[0]; } else if (msg->elements[i].num_values > 1) { return LDB_ERR_CONSTRAINT_VIOLATION; } } else if ((operation == LDB_MODIFY) && (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) == LDB_FLAG_MOD_REPLACE)) { if (msg->elements[i].num_values > 0) { *new_val = &msg->elements[i].values[msg->elements[i].num_values - 1]; } else { return LDB_ERR_UNWILLING_TO_PERFORM; } } else { /* Add operations and LDB_FLAG_MOD_ADD */ if (msg->elements[i].num_values > 0) { *new_val = &msg->elements[i].values[msg->elements[i].num_values - 1]; } else { return LDB_ERR_CONSTRAINT_VIOLATION; } } } return LDB_SUCCESS; } static int setup_io(struct ph_context *ac, const struct ldb_message *client_msg, const struct ldb_message *existing_msg, struct setup_password_fields_io *io) { const struct ldb_val *quoted_utf16, *old_quoted_utf16, *lm_hash, *old_lm_hash; 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; int rodc_krbtgt = 0; *io = (struct setup_password_fields_io) {}; /* Some operations below require kerberos contexts */ if (existing_msg != NULL) { /* * This is a modify operation */ info_msg = existing_msg; } else { /* * This is an add operation */ info_msg = client_msg; } ret = smb_krb5_init_context(ac, (struct loadparm_context *)ldb_get_opaque(ldb, "loadparm"), &io->smb_krb5_context); if (ret != 0) { /* * In the special case of mit krb5.conf vs heimdal, the includedir * statement causes ret == 22 (KRB5_CONFIG_BADFORMAT) to be returned. * We look for this case so that we can give a more instructional * message to the administrator. */ if (ret == KRB5_CONFIG_BADFORMAT || ret == EINVAL) { ldb_asprintf_errstring(ldb, "Failed to setup krb5_context: %s - " "This could be due to an invalid krb5 configuration. " "Please check your system's krb5 configuration is correct.", error_message(ret)); } else { ldb_asprintf_errstring(ldb, "Failed to setup krb5_context: %s", error_message(ret)); } return LDB_ERR_OPERATIONS_ERROR; } io->ac = ac; io->u.userAccountControl = ldb_msg_find_attr_as_uint(info_msg, "userAccountControl", 0); if (info_msg == existing_msg) { /* * We only take pwdLastSet from the existing object * otherwise we leave it as 0. * * If no attribute is available, e.g. on deleted objects * we remember that as UINT64_MAX. */ io->u.pwdLastSet = samdb_result_nttime(info_msg, "pwdLastSet", UINT64_MAX); } io->u.sAMAccountName = ldb_msg_find_attr_as_string(info_msg, "sAMAccountName", NULL); io->u.user_principal_name = ldb_msg_find_attr_as_string(info_msg, "userPrincipalName", NULL); io->u.displayName = ldb_msg_find_attr_as_string(info_msg, "displayName", NULL); /* Ensure it has an objectSID too */ io->u.account_sid = samdb_result_dom_sid(ac, info_msg, "objectSid"); if (io->u.account_sid != NULL) { NTSTATUS status; uint32_t rid = 0; status = dom_sid_split_rid(account_sid, io->u.account_sid, NULL, &rid); if (NT_STATUS_IS_OK(status)) { if (rid == DOMAIN_RID_KRBTGT) { io->u.is_krbtgt = true; } } } rodc_krbtgt = ldb_msg_find_attr_as_int(info_msg, "msDS-SecondaryKrbTgtNumber", 0); if (rodc_krbtgt != 0) { io->u.is_krbtgt = true; } if (io->u.sAMAccountName == NULL) { ldb_asprintf_errstring(ldb, "setup_io: sAMAccountName attribute is missing on %s for attempted password set/change", ldb_dn_get_linearized(info_msg->dn)); return LDB_ERR_CONSTRAINT_VIOLATION; } if (io->u.userAccountControl & UF_INTERDOMAIN_TRUST_ACCOUNT) { struct ldb_control *permit_trust = ldb_request_get_control(ac->req, DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID); if (permit_trust == NULL) { ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; ldb_asprintf_errstring(ldb, "%08X: %s - setup_io: changing the interdomain trust password " "on %s not allowed via LDAP. Use LSA or NETLOGON", W_ERROR_V(WERR_ACCESS_DENIED), ldb_strerror(ret), ldb_dn_get_linearized(info_msg->dn)); return ret; } } /* Only non-trust accounts have restrictions (possibly this test is the * wrong way around, but we like to be restrictive if possible */ io->u.restrictions = !(io->u.userAccountControl & UF_TRUST_ACCOUNT_MASK); if (io->u.is_krbtgt) { io->u.restrictions = 0; io->ac->status->domain_data.pwdHistoryLength = 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, &io->n.cleartext_utf8, &io->og.cleartext_utf8); if (ret != LDB_SUCCESS) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the old password once!"); return ret; } } if (io->n.cleartext_utf8 != NULL) { struct ldb_val *cleartext_utf8_blob; char *p; cleartext_utf8_blob = talloc(io->ac, struct ldb_val); if (!cleartext_utf8_blob) { return ldb_oom(ldb); } *cleartext_utf8_blob = *io->n.cleartext_utf8; /* make sure we have a null terminated string */ p = talloc_strndup(cleartext_utf8_blob, (const char *)io->n.cleartext_utf8->data, io->n.cleartext_utf8->length); if ((p == NULL) && (io->n.cleartext_utf8->length > 0)) { return ldb_oom(ldb); } cleartext_utf8_blob->data = (uint8_t *)p; io->n.cleartext_utf8 = cleartext_utf8_blob; } ret = msg_find_old_and_new_pwd_val(client_msg, "clearTextPassword", ac->req->operation, &io->n.cleartext_utf16, &io->og.cleartext_utf16); if (ret != LDB_SUCCESS) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the old password once!"); return ret; } /* this rather strange looking piece of code is there to handle a ldap client setting a password remotely using the unicodePwd ldap field. The syntax is that the password is in UTF-16LE, with a " at either end. Unfortunately the unicodePwd field is also used to store the nt hashes internally in Samba, and is used in the nt hash format on the wire in DRS replication, so we have a single name for two distinct values. The code below leaves us with a small chance (less than 1 in 2^32) of a mixup, if someone manages to create a MD4 hash which starts and ends in 0x22 0x00, as that would then be treated as a UTF16 password rather than a nthash */ ret = msg_find_old_and_new_pwd_val(client_msg, "unicodePwd", ac->req->operation, "ed_utf16, &old_quoted_utf16); if (ret != LDB_SUCCESS) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the old password once!"); return ret; } /* Checks and converts the actual "unicodePwd" attribute */ if (!ac->hash_values && quoted_utf16 && quoted_utf16->length >= 4 && quoted_utf16->data[0] == '"' && quoted_utf16->data[1] == 0 && quoted_utf16->data[quoted_utf16->length-2] == '"' && quoted_utf16->data[quoted_utf16->length-1] == 0) { struct ldb_val *quoted_utf16_2; if (io->n.cleartext_utf16) { /* refuse the change if someone wants to change with with both UTF16 possibilities at the same time... */ ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the cleartext password as 'unicodePwd' or as 'clearTextPassword'"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* * adapt the quoted UTF16 string to be a real * cleartext one */ quoted_utf16_2 = talloc(io->ac, struct ldb_val); if (quoted_utf16_2 == NULL) { return ldb_oom(ldb); } quoted_utf16_2->data = quoted_utf16->data + 2; quoted_utf16_2->length = quoted_utf16->length-4; io->n.cleartext_utf16 = quoted_utf16_2; io->n.nt_hash = NULL; } else if (quoted_utf16) { /* We have only the hash available -> so no plaintext here */ if (!ac->hash_values) { /* refuse the change if someone wants to change the hash without control specified... */ ldb_asprintf_errstring(ldb, "setup_io: " "it's not allowed to set the NT hash password directly'"); /* this looks odd but this is what Windows does: returns "UNWILLING_TO_PERFORM" on wrong password sets and "CONSTRAINT_VIOLATION" on wrong password changes. */ if (old_quoted_utf16 == NULL) { return LDB_ERR_UNWILLING_TO_PERFORM; } return LDB_ERR_CONSTRAINT_VIOLATION; } io->n.nt_hash = talloc(io->ac, struct samr_Password); if (io->n.nt_hash == NULL) { return ldb_oom(ldb); } memcpy(io->n.nt_hash->hash, quoted_utf16->data, MIN(quoted_utf16->length, sizeof(io->n.nt_hash->hash))); } /* Checks and converts the previous "unicodePwd" attribute */ if (!ac->hash_values && old_quoted_utf16 && old_quoted_utf16->length >= 4 && old_quoted_utf16->data[0] == '"' && old_quoted_utf16->data[1] == 0 && old_quoted_utf16->data[old_quoted_utf16->length-2] == '"' && old_quoted_utf16->data[old_quoted_utf16->length-1] == 0) { struct ldb_val *old_quoted_utf16_2; if (io->og.cleartext_utf16) { /* refuse the change if someone wants to change with both UTF16 possibilities at the same time... */ ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the cleartext password as 'unicodePwd' or as 'clearTextPassword'"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* * adapt the quoted UTF16 string to be a real * cleartext one */ old_quoted_utf16_2 = talloc(io->ac, struct ldb_val); if (old_quoted_utf16_2 == NULL) { return ldb_oom(ldb); } old_quoted_utf16_2->data = old_quoted_utf16->data + 2; old_quoted_utf16_2->length = old_quoted_utf16->length-4; io->og.cleartext_utf16 = old_quoted_utf16_2; io->og.nt_hash = NULL; } else if (old_quoted_utf16) { /* We have only the hash available -> so no plaintext here */ if (!ac->hash_values) { /* refuse the change if someone wants to change the hash without control specified... */ ldb_asprintf_errstring(ldb, "setup_io: " "it's not allowed to set the NT hash password directly'"); return LDB_ERR_UNWILLING_TO_PERFORM; } io->og.nt_hash = talloc(io->ac, struct samr_Password); if (io->og.nt_hash == NULL) { return ldb_oom(ldb); } memcpy(io->og.nt_hash->hash, old_quoted_utf16->data, MIN(old_quoted_utf16->length, sizeof(io->og.nt_hash->hash))); } /* Handles the "dBCSPwd" attribute (LM hash) */ ret = msg_find_old_and_new_pwd_val(client_msg, "dBCSPwd", ac->req->operation, &lm_hash, &old_lm_hash); if (ret != LDB_SUCCESS) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the old password once!"); return ret; } if (((lm_hash != NULL) || (old_lm_hash != NULL))) { /* refuse the change if someone wants to change the LM hash */ ldb_asprintf_errstring(ldb, "setup_io: " "it's not allowed to set the LM hash password (dBCSPwd)'"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* * Handles the password change control if it's specified. It has the * precedence and overrides already specified old password values of * change requests (but that shouldn't happen since the control is * fully internal and only used in conjunction with replace requests!). */ if (ac->change != NULL) { io->og.nt_hash = NULL; } /* refuse the change if someone wants to change the clear- text and supply his own hashes at the same time... */ if ((io->n.cleartext_utf8 || io->n.cleartext_utf16) && (io->n.nt_hash)) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the password in form of cleartext attributes or as hashes"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* refuse the change if someone wants to change the password using both plaintext methods (UTF8 and UTF16) at the same time... */ if (io->n.cleartext_utf8 && io->n.cleartext_utf16) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to set the cleartext password as 'unicodePwd' or as 'userPassword' or as 'clearTextPassword'"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* refuse the change if someone tries to set/change the password by * any method that would leave us without a password! */ if (io->ac->update_password && (!io->n.cleartext_utf8) && (!io->n.cleartext_utf16) && (!io->n.nt_hash)) { ldb_asprintf_errstring(ldb, "setup_io: " "It's not possible to delete the password (changes using the LAN Manager hash alone could be deactivated)!"); /* on "userPassword" and "clearTextPassword" we've to return * something different, since these are virtual attributes */ if ((ldb_msg_find_element(client_msg, "userPassword") != NULL) || (ldb_msg_find_element(client_msg, "clearTextPassword") != NULL)) { return LDB_ERR_CONSTRAINT_VIOLATION; } return LDB_ERR_UNWILLING_TO_PERFORM; } /* * refuse the change if someone wants to compare against a * plaintext or dsdb_control_password_change at the same time * for a "password modify" operation... */ if ((io->og.cleartext_utf8 || io->og.cleartext_utf16) && ac->change) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to provide the old password in form of cleartext attributes or as the dsdb_control_password_change"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* refuse the change if someone wants to compare against both * plaintexts at the same time for a "password modify" operation... */ if (io->og.cleartext_utf8 && io->og.cleartext_utf16) { ldb_asprintf_errstring(ldb, "setup_io: " "it's only allowed to provide the old cleartext password as 'unicodePwd' or as 'userPassword' or as 'clearTextPassword'"); return LDB_ERR_UNWILLING_TO_PERFORM; } /* Decides if we have a password modify or password reset operation */ if (ac->req->operation == LDB_ADD) { /* On "add" we have only "password reset" */ ac->pwd_reset = true; } else if (ac->req->operation == LDB_MODIFY) { struct ldb_control *pav_ctrl = NULL; struct dsdb_control_password_acl_validation *pav = NULL; pav_ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); if (pav_ctrl != NULL) { pav = talloc_get_type_abort(pav_ctrl->data, struct dsdb_control_password_acl_validation); } if (pav == NULL && ac->update_password) { bool ok; /* * If the DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID * control is missing, we require system access! */ ok = dsdb_module_am_system(ac->module); if (!ok) { return ldb_module_operr(ac->module); } } if (pav != NULL) { /* * We assume what the acl module has validated. */ ac->pwd_reset = pav->pwd_reset; } else if (io->og.cleartext_utf8 || io->og.cleartext_utf16 || ac->change) { /* * If we have an old password specified or the * dsdb_control_password_change then for sure * it is a user "password change" */ ac->pwd_reset = false; } else { /* Otherwise we have also here a "password reset" */ ac->pwd_reset = true; } } else { /* this shouldn't happen */ return ldb_operr(ldb); } 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 */ status = samdb_result_passwords_no_lockout(ac, lp_ctx, existing_msg, &io->o.nt_hash); } else { /* Get the old password from the database */ status = samdb_result_passwords(ac, lp_ctx, existing_msg, &io->o.nt_hash); } if (NT_STATUS_EQUAL(status, NT_STATUS_ACCOUNT_LOCKED_OUT)) { return dsdb_module_werror(ac->module, LDB_ERR_CONSTRAINT_VIOLATION, WERR_ACCOUNT_LOCKED_OUT, "Password change not permitted," " account locked out!"); } if (!NT_STATUS_IS_OK(status)) { /* * This only happens if the database has gone weird, * not if we are just missing the passwords */ return ldb_operr(ldb); } io->o.nt_history_len = samdb_result_hashes(ac, existing_msg, "ntPwdHistory", &io->o.nt_history); io->o.supplemental = ldb_msg_find_ldb_val(existing_msg, "supplementalCredentials"); if (io->o.supplemental != NULL) { enum ndr_err_code ndr_err; ndr_err = ndr_pull_struct_blob_all(io->o.supplemental, io->ac, &io->o.scb, (ndr_pull_flags_fn_t)ndr_pull_supplementalCredentialsBlob); if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { status = ndr_map_error2ntstatus(ndr_err); ldb_asprintf_errstring(ldb, "setup_io: failed to pull " "old supplementalCredentialsBlob: %s", nt_errstr(status)); 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 won't 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; } static struct ph_context *ph_init_context(struct ldb_module *module, struct ldb_request *req, bool userPassword, bool update_password) { struct ldb_context *ldb; struct ph_context *ac; struct loadparm_context *lp_ctx = NULL; ldb = ldb_module_get_ctx(module); ac = talloc_zero(req, struct ph_context); if (ac == NULL) { ldb_set_errstring(ldb, "Out of Memory"); return NULL; } ac->module = module; ac->req = req; ac->userPassword = userPassword; ac->update_password = update_password; ac->update_lastset = true; lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), struct loadparm_context); ac->gpg_key_ids = lpcfg_password_hash_gpg_key_ids(lp_ctx); ac->userPassword_schemes = lpcfg_password_hash_userpassword_schemes(lp_ctx); return ac; } static void ph_apply_controls(struct ph_context *ac) { struct ldb_control *ctrl; ac->change_status = false; ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID); if (ctrl != NULL) { ac->change_status = true; /* Mark the "change status" control as uncritical (done) */ ctrl->critical = false; } ac->hash_values = false; ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_HASH_VALUES_OID); if (ctrl != NULL) { ac->hash_values = true; /* Mark the "hash values" control as uncritical (done) */ ctrl->critical = false; } ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID); if (ctrl != NULL) { ac->change = talloc_get_type_abort(ctrl->data, struct dsdb_control_password_change); /* Mark the "change" control as uncritical (done) */ ctrl->critical = false; } ac->pwd_last_set_bypass = false; ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_BYPASS_LAST_SET_OID); if (ctrl != NULL) { ac->pwd_last_set_bypass = true; /* Mark the "bypass pwdLastSet" control as uncritical (done) */ ctrl->critical = false; } ac->pwd_last_set_default = false; ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_DEFAULT_LAST_SET_OID); if (ctrl != NULL) { ac->pwd_last_set_default = true; /* Mark the "bypass pwdLastSet" control as uncritical (done) */ ctrl->critical = false; } ac->smartcard_reset = false; ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID); if (ctrl != NULL) { struct dsdb_control_password_user_account_control *uac = NULL; uint32_t added_flags = 0; uac = talloc_get_type_abort(ctrl->data, struct dsdb_control_password_user_account_control); added_flags = uac->new_flags & ~uac->old_flags; if (added_flags & UF_SMARTCARD_REQUIRED) { ac->smartcard_reset = true; } /* Mark the "smartcard required" control as uncritical (done) */ ctrl->critical = false; } } static int ph_op_callback(struct ldb_request *req, struct ldb_reply *ares) { struct ph_context *ac; ac = talloc_get_type(req->context, struct ph_context); if (!ares) { return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } if (ares->type == LDB_REPLY_REFERRAL) { return ldb_module_send_referral(ac->req, ares->referral); } if ((ares->error != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { /* On success and trivial errors a status control is being * added (used for example by the "samdb_set_password" call) */ ldb_reply_add_control(ares, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, false, ac->status); } if (ares->error != LDB_SUCCESS) { return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } if (ares->type != LDB_REPLY_DONE) { talloc_free(ares); return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } static int password_hash_add_do_add(struct ph_context *ac); static int ph_modify_callback(struct ldb_request *req, struct ldb_reply *ares); static int password_hash_mod_search_self(struct ph_context *ac); static int ph_mod_search_callback(struct ldb_request *req, struct ldb_reply *ares); static int password_hash_mod_do_mod(struct ph_context *ac); /* * LDB callback handler for searching for a user's PSO. Once we have all the * Password Settings that apply to the user, we can continue with the modify * operation */ static int get_pso_data_callback(struct ldb_request *req, struct ldb_reply *ares) { struct ldb_context *ldb = NULL; struct ph_context *ac = NULL; bool domain_complexity = true; bool pso_complexity = true; struct dsdb_user_pwd_settings *settings = NULL; int ret = LDB_SUCCESS; ac = talloc_get_type(req->context, struct ph_context); ldb = ldb_module_get_ctx(ac->module); if (!ares) { ret = LDB_ERR_OPERATIONS_ERROR; goto done; } if (ares->error != LDB_SUCCESS) { return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } switch (ares->type) { case LDB_REPLY_ENTRY: /* check status was initialized by the domain query */ if (ac->status == NULL) { talloc_free(ares); ldb_set_errstring(ldb, "Uninitialized status"); ret = LDB_ERR_OPERATIONS_ERROR; goto done; } /* * use the PSO's values instead of the domain defaults (the PSO * attributes should always exist, but use the domain default * values as a fallback). */ settings = &ac->status->domain_data; settings->store_cleartext = ldb_msg_find_attr_as_bool(ares->message, "msDS-PasswordReversibleEncryptionEnabled", settings->store_cleartext); settings->pwdHistoryLength = ldb_msg_find_attr_as_uint(ares->message, "msDS-PasswordHistoryLength", settings->pwdHistoryLength); settings->maxPwdAge = ldb_msg_find_attr_as_int64(ares->message, "msDS-MaximumPasswordAge", settings->maxPwdAge); settings->minPwdAge = ldb_msg_find_attr_as_int64(ares->message, "msDS-MinimumPasswordAge", settings->minPwdAge); settings->minPwdLength = ldb_msg_find_attr_as_uint(ares->message, "msDS-MinimumPasswordLength", settings->minPwdLength); domain_complexity = (settings->pwdProperties & DOMAIN_PASSWORD_COMPLEX); pso_complexity = ldb_msg_find_attr_as_bool(ares->message, "msDS-PasswordComplexityEnabled", domain_complexity); /* set or clear the complexity bit if required */ if (pso_complexity && !domain_complexity) { settings->pwdProperties |= DOMAIN_PASSWORD_COMPLEX; } else if (domain_complexity && !pso_complexity) { settings->pwdProperties &= ~DOMAIN_PASSWORD_COMPLEX; } if (ac->pso_res != NULL) { DBG_ERR("Too many PSO results for %s\n", ldb_dn_get_linearized(ac->search_res->message->dn)); talloc_free(ac->pso_res); } /* store the PSO result (we may need its lockout settings) */ ac->pso_res = talloc_steal(ac, ares); ret = LDB_SUCCESS; break; case LDB_REPLY_REFERRAL: /* ignore */ talloc_free(ares); ret = LDB_SUCCESS; break; case LDB_REPLY_DONE: talloc_free(ares); /* * perform the next step of the modify operation (this code * shouldn't get called in the 'user add' case) */ if (ac->req->operation == LDB_MODIFY) { ret = password_hash_mod_do_mod(ac); } else { ret = LDB_ERR_OPERATIONS_ERROR; } break; } done: if (ret != LDB_SUCCESS) { struct ldb_reply *new_ares; new_ares = talloc_zero(ac->req, struct ldb_reply); if (new_ares == NULL) { ldb_oom(ldb); return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } new_ares->error = ret; if ((ret != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { /* On success and trivial errors a status control is being * added (used for example by the "samdb_set_password" call) */ ldb_reply_add_control(new_ares, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, false, ac->status); } return ldb_module_done(ac->req, new_ares->controls, new_ares->response, new_ares->error); } return LDB_SUCCESS; } /* * Builds and returns a search request to look up the PSO that applies to * the user in question. Returns NULL if no PSO applies, or could not be found */ static struct ldb_request * build_pso_data_request(struct ph_context *ac) { /* attrs[] is returned from this function in pso_req->op.search.attrs, so it must be static, as otherwise the compiler can put it on the stack */ static const char * const attrs[] = { "msDS-PasswordComplexityEnabled", "msDS-PasswordReversibleEncryptionEnabled", "msDS-PasswordHistoryLength", "msDS-MaximumPasswordAge", "msDS-MinimumPasswordAge", "msDS-MinimumPasswordLength", "msDS-LockoutThreshold", "msDS-LockoutObservationWindow", NULL }; struct ldb_context *ldb = NULL; struct ldb_request *pso_req = NULL; struct ldb_dn *pso_dn = NULL; TALLOC_CTX *mem_ctx = ac; int ret; ldb = ldb_module_get_ctx(ac->module); /* if a PSO applies to the user, we need to lookup the PSO as well */ pso_dn = ldb_msg_find_attr_as_dn(ldb, mem_ctx, ac->search_res->message, "msDS-ResultantPSO"); if (pso_dn == NULL) { return NULL; } ret = ldb_build_search_req(&pso_req, ldb, mem_ctx, pso_dn, LDB_SCOPE_BASE, NULL, attrs, NULL, ac, get_pso_data_callback, ac->dom_req); /* log errors, but continue with the default domain settings */ if (ret != LDB_SUCCESS) { DBG_ERR("Error %d constructing PSO query for user %s\n", ret, ldb_dn_get_linearized(ac->search_res->message->dn)); } LDB_REQ_SET_LOCATION(pso_req); return pso_req; } static int get_domain_data_callback(struct ldb_request *req, struct ldb_reply *ares) { struct ldb_context *ldb; struct ph_context *ac; struct loadparm_context *lp_ctx; struct ldb_request *pso_req = NULL; int ret = LDB_SUCCESS; ac = talloc_get_type(req->context, struct ph_context); ldb = ldb_module_get_ctx(ac->module); if (!ares) { ret = LDB_ERR_OPERATIONS_ERROR; goto done; } if (ares->error != LDB_SUCCESS) { return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } switch (ares->type) { case LDB_REPLY_ENTRY: if (ac->status != NULL) { talloc_free(ares); ldb_set_errstring(ldb, "Too many results"); ret = LDB_ERR_OPERATIONS_ERROR; goto done; } /* Setup the "status" structure (used as control later) */ ac->status = talloc_zero(ac->req, struct dsdb_control_password_change_status); if (ac->status == NULL) { talloc_free(ares); ldb_oom(ldb); ret = LDB_ERR_OPERATIONS_ERROR; goto done; } /* Setup the "domain data" structure */ ac->status->domain_data.pwdProperties = ldb_msg_find_attr_as_uint(ares->message, "pwdProperties", -1); ac->status->domain_data.pwdHistoryLength = ldb_msg_find_attr_as_uint(ares->message, "pwdHistoryLength", -1); ac->status->domain_data.maxPwdAge = ldb_msg_find_attr_as_int64(ares->message, "maxPwdAge", -1); ac->status->domain_data.minPwdAge = ldb_msg_find_attr_as_int64(ares->message, "minPwdAge", -1); ac->status->domain_data.minPwdLength = ldb_msg_find_attr_as_uint(ares->message, "minPwdLength", -1); ac->status->domain_data.store_cleartext = ac->status->domain_data.pwdProperties & DOMAIN_PASSWORD_STORE_CLEARTEXT; /* For a domain DN, this puts things in dotted notation */ /* For builtin domains, this will give details for the host, * but that doesn't really matter, as it's just used for salt * and kerberos principals, which don't exist here */ lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), struct loadparm_context); ac->status->domain_data.dns_domain = lpcfg_dnsdomain(lp_ctx); ac->status->domain_data.realm = lpcfg_realm(lp_ctx); ac->status->domain_data.netbios_domain = lpcfg_sam_name(lp_ctx); ac->status->reject_reason = SAM_PWD_CHANGE_NO_ERROR; if (ac->dom_res != NULL) { talloc_free(ares); ldb_set_errstring(ldb, "Too many results"); ret = LDB_ERR_OPERATIONS_ERROR; goto done; } ac->dom_res = talloc_steal(ac, ares); ret = LDB_SUCCESS; break; case LDB_REPLY_REFERRAL: /* ignore */ talloc_free(ares); ret = LDB_SUCCESS; break; case LDB_REPLY_DONE: talloc_free(ares); /* call the next step */ switch (ac->req->operation) { case LDB_ADD: ret = password_hash_add_do_add(ac); break; case LDB_MODIFY: /* * The user may have an optional PSO applied. If so, * query the PSO to get the Fine-Grained Password Policy * for the user, before we perform the modify */ pso_req = build_pso_data_request(ac); if (pso_req != NULL) { ret = ldb_next_request(ac->module, pso_req); } else { /* no PSO, so we can perform the modify now */ ret = password_hash_mod_do_mod(ac); } break; default: ret = LDB_ERR_OPERATIONS_ERROR; break; } break; } done: if (ret != LDB_SUCCESS) { struct ldb_reply *new_ares; new_ares = talloc_zero(ac->req, struct ldb_reply); if (new_ares == NULL) { ldb_oom(ldb); return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } new_ares->error = ret; if ((ret != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { /* On success and trivial errors a status control is being * added (used for example by the "samdb_set_password" call) */ ldb_reply_add_control(new_ares, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, false, ac->status); } return ldb_module_done(ac->req, new_ares->controls, new_ares->response, new_ares->error); } return LDB_SUCCESS; } static int build_domain_data_request(struct ph_context *ac) { /* attrs[] is returned from this function in ac->dom_req->op.search.attrs, so it must be static, as otherwise the compiler can put it on the stack */ struct ldb_context *ldb; static const char * const attrs[] = { "pwdProperties", "pwdHistoryLength", "maxPwdAge", "minPwdAge", "minPwdLength", "lockoutThreshold", "lockOutObservationWindow", NULL }; int ret; ldb = ldb_module_get_ctx(ac->module); ret = ldb_build_search_req(&ac->dom_req, ldb, ac, ldb_get_default_basedn(ldb), LDB_SCOPE_BASE, NULL, attrs, NULL, ac, get_domain_data_callback, ac->req); LDB_REQ_SET_LOCATION(ac->dom_req); return ret; } static int password_hash_needed(struct ldb_module *module, struct ldb_request *req, struct ph_context **_ac) { struct ldb_context *ldb = ldb_module_get_ctx(module); const char *operation = NULL; const struct ldb_message *msg = NULL; struct ph_context *ac = NULL; const char *passwordAttrs[] = { DSDB_PASSWORD_ATTRIBUTES, NULL }; const char **a = NULL; unsigned int attr_cnt = 0; struct ldb_control *bypass = NULL; struct ldb_control *uac_ctrl = NULL; bool userPassword = dsdb_user_password_support(module, req, req); bool update_password = false; bool processing_needed = false; *_ac = NULL; ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_needed\n"); switch (req->operation) { case LDB_ADD: operation = "add"; msg = req->op.add.message; break; case LDB_MODIFY: operation = "modify"; msg = req->op.mod.message; break; default: return ldb_next_request(module, req); } if (ldb_dn_is_special(msg->dn)) { /* do not manipulate our control entries */ return ldb_next_request(module, req); } bypass = ldb_request_get_control(req, DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID); if (bypass != NULL) { /* Mark the "bypass" control as uncritical (done) */ bypass->critical = false; ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_needed(%s) (bypassing)\n", operation); return password_hash_bypass(module, req); } /* nobody must touch password histories and 'supplementalCredentials' */ if (ldb_msg_find_element(msg, "ntPwdHistory")) { return LDB_ERR_UNWILLING_TO_PERFORM; } if (ldb_msg_find_element(msg, "lmPwdHistory")) { return LDB_ERR_UNWILLING_TO_PERFORM; } if (ldb_msg_find_element(msg, "supplementalCredentials")) { return LDB_ERR_UNWILLING_TO_PERFORM; } /* * If no part of this touches the 'userPassword' OR 'clearTextPassword' * OR 'unicodePwd' OR 'dBCSPwd' we don't need to make any changes. * For password changes/set there should be a 'delete' or a 'modify' * on these attributes. */ for (a = passwordAttrs; *a != NULL; a++) { if ((!userPassword) && (ldb_attr_cmp(*a, "userPassword") == 0)) { continue; } if (ldb_msg_find_element(msg, *a) != NULL) { /* MS-ADTS 3.1.1.3.1.5.2 */ if ((ldb_attr_cmp(*a, "userPassword") == 0) && (dsdb_functional_level(ldb) < DS_DOMAIN_FUNCTION_2003)) { return LDB_ERR_CONSTRAINT_VIOLATION; } ++attr_cnt; } } if (attr_cnt > 0) { update_password = true; processing_needed = true; } if (ldb_msg_find_element(msg, "pwdLastSet")) { processing_needed = true; } uac_ctrl = ldb_request_get_control(req, DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID); if (uac_ctrl != NULL) { struct dsdb_control_password_user_account_control *uac = NULL; uint32_t added_flags = 0; uac = talloc_get_type_abort(uac_ctrl->data, struct dsdb_control_password_user_account_control); added_flags = uac->new_flags & ~uac->old_flags; if (added_flags & UF_SMARTCARD_REQUIRED) { processing_needed = true; } } if (!processing_needed) { return ldb_next_request(module, req); } ac = ph_init_context(module, req, userPassword, update_password); if (!ac) { DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); return ldb_operr(ldb); } ph_apply_controls(ac); /* * Make a copy in order to apply our modifications * to the final update */ ac->update_msg = ldb_msg_copy_shallow(ac, msg); if (ac->update_msg == NULL) { return ldb_oom(ldb); } /* * Remove all password related attributes. */ if (ac->userPassword) { ldb_msg_remove_attr(ac->update_msg, "userPassword"); } ldb_msg_remove_attr(ac->update_msg, "clearTextPassword"); ldb_msg_remove_attr(ac->update_msg, "unicodePwd"); ldb_msg_remove_attr(ac->update_msg, "ntPwdHistory"); ldb_msg_remove_attr(ac->update_msg, "dBCSPwd"); ldb_msg_remove_attr(ac->update_msg, "lmPwdHistory"); ldb_msg_remove_attr(ac->update_msg, "supplementalCredentials"); ldb_msg_remove_attr(ac->update_msg, "pwdLastSet"); *_ac = ac; return LDB_SUCCESS; } static int password_hash_add(struct ldb_module *module, struct ldb_request *req) { struct ldb_context *ldb = ldb_module_get_ctx(module); struct ph_context *ac = NULL; int ret; ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_add\n"); ret = password_hash_needed(module, req, &ac); if (ret != LDB_SUCCESS) { return ret; } if (ac == NULL) { return ret; } /* Make sure we are performing the password set action on a (for us) * valid object. Those are instances of either "user" and/or * "inetOrgPerson". Otherwise continue with the submodules. */ if ((!ldb_msg_check_string_attribute(req->op.add.message, "objectClass", "user")) && (!ldb_msg_check_string_attribute(req->op.add.message, "objectClass", "inetOrgPerson"))) { TALLOC_FREE(ac); if (ldb_msg_find_element(req->op.add.message, "clearTextPassword") != NULL) { ldb_set_errstring(ldb, "'clearTextPassword' is only allowed on objects of class 'user' and/or 'inetOrgPerson'!"); return LDB_ERR_NO_SUCH_ATTRIBUTE; } return ldb_next_request(module, req); } /* get user domain data */ ret = build_domain_data_request(ac); if (ret != LDB_SUCCESS) { return ret; } return ldb_next_request(module, ac->dom_req); } static int password_hash_add_do_add(struct ph_context *ac) { struct ldb_context *ldb = ldb_module_get_ctx(ac->module); struct ldb_request *down_req; struct setup_password_fields_io io; int ret; /* Prepare the internal data structure containing the passwords */ ret = setup_io(ac, ac->req->op.add.message, NULL, &io); if (ret != LDB_SUCCESS) { return ret; } ret = setup_password_fields(&io); if (ret != LDB_SUCCESS) { return ret; } ret = check_password_restrictions_and_log(&io); if (ret != LDB_SUCCESS) { return ret; } ret = setup_smartcard_reset(&io); if (ret != LDB_SUCCESS) { return ret; } ret = update_final_msg(&io); if (ret != LDB_SUCCESS) { return ret; } ret = ldb_build_add_req(&down_req, ldb, ac, ac->update_msg, ac->req->controls, ac, ph_op_callback, ac->req); LDB_REQ_SET_LOCATION(down_req); if (ret != LDB_SUCCESS) { return ret; } return ldb_next_request(ac->module, down_req); } static int password_hash_modify(struct ldb_module *module, struct ldb_request *req) { struct ldb_context *ldb = ldb_module_get_ctx(module); struct ph_context *ac = NULL; const char *passwordAttrs[] = {DSDB_PASSWORD_ATTRIBUTES, NULL}, **l; unsigned int del_attr_cnt, add_attr_cnt, rep_attr_cnt; struct ldb_message_element *passwordAttr; struct ldb_message *msg; struct ldb_request *down_req; struct ldb_control *restore = NULL; int ret; unsigned int i = 0; ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_modify\n"); ret = password_hash_needed(module, req, &ac); if (ret != LDB_SUCCESS) { return ret; } if (ac == NULL) { return ret; } /* use a new message structure so that we can modify it */ msg = ldb_msg_copy_shallow(ac, req->op.mod.message); if (msg == NULL) { return ldb_oom(ldb); } /* - check for single-valued password attributes * (if not return "CONSTRAINT_VIOLATION") * - check that for a password change operation one add and one delete * operation exists * (if not return "CONSTRAINT_VIOLATION" or "UNWILLING_TO_PERFORM") * - check that a password change and a password set operation cannot * be mixed * (if not return "UNWILLING_TO_PERFORM") * - remove all password attributes modifications from the first change * operation (anything without the passwords) - we will make the real * modification later */ del_attr_cnt = 0; add_attr_cnt = 0; rep_attr_cnt = 0; for (l = passwordAttrs; *l != NULL; l++) { if ((!ac->userPassword) && (ldb_attr_cmp(*l, "userPassword") == 0)) { continue; } while ((passwordAttr = ldb_msg_find_element(msg, *l)) != NULL) { unsigned int mtype = LDB_FLAG_MOD_TYPE(passwordAttr->flags); unsigned int nvalues = passwordAttr->num_values; if (mtype == LDB_FLAG_MOD_DELETE) { ++del_attr_cnt; } if (mtype == LDB_FLAG_MOD_ADD) { ++add_attr_cnt; } if (mtype == LDB_FLAG_MOD_REPLACE) { ++rep_attr_cnt; } if ((nvalues != 1) && (mtype == LDB_FLAG_MOD_ADD)) { talloc_free(ac); ldb_asprintf_errstring(ldb, "'%s' attribute must have exactly one value on add operations!", *l); return LDB_ERR_CONSTRAINT_VIOLATION; } if ((nvalues > 1) && (mtype == LDB_FLAG_MOD_DELETE)) { talloc_free(ac); ldb_asprintf_errstring(ldb, "'%s' attribute must have zero or one value(s) on delete operations!", *l); return LDB_ERR_CONSTRAINT_VIOLATION; } ldb_msg_remove_element(msg, passwordAttr); } } if ((del_attr_cnt == 0) && (add_attr_cnt > 0)) { talloc_free(ac); ldb_set_errstring(ldb, "Only the add action for a password change specified!"); return LDB_ERR_UNWILLING_TO_PERFORM; } if ((del_attr_cnt > 1) || (add_attr_cnt > 1)) { talloc_free(ac); ldb_set_errstring(ldb, "Only one delete and one add action for a password change allowed!"); return LDB_ERR_UNWILLING_TO_PERFORM; } if ((rep_attr_cnt > 0) && ((del_attr_cnt > 0) || (add_attr_cnt > 0))) { talloc_free(ac); ldb_set_errstring(ldb, "Either a password change or a password set operation is allowed!"); return LDB_ERR_UNWILLING_TO_PERFORM; } restore = ldb_request_get_control(req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID); if (restore == NULL) { /* * A tombstone reanimation generates a double update * of pwdLastSet. * * So we only remove it without the * DSDB_CONTROL_RESTORE_TOMBSTONE_OID control. */ ldb_msg_remove_attr(msg, "pwdLastSet"); } /* if there was nothing else to be modified skip to next step */ if (msg->num_elements == 0) { return password_hash_mod_search_self(ac); } /* * Now we apply all changes remaining in msg * and remove them from our final update_msg */ for (i = 0; i < msg->num_elements; i++) { ldb_msg_remove_attr(ac->update_msg, msg->elements[i].name); } ret = ldb_build_mod_req(&down_req, ldb, ac, msg, req->controls, ac, ph_modify_callback, req); LDB_REQ_SET_LOCATION(down_req); if (ret != LDB_SUCCESS) { return ret; } return ldb_next_request(module, down_req); } static int ph_modify_callback(struct ldb_request *req, struct ldb_reply *ares) { struct ph_context *ac; ac = talloc_get_type(req->context, struct ph_context); if (!ares) { return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } if (ares->type == LDB_REPLY_REFERRAL) { return ldb_module_send_referral(ac->req, ares->referral); } if (ares->error != LDB_SUCCESS) { return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } if (ares->type != LDB_REPLY_DONE) { talloc_free(ares); return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); } talloc_free(ares); return password_hash_mod_search_self(ac); } static int ph_mod_search_callback(struct ldb_request *req, struct ldb_reply *ares) { struct ldb_context *ldb; struct ph_context *ac; int ret = LDB_SUCCESS; ac = talloc_get_type(req->context, struct ph_context); ldb = ldb_module_get_ctx(ac->module); if (!ares) { ret = LDB_ERR_OPERATIONS_ERROR; goto done; } if (ares->error != LDB_SUCCESS) { return ldb_module_done(ac->req, ares->controls, ares->response, ares->error); } /* we are interested only in the single reply (base search) */ switch (ares->type) { case LDB_REPLY_ENTRY: /* Make sure we are performing the password change action on a * (for us) valid object. Those are instances of either "user" * and/or "inetOrgPerson". Otherwise continue with the * submodules. */ if ((!ldb_msg_check_string_attribute(ares->message, "objectClass", "user")) && (!ldb_msg_check_string_attribute(ares->message, "objectClass", "inetOrgPerson"))) { talloc_free(ares); if (ldb_msg_find_element(ac->req->op.mod.message, "clearTextPassword") != NULL) { ldb_set_errstring(ldb, "'clearTextPassword' is only allowed on objects of class 'user' and/or 'inetOrgPerson'!"); ret = LDB_ERR_NO_SUCH_ATTRIBUTE; goto done; } ret = ldb_next_request(ac->module, ac->req); goto done; } if (ac->search_res != NULL) { talloc_free(ares); ldb_set_errstring(ldb, "Too many results"); ret = LDB_ERR_OPERATIONS_ERROR; goto done; } ac->search_res = talloc_steal(ac, ares); ret = LDB_SUCCESS; break; case LDB_REPLY_REFERRAL: /* ignore anything else for now */ talloc_free(ares); ret = LDB_SUCCESS; break; case LDB_REPLY_DONE: talloc_free(ares); /* get user domain data */ ret = build_domain_data_request(ac); if (ret != LDB_SUCCESS) { return ldb_module_done(ac->req, NULL, NULL, ret); } ret = ldb_next_request(ac->module, ac->dom_req); break; } done: if (ret != LDB_SUCCESS) { return ldb_module_done(ac->req, NULL, NULL, ret); } return LDB_SUCCESS; } static int password_hash_mod_search_self(struct ph_context *ac) { struct ldb_context *ldb; static const char * const attrs[] = { "objectClass", "userAccountControl", "msDS-ResultantPSO", "msDS-User-Account-Control-Computed", "pwdLastSet", "sAMAccountName", "objectSid", "userPrincipalName", "displayName", "supplementalCredentials", "lmPwdHistory", "ntPwdHistory", "dBCSPwd", "unicodePwd", "badPasswordTime", "badPwdCount", "lockoutTime", "msDS-KeyVersionNumber", "msDS-SecondaryKrbTgtNumber", NULL }; struct ldb_request *search_req; int ret; ldb = ldb_module_get_ctx(ac->module); ret = ldb_build_search_req(&search_req, ldb, ac, ac->req->op.mod.message->dn, LDB_SCOPE_BASE, "(objectclass=*)", attrs, NULL, ac, ph_mod_search_callback, ac->req); LDB_REQ_SET_LOCATION(search_req); if (ret != LDB_SUCCESS) { return ret; } return ldb_next_request(ac->module, search_req); } static int password_hash_mod_do_mod(struct ph_context *ac) { struct ldb_context *ldb = ldb_module_get_ctx(ac->module); struct ldb_request *mod_req; struct setup_password_fields_io io; int ret; /* Prepare the internal data structure containing the passwords */ ret = setup_io(ac, ac->req->op.mod.message, ac->search_res->message, &io); if (ret != LDB_SUCCESS) { return ret; } ret = setup_password_fields(&io); if (ret != LDB_SUCCESS) { return ret; } ret = check_password_restrictions_and_log(&io); if (ret != LDB_SUCCESS) { return ret; } ret = setup_smartcard_reset(&io); if (ret != LDB_SUCCESS) { return ret; } ret = update_final_msg(&io); if (ret != LDB_SUCCESS) { return ret; } ret = ldb_build_mod_req(&mod_req, ldb, ac, ac->update_msg, ac->req->controls, ac, ph_op_callback, ac->req); LDB_REQ_SET_LOCATION(mod_req); if (ret != LDB_SUCCESS) { return ret; } return ldb_next_request(ac->module, mod_req); } static const struct ldb_module_ops ldb_password_hash_module_ops = { .name = "password_hash", .add = password_hash_add, .modify = password_hash_modify }; int ldb_password_hash_module_init(const char *version) { #ifdef ENABLE_GPGME const char *gversion = NULL; #endif /* ENABLE_GPGME */ LDB_MODULE_CHECK_VERSION(version); #ifdef ENABLE_GPGME /* * Note: this sets a SIGPIPE handler * if none is active already. See: * https://www.gnupg.org/documentation/manuals/gpgme/Signal-Handling.html#Signal-Handling */ gversion = gpgme_check_version(MINIMUM_GPGME_VERSION); if (gversion == NULL) { fprintf(stderr, "%s() in %s version[%s]: " "gpgme_check_version(%s) not available, " "gpgme_check_version(NULL) => '%s'\n", __func__, __FILE__, version, MINIMUM_GPGME_VERSION, gpgme_check_version(NULL)); return LDB_ERR_UNAVAILABLE; } #endif /* ENABLE_GPGME */ return ldb_register_module(&ldb_password_hash_module_ops); }