#include "auth.h" #include "hex.h" #include "jwt.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct auth { struct dynstr dir, db; }; enum {KEYLEN = 32}; static char *dump_db(const char *const path) { char *ret = NULL; FILE *f = NULL; struct stat sb; if (stat(path, &sb)) { fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); goto end; } else if (sb.st_size > SIZE_MAX - 1) { fprintf(stderr, "%s: %s too big (%llu bytes, %zu max)\n", __func__, path, (unsigned long long)sb.st_size, (size_t)SIZE_MAX); goto end; } else if (!(f = fopen(path, "rb"))) { fprintf(stderr, "%s: fopen(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(ret = malloc(sb.st_size + 1))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } else if (!fread(ret, sb.st_size, 1, f)) { fprintf(stderr, "%s: failed to dump %zu bytes, ferror=%d\n", __func__, (size_t)sb.st_size, ferror(f)); goto end; } ret[sb.st_size] = '\0'; end: if (f && fclose(f)) { fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); free(ret); return NULL; } return ret; } static int find_cookie(const cJSON *const users, const char *const cookie) { const cJSON *u; cJSON_ArrayForEach(u, users) { const cJSON *const n = cJSON_GetObjectItem(u, "name"), *const k = cJSON_GetObjectItem(u, "key"); const char *name, *key; unsigned char dkey[KEYLEN]; if (!n || !(name = cJSON_GetStringValue(n))) { fprintf(stderr, "%s: missing username\n", __func__); return -1; } else if (!k || !(key = cJSON_GetStringValue(k))) { fprintf(stderr, "%s: missing key\n", __func__); return -1; } else if (hex_decode(key, dkey, sizeof dkey)) { fprintf(stderr, "%s: hex_decode failed\n", __func__); return -1; } const int res = jwt_check(cookie, dkey, sizeof dkey); if (!res) return 0; if (res < 0) { fprintf(stderr, "%s: jwt_decode failed\n", __func__); return -1; } } return 1; } int auth_cookie(const struct auth *const a, const struct http_cookie *const c) { int ret = -1; char *db = NULL; cJSON *json = NULL; if (!c->field || !c->value) return 1; else if (!(db = dump_db(a->db.str))) { fprintf(stderr, "%s: dump_db failed\n", __func__); goto end; } else if (!(json = cJSON_Parse(db))) { fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); goto end; } const cJSON *const users = cJSON_GetObjectItem(json, "users"); if (!users) { fprintf(stderr, "%s: could not find users\n", __func__); goto end; } else if (!cJSON_IsArray(users)) { fprintf(stderr, "%s: expected JSON array for users\n", __func__); goto end; } else if ((ret = find_cookie(users, c->value)) < 0) { fprintf(stderr, "%s: find_cookie failed\n", __func__); goto end; } end: free(db); cJSON_Delete(json); return ret; } static int generate_cookie(const cJSON *const json, const char *const path, const char *const name, const char *const key, char **const cookie) { unsigned char dkey[KEYLEN]; int ret = -1; char *jwt = NULL; if (hex_decode(key, dkey, sizeof dkey)) { fprintf(stderr, "%s: hex_decode failed\n", __func__); goto end; } else if (!(jwt = jwt_encode(name, dkey, sizeof dkey))) { fprintf(stderr, "%s: jwt_encode failed\n", __func__); goto end; } else if (!(*cookie = http_cookie_create(name, jwt))) { fprintf(stderr, "%s: http_cookie_create failed\n", __func__); goto end; } ret = 0; end: free(jwt); return ret; } static int compare_pwd(const char *const salt, const char *const password, const char *const exp_password) { int ret = -1; enum {SALT_LEN = SHA256_DIGEST_LENGTH}; unsigned char dec_salt[SALT_LEN], sha256[SHA256_DIGEST_LENGTH]; const size_t slen = strlen(salt), len = strlen(password), n = sizeof dec_salt + len; unsigned char *const salted = malloc(n); if (slen != SALT_LEN * 2) { fprintf(stderr, "%s: unexpected salt length: %zu\n", __func__, slen); goto end; } else if (!salted) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } else if (hex_decode(salt, dec_salt, sizeof dec_salt)) { fprintf(stderr, "%s: hex_decode failed\n", __func__); goto end; } memcpy(salted, dec_salt, sizeof dec_salt); memcpy(salted + sizeof dec_salt, password, len); if (!SHA256(salted, n, sha256)) { fprintf(stderr, "%s: SHA256 (first round) failed\n", __func__); goto end; } enum {ROUNDS = 1000 - 1}; for (int i = 0; i < ROUNDS; i++) if (!SHA256(sha256, sizeof sha256, sha256)) { fprintf(stderr, "%s: SHA256 failed\n", __func__); goto end; } char sha256_str[sizeof "00000000000000000000000000000000" "00000000000000000000000000000000"]; for (struct {char *p; size_t i;} a = {.p = sha256_str}; a.i < sizeof sha256 / sizeof *sha256; a.i++, a.p += 2) sprintf(a.p, "%02x", sha256[a.i]); if (!strcmp(sha256_str, exp_password)) ret = 0; else /* Positive error code for password mismatch. */ ret = 1; end: free(salted); return ret; } int auth_login(const struct auth *const a, const char *const user, const char *const password, char **const cookie) { int ret = -1; const char *const path = a->db.str; char *const db = dump_db(path); cJSON *json = NULL; if (!db) { fprintf(stderr, "%s: dump_db failed\n", __func__); goto end; } else if (!(json = cJSON_Parse(db))) { fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); goto end; } const cJSON *const users = cJSON_GetObjectItem(json, "users"); if (!users) { fprintf(stderr, "%s: could not find users\n", __func__); goto end; } else if (!cJSON_IsArray(users)) { fprintf(stderr, "%s: expected JSON array for users\n", __func__); goto end; } const cJSON *u; cJSON_ArrayForEach(u, users) { const cJSON *const n = cJSON_GetObjectItem(u, "name"), *const s = cJSON_GetObjectItem(u, "salt"), *const p = cJSON_GetObjectItem(u, "password"), *const k = cJSON_GetObjectItem(u, "key"); const char *name, *salt, *pwd, *key; if (!n || !(name = cJSON_GetStringValue(n))) { fprintf(stderr, "%s: missing username\n", __func__); goto end; } else if (!s || !(salt = cJSON_GetStringValue(s))) { fprintf(stderr, "%s: missing salt\n", __func__); goto end; } else if (!p || !(pwd = cJSON_GetStringValue(p))) { fprintf(stderr, "%s: missing password\n", __func__); goto end; } else if (!k || !(key = cJSON_GetStringValue(k))) { fprintf(stderr, "%s: missing key\n", __func__); goto end; } else if (!strcmp(name, user)) { const int res = compare_pwd(salt, password, pwd); if (res < 0) { fprintf(stderr, "%s: generate_cookie failed\n", __func__); goto end; } else if (!res) { if (generate_cookie(json, path, name, key, cookie)) { fprintf(stderr, "%s: generate_cookie failed\n", __func__); goto end; } ret = 0; goto end; } } } ret = 1; end: free(db); cJSON_Delete(json); return ret; } void auth_free(struct auth *const a) { if (a) { dynstr_free(&a->dir); dynstr_free(&a->db); } free(a); } const char *auth_dir(const struct auth *const a) { return a->dir.str; } int auth_quota(const struct auth *const a, const char *const user, bool *const available, unsigned long long *const quota) { int ret = -1; const char *const path = a->db.str; char *const db = dump_db(path); cJSON *json = NULL; if (!db) { fprintf(stderr, "%s: dump_db failed\n", __func__); goto end; } else if (!(json = cJSON_Parse(db))) { fprintf(stderr, "%s: cJSON_Parse failed\n", __func__); goto end; } const cJSON *const users = cJSON_GetObjectItem(json, "users"); if (!users) { fprintf(stderr, "%s: could not find users\n", __func__); goto end; } else if (!cJSON_IsArray(users)) { fprintf(stderr, "%s: expected JSON array for users\n", __func__); goto end; } *available = false; const cJSON *u; cJSON_ArrayForEach(u, users) { const cJSON *const n = cJSON_GetObjectItem(u, "name"), *const q = cJSON_GetObjectItem(u, "quota"); const char *name; if (!n || !(name = cJSON_GetStringValue(n))) { fprintf(stderr, "%s: missing username\n", __func__); goto end; } else if (!strcmp(name, user)) { const char *qs; if (!q || !(qs = cJSON_GetStringValue(q)) || !*qs) { /* Unlimited quota. */ ret = 0; goto end; } char *end; errno = 0; *available = true; *quota = strtoull(qs, &end, 10); const unsigned long long mul = 1024ul * 1024ul; if (errno || *end != '\0') { fprintf(stderr, "%s: invalid quota %s: %s\n", __func__, qs, strerror(errno)); goto end; } else if (*quota >= ULLONG_MAX / mul) { fprintf(stderr, "%s: quota %s too large\n", __func__, qs); goto end; } *quota *= mul; break; } } ret = 0; end: free(db); cJSON_Delete(json); return ret; } static int create_db(const char *const path) { int ret = -1; const int fd = open(path, O_WRONLY | O_CREAT, 0600); if (fd < 0) { fprintf(stderr, "%s: open(2): %s\n", __func__, strerror(errno)); goto end; } static const char db[] = "{\"users\": []}\n"; for (struct {size_t n; const void *p;} a = {.n = strlen(db), .p = db}; a.n;) { const ssize_t n = write(fd, a.p, a.n); if (n < 0) { fprintf(stderr, "%s: write(2): %s\n", __func__, strerror(errno)); goto end; } a.n -= n; a.p = ((const char *)a.p) + n; } printf("Created login database at %s. Please remember to add new users " "with usergen(1).\n", path); ret = 0; end: if (fd >= 0 && close(fd)) { fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); return -1; } return ret; } static int init_db(struct auth *const a) { struct stat sb; const char *const path = a->db.str; if (stat(path, &sb)) { if (errno == ENOENT) return create_db(path); else { fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); return -1; } } return 0; } static char *resolve_cwd(void) { size_t len = 1; char *p = NULL; for (;;) { char *const pp = realloc(p, len); if (!pp) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); break; } p = pp; if (!getcwd(pp, len)) { if (errno != ERANGE) { fprintf(stderr, "%s: getcwd(3): %s\n", __func__, strerror(errno)); break; } else len++; } else return p; } free(p); return NULL; } struct auth *auth_alloc(const char *const dir) { struct auth *const a = malloc(sizeof *a), *ret = NULL; char *abspath = NULL; if (!a) { fprintf(stderr, "%s: malloc(3) auth: %s\n", __func__, strerror(errno)); goto end; } *a = (const struct auth){0}; dynstr_init(&a->db); dynstr_init(&a->dir); if (*dir != '/' && !(abspath = resolve_cwd())) { fprintf(stderr, "%s: resolve_cwd failed\n", __func__); goto end; } else if (abspath && dynstr_append(&a->dir, "%s", abspath)) { fprintf(stderr, "%s: dynstr_append abspath failed\n", __func__); goto end; } else if (dynstr_append(&a->dir, "%s%s", abspath ? "/" : "", dir)) { fprintf(stderr, "%s: dynstr_append dir failed\n", __func__); goto end; } else if (dynstr_append(&a->db, "%s/db.json", a->dir.str)) { fprintf(stderr, "%s: dynstr_append db failed\n", __func__); goto end; } else if (init_db(a)) { fprintf(stderr, "%s: init_db failed\n", __func__); goto end; } ret = a; end: free(abspath); if (!ret) auth_free(a); return ret; }