diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-08 13:50:52 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-08 22:55:44 +0200 |
| commit | 10e42591ac72285736d5cc4ee5e7c2f68dbf1e4b (patch) | |
| tree | 3bb586177e375a6f7f91c0335876faefc28b805c /usergen.c | |
| parent | 805630dbfcd409a5d49bc89102f4183b71f713f9 (diff) | |
| download | slcl-10e42591ac72285736d5cc4ee5e7c2f68dbf1e4b.tar.gz | |
Replace OpenSSL with libsodium and argon2id
The SHA256-based password hashing algorithm used by slcl(1) and
usergen(1) is considered insecure against several kinds of attacks,
including brute force attacks. [1]
Therefore, a stronger password hashing algorithm based on the Argon2id
key derivation function is now used by default. While OpenSSL does
support Argon2id, it is only supported by very recent versions [2],
which are still not packaged by most distributions as of the time of
this writing. [3]
As an alternative to OpenSSL, libsodium [4] had several benefits:
- It provides easy-to-use functions for password hashing, base64
encoding/decoding and other cryptographic primitives used by slcl(1)
and usergen(1).
- It is packaged by most distributions [5], and most often only the patch
version differs, which ensures good compatibility across distributions.
Unfortunately, and as opposed to OpenSSL, libsodium does not come with
command-line tools. Therefore, usergen(1) had to be rewritten in C.
In order to maintain backwards compatiblity with existing databases,
slcl(1) and usergen(1) shall support the insecure, SHA256-based password
hashing algorithm. However, Argon2id shall now be the default choice for
usergen(1).
[1]: https://security.stackexchange.com/questions/195563/why-is-sha-256-not-good-for-passwords
[2]: https://docs.openssl.org/3.3/man7/EVP_KDF-ARGON2/
[3]: https://repology.org/project/openssl/versions
[4]: https://www.libsodium.org/
[5]: https://repology.org/project/libsodium/versions
Diffstat (limited to 'usergen.c')
| -rw-r--r-- | usergen.c | 546 |
1 files changed, 546 insertions, 0 deletions
diff --git a/usergen.c b/usergen.c new file mode 100644 index 0000000..5a0cf3c --- /dev/null +++ b/usergen.c @@ -0,0 +1,546 @@ +#define _POSIX_C_SOURCE 200809L + +#include "hex.h" +#include <dynstr.h> +#include <cjson/cJSON.h> +#include <sodium.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <termios.h> +#include <unistd.h> +#include <errno.h> +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +static cJSON *dump_db(const char *const dir) +{ + int fd = -1; + cJSON *ret = NULL, *c = NULL; + char *buf = NULL; + struct dynstr d; + struct stat sb; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/db.json", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if ((fd = open(d.str, O_RDONLY)) < 0) + { + fprintf(stderr, "%s: open(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (fstat(fd, &sb)) + { + fprintf(stderr, "%s: fstat(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (sb.st_size > SIZE_MAX - 1) + { + fprintf(stderr, "%s: size for %s (%ju) exceeds maximum size (%zu)\n", + __func__, d.str, (uintmax_t)sb.st_size, SIZE_MAX); + goto end; + } + else if (!(buf = malloc(sb.st_size + 1))) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto end; + } + + size_t rem = sb.st_size; + char *p = buf; + + while (rem) + { + const ssize_t r = read(fd, p, rem); + + if (r < 0) + { + fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); + goto end; + } + + rem -= r; + p += r; + } + + buf[sb.st_size] = '\0'; + + if (!(c = cJSON_Parse(buf))) + { + fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); + goto end; + } + + ret = c; + +end: + + if (fd >= 0 && close(fd)) + { + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + ret = NULL; + } + + if (!ret) + cJSON_Delete(c); + + dynstr_free(&d); + free(buf); + return ret; +} + +static int getuser(char *const username, const size_t n) +{ + fputs("Username:\n", stderr); + + if (!fgets(username, n, stdin)) + { + fputs("Failed to obtain username\n", stderr); + return -1; + } + else if (strcspn(username, " \t") != strlen(username)) + { + fputs("Username cannot contain whitespaces\n", stderr); + return -1; + } + + *strchr(username, '\n') = '\0'; + return 0; +} + +static int checkuser(const char *const username, const cJSON *const c) +{ + const cJSON *entry, *const users = cJSON_GetObjectItem(c, "users"); + + if (!users) + { + fputs("Could not find users in database\n", stderr); + return -1; + } + else if (!cJSON_IsArray(users)) + { + fputs("Expected JSON array for users\n", stderr); + return -1; + } + + cJSON_ArrayForEach(entry, users) + { + const cJSON *const u = cJSON_GetObjectItem(entry, "name"); + const char *dbuser; + + if (!u || !(dbuser = cJSON_GetStringValue(u))) + { + fputs("Missing username field in database\n", stderr); + return -1; + } + else if (!strcmp(username, dbuser)) + { + fprintf(stderr, "User %s already in database\n", username); + return -1; + } + } + + return 0; +} + +static int getpass(char *const password, const size_t n) +{ + int ret = -1; + struct termios t, ne; + + fputs("Password:\n", stderr); + + if (tcgetattr(STDIN_FILENO, &t)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + return -1; + } + + ne = t; + ne.c_lflag ^= ECHO; + + if (tcsetattr(STDIN_FILENO, TCSANOW, &ne)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if (!fgets(password, n, stdin)) + { + fputs("Failed to obtain username\n", stderr); + goto restore; + } + + *strchr(password, '\n') = '\0'; + putchar('\n'); + ret = 0; + +restore: + + if (tcsetattr(STDIN_FILENO, TCSANOW, &t)) + { + fprintf(stderr, "%s: tcgetattr(3): %s\n", __func__, strerror(errno)); + ret = -1; + } + + return ret; +} + +static int getquota(unsigned long long *const q) +{ + char s[256], *end; + + fputs("Quota, in MiB (leave empty for unlimited quota):\n", stderr); + + if (!fgets(s, sizeof s, stdin)) + { + fprintf(stderr, "%s: fgets(3): %s\n", __func__, strerror(errno)); + return -1; + } + + *strchr(s, '\n') = '\0'; + + if (!*s) + { + *q = 0; + return 0; + } + + errno = 0; + *q = strtoull(s, &end, 10); + + if (errno) + { + fprintf(stderr, "%s: strtoull(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if (*end) + { + fprintf(stderr, "Invalid quota: %s\n", s); + return -1; + } + + return 0; +} + +static int getmethod(char *const method, const size_t n) +{ + fputs("Password hashing (sha256 [deprecated], argon2id): " + "[argon2id]\n", stderr); + + if (!fgets(method, n, stdin)) + { + fprintf(stderr, "%s: fgets(3): %s\n", __func__, strerror(errno)); + return -1; + } + + *strchr(method, '\n') = '\0'; + + if (!*method) + strcpy(method, "argon2id"); + + return 0; +} + +static cJSON *createobj(cJSON *const c, const char *const username, + const char *const key, const char *const method, + const unsigned long long quota) +{ + cJSON *const ret = cJSON_CreateObject(); + char sq[sizeof "18446744073709551615"]; + const int n = snprintf(sq, sizeof sq, "%llu", quota); + + if (!ret) + { + fprintf(stderr, "%s: cJSON_CreateObject failed\n", __func__); + goto failure; + } + else if (n < 0 || n >= sizeof sq) + { + fprintf(stderr, "%s: snprintf(3) failed with %d\n", __func__, n); + goto failure; + } + else if (!cJSON_AddStringToObject(ret, "name", username) + || !cJSON_AddStringToObject(ret, "key", key) + || !cJSON_AddStringToObject(ret, "method", method) + || (quota && !cJSON_AddStringToObject(ret, "quota", sq))) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + goto failure; + } + + return ret; + +failure: + cJSON_Delete(ret); + return NULL; +} + +static int runsha256(const char *const password, cJSON *const o) +{ + int ret = -1; + enum {ROUNDS = 1000}; + unsigned char salt[32], sha256[crypto_hash_sha256_BYTES]; + const size_t plen = strlen(password), bufsz = sizeof salt + plen; + char hexsha256[sizeof sha256 * 2 + 1], hexsalt[sizeof salt * 2 + 1]; + unsigned char *const buf = malloc(bufsz); + + randombytes_buf(salt, sizeof salt); + + if (!buf) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto end; + } + + memcpy(buf, salt, sizeof salt); + memcpy(buf + sizeof salt, password, plen); + + if (crypto_hash_sha256(sha256, buf, bufsz)) + { + fprintf(stderr, "%s: crypto_hash_sha256 failed\n", __func__); + goto end; + } + + fprintf(stderr, "1/%d", ROUNDS); + + for (int i = 1; i < ROUNDS; i++) + { + if (crypto_hash_sha256(sha256, sha256, sizeof sha256)) + { + fprintf(stderr, "%s: crypto_hash_sha256 failed\n", __func__); + goto end; + } + + fprintf(stderr, "\r%d/%d", i + 1, ROUNDS); + } + + fputc('\n', stderr); + + if (hex_encode(salt, hexsalt, sizeof salt, sizeof hexsalt) + || hex_encode(sha256, hexsha256, sizeof sha256, sizeof hexsha256)) + { + fprintf(stderr, "%s: hex_encode failed\n", __func__); + goto end; + } + else if (!cJSON_AddStringToObject(o, "salt", hexsalt) + || !cJSON_AddStringToObject(o, "password", hexsha256)) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + goto end; + } + + ret = 0; + +end: + free(buf); + return ret; +} + +static int runargon2id(const char *const password, cJSON *const o) +{ + char hashpwd[crypto_pwhash_STRBYTES]; + + if (crypto_pwhash_str(hashpwd, password, strlen(password), + crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE)) + { + fprintf(stderr, "%s: crypto_pwhash_str failed\n", __func__); + return -1; + } + else if (!cJSON_AddStringToObject(o, "password", hashpwd)) + { + fprintf(stderr, "%s: cJSON_AddStringToObject failed\n", __func__); + return -1; + } + + return 0; +} + +static int runmethod(const char *const method, const char *const password, + cJSON *const o) +{ + static const struct m + { + const char *s; + int (*fn)(const char *, cJSON *); + } methods[] = + { + {.s = "sha256", .fn = runsha256}, + {.s = "argon2id", .fn = runargon2id} + }; + + for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) + { + const struct m *const m = &methods[i]; + + if (!strcmp(m->s, method)) + return m->fn(password, o); + } + + fprintf(stderr, "Invalid password hashing method: %s\n", method); + return -1; +} + +static int save_db(const cJSON *const c, const char *const dir) +{ + int ret = -1; + FILE *f = NULL; + char *s = NULL; + struct dynstr d; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/db.json", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (!(f = fopen(d.str, "wb"))) + { + fprintf(stderr, "%s: fopen(3) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (!(s = cJSON_Print(c))) + { + fprintf(stderr, "%s: cJSON_Print failed\n", __func__); + goto end; + } + else if (fprintf(f, "%s", s) < 0) + { + fprintf(stderr, "%s: fprintf(3) failed\n", __func__); + goto end; + } + + ret = 0; + +end: + + if (f && fclose(f)) + { + fprintf(stderr, "%s: fclose(3) %s: %s\n", __func__, d.str ? d.str : "", + strerror(errno)); + ret = -1; + } + + cJSON_free(s); + dynstr_free(&d); + return ret; +} + +static int mkuserdir(const char *const dir, const char *const username) +{ + int ret = -1; + struct dynstr d; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/user", dir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (mkdir(d.str, 0700) && errno != EEXIST) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + else if (dynstr_append(&d, "/%s", username)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (mkdir(d.str, 0700) && errno != EEXIST) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", __func__, d.str, + strerror(errno)); + goto end; + } + + ret = 0; + +end: + dynstr_free(&d); + return ret; +} + +int main(int argc, char *argv[]) +{ + int ret = EXIT_FAILURE; + cJSON *c = NULL, *o, *users; + unsigned long long quota; + unsigned char key[32]; + char username[256], password[sizeof username], method[sizeof username], + hexkey[sizeof key * 2 + 1]; + const char *dir; + + if (argc != 2) + { + fprintf(stderr, "%s <dir>\n", *argv); + goto end; + } + else if (sodium_init()) + { + fprintf(stderr, "%s: sodium_init failed\n", __func__); + goto end; + } + else if (!(c = dump_db((dir = argv[1])))) + { + fprintf(stderr, "%s: dump_db failed\n", __func__); + goto end; + } + + randombytes_buf(key, sizeof key); + + if (hex_encode(key, hexkey, sizeof key, sizeof hexkey)) + { + fprintf(stderr, "%s: hex_encode failed\n", __func__); + goto end; + } + + if (getuser(username, sizeof username) + || checkuser(username, c) + || getpass(password, sizeof password) + || getquota("a) + || getmethod(method, sizeof method) + || !(o = createobj(c, username, hexkey, method, quota))) + goto end; + else if (runmethod(method, password, o)) + { + cJSON_Delete(o); + goto end; + } + else if (!(users = cJSON_GetObjectItem(c, "users"))) + { + fputs("Missing \"users\" array in database\n", stderr); + goto end; + } + else if (!cJSON_IsArray(users)) + { + fputs("Database item \"users\" not an array\n", stderr); + goto end; + } + else if (!cJSON_AddItemToArray(users, o)) + { + fprintf(stderr, "%s: cJSON_AddItemToArray failed\n", __func__); + cJSON_Delete(o); + goto end; + } + else if (mkuserdir(dir, username) + || save_db(c, dir)) + goto end; + + ret = EXIT_SUCCESS; + +end: + cJSON_Delete(c); + return ret; +} |
