feat: multi-user system, bug fixes, security & performance fixes, and more

This commit is contained in:
2026-03-14 13:28:46 +01:00
parent 576ad34f95
commit 261b536041
389 changed files with 231853 additions and 591 deletions
+110 -12
View File
@@ -247,14 +247,98 @@ namespace config {
char* text = serialize(doc);
int textLen = montauk::slen(text);
// Try to open existing file, create if not found
// Delete and recreate to ensure file is exactly the right size
// (no ftruncate syscall available, so this avoids stale trailing data)
montauk::fdelete(path);
int handle = montauk::fcreate(path);
if (handle < 0) {
montauk::mfree(text);
return -1;
}
int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen);
montauk::close(handle);
montauk::mfree(text);
return ret < 0 ? ret : 0;
}
// ---- Per-user config ----
// Build path: "0:/users/<username>/config/<name>.toml"
inline void build_user_path(char* out, int outSz, const char* username, const char* name) {
int p = 0;
const char* prefix = "0:/users/";
while (*prefix && p < outSz - 2) out[p++] = *prefix++;
while (*username && p < outSz - 2) out[p++] = *username++;
const char* mid = "/config/";
while (*mid && p < outSz - 2) out[p++] = *mid++;
while (*name && p < outSz - 6) out[p++] = *name++;
const char* ext = ".toml";
while (*ext && p < outSz - 1) out[p++] = *ext++;
out[p] = '\0';
}
// Ensure per-user config directory exists
inline void ensure_user_dir(const char* username) {
char dir[128];
int p = 0;
const char* prefix = "0:/users/";
while (*prefix && p < 126) dir[p++] = *prefix++;
while (*username && p < 126) dir[p++] = *username++;
dir[p] = '\0';
montauk::fmkdir(dir);
const char* suffix = "/config";
while (*suffix && p < 126) dir[p++] = *suffix++;
dir[p] = '\0';
montauk::fmkdir(dir);
}
// Load a per-user config file
inline toml::Doc load_user(const char* username, const char* name) {
char path[192];
build_user_path(path, sizeof(path), username, name);
int handle = montauk::open(path);
if (handle < 0) {
handle = montauk::fcreate(path);
if (handle < 0) {
montauk::mfree(text);
return -1;
}
toml::Doc doc;
doc.init();
return doc;
}
uint64_t size = montauk::getsize(handle);
if (size == 0) {
montauk::close(handle);
toml::Doc doc;
doc.init();
return doc;
}
char* text = (char*)montauk::malloc(size + 1);
montauk::read(handle, (uint8_t*)text, 0, size);
montauk::close(handle);
text[size] = '\0';
toml::Doc doc = toml::parse(text);
montauk::mfree(text);
return doc;
}
// Save a per-user config file
inline int save_user(const char* username, const char* name, toml::Doc* doc) {
ensure_user_dir(username);
char path[192];
build_user_path(path, sizeof(path), username, name);
char* text = serialize(doc);
int textLen = montauk::slen(text);
montauk::fdelete(path);
int handle = montauk::fcreate(path);
if (handle < 0) {
montauk::mfree(text);
return -1;
}
int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen);
@@ -272,12 +356,28 @@ namespace config {
// ---- In-place modification helpers ----
// Free any dynamically allocated data owned by a value (without freeing key)
namespace detail {
inline void free_value_data(toml::Value* v) {
if (v->type == toml::Type::String && v->str)
montauk::mfree(v->str);
else if (v->type == toml::Type::Array || v->type == toml::Type::Table) {
for (int i = 0; i < v->array.count; i++) {
if (v->array.items[i]) v->array.items[i]->destroy();
}
if (v->array.items) montauk::mfree(v->array.items);
v->array.items = nullptr;
v->array.count = 0;
v->array.cap = 0;
}
}
}
// Set or overwrite a string value in the doc.
inline void set_string(toml::Doc* doc, const char* key, const char* val) {
auto* existing = doc->get(key);
if (existing) {
if (existing->type == toml::Type::String && existing->str)
montauk::mfree(existing->str);
detail::free_value_data(existing);
existing->type = toml::Type::String;
existing->str = toml::Value::dup(val);
} else {
@@ -289,8 +389,7 @@ namespace config {
inline void set_int(toml::Doc* doc, const char* key, int64_t val) {
auto* existing = doc->get(key);
if (existing) {
if (existing->type == toml::Type::String && existing->str)
montauk::mfree(existing->str);
detail::free_value_data(existing);
existing->type = toml::Type::Int;
existing->ival = val;
} else {
@@ -302,8 +401,7 @@ namespace config {
inline void set_bool(toml::Doc* doc, const char* key, bool val) {
auto* existing = doc->get(key);
if (existing) {
if (existing->type == toml::Type::String && existing->str)
montauk::mfree(existing->str);
detail::free_value_data(existing);
existing->type = toml::Type::Bool;
existing->bval = val;
} else {
+4
View File
@@ -151,6 +151,10 @@ namespace heap_detail {
g_initialized = true;
}
// Guard against overflow: size + Header must not wrap
if (size > UINT64_MAX - sizeof(Header) - 15)
return nullptr;
uint64_t needed = size + sizeof(Header);
needed = (needed + 15) & ~15ULL;
+1
View File
@@ -99,6 +99,7 @@ namespace montauk {
}
inline void strncpy(char* dst, const char* src, int max) {
if (max <= 0) return;
int i = 0;
while (src[i] && i < max - 1) { dst[i] = src[i]; i++; }
dst[i] = '\0';
+338
View File
@@ -0,0 +1,338 @@
/*
* 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 (current logged-in user) ----
// Write the current session after successful login.
// Stores username in 0:/config/session.toml so any app can query it.
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 username into buf. Returns true if a session exists.
inline bool get_session_username(char* buf, int bufSz) {
auto doc = config::load("session");
const char* name = doc.get_string("session.username", "");
bool found = (name[0] != '\0');
if (found) {
montauk::strncpy(buf, name, bufSz);
}
doc.destroy();
return found;
}
// Get the current user's home directory. Returns true if a session exists.
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