#define _POSIX_C_SOURCE 200809L #include "hex.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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 \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; }