feat: password Realm, single-chair login, shared parliament context (0.9.12)
Derive Realm keys from a user password (Argon2id, no private key on disk). Login claims the sole chair with handover; peers share parliament context; --new-project clears it. CLI peer/discover helpers and Docker alpine recipe included; headless empty-answer retries raised for unattended runs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
#include "cst_parliament.h"
|
||||
#include "cJSON.h"
|
||||
#include "cst_util.h"
|
||||
#include "monocypher.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
int cst_parliament_path(char *out, size_t n) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
if (os_mkdir_p(dir, 0700) != 0)
|
||||
return -1;
|
||||
return os_path_join(out, n, dir, "parliament.json");
|
||||
}
|
||||
|
||||
static int context_path(char *out, size_t n) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
return os_path_join(out, n, dir, "parliament.context");
|
||||
}
|
||||
|
||||
int cst_parliament_load(cst_parliament_meta_t *m) {
|
||||
if (!m)
|
||||
return -1;
|
||||
memset(m, 0, sizeof(*m));
|
||||
char path[OS_PATH_MAX];
|
||||
if (cst_parliament_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
snprintf(m->status, sizeof(m->status), "%s", "idle");
|
||||
return 0;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
char chunk[4096];
|
||||
size_t n;
|
||||
while ((n = fread(chunk, 1, sizeof(chunk), f)) && b.len < 256 * 1024)
|
||||
cst_buf_append(&b, chunk, n);
|
||||
fclose(f);
|
||||
cJSON *o = cJSON_Parse(b.buf ? b.buf : "{}");
|
||||
cst_buf_free(&b);
|
||||
if (!o)
|
||||
return -1;
|
||||
const cJSON *v;
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "parliament_id");
|
||||
if (cJSON_IsString(v))
|
||||
snprintf(m->parliament_id, sizeof(m->parliament_id), "%s", v->valuestring);
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "project_id");
|
||||
if (cJSON_IsString(v))
|
||||
snprintf(m->project_id, sizeof(m->project_id), "%s", v->valuestring);
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "chair_instance");
|
||||
if (cJSON_IsString(v))
|
||||
snprintf(m->chair_instance, sizeof(m->chair_instance), "%s",
|
||||
v->valuestring);
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "chair_device");
|
||||
if (cJSON_IsString(v))
|
||||
snprintf(m->chair_device, sizeof(m->chair_device), "%s", v->valuestring);
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "revision");
|
||||
if (cJSON_IsNumber(v))
|
||||
m->revision = (unsigned long)v->valuedouble;
|
||||
v = cJSON_GetObjectItemCaseSensitive(o, "status");
|
||||
if (cJSON_IsString(v))
|
||||
snprintf(m->status, sizeof(m->status), "%s", v->valuestring);
|
||||
else
|
||||
snprintf(m->status, sizeof(m->status), "%s", "idle");
|
||||
cJSON_Delete(o);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_parliament_save(const cst_parliament_meta_t *m) {
|
||||
if (!m)
|
||||
return -1;
|
||||
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
|
||||
if (cst_parliament_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
||||
return -1;
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(o, "parliament_id", m->parliament_id);
|
||||
cJSON_AddStringToObject(o, "project_id", m->project_id);
|
||||
cJSON_AddStringToObject(o, "chair_instance", m->chair_instance);
|
||||
cJSON_AddStringToObject(o, "chair_device", m->chair_device);
|
||||
cJSON_AddNumberToObject(o, "revision", (double)m->revision);
|
||||
cJSON_AddStringToObject(o, "status", m->status);
|
||||
char *txt = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
if (!txt)
|
||||
return -1;
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
int ok = 0;
|
||||
if (f) {
|
||||
ok = fputs(txt, f) >= 0 && fputc('\n', f) >= 0;
|
||||
if (fclose(f) != 0)
|
||||
ok = 0;
|
||||
}
|
||||
free(txt);
|
||||
if (!ok) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
if (os_rename_replace(tmp, path) != 0) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *cst_parliament_device_context(void) {
|
||||
cst_buf_t b = {0};
|
||||
char host[128] = "unknown", cwd[OS_PATH_MAX] = ".";
|
||||
#ifdef _WIN32
|
||||
DWORD n = sizeof(host);
|
||||
GetComputerNameA(host, &n);
|
||||
_getcwd(cwd, (int)sizeof(cwd));
|
||||
cst_buf_printf(&b, "os=windows\nhost=%s\ncwd=%s\npid=%d\n", host, cwd,
|
||||
(int)GetCurrentProcessId());
|
||||
#else
|
||||
gethostname(host, sizeof(host));
|
||||
if (!getcwd(cwd, sizeof(cwd)))
|
||||
snprintf(cwd, sizeof(cwd), ".");
|
||||
cst_buf_printf(&b, "os=unix\nhost=%s\ncwd=%s\npid=%d\n", host, cwd,
|
||||
(int)getpid());
|
||||
#ifdef __linux__
|
||||
cst_buf_puts(&b, "platform=linux\n");
|
||||
#elif defined(__APPLE__)
|
||||
cst_buf_puts(&b, "platform=macos\n");
|
||||
#endif
|
||||
#endif
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
int cst_parliament_claim_chair(const char *instance_id,
|
||||
const char *device_label) {
|
||||
if (!instance_id || !instance_id[0])
|
||||
return -1;
|
||||
cst_parliament_meta_t m;
|
||||
if (cst_parliament_load(&m) != 0)
|
||||
return -1;
|
||||
if (!m.parliament_id[0]) {
|
||||
snprintf(m.parliament_id, sizeof(m.parliament_id), "parl-%ld",
|
||||
(long)time(NULL));
|
||||
snprintf(m.status, sizeof(m.status), "%s", "open");
|
||||
}
|
||||
/* Single chair: logging-in device always takes the chair (user will). */
|
||||
snprintf(m.chair_instance, sizeof(m.chair_instance), "%s", instance_id);
|
||||
snprintf(m.chair_device, sizeof(m.chair_device), "%s",
|
||||
device_label ? device_label : "");
|
||||
m.revision++;
|
||||
snprintf(m.status, sizeof(m.status), "%s", "open");
|
||||
return cst_parliament_save(&m);
|
||||
}
|
||||
|
||||
int cst_parliament_context_put(const char *text) {
|
||||
char path[OS_PATH_MAX], tmp[OS_PATH_MAX + 8];
|
||||
if (context_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
if (snprintf(tmp, sizeof(tmp), "%s.tmp", path) >= (int)sizeof(tmp))
|
||||
return -1;
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
if (!f)
|
||||
return -1;
|
||||
int ok = fputs(text ? text : "", f) >= 0;
|
||||
if (fclose(f) != 0)
|
||||
ok = 0;
|
||||
if (!ok) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
if (os_rename_replace(tmp, path) != 0) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
cst_parliament_meta_t m;
|
||||
if (cst_parliament_load(&m) == 0) {
|
||||
m.revision++;
|
||||
cst_parliament_save(&m);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *cst_parliament_context_get(void) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (context_path(path, sizeof(path)) != 0)
|
||||
return strdup("");
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return strdup("");
|
||||
cst_buf_t b = {0};
|
||||
char chunk[4096];
|
||||
size_t n;
|
||||
while ((n = fread(chunk, 1, sizeof(chunk), f)) && b.len < 2 * 1024 * 1024)
|
||||
cst_buf_append(&b, chunk, n);
|
||||
fclose(f);
|
||||
return b.buf ? cst_buf_steal(&b) : strdup("");
|
||||
}
|
||||
|
||||
int cst_parliament_new_project(const char *project_id) {
|
||||
cst_parliament_meta_t m;
|
||||
memset(&m, 0, sizeof(m));
|
||||
snprintf(m.parliament_id, sizeof(m.parliament_id), "parl-%ld",
|
||||
(long)time(NULL));
|
||||
snprintf(m.project_id, sizeof(m.project_id), "%s",
|
||||
project_id && project_id[0] ? project_id : "default");
|
||||
snprintf(m.status, sizeof(m.status), "%s", "open");
|
||||
m.revision = 1;
|
||||
/* Keep chair empty until next login claims it. */
|
||||
if (cst_parliament_save(&m) != 0)
|
||||
return -1;
|
||||
return cst_parliament_context_put("");
|
||||
}
|
||||
|
||||
char *cst_parliament_login(const char *password, const char *instance_id,
|
||||
const char *device_label, char *err, size_t err_sz) {
|
||||
if (err && err_sz)
|
||||
err[0] = 0;
|
||||
if (!password || !password[0] || !instance_id || !instance_id[0]) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "password and instance id required");
|
||||
return NULL;
|
||||
}
|
||||
char dir[OS_PATH_MAX];
|
||||
if (cst_realm_dir(dir, sizeof(dir)) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "realm dir missing");
|
||||
return NULL;
|
||||
}
|
||||
if (!cst_realm_is_password_mode(dir)) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "realm is not password mode; run --realm-password-init");
|
||||
return NULL;
|
||||
}
|
||||
cst_realm_keys_t keys;
|
||||
if (cst_realm_unlock_password(dir, password, &keys) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "login failed (bad password)");
|
||||
return NULL;
|
||||
}
|
||||
/* Ensure this device has an enrolled instance cert. */
|
||||
cst_realm_cert_t cert;
|
||||
uint8_t sk[64];
|
||||
if (cst_realm_instance_load(instance_id, &cert, sk) != 0) {
|
||||
uint8_t seed[32];
|
||||
if (cst_realm_enroll(&keys, instance_id, device_label, 0, &cert, seed, sk) !=
|
||||
0 ||
|
||||
cst_realm_instance_save(instance_id, &cert, seed) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "enroll instance failed");
|
||||
crypto_wipe(keys.seed, sizeof(keys.seed));
|
||||
crypto_wipe(keys.sk, sizeof(keys.sk));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
cst_parliament_meta_t before;
|
||||
cst_parliament_load(&before);
|
||||
char prev_chair[64];
|
||||
snprintf(prev_chair, sizeof(prev_chair), "%s", before.chair_instance);
|
||||
if (cst_parliament_claim_chair(instance_id, device_label) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "claim chair failed");
|
||||
crypto_wipe(keys.seed, sizeof(keys.seed));
|
||||
crypto_wipe(keys.sk, sizeof(keys.sk));
|
||||
return NULL;
|
||||
}
|
||||
char *ctx = cst_parliament_context_get();
|
||||
char *dev = cst_parliament_device_context();
|
||||
cst_buf_t hand = {0};
|
||||
cst_buf_puts(&hand, "LOGIN_OK\n");
|
||||
cst_buf_printf(&hand, "chair=%s\n", instance_id);
|
||||
if (prev_chair[0] && strcmp(prev_chair, instance_id) != 0)
|
||||
cst_buf_printf(&hand, "handover_from=%s\n", prev_chair);
|
||||
else
|
||||
cst_buf_puts(&hand, "handover_from=\n");
|
||||
cst_buf_printf(&hand, "device_context=\n%s", dev ? dev : "");
|
||||
cst_buf_puts(&hand, "--- parliament_context ---\n");
|
||||
cst_buf_puts(&hand, ctx ? ctx : "");
|
||||
free(ctx);
|
||||
free(dev);
|
||||
crypto_wipe(keys.seed, sizeof(keys.seed));
|
||||
crypto_wipe(keys.sk, sizeof(keys.sk));
|
||||
crypto_wipe(sk, sizeof(sk));
|
||||
return cst_buf_steal(&hand);
|
||||
}
|
||||
Reference in New Issue
Block a user