/*
 * 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/>.
 */

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdbool.h>
#include <stdio.h>
#include <time.h>
#include <ctype.h>
#include <nss.h>
#include <nssb64.h>
#include <hasht.h>
#include <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(*(void * const *)p2, *(void * const *)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;
}

char * IPAPWD_ERROR_STRINGS[] = {
    "Password is OK",
    "Account expired",
    "Too soon to change password",
    "Password is too short",
    "Password reuse not permitted",
    "Password is too simple"
};

char * IPAPWD_ERROR_STRING_GENERAL = "Password does not meet the policy requirements";

char * ipapwd_error2string(enum ipapwd_error err) {
   if (err < 0 || err > IPAPWD_POLICY_PWD_COMPLEXITY) {
       /* IPAPWD_POLICY_ERROR or out of boundary, return general error */
       return IPAPWD_ERROR_STRING_GENERAL;
   }

   return IPAPWD_ERROR_STRINGS[err];
}

/**
* @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 = NULL;
    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;
    ordered = NULL;
    ret = IPAPWD_POLICY_OK;

done:
    free(ordered);
    free(hash);
    return ret;
}
