summaryrefslogtreecommitdiffstats
path: root/util/ipa_pwd.c
diff options
context:
space:
mode:
authorSimo Sorce <ssorce@redhat.com>2011-06-30 18:47:05 -0400
committerSimo Sorce <ssorce@redhat.com>2011-08-26 08:24:49 -0400
commita1637d47c0ccd596305567f925ee611c00dfa899 (patch)
tree003f1fc443cb802b685c9935d5c5cec2c3a912d5 /util/ipa_pwd.c
parent452fcdccdccb4340f20c85dd9d4f45ee2b02bf37 (diff)
downloadfreeipa-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.c634
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;
+}