diff options
author | Simo Sorce <ssorce@redhat.com> | 2011-06-30 18:47:05 -0400 |
---|---|---|
committer | Simo Sorce <ssorce@redhat.com> | 2011-08-26 08:24:49 -0400 |
commit | a1637d47c0ccd596305567f925ee611c00dfa899 (patch) | |
tree | 003f1fc443cb802b685c9935d5c5cec2c3a912d5 /util/ipa_pwd.c | |
parent | 452fcdccdccb4340f20c85dd9d4f45ee2b02bf37 (diff) | |
download | freeipa-a1637d47c0ccd596305567f925ee611c00dfa899.tar.gz freeipa-a1637d47c0ccd596305567f925ee611c00dfa899.tar.xz freeipa-a1637d47c0ccd596305567f925ee611c00dfa899.zip |
util: add password policy manipulation functions
Diffstat (limited to 'util/ipa_pwd.c')
-rw-r--r-- | util/ipa_pwd.c | 634 |
1 files changed, 634 insertions, 0 deletions
diff --git a/util/ipa_pwd.c b/util/ipa_pwd.c new file mode 100644 index 000000000..c41617533 --- /dev/null +++ b/util/ipa_pwd.c @@ -0,0 +1,634 @@ +/* + * Password related utils for FreeIPA + * + * Authors: Simo Sorce <ssorce@redhat.com> + * + * Copyright (C) 2011 Simo Sorce, Red Hat + * see file 'COPYING' for use and warranty information + * + * 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 <http://www.gnu.org/licenses/>. + */ + +#define _GNU_SOURCE +#include <stdbool.h> +#include <stdio.h> +#include <time.h> +#include <ctype.h> +#include <nss3/nss.h> +#include <nss3/nssb64.h> +#include <nss3/hasht.h> +#include <nss3/pk11pub.h> +#include <errno.h> +#include "ipa_pwd.h" + +#define GENERALIZED_TIME_LENGTH 15 +#define DEFAULT_HASH_TYPE "{SHA512}" + +/** +* @brief Calculate utf8 string length +* +* @param str The string +* @param blength Integer into which returns the length in bytes +* +* @return Returns the number of utf8 characters, optionally if blength +* is not NULL it will contain the legth in bytes too. +*/ +static int strlen_utf8(char *str, int *blength) +{ + int i, j = 0; + + for (i = 0; str[i]; i++) { + if ((str[i] & 0xC0) != 0x80) { + j++; + } + } + + if (blength) { + *blength = i; + } + + return j; +} + +/** +* @brief Get the next utf8 code point +* +* @param cp The utf8 string +* +* @return ther pointer to the next code point or NULL +*/ +static char *utf8_next(char *cp) +{ + int t, c, i; + int ct = (unsigned char)*cp; + + if (ct == 0) { + return NULL; + } + + if (ct < 0x80) { + return cp + 1; + } + + t = 0xE0; + c = 2; + while (t != 0xFF) { + if (ct < t) { + for (i = 0; i < c && cp[i]; i++) ; + if (i != c) { + return NULL; + } + return cp + c; + } + t = (t >> 1) | 0x80; + c++; + } + + return NULL; +} + +static bool utf8_isdigit(char *p) +{ + if (*p & 0x80) { + return false; + } + return isdigit(*p); +} + +static bool utf8_isalpha(char *p) +{ + if (*p & 0x80) { + return false; + } + return isalpha(*p); +} + +/** +* @brief Get a string in generalize time format and returns time_t +* +* @param timestr The input string +* +* @return the time represented by the string or 0 on error +*/ +time_t ipapwd_gentime_to_time_t(char *timestr) +{ + struct tm tm; + time_t rtime = 0; + int ret; + + if (timestr != NULL) { + + memset(&tm, 0, sizeof(struct tm)); + ret = sscanf(timestr, "%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; + + rtime = timegm(&tm); + } + } + + return rtime; +} + +static int ipapwd_gentime_cmp(const void *p1, const void *p2) +{ + /* generalized time can be compared directly as ASCII codes + * are ordered numerically so a higher char value corresponds to + * a higher letter or number */ + + /* return youngest first by inverting terms */ + return memcmp(p2, p1, GENERALIZED_TIME_LENGTH); +} + +#define SHA_SALT_LENGTH 8 + +/* SHA*_LENGTH leghts come from nss3/hasht.h */ +#define SHA_HASH_MAX_LENGTH SHA512_LENGTH + +static int ipapwd_hash_type_to_alg(char *hash_type, + SECOidTag *hash_alg, + unsigned int *hash_alg_len) +{ + if (strncmp("{SSHA}", hash_type, 6) == 0) { + *hash_alg = SEC_OID_SHA1; + *hash_alg_len = SHA1_LENGTH; + return 0; + } + if (strncmp("{SHA256}", hash_type, 8) == 0) { + *hash_alg = SEC_OID_SHA256; + *hash_alg_len = SHA256_LENGTH; + return 0; + } + if (strncmp("{SHA384}", hash_type, 8) == 0) { + *hash_alg = SEC_OID_SHA384; + *hash_alg_len = SHA384_LENGTH; + return 0; + } + if (strncmp("{SHA512}", hash_type, 8) == 0) { + *hash_alg = SEC_OID_SHA512; + *hash_alg_len = SHA512_LENGTH; + return 0; + } + + return -1; +} + +/** +* @brief Hashes a password using the hash_type requested +* +* @param password The cleartext password to hash +* @param psalt An 8 byte salt, if NULL a random one is used +* @param hash_type The hash type ({SSHA}, {SHA256}, {SHA384}, {SHA512}) +* @param full_hash The resulting hash with the salt appended +* +* @return 0 on success, -1 on error. +*/ +static int ipapwd_hash_password(char *password, + char *hash_type, + unsigned char *salt, + unsigned char **full_hash, + unsigned int *full_hash_len) +{ + unsigned char *fh = NULL; + unsigned int fhl = 0; + unsigned char *pwd = (unsigned char *)password; + unsigned int pwdlen = strlen(password); + unsigned char saltbuf[SHA_SALT_LENGTH]; + unsigned char hash[SHA_HASH_MAX_LENGTH]; + unsigned int hash_len; + SECOidTag hash_alg; + unsigned int hash_alg_len; + PK11Context *ctx = NULL; + int ret; + + NSS_NoDB_Init("."); + + if (!salt) { + PK11_GenerateRandom(saltbuf, SHA_SALT_LENGTH); + salt = saltbuf; + } + + ret = ipapwd_hash_type_to_alg(hash_type, &hash_alg, &hash_alg_len); + if (ret) { + return -1; + } + + ctx = PK11_CreateDigestContext(hash_alg); + if (ctx == NULL) { + return -1; + } + + memset(hash, 0, sizeof(hash)); + + ret = PK11_DigestBegin(ctx); + if (ret == SECSuccess) { + ret = PK11_DigestOp(ctx, pwd, pwdlen); + } + if (ret == SECSuccess) { + ret = PK11_DigestOp(ctx, salt, SHA_SALT_LENGTH); + } + if (ret == SECSuccess) { + ret = PK11_DigestFinal(ctx, hash, &hash_len, hash_alg_len); + } + if (ret != SECSuccess) { + ret = -1; + goto done; + } + if (hash_len != hash_alg_len) { + ret = -1; + goto done; + } + + fhl = hash_len + SHA_SALT_LENGTH; + fh = malloc(fhl + 1); + if (!fh) { + ret = -1; + goto done; + } + memcpy(fh, hash, hash_len); + memcpy(fh + hash_len, salt, SHA_SALT_LENGTH); + memset(fh + fhl, '\0', 1); + +done: + PK11_DestroyContext(ctx, 1); + *full_hash = fh; + *full_hash_len = fhl; + return ret; +} + +/** +* @brief Compares the provided password with a history element +* +* @param password A cleartext password +* @param historyString A history element. +* +* A history element is a base64 string of a hash+salt buffer, prepended +* by the hash type enclosed within curly braces. +* +* @return 0 if password matches, 1 if it doesn't and -1 on errors. +*/ +static int ipapwd_cmp_password(char *password, char *historyString) +{ + char *hash_type; + char *b64part; + size_t b64_len; + SECItem *item; + unsigned char *salt; + unsigned char *hash = NULL; + unsigned int hash_len; + int ret; + + NSS_NoDB_Init("."); + + hash_type = historyString; + b64part = strchr(historyString, '}'); + if (!b64part) { + return -1; + } + b64part++; + b64_len = strlen(b64part); + + item = NSSBase64_DecodeBuffer(NULL, NULL, b64part, b64_len); + if (!item) { + return -1; + } + if (item->len <= SHA_SALT_LENGTH) { + ret = -1; + goto done; + } + + salt = item->data + (item->len - SHA_SALT_LENGTH); + ret = ipapwd_hash_password(password, hash_type, salt, &hash, &hash_len); + if (ret != 0) { + goto done; + } + + if (hash_len != item->len) { + ret = 1; + goto done; + } + + if (memcmp(item->data, hash, hash_len)) { + ret = 1; + goto done; + } + + ret = 0; + +done: + SECITEM_FreeItem(item, 1); + free(hash); + return ret; +} + +/** +* @brief Returns a history element string +* +* A history element is a base64 string of a hash+salt buffer, prepended +* by the hash type enclosed within curly braces. +* +* @param hash_time The time at which the hash has been created +* @param hash_type The hash type ({SSHA}, {SHA256}, {SHA384}, {SHA512}) +* @param hash A binary buffer containing hash+salt +* @param hash_len The length of the hash binary buffer +* +* @return A history element string or NULL on error. +*/ +static char *ipapwd_hash_to_history(time_t hash_time, + char *hash_type, + unsigned char *hash, + unsigned int hash_len) +{ + struct tm utctime; + char timestr[GENERALIZED_TIME_LENGTH+1]; + SECItem item; + char *encoded; + char *history; + int ret; + + if (!gmtime_r(&hash_time, &utctime)) { + return NULL; + } + strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime); + + NSS_NoDB_Init("."); + + item.type = siBuffer; + item.data = hash; + item.len = hash_len; + + encoded = NSSBase64_EncodeItem(NULL, NULL, 0, &item); + if (!encoded) { + return NULL; + } + + ret = asprintf(&history, "%s%s%s", timestr, hash_type, encoded); + if (ret == -1) { + history = NULL; + } + + free(encoded); + return history; +} + +/** +* @brief Funtion used to check password policies on a password change. +* +* @param policy The policy to check against +* @param password The new password +* @param cur_time The current time, usually set to time(NULL) +* @param acct_expiration Account expiration +* @param pwd_expiration Password expiration +* @param last_pwd_change Last Password change +* @param pwd_history Password history (must include current password) +* +* @return 0 if ok, or appropriate IPAPWD error otherwise. +*/ +int ipapwd_check_policy(struct ipapwd_policy *policy, + char *password, + time_t cur_time, + time_t acct_expiration, + time_t pwd_expiration, + time_t last_pwd_change, + char **pwd_history) +{ + int pwdlen, blen; + int ret; + + if (!policy || !password) { + return IPAPWD_POLICY_ERROR; + } + + /* check account is not expired. Ignore unixtime = 0 (Jan 1 1970) */ + if (acct_expiration != 0) { + /* if expiration date is set check it */ + if (cur_time > acct_expiration) { + return IPAPWD_POLICY_ACCOUNT_EXPIRED; + } + } + + if (policy->min_pwd_life != 0) { + /* check for reset cases */ + if (last_pwd_change != 0 && pwd_expiration != last_pwd_change) { + /* 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 */ + + if (cur_time < last_pwd_change + policy->min_pwd_life) { + return IPAPWD_POLICY_PWD_TOO_YOUNG; + } + } + } + + pwdlen = strlen_utf8(password, &blen); + + if (policy->min_pwd_length) { + if (pwdlen < policy->min_pwd_length) { + return IPAPWD_POLICY_PWD_TOO_SHORT; + } + } + + if (policy->min_complexity) { + 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, *n; + int size, len; + + /* we want the actual lenght in bytes here */ + len = blen; + + p = password; + while (p && *p) { + if (utf8_isdigit(p)) { + num_digits++; + /* alpha/lower/upper, is checked only for pure ASCII chars */ + } else if (utf8_isalpha(p)) { + num_alphas++; + if (islower(*p)) { + num_lowers++; + } else { + num_uppers++; + } + } else { + if (*p & 0x80) { + num_8bit++; + } else { + num_specials++; + } + } + + n = utf8_next(p); + if (n != NULL) { + size = n - p; + len -= size; + if ((len > size) && (memcmp(p, n, size) == 0)) { + num_repeated++; + if (max_repeated < num_repeated) { + max_repeated = num_repeated; + } + } else { + num_repeated = 0; + } + } + p = n; + } + + /* 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++; + } + if (max_repeated > 1) { + num_categories--; + } + + if (num_categories < policy->min_complexity) { + return IPAPWD_POLICY_PWD_COMPLEXITY; + } + } + + if (pwd_history) { + char *hash; + int i; + + for (i = 0; pwd_history[i]; i++) { + hash = pwd_history[i] + GENERALIZED_TIME_LENGTH; + + ret = ipapwd_cmp_password(password, hash); + if (ret == 0) { + return IPAPWD_POLICY_PWD_IN_HISTORY; + } + } + } + + return IPAPWD_POLICY_OK; +} + +/** +* @brief Generate a new password history using the new password +* +* @param password Clear text password +* @param cur_time Current time, usually time(NULL) +* @param history_length The history max length +* @param pwd_history The current password history array (can be NULL) +* @param new_pwd_history The new password history array (must be freed by +* caller) +* +* @return 0 on success, IPAPWD_POLICY_ERROR on error. +*/ +int ipapwd_generate_new_history(char *password, + time_t cur_time, + int history_length, + char **pwd_history, + char ***new_pwd_history, + int *new_pwd_hlen) +{ + unsigned char *hash = NULL; + unsigned int hash_len; + char *new_element; + char **ordered; + int c, i, n; + int len; + int ret; + + if (history_length == 0) { + return EINVAL; + } + + /* hardcode best hash we know about for now */ + ret = ipapwd_hash_password(password, DEFAULT_HASH_TYPE, NULL, + &hash, &hash_len); + if (ret != 0) { + return IPAPWD_POLICY_ERROR; + } + + new_element = ipapwd_hash_to_history(cur_time, DEFAULT_HASH_TYPE, + hash, hash_len); + if (!new_element) { + ret = IPAPWD_POLICY_ERROR; + goto done; + } + + for (c = 0; pwd_history && pwd_history[c]; c++) /* count */ ; + + if (c < history_length) { + c = history_length; + } + + ordered = calloc(c + 1, sizeof(char *)); + if (!ordered) { + ret = IPAPWD_POLICY_ERROR; + goto done; + } + + for (i = 0, n = 0; pwd_history && pwd_history[i]; i++) { + len = strlen(pwd_history[i]); + if (len < GENERALIZED_TIME_LENGTH) { + /* garbage, ignore */ + continue; + } + ordered[n] = strdup(pwd_history[i]); + if (!ordered[n]) { + ret = IPAPWD_POLICY_ERROR; + goto done; + } + n++; + } + + if (n) { + qsort(ordered, n, sizeof(char *), ipapwd_gentime_cmp); + } + + if (n >= history_length) { + for (i = history_length; i < n; i++) { + free(ordered[i]); + } + n = history_length; + } else { + n++; + } + ordered[n - 1] = new_element; + ordered[n] = NULL; + + *new_pwd_history = ordered; + *new_pwd_hlen = n; + ret = IPAPWD_POLICY_OK; + +done: + free(hash); + return ret; +} |