slcl/auth.c

602 lines
14 KiB
C

#include "auth.h"
#include "hex.h"
#include "jwt.h"
#include <libweb/http.h>
#include <cjson/cJSON.h>
#include <dynstr.h>
#include <openssl/sha.h>
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
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;
}