aboutsummaryrefslogtreecommitdiff
path: root/usergen.c
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-10-08 13:50:52 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-10-08 22:55:44 +0200
commit10e42591ac72285736d5cc4ee5e7c2f68dbf1e4b (patch)
tree3bb586177e375a6f7f91c0335876faefc28b805c /usergen.c
parent805630dbfcd409a5d49bc89102f4183b71f713f9 (diff)
downloadslcl-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.c546
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(&quota)
+ || 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;
+}