diff options
4 files changed, 127 insertions, 448 deletions
diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am b/daemons/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am index cc041aa4f..bdc583566 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/Makefile.am @@ -2,7 +2,8 @@ NULL = PLUGIN_COMMON_DIR=../common KRB5_UTIL_DIR= ../../../util -KRB5_UTIL_SRCS = $(KRB5_UTIL_DIR)/ipa_krb5.c +KRB5_UTIL_SRCS = $(KRB5_UTIL_DIR)/ipa_krb5.c \ + $(KRB5_UTIL_DIR)/ipa_pwd.c INCLUDES = \ -I. \ diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c index 68d1703b5..91ef0208c 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c @@ -120,6 +120,24 @@ static int filter_keys(struct ipapwd_krbcfg *krbcfg, return 0; } +static int ipapwd_to_ldap_pwpolicy_error(int ipapwderr) +{ + switch (ipapwderr) { + case IPAPWD_POLICY_ACCOUNT_EXPIRED: + return LDAP_PWPOLICY_PWDMODNOTALLOWED; + case IPAPWD_POLICY_PWD_TOO_YOUNG: + return LDAP_PWPOLICY_PWDTOOYOUNG; + case IPAPWD_POLICY_PWD_TOO_SHORT: + return LDAP_PWPOLICY_PWDTOOSHORT; + case IPAPWD_POLICY_PWD_IN_HISTORY: + return LDAP_PWPOLICY_PWDINHISTORY; + case IPAPWD_POLICY_PWD_COMPLEXITY: + return LDAP_PWPOLICY_INVALIDPWDSYNTAX; + } + /* in case of unhandled error return access denied */ + return LDAP_PWPOLICY_PWDMODNOTALLOWED; +} + static int ipapwd_chpwop(Slapi_PBlock *pb, struct ipapwd_krbcfg *krbcfg) { @@ -374,12 +392,13 @@ parse_req_done: ret = ipapwd_CheckPolicy(&pwdata); if (ret) { errMesg = "Password Fails to meet minimum strength criteria"; - if (ret & IPAPWD_POLICY_ERROR) { - slapi_pwpolicy_make_response_control(pb, -1, -1, ret & IPAPWD_POLICY_MASK); - rc = LDAP_CONSTRAINT_VIOLATION; - } else { + if (ret == IPAPWD_POLICY_ERROR) { errMesg = "Internal error"; rc = ret; + } else { + ret = ipapwd_to_ldap_pwpolicy_error(ret); + slapi_pwpolicy_make_response_control(pb, -1, -1, ret); + rc = LDAP_CONSTRAINT_VIOLATION; } goto free_and_return; } diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h index 7d81bda3d..e430db73d 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd.h @@ -61,6 +61,7 @@ #include <openssl/md4.h> #include "ipa_krb5.h" +#include "ipa_pwd.h" #define IPAPWD_PLUGIN_NAME "ipa-pwd-extop" #define IPAPWD_FEATURE_DESC "IPA Password Manager" @@ -80,10 +81,9 @@ struct ipapwd_data { char *dn; char *password; time_t timeNow; - time_t lastPwChange; time_t expireTime; int changetype; - int pwHistoryLen; + struct ipapwd_policy policy; }; struct ipapwd_operation { @@ -94,11 +94,6 @@ struct ipapwd_operation { #define GENERALIZED_TIME_LENGTH 15 -#define IPAPWD_POLICY_MASK 0x0FF -#define IPAPWD_POLICY_ERROR 0x100 -#define IPAPWD_POLICY_OK 0 - - /* from ipapwd_common.c */ struct ipapwd_krbcfg { krb5_context krbctx; diff --git a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd_common.c b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd_common.c index f36fc774b..c111eb1b3 100644 --- a/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd_common.c +++ b/daemons/ipa-slapi-plugins/ipa-pwd-extop/ipapwd_common.c @@ -329,8 +329,9 @@ static int ipapwd_rdn_count(const char *dn) return rdnc; } -static int ipapwd_getPolicy(const char *dn, - Slapi_Entry *target, Slapi_Entry **e) +int ipapwd_getPolicy(const char *dn, + Slapi_Entry *target, + struct ipapwd_policy *policy) { const char *krbPwdPolicyReference; const char *pdn; @@ -347,6 +348,7 @@ static int ipapwd_getPolicy(const char *dn, int buffer_flags=0; Slapi_ValueSet* results = NULL; char* actual_type_name = NULL; + int tmpint; LOG_TRACE("Searching policy for [%s]\n", dn); @@ -379,8 +381,6 @@ static int ipapwd_getPolicy(const char *dn, scope = LDAP_SCOPE_SUBTREE; } - *e = NULL; - pb = slapi_pblock_new(); slapi_search_internal_set_pb(pb, pdn, scope, @@ -413,10 +413,8 @@ static int ipapwd_getPolicy(const char *dn, /* if there is only one, return that */ if (i == 1) { - *e = slapi_entry_dup(es[0]); - - ret = 0; - goto done; + pe = es[0]; + goto fill; } /* count number of RDNs in DN */ @@ -463,8 +461,27 @@ static int ipapwd_getPolicy(const char *dn, goto done; } - *e = slapi_entry_dup(pe); +fill: + policy->min_pwd_life = slapi_entry_attr_get_int(pe, "krbMinPwdLife"); + + tmpint = slapi_entry_attr_get_int(pe, "krbMaxPwdLife"); + if (tmpint != 0) { + policy->max_pwd_life = tmpint; + } + + tmpint = slapi_entry_attr_get_int(pe, "krbPwdMinLength"); + if (tmpint != 0) { + policy->min_pwd_length = tmpint; + } + + policy->history_length = slapi_entry_attr_get_int(pe, + "krbPwdHistoryLength"); + + policy->min_complexity = slapi_entry_attr_get_int(pe, + "krbPwdMinDiffChars"); + ret = 0; + done: if (results) { pwd_values_free(&results, &actual_type_name, buffer_flags); @@ -477,24 +494,6 @@ done: return ret; } -static Slapi_Value *ipapwd_strip_pw_date(Slapi_Value *pw) -{ - const char *pwstr; - - pwstr = slapi_value_get_string(pw); - return slapi_value_new_string(&pwstr[GENERALIZED_TIME_LENGTH]); -} - -/* searches the directory and finds the policy closest to the DN */ -/* return 0 on success, -1 on error or if no policy is found */ -static int ipapwd_sv_pw_cmp(const void *pv1, const void *pv2) -{ - const char *pw1 = slapi_value_get_string(*((Slapi_Value **)pv1)); - const char *pw2 = slapi_value_get_string(*((Slapi_Value **)pv2)); - - return strncmp(pw1, pw2, GENERALIZED_TIME_LENGTH); -} - /*==Common-public-functions=============================================*/ @@ -620,62 +619,19 @@ done: return rc; } -/* 90 days default pwd max lifetime */ -#define IPAPWD_DEFAULT_PWDLIFE (90 * 24 *3600) -#define IPAPWD_DEFAULT_MINLEN 0 - /* check password strenght and history */ int ipapwd_CheckPolicy(struct ipapwd_data *data) { - char *krbPrincipalExpiration = NULL; - char *krbLastPwdChange = NULL; - char *krbPasswordExpiration = NULL; - int krbMaxPwdLife = IPAPWD_DEFAULT_PWDLIFE; - int krbPwdMinLength = IPAPWD_DEFAULT_MINLEN; - int krbPwdMinDiffChars = 0; - int krbMinPwdLife = 0; - int pwdCharLen = 0; - Slapi_Entry *policy = NULL; - Slapi_Attr *passwordHistory = NULL; - struct tm tm; - int tmp, ret; - char *old_pw; - - /* check account is not expired. Ignore unixtime = 0 (Jan 1 1970) */ - krbPrincipalExpiration = - slapi_entry_attr_get_charptr(data->target, "krbPrincipalExpiration"); - if (krbPrincipalExpiration && - (strcasecmp("19700101000000Z", krbPrincipalExpiration) != 0)) { - /* if expiration date is set check it */ - memset(&tm, 0, sizeof(struct tm)); - ret = sscanf(krbPrincipalExpiration, - "%04u%02u%02u%02u%02u%02u", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, - &tm.tm_hour, &tm.tm_min, &tm.tm_sec); - - if (ret == 6) { - tm.tm_year -= 1900; - tm.tm_mon -= 1; - - if (data->timeNow > timegm(&tm)) { - LOG_TRACE("Account Expired"); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDMODNOTALLOWED; - } - } - /* FIXME: else error out ? */ - } - slapi_ch_free_string(&krbPrincipalExpiration); - - /* find the entry with the password policy */ - ret = ipapwd_getPolicy(data->dn, data->target, &policy); - if (ret) { - LOG_TRACE("No password policy"); - goto no_policy; - } + struct ipapwd_policy pol = {0}; + time_t acct_expiration; + time_t pwd_expiration; + time_t last_pwd_change; + char **pwd_history; + char *tmpstr; + int ret; - /* Retrieve Max History Len */ - data->pwHistoryLen = - slapi_entry_attr_get_int(policy, "krbPwdHistoryLength"); + pol.max_pwd_life = IPAPWD_DEFAULT_PWDLIFE; + pol.min_pwd_length = IPAPWD_DEFAULT_MINLEN; if (data->changetype != IPA_CHANGETYPE_NORMAL) { /* We must skip policy checks (Admin change) but @@ -685,279 +641,51 @@ int ipapwd_CheckPolicy(struct ipapwd_data *data) data->expireTime = data->timeNow; } - /* skip policy checks */ - slapi_entry_free(policy); - goto no_policy; - } - - /* first of all check current password, if any */ - old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword"); - if (old_pw) { - Slapi_Value *cpw[2] = {NULL, NULL}; - Slapi_Value *pw; - - cpw[0] = slapi_value_new_string(old_pw); - pw = slapi_value_new_string(data->password); - if (!pw) { - LOG_OOM(); - slapi_entry_free(policy); - slapi_ch_free_string(&old_pw); - slapi_value_free(&cpw[0]); - slapi_value_free(&pw); - return LDAP_OPERATIONS_ERROR; - } - - ret = slapi_pw_find_sv(cpw, pw); - slapi_ch_free_string(&old_pw); - slapi_value_free(&cpw[0]); - slapi_value_free(&pw); - - if (ret == 0) { - LOG_TRACE("Password in history\n"); - slapi_entry_free(policy); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY; - } - } - - krbPasswordExpiration = - slapi_entry_attr_get_charptr(data->target, "krbPasswordExpiration"); - krbLastPwdChange = - slapi_entry_attr_get_charptr(data->target, "krbLastPwdChange"); - /* if no previous change, it means this is probably a new account - * or imported, log and just ignore */ - if (krbLastPwdChange) { - - memset(&tm, 0, sizeof(struct tm)); - ret = sscanf(krbLastPwdChange, - "%04u%02u%02u%02u%02u%02u", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, - &tm.tm_hour, &tm.tm_min, &tm.tm_sec); - - if (ret == 6) { - tm.tm_year -= 1900; - tm.tm_mon -= 1; - data->lastPwChange = timegm(&tm); - } - /* FIXME: *else* report an error ? */ + /* do not load policies */ } else { - LOG_TRACE("Warning: Last Password Change Time is not available\n"); - } - - /* Check min age */ - krbMinPwdLife = slapi_entry_attr_get_int(policy, "krbMinPwdLife"); - /* if no default then treat it as no limit */ - if (krbMinPwdLife != 0) { - - /* check for reset cases */ - if (krbLastPwdChange == NULL || - ((krbPasswordExpiration != NULL) && - strcmp(krbPasswordExpiration, krbLastPwdChange) == 0)) { - /* Expiration and last change time are the same or - * missing this happens only when a password is reset - * by an admin or the account is new or no expiration - * policy is set, PASS */ - LOG_TRACE("Ignore krbMinPwdLife Expiration, not enough info\n"); - - } else if (data->timeNow < data->lastPwChange + krbMinPwdLife) { - LOG_TRACE("Too soon to change password\n"); - slapi_entry_free(policy); - slapi_ch_free_string(&krbPasswordExpiration); - slapi_ch_free_string(&krbLastPwdChange); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOYOUNG; - } - } - - /* free strings or we leak them */ - slapi_ch_free_string(&krbPasswordExpiration); - slapi_ch_free_string(&krbLastPwdChange); - - /* Retrieve min length */ - tmp = slapi_entry_attr_get_int(policy, "krbPwdMinLength"); - if (tmp != 0) { - krbPwdMinLength = tmp; - } - - /* check complexity */ - /* FIXME: this code is partially based on Directory Server code, - * the plan is to merge this code later making it available - * trough a pulic DS API for slapi plugins */ - krbPwdMinDiffChars = - slapi_entry_attr_get_int(policy, "krbPwdMinDiffChars"); - if (krbPwdMinDiffChars != 0) { - int num_digits = 0; - int num_alphas = 0; - int num_uppers = 0; - int num_lowers = 0; - int num_specials = 0; - int num_8bit = 0; - int num_repeated = 0; - int max_repeated = 0; - int num_categories = 0; - char *p, *pwd; - - pwd = strdup(data->password); - - /* check character types */ - p = pwd; - while (p && *p) { - if (ldap_utf8isdigit(p)) { - num_digits++; - } else if (ldap_utf8isalpha(p)) { - num_alphas++; - if (slapi_utf8isLower((unsigned char *)p)) { - num_lowers++; - } else { - num_uppers++; - } - } else { - /* check if this is an 8-bit char */ - if (*p & 128) { - num_8bit++; - } else { - num_specials++; - } - } - - /* check for repeating characters. If this is the - first char of the password, no need to check */ - if (pwd != p) { - int len = ldap_utf8len(p); - char *prev_p = ldap_utf8prev(p); - - if (len == ldap_utf8len(prev_p)) { - if (memcmp(p, prev_p, len) == 0) { - num_repeated++; - if (max_repeated < num_repeated) { - max_repeated = num_repeated; - } - } else { - num_repeated = 0; - } - } else { - num_repeated = 0; - } - } - - p = ldap_utf8next(p); - } - - free(pwd); - p = pwd = NULL; - /* tally up the number of character categories */ - if (num_digits > 0) ++num_categories; - if (num_uppers > 0) ++num_categories; - if (num_lowers > 0) ++num_categories; - if (num_specials > 0) ++num_categories; - if (num_8bit > 0) ++num_categories; - - /* FIXME: the kerberos plicy schema does not define separated - * threshold values, so just treat anything as a category, - * we will fix this when we merge with DS policies */ - - if (max_repeated > 1) --num_categories; - - if (num_categories < krbPwdMinDiffChars) { - LOG_TRACE("Password not complex enough\n"); - slapi_entry_free(policy); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_INVALIDPWDSYNTAX; + /* find the entry with the password policy */ + ret = ipapwd_getPolicy(data->dn, data->target, &pol); + if (ret) { + LOG_TRACE("No password policy, use defaults"); } } - /* Check password history */ - ret = slapi_entry_attr_find(data->target, - "passwordHistory", &passwordHistory); - if (ret == 0) { - int err, hint, count, i, j; - const char *pwstr; - Slapi_Value **pH; - Slapi_Value *pw; - - hint = 0; - count = 0; - err = slapi_attr_get_numvalues(passwordHistory, &count); - /* check history only if we have one */ - if (count > 0 && data->pwHistoryLen > 0) { - pH = calloc(count + 2, sizeof(Slapi_Value *)); - if (!pH) { - LOG_OOM(); - slapi_entry_free(policy); - return LDAP_OPERATIONS_ERROR; - } - - i = 0; - hint = slapi_attr_first_value(passwordHistory, &pw); - while (hint != -1) { - pwstr = slapi_value_get_string(pw); - /* if shorter than GENERALIZED_TIME_LENGTH, it - * is garbage, we never set timeless entries */ - if (pwstr && - (strlen(pwstr) > GENERALIZED_TIME_LENGTH)) { - pH[i] = pw; - i++; - } - hint = slapi_attr_next_value(passwordHistory, hint, &pw); - } - - qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp); + tmpstr = slapi_entry_attr_get_charptr(data->target, + "krbPrincipalExpiration"); + acct_expiration = ipapwd_gentime_to_time_t(tmpstr); + slapi_ch_free_string(&tmpstr); - if (i > data->pwHistoryLen) { - i = data->pwHistoryLen; - pH[i] = NULL; - } + tmpstr = slapi_entry_attr_get_charptr(data->target, + "krbPasswordExpiration"); + pwd_expiration = ipapwd_gentime_to_time_t(tmpstr); + slapi_ch_free_string(&tmpstr); - for (j = 0; pH[j]; j++) { - pH[j] = ipapwd_strip_pw_date(pH[j]); - } + tmpstr = slapi_entry_attr_get_charptr(data->target, + "krbLastPwdChange"); + last_pwd_change = ipapwd_gentime_to_time_t(tmpstr); + slapi_ch_free_string(&tmpstr); - pw = slapi_value_new_string(data->password); - if (!pw) { - LOG_OOM(); - slapi_entry_free(policy); - free(pH); - return LDAP_OPERATIONS_ERROR; - } + pwd_history = slapi_entry_attr_get_charray(data->target, + "passwordHistory"); - err = slapi_pw_find_sv(pH, pw); + /* check policy */ + ret = ipapwd_check_policy(&pol, data->password, + data->timeNow, + acct_expiration, + pwd_expiration, + last_pwd_change, + pwd_history); - for (j = 0; pH[j]; j++) { - slapi_value_free(&pH[j]); - } - slapi_value_free(&pw); - free(pH); - - if (err == 0) { - LOG_TRACE("Password in history\n"); - slapi_entry_free(policy); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY; - } - } - } - - /* Calculate max age */ - tmp = slapi_entry_attr_get_int(policy, "krbMaxPwdLife"); - if (tmp != 0) { - krbMaxPwdLife = tmp; - } - - slapi_entry_free(policy); - -no_policy: - - /* check min lenght */ - pwdCharLen = ldap_utf8characters(data->password); - - if (pwdCharLen < krbPwdMinLength) { - LOG_TRACE("Password too short (%d < %d)\n", - pwdCharLen, krbPwdMinLength); - return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOSHORT; - } + slapi_ch_array_free(pwd_history); if (data->expireTime == 0) { - data->expireTime = data->timeNow + krbMaxPwdLife; + data->expireTime = data->timeNow + pol.max_pwd_life; } - return IPAPWD_POLICY_OK; + data->policy = pol; + + return ret; } /* Searches the dn in directory, @@ -1154,10 +882,12 @@ int ipapwd_SetPassword(struct ipapwd_krbcfg *krbcfg, "userPassword", data->password); /* set password history */ - pwvals = ipapwd_setPasswordHistory(smods, data); - if (pwvals) { - slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, - "passwordHistory", pwvals); + if (data->policy.history_length > 0) { + pwvals = ipapwd_setPasswordHistory(smods, data); + if (pwvals) { + slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, + "passwordHistory", pwvals); + } } /* FIXME: @@ -1186,111 +916,45 @@ Slapi_Value **ipapwd_setPasswordHistory(Slapi_Mods *smods, struct ipapwd_data *data) { Slapi_Value **pH = NULL; - Slapi_Attr *passwordHistory = NULL; - char timestr[GENERALIZED_TIME_LENGTH+1]; - char *histr, *old_pw; - struct tm utctime; - int ret, pc; + char **pwd_history = NULL; + char **new_pwd_history = NULL; + int n = 0; + int ret; + int i; - old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword"); - if (!old_pw) { - /* no old password to store, just return */ - return NULL; - } + pwd_history = slapi_entry_attr_get_charray(data->target, + "passwordHistory"); - if (!gmtime_r(&(data->timeNow), &utctime)) { - LOG_FATAL("failed to retrieve current date (buggy gmtime_r ?)\n"); - return NULL; - } - strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime); + ret = ipapwd_generate_new_history(data->password, data->timeNow, + data->policy.history_length, + pwd_history, &new_pwd_history, &n); - histr = slapi_ch_smprintf("%s%s", timestr, old_pw); - if (!histr) { - LOG_OOM(); - return NULL; + if (ret) { + LOG_FATAL("failed to generate new password history!\n"); + goto done; } - /* retrieve current history */ - ret = slapi_entry_attr_find(data->target, - "passwordHistory", &passwordHistory); - if (ret == 0) { - int err, hint, count, i, j; - const char *pwstr; - Slapi_Value *pw; - - hint = 0; - count = 0; - err = slapi_attr_get_numvalues(passwordHistory, &count); - /* if we have one */ - if (err == 0 && count > 0 && data->pwHistoryLen > 0) { - pH = calloc(count + 2, sizeof(Slapi_Value *)); - if (!pH) { - LOG_OOM(); - free(histr); - return NULL; - } - - i = 0; - hint = slapi_attr_first_value(passwordHistory, &pw); - while (hint != -1) { - pwstr = slapi_value_get_string(pw); - /* if shorter than GENERALIZED_TIME_LENGTH, it - * is garbage, we never set timeless entries */ - if (pwstr && - (strlen(pwstr) > GENERALIZED_TIME_LENGTH)) { - pH[i] = pw; - i++; - } - hint = slapi_attr_next_value(passwordHistory, hint, &pw); - } - - qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp); - - if (i >= data->pwHistoryLen) { - /* need to rotate out the first entry */ - for (j = 0; j < data->pwHistoryLen; j++) { - pH[j] = pH[j + 1]; - } - - i = data->pwHistoryLen; - pH[i] = NULL; - i--; - } - - pc = i; - - /* copy only interesting entries */ - for (i = 0; i < pc; i++) { - pH[i] = slapi_value_dup(pH[i]); - if (pH[i] == NULL) { - LOG_OOM(); - while (i) { - i--; - slapi_value_free(&pH[i]); - } - free(pH); - free(histr); - return NULL; - } - } - } + pH = (Slapi_Value **)slapi_ch_calloc(n + 1, sizeof(Slapi_Value *)); + if (!pH) { + LOG_OOM(); + goto done; } - if (pH == NULL) { - pH = calloc(2, sizeof(Slapi_Value *)); - if (!pH) { + for (i = 0; i < n; i++) { + pH[i] = slapi_value_new_string(new_pwd_history[i]); + if (!pH[i]) { + ipapwd_free_slapi_value_array(&pH); LOG_OOM(); - free(histr); - return NULL; + goto done; } - pc = 0; } - /* add new history value */ - pH[pc] = slapi_value_new_string(histr); - - free(histr); - +done: + slapi_ch_array_free(pwd_history); + for (i = 0; i < n; i++) { + free(new_pwd_history[i]); + } + free(new_pwd_history); return pH; } |