CC = gcc
CFLAGS = -Wall -Wextra -O2
-LIBS = -lpam -lmicrohttpd
-SRC = src/main.c src/pam_auth.c
+LIBS = -lpam -lmicrohttpd -lpthread
+SRC = src/main.c src/pam_auth.c src/session.c src/user.c
OUT = crimata-auth
.PHONY: all clean
#include <string.h>
#include <microhttpd.h>
#include "pam_auth.h"
+#include "session.h"
+#include "user.h"
#define PORT 7700
#define MAX_BODY 4096
if (!pos) return NULL;
pos += strlen(search);
- while (*pos == ' ' || *pos == ':' || *pos == ' ') pos++;
+ while (*pos == ' ' || *pos == ':') pos++;
if (*pos != '"') return NULL;
pos++;
struct MHD_Response *resp = MHD_create_response_from_buffer(
strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
+ MHD_add_response_header(resp, "Access-Control-Allow-Origin", "*");
enum MHD_Result ret = MHD_queue_response(conn, status, resp);
MHD_destroy_response(resp);
return ret;
}
+/* Extract Bearer token from Authorization header */
+static const char *bearer_token(struct MHD_Connection *conn)
+{
+ const char *auth = MHD_lookup_connection_value(conn, MHD_HEADER_KIND, "Authorization");
+ if (!auth) return NULL;
+ if (strncmp(auth, "Bearer ", 7) != 0) return NULL;
+ return auth + 7;
+}
+
/* ── Request context ──────────────────────────────────────────────────────── */
typedef struct {
return send_json(conn, MHD_HTTP_OK, "{\"status\":\"ok\"}");
}
+/* POST /auth { username, password } → { token } */
static enum MHD_Result handle_auth(struct MHD_Connection *conn, const char *body)
{
char username[256] = {0};
"{\"success\":false,\"error\":\"username and password required\"}");
}
- int result = authenticate(username, password);
- if (result == 0) {
- return send_json(conn, MHD_HTTP_OK, "{\"success\":true}");
+ if (authenticate(username, password) != 0) {
+ return send_json(conn, MHD_HTTP_UNAUTHORIZED,
+ "{\"success\":false,\"error\":\"invalid credentials\"}");
+ }
+
+ const char *token = session_create(username);
+ if (!token) {
+ return send_json(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
+ "{\"success\":false,\"error\":\"no session slots\"}");
}
- return send_json(conn, MHD_HTTP_UNAUTHORIZED,
- "{\"success\":false,\"error\":\"invalid credentials\"}");
+
+ char resp[320];
+ snprintf(resp, sizeof(resp),
+ "{\"success\":true,\"token\":\"%s\",\"username\":\"%s\"}", token, username);
+ return send_json(conn, MHD_HTTP_OK, resp);
+}
+
+/* GET /me Authorization: Bearer <token> → { username } */
+static enum MHD_Result handle_me(struct MHD_Connection *conn)
+{
+ const char *token = bearer_token(conn);
+ if (!token) {
+ return send_json(conn, MHD_HTTP_UNAUTHORIZED,
+ "{\"error\":\"missing token\"}");
+ }
+
+ const char *username = session_lookup(token);
+ if (!username) {
+ return send_json(conn, MHD_HTTP_UNAUTHORIZED,
+ "{\"error\":\"invalid token\"}");
+ }
+
+ char resp[320];
+ snprintf(resp, sizeof(resp), "{\"username\":\"%s\"}", username);
+ return send_json(conn, MHD_HTTP_OK, resp);
+}
+
+/* POST /logout Authorization: Bearer <token> */
+static enum MHD_Result handle_logout(struct MHD_Connection *conn)
+{
+ const char *token = bearer_token(conn);
+ if (token) session_destroy(token);
+ return send_json(conn, MHD_HTTP_OK, "{\"success\":true}");
+}
+
+/* POST /users { username, password } → create Linux user */
+static enum MHD_Result handle_create_user(struct MHD_Connection *conn, const char *body)
+{
+ /* Caller must be authenticated */
+ const char *token = bearer_token(conn);
+ if (!token || !session_lookup(token)) {
+ return send_json(conn, MHD_HTTP_UNAUTHORIZED,
+ "{\"success\":false,\"error\":\"authentication required\"}");
+ }
+
+ char username[256] = {0};
+ char password[256] = {0};
+
+ if (!json_get(body, "username", username, sizeof(username)) ||
+ !json_get(body, "password", password, sizeof(password))) {
+ return send_json(conn, MHD_HTTP_BAD_REQUEST,
+ "{\"success\":false,\"error\":\"username and password required\"}");
+ }
+
+ if (user_create(username, password) != 0) {
+ return send_json(conn, MHD_HTTP_CONFLICT,
+ "{\"success\":false,\"error\":\"could not create user\"}");
+ }
+
+ char resp[320];
+ snprintf(resp, sizeof(resp), "{\"success\":true,\"username\":\"%s\"}", username);
+ return send_json(conn, MHD_HTTP_CREATED, resp);
}
/* ── Main handler ─────────────────────────────────────────────────────────── */
if (strcmp(url, "/health") == 0 && strcmp(method, "GET") == 0)
return handle_health(conn);
- /* Auth — collect body first */
- if (strcmp(url, "/auth") == 0 && strcmp(method, "POST") == 0) {
+ /* GET /me */
+ if (strcmp(url, "/me") == 0 && strcmp(method, "GET") == 0)
+ return handle_me(conn);
+
+ /* Routes that need a request body */
+ if ((strcmp(url, "/auth") == 0 ||
+ strcmp(url, "/logout") == 0 ||
+ strcmp(url, "/users") == 0) &&
+ (strcmp(method, "POST") == 0))
+ {
if (!*con_cls) {
request_ctx_t *ctx = calloc(1, sizeof(request_ctx_t));
if (!ctx) return MHD_NO;
return MHD_YES;
}
- return handle_auth(conn, ctx->body);
+ if (strcmp(url, "/auth") == 0) return handle_auth(conn, ctx->body);
+ if (strcmp(url, "/logout") == 0) return handle_logout(conn);
+ if (strcmp(url, "/users") == 0) return handle_create_user(conn, ctx->body);
}
return send_json(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"not found\"}");
--- /dev/null
+#include "session.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <time.h>
+#include <pthread.h>
+
+#define MAX_SESSIONS 1024
+#define TOKEN_LEN 32
+
+typedef struct {
+ char token[TOKEN_LEN + 1];
+ char username[256];
+ int active;
+} session_t;
+
+static session_t sessions[MAX_SESSIONS];
+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
+
+/* Generate a random hex token */
+static void gen_token(char *out)
+{
+ static const char hex[] = "0123456789abcdef";
+ unsigned char buf[TOKEN_LEN / 2];
+ FILE *f = fopen("/dev/urandom", "rb");
+ if (f) {
+ fread(buf, 1, sizeof(buf), f);
+ fclose(f);
+ } else {
+ /* fallback: not cryptographically strong but avoids hanging */
+ srand((unsigned)time(NULL));
+ for (size_t i = 0; i < sizeof(buf); i++)
+ buf[i] = (unsigned char)rand();
+ }
+ for (size_t i = 0; i < sizeof(buf); i++) {
+ out[i * 2] = hex[buf[i] >> 4];
+ out[i * 2 + 1] = hex[buf[i] & 0xf];
+ }
+ out[TOKEN_LEN] = '\0';
+}
+
+const char *session_create(const char *username)
+{
+ pthread_mutex_lock(&lock);
+
+ /* Find a free slot */
+ for (int i = 0; i < MAX_SESSIONS; i++) {
+ if (!sessions[i].active) {
+ gen_token(sessions[i].token);
+ strncpy(sessions[i].username, username, sizeof(sessions[i].username) - 1);
+ sessions[i].username[sizeof(sessions[i].username) - 1] = '\0';
+ sessions[i].active = 1;
+ pthread_mutex_unlock(&lock);
+ return sessions[i].token;
+ }
+ }
+
+ pthread_mutex_unlock(&lock);
+ return NULL; /* no free slots */
+}
+
+const char *session_lookup(const char *token)
+{
+ if (!token || !*token) return NULL;
+
+ pthread_mutex_lock(&lock);
+ for (int i = 0; i < MAX_SESSIONS; i++) {
+ if (sessions[i].active &&
+ strncmp(sessions[i].token, token, TOKEN_LEN) == 0) {
+ pthread_mutex_unlock(&lock);
+ return sessions[i].username;
+ }
+ }
+ pthread_mutex_unlock(&lock);
+ return NULL;
+}
+
+void session_destroy(const char *token)
+{
+ if (!token || !*token) return;
+
+ pthread_mutex_lock(&lock);
+ for (int i = 0; i < MAX_SESSIONS; i++) {
+ if (sessions[i].active &&
+ strncmp(sessions[i].token, token, TOKEN_LEN) == 0) {
+ sessions[i].active = 0;
+ memset(sessions[i].token, 0, sizeof(sessions[i].token));
+ memset(sessions[i].username, 0, sizeof(sessions[i].username));
+ break;
+ }
+ }
+ pthread_mutex_unlock(&lock);
+}
--- /dev/null
+#ifndef SESSION_H
+#define SESSION_H
+
+/* Create a session for username — returns token string (valid until destroyed) */
+const char *session_create(const char *username);
+
+/* Look up a token — returns username or NULL if not found */
+const char *session_lookup(const char *token);
+
+/* Destroy a session */
+void session_destroy(const char *token);
+
+#endif
--- /dev/null
+#include "user.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+/*
+ * Run a command with two arguments, writing optional stdin data to it.
+ * Returns the exit code, or -1 on fork/exec failure.
+ */
+static int run_cmd(const char *path, const char *const argv[],
+ const char *stdin_data)
+{
+ int pipefd[2] = {-1, -1};
+
+ if (stdin_data) {
+ if (pipe(pipefd) != 0) return -1;
+ }
+
+ pid_t pid = fork();
+ if (pid < 0) return -1;
+
+ if (pid == 0) {
+ /* child */
+ if (stdin_data) {
+ close(pipefd[1]);
+ dup2(pipefd[0], STDIN_FILENO);
+ close(pipefd[0]);
+ }
+ execv(path, (char *const *)argv);
+ _exit(127);
+ }
+
+ /* parent */
+ if (stdin_data) {
+ close(pipefd[0]);
+ size_t len = strlen(stdin_data);
+ write(pipefd[1], stdin_data, len);
+ close(pipefd[1]);
+ }
+
+ int status;
+ waitpid(pid, &status, 0);
+ if (WIFEXITED(status)) return WEXITSTATUS(status);
+ return -1;
+}
+
+int user_create(const char *username, const char *password)
+{
+ /* Validate: username must be non-empty and only contain safe chars */
+ if (!username || !*username || !password || !*password) return -1;
+ for (const char *p = username; *p; p++) {
+ char c = *p;
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '_' || c == '-'))
+ return -1;
+ }
+
+ /* useradd -m -s /bin/bash <username> */
+ const char *useradd_argv[] = {
+ "/usr/sbin/useradd", "-m", "-s", "/bin/bash", username, NULL
+ };
+ int rc = run_cmd("/usr/sbin/useradd", useradd_argv, NULL);
+ if (rc != 0) return -1;
+
+ /* chpasswd reads "username:password\n" from stdin */
+ char chpasswd_input[512];
+ snprintf(chpasswd_input, sizeof(chpasswd_input), "%s:%s\n", username, password);
+
+ const char *chpasswd_argv[] = { "/usr/sbin/chpasswd", NULL };
+ rc = run_cmd("/usr/sbin/chpasswd", chpasswd_argv, chpasswd_input);
+
+ /* Zero out the password from the stack before returning */
+ memset(chpasswd_input, 0, sizeof(chpasswd_input));
+
+ return (rc == 0) ? 0 : -1;
+}
--- /dev/null
+#ifndef USER_H
+#define USER_H
+
+/*
+ * Create a new Linux user with useradd + set password via chpasswd.
+ * Returns 0 on success, -1 on error (user already exists, permission denied, etc.)
+ */
+int user_create(const char *username, const char *password);
+
+#endif
<link rel="stylesheet" href="styles/window.css" />
<link rel="stylesheet" href="styles/dock.css" />
<link rel="stylesheet" href="styles/cursor.css" />
+ <link rel="stylesheet" href="styles/login.css" />
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body x-data="os" @keydown.window="handleKeydown">
+ <!-- Login overlay (shown until authenticated) -->
+ <div id="login-overlay" x-show="!authed" x-cloak>
+ <div class="login-card">
+ <h1>Crimata</h1>
+ <input
+ type="text"
+ x-model="loginUsername"
+ placeholder="Username"
+ @keydown.enter="$refs.loginPassword.focus()"
+ autocomplete="username"
+ />
+ <input
+ type="password"
+ x-model="loginPassword"
+ placeholder="Password"
+ @keydown.enter="submitLogin"
+ x-ref="loginPassword"
+ autocomplete="current-password"
+ />
+ <button @click="submitLogin">Sign in</button>
+ <div class="login-error" x-text="loginError"></div>
+ </div>
+ </div>
+
<!-- Infinite canvas -->
<div id="canvas"
@mousedown="startPan"
<span class="dock-dot" x-show="app.running"></span>
</div>
</template>
+ <div id="dock-user" x-show="currentUser" x-text="currentUser"></div>
</div>
<script src="js/os.js"></script>
document.addEventListener('alpine:init', () => {
Alpine.data('os', () => ({
+ // ── Auth state ────────────────────────────────────────────────────────
+ authed: false,
+ currentUser: '',
+ loginUsername: '',
+ loginPassword: '',
+ loginError: '',
+
+ async submitLogin() {
+ this.loginError = ''
+ try {
+ const res = await fetch('/auth', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ username: this.loginUsername,
+ password: this.loginPassword,
+ }),
+ })
+ const data = await res.json()
+ if (!res.ok || !data.success) {
+ this.loginError = data.error || 'Login failed'
+ return
+ }
+ sessionStorage.setItem('auth_token', data.token)
+ this.currentUser = data.username
+ this.loginPassword = ''
+ this.authed = true
+ await this.loadApps()
+ const bio = this.installedApps.find(a => a.id === 'bio')
+ if (bio) this.openApp(bio)
+ } catch (e) {
+ this.loginError = 'Auth service unreachable'
+ }
+ },
+
+ async checkAuth() {
+ const token = sessionStorage.getItem('auth_token')
+ if (!token) return
+
+ try {
+ const res = await fetch('/me', {
+ headers: { 'Authorization': `Bearer ${token}` },
+ })
+ if (res.ok) {
+ const data = await res.json()
+ this.currentUser = data.username
+ this.authed = true
+ } else {
+ sessionStorage.removeItem('auth_token')
+ }
+ } catch (_) {
+ /* auth service down — leave authed=false */
+ }
+ },
+
// ── Canvas pan state ──────────────────────────────────────────────────
offset: { x: 0, y: 0 },
isPanning: false,
submitQuery() {
const query = this.cursorQuery.trim().toLowerCase()
- // Built-in commands
- if (query === 'login') {
- this.openApp({ id: 'login', name: 'Login', url: '/login', icon: '🔐' })
- this.exitInputMode()
- return
- }
-
// Check if it matches an installed app name
const app = this.installedApps.find(a => a.name.toLowerCase() === query)
if (app) { this.openApp(app); this.exitInputMode(); return }
// ── Init ──────────────────────────────────────────────────────────────
async init() {
- await this.loadApps()
-
- // Refresh running status every 10s
- setInterval(() => this.loadApps(), 10_000)
-
- // Open bio full-screen on load
- const bio = this.installedApps.find(a => a.id === 'bio')
- if (bio) this.openApp(bio)
+ await this.checkAuth()
+
+ if (this.authed) {
+ await this.loadApps()
+ // Refresh running status every 10s
+ setInterval(() => this.loadApps(), 10_000)
+ // Open bio full-screen on load
+ const bio = this.installedApps.find(a => a.id === 'bio')
+ if (bio) this.openApp(bio)
+ }
}
}))
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+[x-cloak] { display: none !important; }
html, body {
width: 100%;
--- /dev/null
+#login-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 99999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(24px);
+ -webkit-backdrop-filter: blur(24px);
+}
+
+.login-card {
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 20px;
+ padding: 40px 48px;
+ width: 360px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ color: #fff;
+}
+
+.login-card h1 {
+ font-size: 1.4rem;
+ font-weight: 600;
+ text-align: center;
+ letter-spacing: -0.02em;
+}
+
+.login-card input {
+ width: 100%;
+ padding: 12px 16px;
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ background: rgba(255, 255, 255, 0.08);
+ color: #fff;
+ font-size: 0.95rem;
+ outline: none;
+ transition: border-color 0.15s;
+}
+
+.login-card input::placeholder {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.login-card input:focus {
+ border-color: rgba(255, 255, 255, 0.55);
+}
+
+.login-card button {
+ padding: 12px;
+ border-radius: 10px;
+ border: none;
+ background: rgba(255, 255, 255, 0.9);
+ color: #111;
+ font-size: 0.95rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.login-card button:hover {
+ background: #fff;
+}
+
+.login-error {
+ font-size: 0.85rem;
+ color: #ff6b6b;
+ text-align: center;
+ min-height: 1.2em;
+}
+
+#dock-user {
+ font-size: 0.75rem;
+ color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ padding: 0 8px;
+ cursor: default;
+ user-select: none;
+}