From 33936a05ac14136491fd461e1c6beea0e5269161 Mon Sep 17 00:00:00 2001 From: Samuel Fadel Date: Tue, 11 Apr 2023 10:36:09 +0200 Subject: Added LICENSE and implemented charset restrictions. As per my usage, lpass is now complete. * LICENSE: Added. * README.md: Updated with more information. * lpass.c: Implemented charset restrictions. --- README.md | 18 +++++- lpass.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 169 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index e385979..a17b169 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ # lpass +A simple implementation of +[lesspass](https://github.com/lesspass/lesspass) in C. -A simple implementation of [lesspass](https://github.com/lesspass/lesspass) in C. +# Memory safety disclaimers +This implementation only uses dynamic memory allocation as implemented +within OpenSSL to handle big integers (`BIGNUM` in OpenSSL lingo). All +allocations should be checked for success (non-`NULL`), please send a +patch if you find that this is not the case. The same is true for the +status of OpenSSL functions that return a status code. + +String manipulation is done only on statically-allocated strings, +while also using the versions whose limits can be specified (e.g., +`snprintf` versus `sprintf` and `strncpy` versus `strcpy`). + +This code uses variable-length array declarations for simplicity. + +# Dependencies +A recent version of OpenSSL. diff --git a/lpass.c b/lpass.c index 44f88a2..ef94412 100644 --- a/lpass.c +++ b/lpass.c @@ -1,3 +1,23 @@ +/* + * 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 . + */ + #include #include #include @@ -17,19 +37,14 @@ #define CHAR_SUBSET_DIGITS "0123456789" #define CHAR_SUBSET_SYMBOLS "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" -const char *CHAR_SET = (CHAR_SUBSET_LOWER - CHAR_SUBSET_UPPER - CHAR_SUBSET_DIGITS - CHAR_SUBSET_SYMBOLS); - enum CharSet { - CHAR_SET_LOWER = 1 << 0, - CHAR_SET_UPPER = 1 << 1, - CHAR_SET_DIGITS = 1 << 2, - CHAR_SET_SYMBOLS = 1 << 3, + CHARSET_LOWER = 1 << 0, + CHARSET_UPPER = 1 << 1, + CHARSET_DIGITS = 1 << 2, + CHARSET_SYMBOLS = 1 << 3, }; -static BIGNUM * +BIGNUM * calc_entropy(const char *site, const char *login, uint64_t counter, @@ -66,13 +81,28 @@ consume_entropy(char *generated_pass, BIGNUM *entropy, const char *charset, int 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; + } + int charsetlen = strlen(charset); - BN_set_word(bn_charsetlen, charsetlen); + if (BN_set_word(bn_charsetlen, charsetlen) == 0) { + retval = 0; + goto consume_entropy_cleanup; + } for (int passlen = strlen(generated_pass); passlen < maxlen; passlen++) { BN_div(entropy, bn_remainder, entropy, bn_charsetlen, ctx); - int remainder = BN_get_word(bn_remainder); + + /* + * If `remainder` cannot store the value in `bn_remainder`, it + * will contain a very large number. Abort by checking if the + * remainder is too larger according to `passlen`, 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; @@ -95,13 +125,29 @@ insert_str_pseudo_randomly(char *generated_pass, BIGNUM *entropy, const char *s) 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 larger according to `passlen`, 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 `generated_pass`. * 1. Copy the part that would need to be shifted into `buf`. @@ -122,24 +168,69 @@ insert_str_pseudo_randomly_cleanup: return retval; } -static void -render_pass(BIGNUM *entropy, char *out, int length) +static int +charsets_has_set(uint8_t charsets, enum CharSet set) +{ + return (charsets & set) != 0; +} + +static int +build_charset(char *charset, uint8_t allowed_charsets) +{ + int c = 0; + charset[0] = '\0'; + if (charsets_has_set(allowed_charsets, CHARSET_LOWER)) { + strcat(charset, CHAR_SUBSET_LOWER); + c++; + } + if (charsets_has_set(allowed_charsets, CHARSET_UPPER)) { + strcat(charset, CHAR_SUBSET_UPPER); + c++; + } + if (charsets_has_set(allowed_charsets, CHARSET_DIGITS)) { + strcat(charset, CHAR_SUBSET_DIGITS); + c++; + } + if (charsets_has_set(allowed_charsets, CHARSET_SYMBOLS)) { + strcat(charset, CHAR_SUBSET_SYMBOLS); + c++; + } + return c; +} + +int +render_pass(BIGNUM *entropy, uint8_t allowed_charsets, char *out, int length) { - /* XXX: FIXME: implement charset restrictions */ - int num_charsets = 4; - consume_entropy(out, entropy, CHAR_SET, length - num_charsets); + char charset[MAX_BUF + 1]; + int num_charsets = build_charset(charset, allowed_charsets); + if (consume_entropy(out, entropy, charset, length - num_charsets) == 0) { + return 0; + } /* * After generating the initial password, add one character of - * each charset to ensure at least one is there. + * 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)); - consume_entropy(&str_to_add[0], entropy, CHAR_SUBSET_LOWER, 1); - consume_entropy(&str_to_add[1], entropy, CHAR_SUBSET_UPPER, 1); - consume_entropy(&str_to_add[2], entropy, CHAR_SUBSET_DIGITS, 1); - consume_entropy(&str_to_add[3], entropy, CHAR_SUBSET_SYMBOLS, 1); - insert_str_pseudo_randomly(out, entropy, str_to_add); + int c = 0; + if (charsets_has_set(allowed_charsets, CHARSET_LOWER) + && consume_entropy(&str_to_add[c++], entropy, CHAR_SUBSET_LOWER, 1) == 0) { + return 0; + } + if (charsets_has_set(allowed_charsets, CHARSET_UPPER) + && consume_entropy(&str_to_add[c++], entropy, CHAR_SUBSET_UPPER, 1) == 0) { + return 0; + } + if (charsets_has_set(allowed_charsets, CHARSET_DIGITS) + && consume_entropy(&str_to_add[c++], entropy, CHAR_SUBSET_DIGITS, 1) == 0) { + return 0; + } + if (charsets_has_set(allowed_charsets, CHARSET_SYMBOLS) + && consume_entropy(&str_to_add[c++], entropy, CHAR_SUBSET_SYMBOLS, 1) == 0) { + return 0; + } + return insert_str_pseudo_randomly(out, entropy, str_to_add); } static void @@ -160,7 +251,11 @@ usage() fputs("\nOptions:\n", stderr); fputs(" -L LENGTH, --length LENGTH\n", stderr); fputs(" password length (default: 16)\n", stderr); - fputs(" --no-symbols no symbols in password\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); + fputs(" --no-symbols no special symbols (" CHAR_SUBSET_SYMBOLS ")\n", stderr); + 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); } @@ -168,35 +263,49 @@ usage() int main(int argc, char *argv[]) { - const char *master_pass = getenv("LESSPASS_MASTER_PASSWORD"); - if (master_pass == NULL) { - err("environment variable LESSPASS_MASTER_PASSWORD is not set"); - } - int c; int length = DEFAULT_LENGTH; - uint8_t charsets = 0; + uint8_t allowed_charsets = CHARSET_LOWER + | CHARSET_UPPER + | CHARSET_DIGITS + | CHARSET_SYMBOLS; for (;;) { int option_index = 0; static struct option long_options[] = { + {"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 } }; - c = getopt_long(argc, argv, "L:", long_options, &option_index); + c = getopt_long(argc, argv, "hL:", long_options, &option_index); if (c == -1) { break; } switch (c) { case 0: - if (option_index == 1) { - /* TODO: no-symbols */ + 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 'L': - if (!optarg || sscanf(optarg, "%d", &length) == 0 || length < 1) { + if (!optarg + || sscanf(optarg, "%d", &length) == 0 + || length < 1 + || length >= MAX_BUF) { usage(); exit(EXIT_FAILURE); } @@ -207,6 +316,11 @@ main(int argc, char *argv[]) } } + 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. @@ -227,7 +341,10 @@ main(int argc, char *argv[]) char generated_pass[length + 1]; memset(generated_pass, 0, sizeof(generated_pass)); - render_pass(entropy, generated_pass, length); + if (render_pass(entropy, allowed_charsets, generated_pass, length) == 0) { + BN_free(entropy); + err("Failed to generate password"); + } printf("%s\n", generated_pass); BN_free(entropy); return EXIT_SUCCESS; -- cgit v1.2.3