/* auth_store.c — SQLite + PBKDF2 implementation of the web-UI account store. */ #include "auth_store.h" #include #include #include #include #include #include #include #include #define PBKDF2_ITERS 210000 #define SALT_BYTES 16 #define HASH_BYTES 32 struct auth_store { sqlite3 *db; pthread_mutex_t lock; }; static void to_hex(const unsigned char *in, size_t n, char *out) { static const char hex[] = "0123456789abcdef"; for (size_t i = 0; i < n; i++) { out[i * 2] = hex[in[i] >> 4]; out[i * 2 + 1] = hex[in[i] & 0xf]; } out[n * 2] = 0; } static int from_hex(const char *in, unsigned char *out, size_t out_n) { size_t len = strlen(in); if (len != out_n * 2) return -1; for (size_t i = 0; i < out_n; i++) { char c[3] = { in[i * 2], in[i * 2 + 1], 0 }; char *end; long v = strtol(c, &end, 16); if (end != c + 2) return -1; out[i] = (unsigned char)v; } return 0; } /* Derive a hash for `password` with the given salt + iteration count. */ static bool derive(const char *password, const unsigned char *salt, size_t salt_n, int iters, unsigned char out[HASH_BYTES]) { return PKCS5_PBKDF2_HMAC(password, (int)strlen(password), salt, (int)salt_n, iters, EVP_sha256(), HASH_BYTES, out) == 1; } static bool valid_role(const char *role) { return role && (strcmp(role, "admin") == 0 || strcmp(role, "user") == 0); } auth_store *auth_store_open(const char *path) { auth_store *s = calloc(1, sizeof *s); if (!s) return NULL; if (pthread_mutex_init(&s->lock, NULL) != 0) { free(s); return NULL; } if (sqlite3_open(path, &s->db) != SQLITE_OK) { sqlite3_close(s->db); pthread_mutex_destroy(&s->lock); free(s); return NULL; } sqlite3_busy_timeout(s->db, 4000); const char *schema = "PRAGMA journal_mode=WAL;" "CREATE TABLE IF NOT EXISTS users (" " id INTEGER PRIMARY KEY," " username TEXT NOT NULL UNIQUE COLLATE NOCASE," " pw_hash TEXT NOT NULL," " pw_salt TEXT NOT NULL," " pw_iters INTEGER NOT NULL," " role TEXT NOT NULL DEFAULT 'user'," " created_at INTEGER NOT NULL);"; char *err = NULL; if (sqlite3_exec(s->db, schema, NULL, NULL, &err) != SQLITE_OK) { sqlite3_free(err); auth_store_close(s); return NULL; } return s; } void auth_store_close(auth_store *s) { if (!s) return; if (s->db) sqlite3_close(s->db); pthread_mutex_destroy(&s->lock); free(s); } /* Run a "SELECT count(*) ... " style query returning a single integer. */ static int count_query(auth_store *s, const char *sql) { sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(s->db, sql, -1, &st, NULL) != SQLITE_OK) return -1; int n = -1; if (sqlite3_step(st) == SQLITE_ROW) n = sqlite3_column_int(st, 0); sqlite3_finalize(st); return n; } int auth_store_user_count(auth_store *s) { if (!s) return -1; pthread_mutex_lock(&s->lock); int n = count_query(s, "SELECT count(*) FROM users;"); pthread_mutex_unlock(&s->lock); return n; } int auth_store_admin_count(auth_store *s) { if (!s) return -1; pthread_mutex_lock(&s->lock); int n = count_query(s, "SELECT count(*) FROM users WHERE role='admin';"); pthread_mutex_unlock(&s->lock); return n; } bool auth_store_user_exists(auth_store *s, const char *username) { if (!s || !username) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool found = false; if (sqlite3_prepare_v2(s->db, "SELECT 1 FROM users WHERE username=?;", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, username, -1, SQLITE_STATIC); found = sqlite3_step(st) == SQLITE_ROW; } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return found; } bool auth_store_verify(auth_store *s, const char *username, const char *password, char *role_out, size_t role_sz) { if (!s || !username || !password) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "SELECT pw_hash, pw_salt, pw_iters, role FROM users WHERE username=?;", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, username, -1, SQLITE_STATIC); if (sqlite3_step(st) == SQLITE_ROW) { const char *hash_hex = (const char *)sqlite3_column_text(st, 0); const char *salt_hex = (const char *)sqlite3_column_text(st, 1); int iters = sqlite3_column_int(st, 2); const char *role = (const char *)sqlite3_column_text(st, 3); unsigned char salt[SALT_BYTES], want[HASH_BYTES], got[HASH_BYTES]; if (hash_hex && salt_hex && from_hex(salt_hex, salt, SALT_BYTES) == 0 && from_hex(hash_hex, want, HASH_BYTES) == 0 && derive(password, salt, SALT_BYTES, iters, got) && CRYPTO_memcmp(want, got, HASH_BYTES) == 0) { ok = true; if (role_out && role) snprintf(role_out, role_sz, "%s", role); } } } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; } /* Compute a fresh salt + hash for `password`, hex-encoded into the buffers. */ static bool make_hash(const char *password, char salt_hex[SALT_BYTES * 2 + 1], char hash_hex[HASH_BYTES * 2 + 1]) { unsigned char salt[SALT_BYTES], hash[HASH_BYTES]; if (RAND_bytes(salt, SALT_BYTES) != 1) return false; if (!derive(password, salt, SALT_BYTES, PBKDF2_ITERS, hash)) return false; to_hex(salt, SALT_BYTES, salt_hex); to_hex(hash, HASH_BYTES, hash_hex); return true; } bool auth_store_create_user(auth_store *s, const char *username, const char *password, const char *role) { if (!s || !username || !*username || !password || !*password) return false; if (!valid_role(role)) role = "user"; char salt_hex[SALT_BYTES * 2 + 1], hash_hex[HASH_BYTES * 2 + 1]; if (!make_hash(password, salt_hex, hash_hex)) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "INSERT INTO users (username, pw_hash, pw_salt, pw_iters, role, created_at)" " VALUES (?,?,?,?,?,?);", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, username, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, hash_hex, -1, SQLITE_STATIC); sqlite3_bind_text(st, 3, salt_hex, -1, SQLITE_STATIC); sqlite3_bind_int(st, 4, PBKDF2_ITERS); sqlite3_bind_text(st, 5, role, -1, SQLITE_STATIC); sqlite3_bind_int64(st, 6, (sqlite3_int64)time(NULL)); ok = sqlite3_step(st) == SQLITE_DONE; /* false on UNIQUE conflict */ } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; } bool auth_store_set_password(auth_store *s, const char *username, const char *password) { if (!s || !username || !password || !*password) return false; char salt_hex[SALT_BYTES * 2 + 1], hash_hex[HASH_BYTES * 2 + 1]; if (!make_hash(password, salt_hex, hash_hex)) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "UPDATE users SET pw_hash=?, pw_salt=?, pw_iters=? WHERE username=?;", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, hash_hex, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, salt_hex, -1, SQLITE_STATIC); sqlite3_bind_int(st, 3, PBKDF2_ITERS); sqlite3_bind_text(st, 4, username, -1, SQLITE_STATIC); ok = sqlite3_step(st) == SQLITE_DONE && sqlite3_changes(s->db) > 0; } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; } bool auth_store_set_role(auth_store *s, const char *username, const char *role) { if (!s || !username || !valid_role(role)) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "UPDATE users SET role=? WHERE username=?;", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, role, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, username, -1, SQLITE_STATIC); ok = sqlite3_step(st) == SQLITE_DONE && sqlite3_changes(s->db) > 0; } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; } bool auth_store_delete_user(auth_store *s, const char *username) { if (!s || !username) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "DELETE FROM users WHERE username=?;", -1, &st, NULL) == SQLITE_OK) { sqlite3_bind_text(st, 1, username, -1, SQLITE_STATIC); ok = sqlite3_step(st) == SQLITE_DONE && sqlite3_changes(s->db) > 0; } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; } bool auth_store_list_users(auth_store *s, json_t *out) { if (!s || !json_is_array(out)) return false; pthread_mutex_lock(&s->lock); sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(s->db, "SELECT username, role, created_at FROM users ORDER BY username COLLATE NOCASE;", -1, &st, NULL) == SQLITE_OK) { ok = true; while (sqlite3_step(st) == SQLITE_ROW) { const char *u = (const char *)sqlite3_column_text(st, 0); const char *r = (const char *)sqlite3_column_text(st, 1); json_array_append_new(out, json_pack("{s:s,s:s,s:I}", "username", u ? u : "", "role", r ? r : "user", "createdAt", (json_int_t)sqlite3_column_int64(st, 2))); } } sqlite3_finalize(st); pthread_mutex_unlock(&s->lock); return ok; }