aboutsummaryrefslogtreecommitdiff
/*
 * lpass.c
 * lpass is a LessPass clone implemented in C.
 *
 * This file is part of lpass.
 *
 * lpass 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.
 *
 * lpass 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 lpass. If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <getopt.h>
#include <openssl/bn.h>
#include <openssl/evp.h>

static const int MAX_BUF = 1024;
static const int ENTROPY_ITERATIONS = 100000;
static const int ENTROPY_KEY_LENGTH = 32;
static const int DEFAULT_LENGTH = 16;

static const char CHAR_SUBSET_LOWER[] = "abcdefghijklmnopqrstuvwxyz";
static const char CHAR_SUBSET_UPPER[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
static const char CHAR_SUBSET_DIGITS[] = "0123456789";
static const char CHAR_SUBSET_SYMBOLS[] = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";

enum CharSet {
    CHARSET_LOWER   = 1 << 0,
    CHARSET_UPPER   = 1 << 1,
    CHARSET_DIGITS  = 1 << 2,
    CHARSET_SYMBOLS = 1 << 3,
};

BIGNUM *
calc_entropy(const char *site,
             const char *login,
             uint64_t counter,
             const char *master_pass)
{
    char salt[MAX_BUF + 1];
    int saltlen = snprintf(salt, MAX_BUF, "%s%s%lx", site, login, counter);
    if (saltlen > MAX_BUF) {
        return NULL;
    }

    unsigned char key[ENTROPY_KEY_LENGTH];
    int status = PKCS5_PBKDF2_HMAC(
        master_pass,
        strlen(master_pass),
        (const unsigned char *) salt,
        saltlen,
        ENTROPY_ITERATIONS,
        EVP_sha256(),
        ENTROPY_KEY_LENGTH,
        key);
    if (status == 0) {
        return NULL;
    }

    /* NULL as last arg allocates a new BIGNUM */
    return BN_bin2bn(key, ENTROPY_KEY_LENGTH, NULL);
}

static int
consume_entropy(BIGNUM *entropy, const char *charset, char *pass, size_t num_iter)
{
    int retval = 1;
    BN_CTX *ctx = BN_CTX_new();
    BIGNUM *bn_charsetlen = BN_new();
    BIGNUM *bn_remainder = BN_new();
    if (ctx == NULL || bn_charsetlen == NULL || bn_remainder == NULL) {
        retval = 0;
        goto consume_entropy_cleanup;
    }

    size_t charsetlen = strlen(charset);
    if (BN_set_word(bn_charsetlen, charsetlen) == 0) {
        retval = 0;
        goto consume_entropy_cleanup;
    }
    while (num_iter-- > 0) {
        BN_div(entropy, bn_remainder, entropy, bn_charsetlen, ctx);

        /*
         * If `remainder` cannot store the value in `bn_remainder`, it
         * will contain a very large number. Abort by checking if the
         * remainder is too large, since that is also a failure case
         * anyway.
         */
        uint64_t remainder = BN_get_word(bn_remainder);
        if (remainder >= charsetlen) {
            retval = 0;
            goto consume_entropy_cleanup;
        }
        *pass++ = charset[remainder];
    }
consume_entropy_cleanup:
    *pass = '\0';
    BN_free(bn_remainder);
    BN_free(bn_charsetlen);
    BN_CTX_free(ctx);
    return retval;
}

static int
insert_str_pseudo_randomly(BIGNUM *entropy, const char *s, char *pass)
{
    int retval = 1;
    char buf[MAX_BUF + 1];
    uint64_t passlen = (uint64_t) strlen(pass);
    BN_CTX *ctx = BN_CTX_new();
    BIGNUM *bn_passlen = BN_new();
    BIGNUM *bn_remainder = BN_new();
    if (ctx == NULL || bn_passlen == NULL || bn_remainder == NULL) {
        retval = 0;
        goto insert_str_pseudo_randomly_cleanup;
    }
    for (char c = *s; *s != '\0'; c = *(++s)) {
        if (BN_set_word(bn_passlen, passlen) == 0) {
            retval = 0;
            goto insert_str_pseudo_randomly_cleanup;
        }
        BN_div(entropy, bn_remainder, entropy, bn_passlen, ctx);

        /*
         * If `remainder` cannot store the value in `bn_remainder`, it
         * will contain a very large number. Abort by checking if the
         * remainder is too large, since that is also a failure case
         * anyway.
         */
        uint64_t remainder = BN_get_word(bn_remainder);
        if (remainder >= passlen) {
            retval = 0;
            goto insert_str_pseudo_randomly_cleanup;
        }

        /*
         * Idea here is to add the char `c` at position `remainder` in
         * `pass`. 1. Copy the part that would need to be shifted into
         * `buf`.
         */
        strncpy(buf, &pass[remainder], passlen - remainder);
        /*
         * 2. Add new character, then copy `buf` back into `pass`.
         */
        pass[remainder] = c;
        strncpy(&pass[remainder + 1], buf, passlen - remainder);
        passlen++;
    }
insert_str_pseudo_randomly_cleanup:
    pass[passlen] = '\0';
    BN_free(bn_remainder);
    BN_free(bn_passlen);
    BN_CTX_free(ctx);
    return retval;
}

static int
charsets_has_set(uint8_t charsets, enum CharSet set)
{
    return (charsets & set) != 0;
}

static size_t
build_charset(char *charset, uint8_t allowed_charsets)
{
    size_t count = 0;
    charset[0] = '\0';
    if (charsets_has_set(allowed_charsets, CHARSET_LOWER)) {
        strcat(charset, CHAR_SUBSET_LOWER);
        count++;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_UPPER)) {
        strcat(charset, CHAR_SUBSET_UPPER);
        count++;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_DIGITS)) {
        strcat(charset, CHAR_SUBSET_DIGITS);
        count++;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_SYMBOLS)) {
        strcat(charset, CHAR_SUBSET_SYMBOLS);
        count++;
    }
    return count;
}

int
render_pass(BIGNUM *entropy, uint8_t allowed_charsets, char *out, size_t length)
{
    char charset[MAX_BUF + 1];
    size_t num_charsets = build_charset(charset, allowed_charsets);
    if (consume_entropy(entropy, charset, out, length - num_charsets) == 0) {
        return 0;
    }

    /*
     * After generating the initial password, add one character of
     * each charset to ensure at least one from that charset is there.
     */
    char str_to_add[num_charsets + 1];
    memset(str_to_add, 0, sizeof(str_to_add));
    size_t count = 0;
    if (charsets_has_set(allowed_charsets, CHARSET_LOWER)
        && consume_entropy(entropy, CHAR_SUBSET_LOWER, &str_to_add[count++], 1) == 0) {
        return 0;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_UPPER)
        && consume_entropy(entropy, CHAR_SUBSET_UPPER, &str_to_add[count++], 1) == 0) {
        return 0;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_DIGITS)
        && consume_entropy(entropy, CHAR_SUBSET_DIGITS, &str_to_add[count++], 1) == 0) {
        return 0;
    }
    if (charsets_has_set(allowed_charsets, CHARSET_SYMBOLS)
        && consume_entropy(entropy, CHAR_SUBSET_SYMBOLS, &str_to_add[count++], 1) == 0) {
        return 0;
    }
    return insert_str_pseudo_randomly(entropy, str_to_add, out);
}

static void
err(const char *msg)
{
    fprintf(stderr, "lpass: %s\n", msg);
    exit(EXIT_FAILURE);
}

static void
usage()
{
    fputs("usage: lpass SITE LOGIN [OPTIONS]\n", stderr);
    fputs("\nlpass is a C implementation of LessPass, a stateless password manager.\n", stderr);
    fputs("\nPositional arguments:\n", stderr);
    fputs("  SITE    The domain name or URL used in password generation.\n", stderr);
    fputs("  LOGIN   The username/login used in that domain or URL.\n", stderr);
    fputs("\nOptions:\n", stderr);
    fputs("  -C COUNTER, --counter COUNTER\n", stderr);
    fputs("                  password counter (default: 1)\n", stderr);
    fputs("  -L LENGTH, --length LENGTH\n", stderr);
    fputs("                  password length (default: 16)\n", stderr);
    fputs("  --no-lower      no lowercase letters in password\n", stderr);
    fputs("  --no-upper      no uppercase letters in password\n", stderr);
    fputs("  --no-digits     no digits in password\n", stderr);
    fprintf(stderr,
          "  --no-symbols    no special symbols: %s\n", CHAR_SUBSET_SYMBOLS);
    fputs("  -h, --help      output this message and exit\n", stderr);
    fputs("\nThe environment variable LESSPASS_MASTER_PASSWORD must be set with the master\n", stderr);
    fputs("password for producing the desired password.\n", stderr);
}

int
main(int argc, char *argv[])
{
    uint64_t pass_counter = 1;
    size_t passlen = DEFAULT_LENGTH;
    uint8_t allowed_charsets = CHARSET_LOWER
        | CHARSET_UPPER
        | CHARSET_DIGITS
        | CHARSET_SYMBOLS;
    for (;;) {
        int option_index = 0;
        static struct option long_options[] = {
            {"counter",    required_argument, 0, 'C'},
            {"help",       no_argument,       0, 'h'},
            {"length",     required_argument, 0, 'L'},
            {"no-lower",   no_argument,       0,  0 },
            {"no-upper",   no_argument,       0,  0 },
            {"no-digits",  no_argument,       0,  0 },
            {"no-symbols", no_argument,       0,  0 },
            {0,            0,                 0,  0 }
        };

        int c = getopt_long(argc, argv, "hC:L:", long_options, &option_index);
        if (c == -1) {
            break;
        }

        switch (c) {
        case 0:
            if (strcmp("no-lower", long_options[option_index].name) == 0) {
                allowed_charsets &= ~CHARSET_LOWER;
            } else if (strcmp("no-upper", long_options[option_index].name) == 0) {
                allowed_charsets &= ~CHARSET_UPPER;
            } else if (strcmp("no-digits", long_options[option_index].name) == 0) {
                allowed_charsets &= ~CHARSET_DIGITS;
            } else if (strcmp("no-symbols", long_options[option_index].name) == 0) {
                allowed_charsets &= ~CHARSET_SYMBOLS;
            }
            break;
        case 'h':
            usage();
            exit(EXIT_SUCCESS);
        case 'C':
            if (!optarg
                || sscanf(optarg, "%lu", &pass_counter) == 0) {
                usage();
                exit(EXIT_FAILURE);
            }
            break;
        case 'L':
            if (!optarg
                || sscanf(optarg, "%zu", &passlen) == 0
                || passlen < 1
                || passlen >= MAX_BUF) {
                usage();
                exit(EXIT_FAILURE);
            }
            break;
        default:
            usage();
            exit(EXIT_FAILURE);
        }
    }

    const char *master_pass = getenv("LESSPASS_MASTER_PASSWORD");
    if (master_pass == NULL) {
        err("environment variable LESSPASS_MASTER_PASSWORD is not set");
    }

    /*
     * We are still looking for 2 positional arguments, abort if they
     * are not there.
     */
    if (optind > argc - 2) {
        usage();
        exit(EXIT_FAILURE);
    }

    const char *site = argv[optind++];
    const char *login = argv[optind++];
    BIGNUM *entropy = calc_entropy(site, login, pass_counter, master_pass);
    if (entropy == NULL) {
        err("Failed to calculate entropy");
    }

    char generated_pass[passlen + 1];
    if (render_pass(entropy, allowed_charsets, generated_pass, passlen) == 0) {
        BN_free(entropy);
        err("Failed to generate password");
    }
    printf("%s\n", generated_pass);
    BN_free(entropy);
    return EXIT_SUCCESS;
}