/*
 * libpwquality main API code for quality checking
 *
 * See the end of the file for Copyright and License Information
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#ifdef HAVE_CRACK_H
#include <crack.h>
#endif
#include <sys/types.h>
#include <pwd.h>
#include <unistd.h>

#include "pwquality.h"
#include "pwqprivate.h"

#ifdef MIN
#undef MIN
#endif
#define MIN(_a, _b) (((_a) < (_b)) ? (_a) : (_b))

/* Helper functions */

/*
 * test for a palindrome - like `R A D A R' or `M A D A M'
 */
static int
palindrome(const char *new)
{
        int i, j;

        i = strlen (new);

        for (j = 0; j < i; j++)
                if (new[i - j - 1] != new[j])
                        return 0;

        return 1;
}

/*
 * Calculate how different two strings are in terms of the number of
 * character removals, additions, and changes needed to go from one to
 * the other
 */

static int 
distdifferent(const char *old, const char *new,
              size_t i, size_t j)
{
        char c, d;

        if ((i == 0) || (strlen(old) < i)) {
                c = 0;
        } else {
                c = old[i - 1];
        }

        if ((j == 0) || (strlen(new) < j)) {
                d = 0;
        } else {
                d = new[j - 1];
        }
        return (c != d);
}

static int
distcalculate(int **distances, const char *old, const char *new,
              size_t i, size_t j)
{
        int tmp = 0;

        if (distances[i][j] != -1) {
                return distances[i][j];
        }

        tmp = distcalculate(distances, old, new, i - 1, j - 1);
        tmp = MIN(tmp, distcalculate(distances, old, new, i, j - 1));
        tmp = MIN(tmp, distcalculate(distances, old, new, i - 1,     j));
        tmp += distdifferent(old, new, i, j);

        distances[i][j] = tmp;

        return tmp;
}

static int
distance(const char *old, const char *new)
{
        int **distances = NULL;
        size_t m, n, i, j;
        int r = -1;

        m = strlen(old);
        n = strlen(new);
        distances = calloc(m + 1, sizeof(int*));
        if (distances == NULL)
                return -1;

        for (i = 0; i <= m; i++) {
                distances[i] = calloc(n + 1, sizeof(int));
                if (distances[i] == NULL)
                        goto allocfail;

                for(j = 0; j <= n; j++) {
                        distances[i][j] = -1;
                }
        }

        for (i = 0; i <= m; i++) {
                distances[i][0] = i;
        }

        for (j = 0; j <= n; j++) {
                distances[0][j] = j;
        }

        r = distcalculate(distances, old, new, m, n);

allocfail:
        for (i = 0; i <= m; i++) {
                if (distances[i]) {
                        memset(distances[i], 0, sizeof(int) * (n + 1));
                        free(distances[i]);
                }
        }
        free(distances);

        return r;
}

static int
similar(pwquality_settings_t *pwq,
        const char *old, const char *new)
{
        int dist;

        dist = distance(old, new);

        if (dist < 0)
                return PWQ_ERROR_MEM_ALLOC;

        if (dist >= pwq->diff_ok) {
                return 0;
        }

        if (strlen(new) >= (strlen(old) * 2)) {
                return 0;
        }

        /* passwords are too similar */
        return PWQ_ERROR_TOO_SIMILAR;
}

/*
 * count classes of charecters
 */

static int
numclass(const char *new)
{
        int digits = 0;
        int uppers = 0;
        int lowers = 0;
        int others = 0;
        int total_class;
        int i;

        for (i = 0; new[i]; i++) {
                if (isdigit(new[i]))
                        digits = 1;
                else if (isupper(new[i]))
                        uppers = 1;
                else if (islower(new[i]))
                        lowers = 1;
                else
                        others = 1;
        }

        total_class = digits + uppers + lowers + others;

        return total_class;
}

/*
 * a nice mix of characters
 * the credit (if positive) is a maximum value that is subtracted from
 * the minimum allowed size of the password if letters of the class are
 * present in the password
 */
static int
simple(pwquality_settings_t *pwq, const char *new, void **auxerror)
{
        int digits = 0;
        int uppers = 0;
        int lowers = 0;
        int others = 0;
        int size;
        int i;
        enum { NONE, DIGIT, UCASE, LCASE, OTHER } prevclass = NONE;
        int sameclass = 0;

        for (i = 0; new[i]; i++) {
                if (isdigit(new[i])) {
                        digits++;
                        if (prevclass != DIGIT) {
                                prevclass = DIGIT;
                                sameclass = 1;
                        } else
                                sameclass++;
                }
                else if (isupper(new[i])) {
                        uppers++;
                        if (prevclass != UCASE) {
                                prevclass = UCASE;
                                sameclass = 1;
                        } else
                                sameclass++;
                }
                else if (islower(new[i])) {
                        lowers++;
                        if (prevclass != LCASE) {
                                prevclass = LCASE;
                                sameclass = 1;
                        } else
                                sameclass++;
                }
                else {
                        others++;
                        if (prevclass != OTHER) {
                                prevclass = OTHER;
                                sameclass = 1;
                        } else
                                sameclass++;
                }
                if (pwq->max_class_repeat > 1 && sameclass > pwq->max_class_repeat) {
                        if (auxerror)
                                *auxerror = (void *)(long)pwq->max_class_repeat;
                        return PWQ_ERROR_MAX_CLASS_REPEAT;
                }
        }

        if ((pwq->dig_credit >= 0) && (digits > pwq->dig_credit))
                digits = pwq->dig_credit;

        if ((pwq->up_credit >= 0) && (uppers > pwq->up_credit))
                uppers = pwq->up_credit;

        if ((pwq->low_credit >= 0) && (lowers > pwq->low_credit))
                lowers = pwq->low_credit;

        if ((pwq->oth_credit >= 0) && (others > pwq->oth_credit))
                others = pwq->oth_credit;

        size = pwq->min_length;

        if (pwq->dig_credit >= 0)
                size -= digits;
        else if (digits < -pwq->dig_credit) {
                if (auxerror)
                        *auxerror = (void *)(long)-pwq->dig_credit;
                return PWQ_ERROR_MIN_DIGITS;
        }

        if (pwq->up_credit >= 0)
                size -= uppers;
        else if (uppers < -pwq->up_credit) {
                if (auxerror)
                        *auxerror = (void *)(long)-pwq->up_credit;
                return PWQ_ERROR_MIN_UPPERS;
        }

        if (pwq->low_credit >= 0)
                size -= lowers;
        else if (lowers < -pwq->low_credit) {
                if (auxerror)
                        *auxerror = (void *)(long)-pwq->low_credit;
                return PWQ_ERROR_MIN_LOWERS;
        }

        if (pwq->oth_credit >= 0)
                size -= others;
        else if (others < -pwq->oth_credit) {
                if (auxerror)
                        *auxerror = (void *)(long)-pwq->oth_credit;
                return PWQ_ERROR_MIN_OTHERS;
        }

        if (size <= i)
                return 0;

        if (auxerror)
                *auxerror = (void *)(long)size;

        return PWQ_ERROR_MIN_LENGTH;
}

/*
 * too many same consecutive characters
 */

static int
consecutive(pwquality_settings_t *pwq, const char *new, void **auxerror)
{
        char c;
        int i;
        int same;

        if (pwq->max_repeat == 0)
                return 0;

        for (i = 0; new[i]; i++) {
                if (i > 0 && new[i] == c) {
                        ++same;
                        if (same > pwq->max_repeat) {
                                if (auxerror)
                                        *auxerror = (void *)(long)pwq->max_repeat;
                                return 1;
                        }
                } else {
                        c = new[i];
                        same = 1;
                }
        }
        return 0;
}

static int sequence(pwquality_settings_t *pwq, const char *new, void **auxerror)
{
        char c;
        int i;
        int sequp = 1;
        int seqdown = 1;

        if (pwq->max_sequence == 0)
                return 0;

        if (new[0] == '\0')
                return 0;

        for (i = 1; new[i]; i++) {
                c = new[i-1];
                if (new[i] == c+1) {
                        ++sequp;
                        if (sequp > pwq->max_sequence) {
                                if (auxerror)
                                        *auxerror = (void *)(long)pwq->max_sequence;
                                return 1;
                        }
                        seqdown = 1;
                } else if (new[i] == c-1) {
                        ++seqdown;
                        if (seqdown > pwq->max_sequence) {
                                if (auxerror)
                                        *auxerror = (void *)(long)pwq->max_sequence;
                                return 1;
                        }
                        sequp = 1;
                } else {
                        sequp = 1;
                        seqdown = 1;
                }
        }
        return 0;
}

static int
wordcheck(pwquality_settings_t *pwq, const char *new,
          char *word)
{
        char *f, *b;
        int dist, wordlen = strlen(word);

        /* No point to check for word in password for 1-3 char
         * words; it will be contained one way or another anyway. */
        if (wordlen < PWQ_MIN_WORD_LENGTH)
                return 0;

        if (strstr(new, word) != NULL)
                return PWQ_ERROR_BAD_WORDS;

        dist = distance(new, word);
        if (dist >= 0 && dist < PWQ_DEFAULT_DIFF_OK)
                return PWQ_ERROR_BAD_WORDS;

        /* now reverse the wordname, we can do that in place
                as it is strdup-ed */
        f = word;
        b = word + wordlen - 1;
        while (f < b) {
                char c;

                c = *f;
                *f = *b;
                *b = c;
                --b;
                ++f;
        }

        if (strstr(new, word) != NULL)
                return PWQ_ERROR_BAD_WORDS;

        dist = distance(new, word);
        if (dist >= 0 && dist < PWQ_DEFAULT_DIFF_OK)
                return PWQ_ERROR_BAD_WORDS;

        return 0;
}

static int
usercheck(pwquality_settings_t *pwq, const char *new,
          char *user)
{
        int i, userlen;
        int rv = 0;
        char *subuser = calloc(pwq->user_substr+1, sizeof(char));

        if (subuser == NULL) {
                return PWQ_ERROR_MEM_ALLOC;
        }

        userlen = strlen(user);
        if (pwq->user_substr >= PWQ_MIN_WORD_LENGTH &&
            userlen > pwq->user_substr) {
                for(i = 0; !rv && (i <= userlen - pwq->user_substr); i++) {
                        strncpy(subuser, user+i, pwq->user_substr+1);
                        subuser[pwq->user_substr] = '\0';
                        rv = wordcheck(pwq, new, subuser);
                }
        }
        else {
                // if we already tested substrings, there's no need to test
                // the whole username; all substrings would've been found :)
                if (!rv)
                        rv = wordcheck(pwq, new, user);
        }
        // translate wordcheck return
        if (rv == PWQ_ERROR_BAD_WORDS)
                rv = PWQ_ERROR_USER_CHECK;
        free(subuser);
        return rv;
}

static char *
str_lower(char *string)
{
	char *cp;

	if (!string)
		return NULL;

	for (cp = string; *cp; cp++)
		*cp = tolower(*cp);
	return string;
}

static int
wordlistcheck(pwquality_settings_t *pwq, const char *new,
              const char *wordlist)
{
        char *list;
        char *p;
        char *next;

        if (wordlist == NULL)
                return 0;

        if ((list = strdup(wordlist)) == NULL) {
                return PWQ_ERROR_MEM_ALLOC;
        }

        for (p = list;;p = next + 1) {
                next = strchr(p, ' ');
                if (next)
                        *next = '\0';

                if (strlen(p) >= PWQ_MIN_WORD_LENGTH) {
                        str_lower(p);
                        if (wordcheck(pwq, new, p)) {
                                free(list);
                                return PWQ_ERROR_BAD_WORDS;
                        }
                }

                if (!next)
                        break;
        }

        free(list);
        return 0;
}

static int
gecoscheck(pwquality_settings_t *pwq, const char *new,
           const char *user)
{
        struct passwd pwd;
        struct passwd *result;
        char *buf;
        long bufsize;
        int rv;

        bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
        if (bufsize == -1 || bufsize > PWQ_MAX_PASSWD_BUF_LEN)
                bufsize = PWQ_MAX_PASSWD_BUF_LEN;
        buf = malloc(bufsize);
        if (buf == NULL)
                return PWQ_ERROR_MEM_ALLOC;

        if (getpwnam_r(user, &pwd, buf, bufsize, &result) != 0 ||
                result == NULL) {
                free(buf);
                return 0;
        }

        rv = wordlistcheck(pwq, new, result->pw_gecos);
        if (rv == PWQ_ERROR_BAD_WORDS)
                rv = PWQ_ERROR_GECOS_CHECK;

        free(buf);
        return rv;
}

static char *
x_strdup(const char *string)
{
        if (!string)
                return NULL;
        return strdup(string);
}

static int
password_check(pwquality_settings_t *pwq,
               const char *new, const char *old, const char *user,
               void **auxerror)
{
        int rv = 0;
        char *oldmono = NULL, *newmono, *wrapped = NULL;
        char *usermono = NULL;

        newmono = str_lower(x_strdup(new));
        if (!newmono)
                rv = PWQ_ERROR_MEM_ALLOC;

        if (!rv && user) {
                usermono = str_lower(x_strdup(user));
                if (!usermono)
                        rv = PWQ_ERROR_MEM_ALLOC;
        }

        if (!rv && old) {
                oldmono = str_lower(x_strdup(old));
                if (oldmono)
                        wrapped = malloc(strlen(oldmono) * 2 + 1);
                if (wrapped) {
                        strcpy (wrapped, oldmono);
                        strcat (wrapped, oldmono);
                } else {
                        rv = PWQ_ERROR_MEM_ALLOC;
                }
        }

        if (!rv && palindrome(newmono))
                rv = PWQ_ERROR_PALINDROME;

        if (!rv && oldmono && strcmp(oldmono, newmono) == 0)
                rv = PWQ_ERROR_CASE_CHANGES_ONLY;

        if (!rv && oldmono)
                rv = similar(pwq, oldmono, newmono);

        if (!rv)
                rv = simple(pwq, new, auxerror);

        if (!rv && wrapped && strstr(wrapped, newmono))
                rv = PWQ_ERROR_ROTATED;

        if (!rv && numclass(new) < pwq->min_class) {
                rv = PWQ_ERROR_MIN_CLASSES;
                if (auxerror) {
                        *auxerror = (void *)(long)pwq->min_class;
                }
        }

        if (!rv && consecutive(pwq, new, auxerror))
                rv = PWQ_ERROR_MAX_CONSECUTIVE;

        if (!rv && sequence(pwq, new, auxerror))
                rv = PWQ_ERROR_MAX_SEQUENCE;

        if (!rv && usermono && pwq->user_check)
                rv = usercheck(pwq, newmono, usermono);

        if (!rv && user && pwq->gecos_check)
                rv = gecoscheck(pwq, newmono, user);

        if (!rv)
                rv = wordlistcheck(pwq, newmono, pwq->bad_words);

        if (newmono) {
                memset(newmono, 0, strlen(newmono));
                free(newmono);
        }

        free(usermono);

        if (oldmono) {
                memset(oldmono, 0, strlen(oldmono));
                free(oldmono);
        }

        if (wrapped) {
                memset(wrapped, 0, strlen(wrapped));
                free(wrapped);
        }

        return rv;
}

/* this algorithm is an arbitrary one, fine-tuned by testing */
static int
password_score(pwquality_settings_t *pwq, const char *password)
{
        int len;
        int score;
        int i;
        int j;
        unsigned char freq[256];
        unsigned char *buf;

        len = strlen(password);

        if ((buf = malloc(len)) == NULL)
                /* should get enough memory to obtain a nice score */
                return PWQ_ERROR_MEM_ALLOC;

        score = (len - pwq->min_length) * 2;

        memcpy(buf, password, len);

        for (j = 0; j < 3; j++) {

                memset(freq, 0, sizeof(freq));

                for (i = 0; i < len - j; i++) {
                        ++freq[buf[i]];
                        if (i < len - j - 1)
                                buf[i] = abs(buf[i] - buf[i+1]);
                }

                for (i = 0; i < (int)sizeof(freq); i++) {
                        if (freq[i])
                                ++score;
                }
        }

        memset(buf, 0, len);
        free(buf);

        score += numclass(password) * 2;

        score = (score * 100)/(3 * pwq->min_length +
                               + PWQ_NUM_CLASSES * 2);

        score -= 50;

        if (score > 100)
                score = 100;
        if (score < 0)
                score = 0;

        return score;
}

/* check the password according to the settings
 * it returns either score <0-100> or negative error number;
 * the old password is optional */
int
pwquality_check(pwquality_settings_t *pwq, const char *password,
        const char *oldpassword, const char *user, void **auxerror)
{
        const char *msg;
        int score;

        if (auxerror)
                *auxerror = NULL;

        if (password == NULL || *password == '\0') {
                return PWQ_ERROR_EMPTY_PASSWORD;
        }

        if (user && *user == '\0')
                user = NULL;

        if (oldpassword && *oldpassword == '\0')
                oldpassword = NULL;

        if (oldpassword && strcmp(oldpassword, password) == 0) {
                return PWQ_ERROR_SAME_PASSWORD;
        }

        if (pwq->diff_ok == 0)
                oldpassword = NULL;

        score = password_check(pwq, password, oldpassword, user, auxerror);

        if (score != 0)
                return score;

        #ifdef HAVE_CRACK_H
        if (pwq->dict_check) {
                msg = FascistCheck(password, pwq->dict_path);
                if (msg) {
                        if (auxerror)
                                *auxerror = (void *)msg;
                        return PWQ_ERROR_CRACKLIB_CHECK;
                }
        }
        #endif

        score = password_score(pwq, password);

        return score;
}

/*
 * Copyright (c) Cristian Gafton <gafton@redhat.com>, 1996.
 *                                              All rights reserved
 * Copyright (c) Red Hat, Inc, 2011, 2015
 * Copyright (c) Tomas Mraz <tm@t8m.info>, 2011, 2015
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, and the entire permission notice in its entirety,
 *    including the disclaimer of warranties.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior
 *    written permission.
 *
 * ALTERNATIVELY, this product may be distributed under the terms of
 * the GNU General Public License version 2 or later, in which case the
 * provisions of the GPL are required INSTEAD OF the above restrictions.
 *
 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The following copyright was appended for the long password support
 * added with the libpam 0.58 release:
 *
 * Modificaton Copyright (c) Philip W. Dalrymple III <pwd@mdtsoft.com>
 *       1997. All rights reserved
 *
 * THE MODIFICATION THAT PROVIDES SUPPORT FOR LONG PASSWORD TYPE CHECKING TO
 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
