334 lines
10 KiB
C++
334 lines
10 KiB
C++
/*
|
|
* user.h
|
|
* User database, password hashing, and authentication for MontaukOS
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#pragma once
|
|
#include <montauk/syscall.h>
|
|
#include <montauk/string.h>
|
|
#include <montauk/heap.h>
|
|
#include <montauk/config.h>
|
|
#include <bearssl_hash.h>
|
|
|
|
namespace montauk {
|
|
namespace user {
|
|
|
|
static constexpr int MAX_USERS = 16;
|
|
|
|
struct UserInfo {
|
|
char username[32];
|
|
char display_name[64];
|
|
char password_hash[65]; // 64 hex chars + null
|
|
char salt[33]; // 32 hex chars + null
|
|
char role[16];
|
|
};
|
|
|
|
// ---- String helpers ----
|
|
|
|
inline void str_cat(char* dst, const char* src, int max) {
|
|
int len = montauk::slen(dst);
|
|
int i = 0;
|
|
while (src[i] && len < max - 1) {
|
|
dst[len++] = src[i++];
|
|
}
|
|
dst[len] = '\0';
|
|
}
|
|
|
|
inline void build_user_key(char* out, int sz, const char* username, const char* field) {
|
|
int p = 0;
|
|
const char* prefix = "users.";
|
|
while (*prefix && p < sz - 1) out[p++] = *prefix++;
|
|
while (*username && p < sz - 1) out[p++] = *username++;
|
|
if (p < sz - 1) out[p++] = '.';
|
|
while (*field && p < sz - 1) out[p++] = *field++;
|
|
out[p] = '\0';
|
|
}
|
|
|
|
// ---- Path helpers ----
|
|
|
|
inline void home_dir(const char* username, char* out, int sz) {
|
|
int p = 0;
|
|
const char* prefix = "0:/users/";
|
|
while (*prefix && p < sz - 1) out[p++] = *prefix++;
|
|
while (*username && p < sz - 1) out[p++] = *username++;
|
|
out[p] = '\0';
|
|
}
|
|
|
|
inline void config_dir(const char* username, char* out, int sz) {
|
|
home_dir(username, out, sz);
|
|
str_cat(out, "/config", sz);
|
|
}
|
|
|
|
// ---- Hex conversion helpers ----
|
|
|
|
inline void bytes_to_hex(const uint8_t* bytes, int len, char* out) {
|
|
const char* digits = "0123456789abcdef";
|
|
for (int i = 0; i < len; i++) {
|
|
out[i * 2] = digits[(bytes[i] >> 4) & 0x0F];
|
|
out[i * 2 + 1] = digits[bytes[i] & 0x0F];
|
|
}
|
|
out[len * 2] = '\0';
|
|
}
|
|
|
|
inline int hex_char_val(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
|
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
|
|
return -1;
|
|
}
|
|
|
|
inline int hex_to_bytes(const char* hex, uint8_t* out, int maxBytes) {
|
|
int i = 0;
|
|
while (hex[i * 2] && hex[i * 2 + 1] && i < maxBytes) {
|
|
int hi = hex_char_val(hex[i * 2]);
|
|
int lo = hex_char_val(hex[i * 2 + 1]);
|
|
if (hi < 0 || lo < 0) return i;
|
|
out[i] = (uint8_t)((hi << 4) | lo);
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
// ---- Password hashing (SHA-256) ----
|
|
|
|
inline void hash_password(const char* password, const char* salt_hex, char* out_hex64) {
|
|
uint8_t salt_bytes[16];
|
|
int salt_len = hex_to_bytes(salt_hex, salt_bytes, 16);
|
|
int pass_len = montauk::slen(password);
|
|
|
|
br_sha256_context ctx;
|
|
br_sha256_init(&ctx);
|
|
br_sha256_update(&ctx, salt_bytes, salt_len);
|
|
br_sha256_update(&ctx, password, pass_len);
|
|
|
|
uint8_t hash[32];
|
|
br_sha256_out(&ctx, hash);
|
|
bytes_to_hex(hash, 32, out_hex64);
|
|
}
|
|
|
|
inline bool verify_password(const char* password, const char* salt_hex, const char* stored_hex64) {
|
|
char computed[65];
|
|
hash_password(password, salt_hex, computed);
|
|
|
|
int diff = 0;
|
|
for (int i = 0; i < 64; i++) {
|
|
diff |= computed[i] ^ stored_hex64[i];
|
|
}
|
|
return diff == 0;
|
|
}
|
|
|
|
// ---- User database I/O ----
|
|
|
|
inline int load_users(UserInfo* buf, int max) {
|
|
auto doc = config::load("users");
|
|
int count = 0;
|
|
|
|
for (int i = 0; i < doc.entries.count && count < max; i++) {
|
|
auto* v = doc.entries.items[i];
|
|
if (!v->key) continue;
|
|
|
|
if (!montauk::starts_with(v->key, "users.")) continue;
|
|
const char* after = v->key + 6;
|
|
|
|
int dot = -1;
|
|
for (int j = 0; after[j]; j++) {
|
|
if (after[j] == '.') { dot = j; break; }
|
|
}
|
|
if (dot <= 0) continue;
|
|
|
|
if (!montauk::streq(after + dot + 1, "display_name")) continue;
|
|
|
|
UserInfo* u = &buf[count];
|
|
montauk::memset(u, 0, sizeof(UserInfo));
|
|
int ulen = dot < 31 ? dot : 31;
|
|
montauk::memcpy(u->username, after, ulen);
|
|
u->username[ulen] = '\0';
|
|
|
|
if (v->type == montauk::toml::Type::String && v->str)
|
|
montauk::strncpy(u->display_name, v->str, sizeof(u->display_name));
|
|
|
|
char prefix[64];
|
|
int plen = 0;
|
|
const char* pfx = "users.";
|
|
while (*pfx) prefix[plen++] = *pfx++;
|
|
for (int j = 0; j < ulen; j++) prefix[plen++] = u->username[j];
|
|
prefix[plen] = '\0';
|
|
|
|
char key[96];
|
|
|
|
montauk::strcpy(key, prefix);
|
|
str_cat(key, ".password_hash", 96);
|
|
const char* ph = doc.get_string(key, "");
|
|
montauk::strncpy(u->password_hash, ph, sizeof(u->password_hash));
|
|
|
|
montauk::strcpy(key, prefix);
|
|
str_cat(key, ".salt", 96);
|
|
const char* s = doc.get_string(key, "");
|
|
montauk::strncpy(u->salt, s, sizeof(u->salt));
|
|
|
|
montauk::strcpy(key, prefix);
|
|
str_cat(key, ".role", 96);
|
|
const char* r = doc.get_string(key, "user");
|
|
montauk::strncpy(u->role, r, sizeof(u->role));
|
|
|
|
count++;
|
|
}
|
|
|
|
doc.destroy();
|
|
return count;
|
|
}
|
|
|
|
inline int save_users(const UserInfo* buf, int count) {
|
|
montauk::toml::Doc doc;
|
|
doc.init();
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
const UserInfo* u = &buf[i];
|
|
char key[96];
|
|
|
|
build_user_key(key, 96, u->username, "display_name");
|
|
config::set_string(&doc, key, u->display_name);
|
|
|
|
build_user_key(key, 96, u->username, "password_hash");
|
|
config::set_string(&doc, key, u->password_hash);
|
|
|
|
build_user_key(key, 96, u->username, "salt");
|
|
config::set_string(&doc, key, u->salt);
|
|
|
|
build_user_key(key, 96, u->username, "role");
|
|
config::set_string(&doc, key, u->role);
|
|
}
|
|
|
|
int ret = config::save("users", &doc);
|
|
doc.destroy();
|
|
return ret;
|
|
}
|
|
|
|
// ---- Authentication ----
|
|
|
|
inline bool authenticate(const char* username, const char* password) {
|
|
UserInfo users[MAX_USERS];
|
|
int count = load_users(users, MAX_USERS);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (montauk::streq(users[i].username, username)) {
|
|
return verify_password(password, users[i].salt, users[i].password_hash);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---- User management ----
|
|
|
|
inline bool create_user(const char* username, const char* display_name,
|
|
const char* password, const char* role) {
|
|
UserInfo users[MAX_USERS];
|
|
int count = load_users(users, MAX_USERS);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (montauk::streq(users[i].username, username)) return false;
|
|
}
|
|
if (count >= MAX_USERS) return false;
|
|
|
|
UserInfo* u = &users[count];
|
|
montauk::memset(u, 0, sizeof(UserInfo));
|
|
montauk::strncpy(u->username, username, 31);
|
|
montauk::strncpy(u->display_name, display_name, 63);
|
|
montauk::strncpy(u->role, role, 15);
|
|
|
|
uint8_t salt_bytes[16];
|
|
montauk::getrandom(salt_bytes, 16);
|
|
bytes_to_hex(salt_bytes, 16, u->salt);
|
|
|
|
hash_password(password, u->salt, u->password_hash);
|
|
|
|
count++;
|
|
save_users(users, count);
|
|
|
|
// Create home directory structure
|
|
char dir[128];
|
|
home_dir(username, dir, sizeof(dir));
|
|
montauk::fmkdir(dir);
|
|
|
|
char subdir[128];
|
|
montauk::strcpy(subdir, dir);
|
|
str_cat(subdir, "/config", 128);
|
|
montauk::fmkdir(subdir);
|
|
|
|
return true;
|
|
}
|
|
|
|
inline bool delete_user(const char* username) {
|
|
UserInfo users[MAX_USERS];
|
|
int count = load_users(users, MAX_USERS);
|
|
|
|
int idx = -1;
|
|
for (int i = 0; i < count; i++) {
|
|
if (montauk::streq(users[i].username, username)) { idx = i; break; }
|
|
}
|
|
if (idx < 0) return false;
|
|
|
|
for (int i = idx; i < count - 1; i++) {
|
|
users[i] = users[i + 1];
|
|
}
|
|
count--;
|
|
|
|
save_users(users, count);
|
|
return true;
|
|
}
|
|
|
|
inline bool change_password(const char* username, const char* new_password) {
|
|
UserInfo users[MAX_USERS];
|
|
int count = load_users(users, MAX_USERS);
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (montauk::streq(users[i].username, username)) {
|
|
uint8_t salt_bytes[16];
|
|
montauk::getrandom(salt_bytes, 16);
|
|
bytes_to_hex(salt_bytes, 16, users[i].salt);
|
|
|
|
hash_password(new_password, users[i].salt, users[i].password_hash);
|
|
|
|
save_users(users, count);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---- Session state and current user helpers ----
|
|
|
|
// Persist login session state after successful login.
|
|
// Current processes should use getuser() for their active username.
|
|
inline void set_session(const char* username) {
|
|
montauk::toml::Doc doc;
|
|
doc.init();
|
|
config::set_string(&doc, "session.username", username);
|
|
config::save("session", &doc);
|
|
doc.destroy();
|
|
}
|
|
|
|
// Clear the session (on logout).
|
|
inline void clear_session() {
|
|
montauk::fdelete("0:/config/session.toml");
|
|
}
|
|
|
|
// Read the current process username into buf. Returns true if one is set.
|
|
inline bool get_session_username(char* buf, int bufSz) {
|
|
if (buf == nullptr || bufSz <= 0) return false;
|
|
int len = montauk::getuser(buf, (uint64_t)bufSz);
|
|
return len > 0 && buf[0] != '\0';
|
|
}
|
|
|
|
// Get the current process user's home directory. Returns true if a user is set.
|
|
inline bool get_home_dir(char* buf, int bufSz) {
|
|
char username[32];
|
|
if (!get_session_username(username, sizeof(username))) return false;
|
|
home_dir(username, buf, bufSz);
|
|
return true;
|
|
}
|
|
|
|
} // namespace user
|
|
} // namespace montauk
|