feat: multi-user system, bug fixes, security & performance fixes, and more
This commit is contained in:
@@ -60,6 +60,12 @@ struct DesktopState {
|
||||
int window_count;
|
||||
int focused_window;
|
||||
|
||||
// Current user context
|
||||
char current_user[32];
|
||||
char home_dir[128]; // "0:/users/<username>"
|
||||
char user_config_dir[128]; // "0:/users/<username>/config"
|
||||
bool is_admin;
|
||||
|
||||
Montauk::MouseState mouse;
|
||||
uint8_t prev_buttons;
|
||||
|
||||
@@ -93,9 +99,11 @@ struct DesktopState {
|
||||
SvgIcon icon_settings;
|
||||
SvgIcon icon_reboot;
|
||||
SvgIcon icon_shutdown;
|
||||
SvgIcon icon_logout;
|
||||
|
||||
SvgIcon icon_procmgr;
|
||||
SvgIcon icon_mandelbrot;
|
||||
SvgIcon icon_volume;
|
||||
|
||||
// External apps discovered from 0:/apps/ manifests
|
||||
ExternalApp external_apps[MAX_EXTERNAL_APPS];
|
||||
@@ -109,6 +117,14 @@ struct DesktopState {
|
||||
uint64_t net_cfg_last_poll;
|
||||
Rect net_icon_rect;
|
||||
|
||||
bool vol_popup_open;
|
||||
Rect vol_icon_rect;
|
||||
int vol_level; // 0-100
|
||||
bool vol_muted;
|
||||
int vol_pre_mute; // volume before mute
|
||||
bool vol_dragging; // slider drag in progress
|
||||
uint64_t vol_last_poll;
|
||||
|
||||
int screen_w, screen_h;
|
||||
|
||||
// IDs of external windows we've sent a close event to but that haven't
|
||||
@@ -119,6 +135,15 @@ struct DesktopState {
|
||||
int closing_ext_count;
|
||||
|
||||
DesktopSettings settings;
|
||||
|
||||
// Lock screen state
|
||||
bool screen_locked;
|
||||
char lock_password[64];
|
||||
int lock_password_len;
|
||||
char lock_error[128];
|
||||
bool lock_show_error;
|
||||
char lock_display_name[64];
|
||||
SvgIcon icon_lock;
|
||||
};
|
||||
|
||||
// Forward declarations - implemented in main.cpp
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
* http.hpp
|
||||
* Simple HTTP request builder and response parser for MontaukOS
|
||||
* Wraps tls::https_fetch() and raw sockets for ergonomic HTTP usage.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <tls/tls.hpp>
|
||||
|
||||
namespace http {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Response
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct Response {
|
||||
int status; // HTTP status code (200, 404, etc.) or -1 on error
|
||||
const char* headers; // Pointer into raw buffer (header block)
|
||||
int headers_len;
|
||||
const char* body; // Pointer into raw buffer (body)
|
||||
int body_len;
|
||||
char* raw; // Owned buffer — caller must free with montauk::mfree()
|
||||
int raw_len;
|
||||
};
|
||||
|
||||
// Parse raw HTTP response in-place. Sets pointers into buf (does not copy).
|
||||
// Returns status code, or -1 if unparseable.
|
||||
inline int parse_response(char* buf, int len, Response* out) {
|
||||
out->raw = buf;
|
||||
out->raw_len = len;
|
||||
out->status = -1;
|
||||
out->headers = nullptr;
|
||||
out->headers_len = 0;
|
||||
out->body = nullptr;
|
||||
out->body_len = 0;
|
||||
|
||||
if (len < 12) return -1; // "HTTP/1.x NNN"
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN"
|
||||
const char* p = buf;
|
||||
while (*p && *p != ' ') p++;
|
||||
if (*p != ' ') return -1;
|
||||
p++;
|
||||
int code = 0;
|
||||
for (int i = 0; i < 3 && *p >= '0' && *p <= '9'; i++, p++)
|
||||
code = code * 10 + (*p - '0');
|
||||
out->status = code;
|
||||
|
||||
// Headers start after the status line
|
||||
const char* hdr_start = buf;
|
||||
while (hdr_start < buf + len - 1) {
|
||||
if (*hdr_start == '\r' && *(hdr_start + 1) == '\n') { hdr_start += 2; break; }
|
||||
if (*hdr_start == '\n') { hdr_start++; break; }
|
||||
hdr_start++;
|
||||
}
|
||||
out->headers = hdr_start;
|
||||
|
||||
// Find \r\n\r\n boundary between headers and body
|
||||
for (const char* s = hdr_start; s < buf + len - 3; s++) {
|
||||
if (s[0] == '\r' && s[1] == '\n' && s[2] == '\r' && s[3] == '\n') {
|
||||
out->headers_len = (int)(s - hdr_start);
|
||||
out->body = s + 4;
|
||||
out->body_len = len - (int)(out->body - buf);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
// No body separator found — entire remainder is headers
|
||||
out->headers_len = len - (int)(hdr_start - buf);
|
||||
return code;
|
||||
}
|
||||
|
||||
// Find a header value by name (case-insensitive match on the name).
|
||||
// Writes value into out_val (up to max_len), returns true if found.
|
||||
inline bool get_header(const Response* resp, const char* name, char* out_val, int max_len) {
|
||||
if (!resp->headers || resp->headers_len == 0) return false;
|
||||
int name_len = montauk::slen(name);
|
||||
const char* p = resp->headers;
|
||||
const char* end = resp->headers + resp->headers_len;
|
||||
|
||||
while (p < end) {
|
||||
// Case-insensitive prefix match
|
||||
bool match = true;
|
||||
if (p + name_len >= end) { match = false; }
|
||||
else {
|
||||
for (int i = 0; i < name_len; i++) {
|
||||
char a = p[i], b = name[i];
|
||||
if (a >= 'A' && a <= 'Z') a += 32;
|
||||
if (b >= 'A' && b <= 'Z') b += 32;
|
||||
if (a != b) { match = false; break; }
|
||||
}
|
||||
if (match && p[name_len] != ':') match = false;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const char* v = p + name_len + 1;
|
||||
while (v < end && *v == ' ') v++; // skip OWS
|
||||
int i = 0;
|
||||
while (v < end && *v != '\r' && *v != '\n' && i < max_len - 1)
|
||||
out_val[i++] = *v++;
|
||||
out_val[i] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip to next line
|
||||
while (p < end && *p != '\n') p++;
|
||||
if (p < end) p++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free a response's raw buffer.
|
||||
inline void free_response(Response* resp) {
|
||||
if (resp->raw) { montauk::mfree(resp->raw); resp->raw = nullptr; }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Request builder (internal)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
inline int build_request(char* buf, int buf_size,
|
||||
const char* method, const char* host,
|
||||
const char* path, const char* content_type,
|
||||
const char* body_data, int body_len,
|
||||
const char* extra_headers) {
|
||||
char* p = buf;
|
||||
char* end = buf + buf_size - 1;
|
||||
|
||||
auto append = [&](const char* s) {
|
||||
while (*s && p < end) *p++ = *s++;
|
||||
};
|
||||
auto append_int = [&](int n) {
|
||||
char tmp[16]; int ti = 0;
|
||||
if (n == 0) { if (p < end) *p++ = '0'; return; }
|
||||
while (n > 0) { tmp[ti++] = '0' + (n % 10); n /= 10; }
|
||||
for (int j = ti - 1; j >= 0 && p < end; j--) *p++ = tmp[j];
|
||||
};
|
||||
|
||||
// Request line
|
||||
append(method); append(" "); append(path); append(" HTTP/1.1\r\n");
|
||||
|
||||
// Host
|
||||
append("Host: "); append(host); append("\r\n");
|
||||
|
||||
// Content headers (for POST/PUT/PATCH)
|
||||
if (body_data && body_len > 0) {
|
||||
if (content_type) {
|
||||
append("Content-Type: "); append(content_type); append("\r\n");
|
||||
}
|
||||
append("Content-Length: "); append_int(body_len); append("\r\n");
|
||||
}
|
||||
|
||||
// Extra headers (caller-supplied, must include \r\n terminators)
|
||||
if (extra_headers) append(extra_headers);
|
||||
|
||||
append("Connection: close\r\n");
|
||||
append("\r\n");
|
||||
|
||||
int header_len = (int)(p - buf);
|
||||
|
||||
// Append body
|
||||
if (body_data && body_len > 0 && header_len + body_len < buf_size) {
|
||||
montauk::memcpy(p, body_data, body_len);
|
||||
p += body_len;
|
||||
}
|
||||
|
||||
return (int)(p - buf);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// GET request over HTTPS. Returns parsed response. Caller must free_response().
|
||||
inline Response get(const char* host, const char* path,
|
||||
const tls::TrustAnchors& tas,
|
||||
int resp_buf_size = 32768,
|
||||
const char* extra_headers = nullptr,
|
||||
tls::AbortCheckFn abort_check = nullptr) {
|
||||
Response resp = {};
|
||||
resp.status = -1;
|
||||
|
||||
uint32_t ip = montauk::resolve(host);
|
||||
if (!ip) return resp;
|
||||
|
||||
char req[1024];
|
||||
int reqLen = build_request(req, sizeof(req), "GET", host, path,
|
||||
nullptr, nullptr, 0, extra_headers);
|
||||
|
||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
||||
if (!buf) return resp;
|
||||
|
||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
||||
buf, resp_buf_size - 1, abort_check);
|
||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
||||
buf[n] = 0;
|
||||
|
||||
parse_response(buf, n, &resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// POST request over HTTPS. Returns parsed response. Caller must free_response().
|
||||
inline Response post(const char* host, const char* path,
|
||||
const char* content_type,
|
||||
const char* body_data, int body_len,
|
||||
const tls::TrustAnchors& tas,
|
||||
int resp_buf_size = 32768,
|
||||
const char* extra_headers = nullptr,
|
||||
tls::AbortCheckFn abort_check = nullptr) {
|
||||
Response resp = {};
|
||||
resp.status = -1;
|
||||
|
||||
uint32_t ip = montauk::resolve(host);
|
||||
if (!ip) return resp;
|
||||
|
||||
int req_size = 1024 + body_len;
|
||||
char* req = (char*)montauk::malloc(req_size);
|
||||
if (!req) return resp;
|
||||
|
||||
int reqLen = build_request(req, req_size, "POST", host, path,
|
||||
content_type, body_data, body_len,
|
||||
extra_headers);
|
||||
|
||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
||||
if (!buf) { montauk::mfree(req); return resp; }
|
||||
|
||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
||||
buf, resp_buf_size - 1, abort_check);
|
||||
montauk::mfree(req);
|
||||
|
||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
||||
buf[n] = 0;
|
||||
|
||||
parse_response(buf, n, &resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Generic request over HTTPS (PUT, PATCH, DELETE, etc.).
|
||||
inline Response request(const char* method,
|
||||
const char* host, const char* path,
|
||||
const char* content_type,
|
||||
const char* body_data, int body_len,
|
||||
const tls::TrustAnchors& tas,
|
||||
int resp_buf_size = 32768,
|
||||
const char* extra_headers = nullptr,
|
||||
tls::AbortCheckFn abort_check = nullptr) {
|
||||
Response resp = {};
|
||||
resp.status = -1;
|
||||
|
||||
uint32_t ip = montauk::resolve(host);
|
||||
if (!ip) return resp;
|
||||
|
||||
int req_size = 1024 + (body_len > 0 ? body_len : 0);
|
||||
char* req = (char*)montauk::malloc(req_size);
|
||||
if (!req) return resp;
|
||||
|
||||
int reqLen = build_request(req, req_size, method, host, path,
|
||||
content_type, body_data, body_len,
|
||||
extra_headers);
|
||||
|
||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
||||
if (!buf) { montauk::mfree(req); return resp; }
|
||||
|
||||
int n = tls::https_fetch(host, ip, 443, req, reqLen, tas,
|
||||
buf, resp_buf_size - 1, abort_check);
|
||||
montauk::mfree(req);
|
||||
|
||||
if (n <= 0) { montauk::mfree(buf); return resp; }
|
||||
buf[n] = 0;
|
||||
|
||||
parse_response(buf, n, &resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// Plain HTTP (no TLS) GET over port 80.
|
||||
inline Response get_plain(const char* host, const char* path,
|
||||
int resp_buf_size = 32768,
|
||||
const char* extra_headers = nullptr) {
|
||||
Response resp = {};
|
||||
resp.status = -1;
|
||||
|
||||
uint32_t ip = montauk::resolve(host);
|
||||
if (!ip) return resp;
|
||||
|
||||
char req[1024];
|
||||
int reqLen = build_request(req, sizeof(req), "GET", host, path,
|
||||
nullptr, nullptr, 0, extra_headers);
|
||||
|
||||
int sock = montauk::socket(Montauk::SOCK_TCP);
|
||||
if (sock < 0) return resp;
|
||||
if (montauk::connect(sock, ip, 80) < 0) { montauk::closesocket(sock); return resp; }
|
||||
|
||||
montauk::send(sock, req, reqLen);
|
||||
|
||||
char* buf = (char*)montauk::malloc(resp_buf_size);
|
||||
if (!buf) { montauk::closesocket(sock); return resp; }
|
||||
|
||||
int total = 0;
|
||||
while (total < resp_buf_size - 1) {
|
||||
int n = montauk::recv(sock, buf + total, resp_buf_size - 1 - total);
|
||||
if (n <= 0) break;
|
||||
total += n;
|
||||
}
|
||||
montauk::closesocket(sock);
|
||||
|
||||
if (total <= 0) { montauk::mfree(buf); return resp; }
|
||||
buf[total] = 0;
|
||||
|
||||
parse_response(buf, total, &resp);
|
||||
return resp;
|
||||
}
|
||||
|
||||
} // namespace http
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user