diff --git a/.claude/skills/README.md b/.claude/skills/README.md new file mode 100644 index 0000000..4e1c644 --- /dev/null +++ b/.claude/skills/README.md @@ -0,0 +1,21 @@ +# vault1984 Claude Code Skills + +Reusable workflows for Claude Code sessions. Invoke by name at the start of a session. + +## Available Skills + +*(none yet — add as patterns emerge)* + +## How to Use + +At the start of a Claude Code session: +``` +Use the skill: .claude/skills//SKILL.md +``` + +## Candidates (add when ready) + +- `add-pop` — how to add a new POP node (binary build, telemetry config, status DB entry) +- `fips-verify` — how to verify BoringCrypto is active in a build +- `deploy` — build + deploy procedure for app vs website +- `webauthn` — WebAuthn L2 implementation context diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..15e3232 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# vault1984 + +Zero-knowledge password manager. Infrastructure is the moat. FIPS 140-3, BoringCrypto, built for trust. + +## Ground Rules + +Johan is the architect. You are the collaborator. Same principles as inou: + +1. **Discussion first.** Default is conversation. No code until asked ("do it", "implement it"). +2. **Minimal diffs.** Change only what's requested. No drive-by cleanups. +3. **Less code, better architecture.** If something needs a lot of code, the design is probably wrong. +4. **Ask, don't assume.** Ambiguous request → ask. Don't pick an interpretation and run. +5. **No unsolicited files.** No new docs, tests, or helpers unless explicitly asked. +6. **Mention concerns once, then execute.** Johan has reasons. Respect them. + +## Architecture + +``` +app/ — vault1984 server (Go, FIPS 140-3) +cli/ — v1984 CLI client +crypto/ — crypto primitives (BoringCrypto) +website/ — vault1984.com marketing site +docs/ — design documentation +``` + +**Build:** Always use `GOEXPERIMENT=boringcrypto` (set in Makefile). Required for FIPS 140-3. + +```bash +make deploy # build + test + restart everything +make deploy-app # app only +make deploy-web # website only +make status # check what's running +``` + +## Environments + +| Environment | Host | Purpose | +|-------------|------|---------| +| HQ / NOC | noc.vault1984.com (185.218.204.47) | Hans runs this — Hans' domain | +| Forge (local) | 192.168.1.16 | Development | + +**SSH:** `root@185.218.204.47` (HQ/Hans), `ssh johan@192.168.1.16` (forge) + +## Security Non-Negotiables + +- **FIPS 140-3** via `GOEXPERIMENT=boringcrypto` — never build without it +- **Zero-knowledge** — server never sees plaintext credentials +- **WebAuthn L2** — hardware key support (in progress) +- No logging of credential content, ever + +## Current Status (Mar 2026) + +- Binary builds: amd64 + arm64, telemetry flag support +- POP nodes: HQ (Zürich), Virginia (us-east-1), Singapore (ap-southeast-1) +- Telemetry: binary supports `--telemetry-*` flags; HQ dashboard `/telemetry` handler pending +- WebAuthn L2: in progress +- Permanent VAULT_KEY handling: pending + +## Data Access Architecture + +All DB operations go through named functions in `app/lib/dbcore.go`. **No direct SQL outside dbcore.go.** + +Choke points: +- `EntryCreate/Get/Update/Delete/List/Search` — all credential entry operations +- `SessionCreate/Get/Delete` — session management +- `AuditLog` — every security event goes here, no exceptions + +**FORBIDDEN outside dbcore.go:** +- `db.QueryRow()`, `db.Exec()`, `db.Query()` — direct SQL is a violation (one exception: `telemetry.go` — isolated, non-security code) +- New wrapper functions that bypass the named choke points +- Any modification to `dbcore.go` without Johan's explicit approval + +**Encryption:** All credential fields are encrypted with the vault key via Pack/Unpack in dbcore.go. This is the ONLY encryption path. Never encrypt/decrypt fields outside of it. + +## Key Files + +- `L2_AGENT_ENCRYPTION.md` — WebAuthn L2 encryption spec +- `docs/` — architecture docs +- `app/cmd/vault1984` — main entry point diff --git a/cli/build/src/keystore.o b/cli/build/src/keystore.o index 677df93..0aacd3d 100644 Binary files a/cli/build/src/keystore.o and b/cli/build/src/keystore.o differ diff --git a/cli/build/src/main.o b/cli/build/src/main.o index a8c62bd..6340b5a 100644 Binary files a/cli/build/src/main.o and b/cli/build/src/main.o differ diff --git a/cli/src/keystore.c b/cli/src/keystore.c index 225a815..48ee4c2 100644 --- a/cli/src/keystore.c +++ b/cli/src/keystore.c @@ -1,10 +1,16 @@ /* - * vault1984 CLI — config and key storage + * v1984 CLI — config and key storage * - * Files stored in ~/.vault1984/ with strict permissions: - * config — key=value (vault_url, username) - * api_key — API authentication key - * l2.key — 16-byte L2 encryption key (binary) + * Config file format (binary): + * [4 bytes] magic "V19\x01" + * [16 bytes] HMAC-SHA256 signature (truncated) + * [12 bytes] AES-GCM nonce + * [N bytes] AES-128-GCM ciphertext (encrypted JSON config) + * [16 bytes] AES-GCM tag (appended by GCM) + * + * Encryption key derived from a constant embedded in the binary. + * This is an inconvenience barrier — prevents casual `cat` and + * copy-paste exfiltration. Real security is vault-side. */ #define _POSIX_C_SOURCE 200809L @@ -18,15 +24,49 @@ #include #include +#include "bearssl.h" + #ifdef _WIN32 #include -#include -#define mkdir_700(p) _mkdir(p) +#define mkdir_p(p) _mkdir(p) #else #include -#define mkdir_700(p) mkdir(p, 0700) +#define mkdir_p(p) mkdir(p, 0755) #endif +/* Config encryption key — derived from a string that also appears in the web UI */ +static const char CONFIG_SEED[] = "vault1984-config-v1"; +static const unsigned char CONFIG_MAGIC[4] = { 'V', '1', '9', 0x01 }; + +/* Derive 16-byte config encryption key from seed */ +static void derive_config_key(unsigned char out[16]) { + br_hkdf_context hkdf; + br_hkdf_init(&hkdf, &br_sha256_vtable, NULL, 0); + br_hkdf_inject(&hkdf, (const unsigned char *)CONFIG_SEED, sizeof(CONFIG_SEED) - 1); + br_hkdf_flip(&hkdf); + br_hkdf_produce(&hkdf, (const unsigned char *)"key", 3, out, 16); +} + +/* Derive 16-byte HMAC key from seed */ +static void derive_hmac_key(unsigned char out[16]) { + br_hkdf_context hkdf; + br_hkdf_init(&hkdf, &br_sha256_vtable, NULL, 0); + br_hkdf_inject(&hkdf, (const unsigned char *)CONFIG_SEED, sizeof(CONFIG_SEED) - 1); + br_hkdf_flip(&hkdf); + br_hkdf_produce(&hkdf, (const unsigned char *)"hmac", 4, out, 16); +} + +/* Compute HMAC-SHA256 signature over data, truncated to 16 bytes */ +static void compute_sig(const unsigned char *hmac_key, const unsigned char *data, + size_t data_len, unsigned char out[16]) { + br_hmac_key_context kc; + br_hmac_key_init(&kc, &br_sha256_vtable, hmac_key, 16); + br_hmac_context hc; + br_hmac_init(&hc, &kc, 16); + br_hmac_update(&hc, data, data_len); + br_hmac_out(&hc, out); +} + static int get_config_dir(char *buf, size_t len) { const char *home = getenv("HOME"); if (!home) home = getenv("USERPROFILE"); @@ -34,75 +74,113 @@ static int get_config_dir(char *buf, size_t len) { fprintf(stderr, "error: cannot determine home directory\n"); return -1; } - snprintf(buf, len, "%s/.vault1984", home); + snprintf(buf, len, "%s/.config/v1984", home); return 0; } static int ensure_dir(const char *path) { struct stat st; - if (stat(path, &st) == 0) { - if (S_ISDIR(st.st_mode)) return 0; - fprintf(stderr, "error: %s exists but is not a directory\n", path); - return -1; + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) return 0; + + /* Create parent (.config) if needed */ + char parent[512]; + snprintf(parent, sizeof(parent), "%s", path); + char *slash = strrchr(parent, '/'); + if (slash) { + *slash = '\0'; + struct stat pst; + if (stat(parent, &pst) != 0) mkdir_p(parent); } - if (mkdir_700(path) != 0) { + + if (mkdir_p(path) != 0 && errno != EEXIST) { fprintf(stderr, "error: cannot create %s: %s\n", path, strerror(errno)); return -1; } return 0; } -static int write_file(const char *path, const void *data, size_t len) { - FILE *f = fopen(path, "wb"); - if (!f) { - fprintf(stderr, "error: cannot write %s: %s\n", path, strerror(errno)); - return -1; +int keystore_init(const char *vault_url, const char *agent_name, + const unsigned char *l2_key, size_t l2_key_len) { + if (l2_key_len != 16) { + fprintf(stderr, "error: L2 key must be exactly 16 bytes\n"); + return 1; } - fwrite(data, 1, len, f); - fclose(f); -#ifndef _WIN32 - chmod(path, 0600); -#endif - return 0; -} -static int read_file(const char *path, char *buf, size_t len) { - FILE *f = fopen(path, "rb"); - if (!f) return -1; - size_t n = fread(buf, 1, len - 1, f); - fclose(f); - buf[n] = '\0'; - while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r')) buf[--n] = '\0'; - return 0; -} - -int keystore_init(const char *vault_url, const char *username, const char *api_key) { char dir[512]; if (get_config_dir(dir, sizeof(dir)) != 0) return 1; if (ensure_dir(dir) != 0) return 1; - /* Strip trailing slash from vault_url */ - char clean_url[512]; - snprintf(clean_url, sizeof(clean_url), "%s", vault_url); - size_t ulen = strlen(clean_url); - while (ulen > 0 && clean_url[ulen - 1] == '/') clean_url[--ulen] = '\0'; + /* Build JSON config */ + char json[1024]; + char l2_b64[32]; + base64_encode(l2_key, 16, l2_b64, sizeof(l2_b64)); - /* Write config */ + int json_len = snprintf(json, sizeof(json), + "{\"vault_url\":\"%s\",\"agent_name\":\"%s\",\"l2_key\":\"%s\"}", + vault_url, agent_name, l2_b64); + + /* Derive keys */ + unsigned char enc_key[16], hmac_key[16]; + derive_config_key(enc_key); + derive_hmac_key(hmac_key); + + /* Generate nonce */ + unsigned char nonce[12]; + br_hmac_drbg_context drbg; + br_hmac_drbg_init(&drbg, &br_sha256_vtable, "v1984-init", 10); + br_prng_seeder seeder = br_prng_seeder_system(NULL); + if (seeder) seeder(&drbg.vtable); + br_hmac_drbg_generate(&drbg, nonce, 12); + + /* Encrypt: AES-128-GCM */ + size_t ct_len = (size_t)json_len + 16; /* ciphertext + tag */ + unsigned char *ct = malloc(ct_len); + memcpy(ct, json, (size_t)json_len); + + br_aes_ct64_ctr_keys aes_ctx; + br_aes_ct64_ctr_init(&aes_ctx, enc_key, 16); + br_gcm_context gcm; + br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64); + br_gcm_reset(&gcm, nonce, 12); + br_gcm_flip(&gcm); + br_gcm_run(&gcm, 1, ct, (size_t)json_len); + br_gcm_get_tag(&gcm, ct + json_len); + + /* Build file: magic + sig + nonce + ciphertext+tag */ + size_t file_len = 4 + 16 + 12 + ct_len; + unsigned char *file_data = malloc(file_len); + memcpy(file_data, CONFIG_MAGIC, 4); + /* sig placeholder — filled after */ + memcpy(file_data + 4 + 16, nonce, 12); + memcpy(file_data + 4 + 16 + 12, ct, ct_len); + + /* Compute HMAC over nonce + ciphertext (everything after sig) */ + compute_sig(hmac_key, file_data + 4 + 16, 12 + ct_len, file_data + 4); + + /* Write file */ char path[512]; - char config_data[1024]; - snprintf(config_data, sizeof(config_data), - "vault_url=%s\nusername=%s\n", clean_url, username); snprintf(path, sizeof(path), "%s/config", dir); - if (write_file(path, config_data, strlen(config_data)) != 0) return 1; + FILE *f = fopen(path, "wb"); + if (!f) { + fprintf(stderr, "error: cannot write %s: %s\n", path, strerror(errno)); + free(ct); free(file_data); + return 1; + } + fwrite(file_data, 1, file_len, f); + fclose(f); - /* Write API key */ - snprintf(path, sizeof(path), "%s/api_key", dir); - if (write_file(path, api_key, strlen(api_key)) != 0) return 1; + free(ct); + free(file_data); + + /* Clear sensitive data from stack */ + memset(json, 0, sizeof(json)); + memset(enc_key, 0, 16); + memset(hmac_key, 0, 16); fprintf(stderr, "v1984: initialized\n"); - fprintf(stderr, " vault: %s\n", clean_url); - fprintf(stderr, " user: %s\n", username); - fprintf(stderr, " config: %s/\n", dir); + fprintf(stderr, " vault: %s\n", vault_url); + fprintf(stderr, " agent: %s\n", agent_name); + fprintf(stderr, " config: %s/config\n", dir); return 0; } @@ -112,50 +190,127 @@ int keystore_load(struct v84_config *cfg) { char dir[512]; if (get_config_dir(dir, sizeof(dir)) != 0) return -1; - /* Read config */ char path[512]; - char config_data[1024]; snprintf(path, sizeof(path), "%s/config", dir); - if (read_file(path, config_data, sizeof(config_data)) != 0) { - fprintf(stderr, "error: not initialized. Run: v1984 init --vault --user --key \n"); - return -1; - } - - /* Parse key=value lines */ - char *line = strtok(config_data, "\n"); - while (line) { - char *eq = strchr(line, '='); - if (eq) { - *eq = '\0'; - const char *key = line; - const char *val = eq + 1; - if (strcmp(key, "vault_url") == 0) { - snprintf(cfg->vault_url, sizeof(cfg->vault_url), "%s", val); - } else if (strcmp(key, "username") == 0) { - snprintf(cfg->username, sizeof(cfg->username), "%s", val); - } - } - line = strtok(NULL, "\n"); - } - - /* Read API key */ - snprintf(path, sizeof(path), "%s/api_key", dir); - if (read_file(path, cfg->api_key, sizeof(cfg->api_key)) != 0) { - fprintf(stderr, "error: api_key missing. Run: v1984 init --vault --user --key \n"); - return -1; - } - - /* Read L2 key if present */ - snprintf(path, sizeof(path), "%s/l2.key", dir); FILE *f = fopen(path, "rb"); - if (f) { - size_t n = fread(cfg->l2_key, 1, 16, f); - fclose(f); - cfg->has_l2_key = (n == 16); + if (!f) { + fprintf(stderr, "error: not initialized. Run: v1984 init\n"); + return -1; } + fseek(f, 0, SEEK_END); + long file_len = ftell(f); + fseek(f, 0, SEEK_SET); + + if (file_len < 4 + 16 + 12 + 16) { /* magic + sig + nonce + min ciphertext */ + fprintf(stderr, "error: config file corrupt\n"); + fclose(f); + return -1; + } + + unsigned char *file_data = malloc((size_t)file_len); + fread(file_data, 1, (size_t)file_len, f); + fclose(f); + + /* Check magic */ + if (memcmp(file_data, CONFIG_MAGIC, 4) != 0) { + fprintf(stderr, "error: config file corrupt (bad magic)\n"); + free(file_data); + return -1; + } + + /* Derive keys */ + unsigned char enc_key[16], hmac_key[16]; + derive_config_key(enc_key); + derive_hmac_key(hmac_key); + + /* Verify HMAC */ + unsigned char expected_sig[16]; + size_t payload_len = (size_t)file_len - 4 - 16; /* nonce + ct+tag */ + compute_sig(hmac_key, file_data + 4 + 16, payload_len, expected_sig); + + if (memcmp(file_data + 4, expected_sig, 16) != 0) { + fprintf(stderr, "error: config file tampered\n"); + free(file_data); + memset(enc_key, 0, 16); + memset(hmac_key, 0, 16); + return -1; + } + + /* Decrypt */ + unsigned char *nonce = file_data + 4 + 16; + unsigned char *ct = file_data + 4 + 16 + 12; + size_t ct_len = (size_t)file_len - 4 - 16 - 12; + size_t pt_len = ct_len - 16; + + unsigned char *pt = malloc(ct_len); + memcpy(pt, ct, ct_len); + + br_aes_ct64_ctr_keys aes_ctx; + br_aes_ct64_ctr_init(&aes_ctx, enc_key, 16); + br_gcm_context gcm; + br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64); + br_gcm_reset(&gcm, nonce, 12); + br_gcm_flip(&gcm); + br_gcm_run(&gcm, 0, pt, pt_len); + + if (!br_gcm_check_tag(&gcm, pt + pt_len)) { + fprintf(stderr, "error: config decryption failed\n"); + free(file_data); free(pt); + memset(enc_key, 0, 16); + return -1; + } + pt[pt_len] = '\0'; + + free(file_data); + memset(enc_key, 0, 16); + memset(hmac_key, 0, 16); + + /* Parse JSON manually (avoid cJSON dependency in keystore) */ + /* Extract vault_url */ + char *p = strstr((char *)pt, "\"vault_url\":\""); + if (p) { + p += 13; + char *end = strchr(p, '"'); + if (end) { + size_t len = (size_t)(end - p); + if (len >= sizeof(cfg->vault_url)) len = sizeof(cfg->vault_url) - 1; + memcpy(cfg->vault_url, p, len); + cfg->vault_url[len] = '\0'; + } + } + + /* Extract agent_name */ + p = strstr((char *)pt, "\"agent_name\":\""); + if (p) { + p += 14; + char *end = strchr(p, '"'); + if (end) { + size_t len = (size_t)(end - p); + if (len >= sizeof(cfg->agent_name)) len = sizeof(cfg->agent_name) - 1; + memcpy(cfg->agent_name, p, len); + cfg->agent_name[len] = '\0'; + } + } + + /* Extract l2_key (base64 → binary) */ + p = strstr((char *)pt, "\"l2_key\":\""); + if (p) { + p += 10; + char *end = strchr(p, '"'); + if (end) { + *end = '\0'; + size_t key_len = 0; + base64_decode(p, cfg->l2_key, 16, &key_len); + } + } + + /* Clear plaintext from memory */ + memset(pt, 0, pt_len); + free(pt); + if (!cfg->vault_url[0]) { - fprintf(stderr, "error: config missing vault_url. Run: v1984 init --vault --user --key \n"); + fprintf(stderr, "error: config missing vault_url\n"); return -1; } diff --git a/cli/src/keystore.h b/cli/src/keystore.h index d5844a7..12afbfb 100644 --- a/cli/src/keystore.h +++ b/cli/src/keystore.h @@ -1,22 +1,28 @@ /* - * vault1984 CLI — config and key storage + * v1984 CLI — config and key storage + * + * Config is encrypted with a static key (inconvenience barrier) + * and signed with HMAC (tamper detection). Not security theater — + * the real protection is vault-side rate limiting, IP whitelisting, + * and lockout. */ #ifndef V84_KEYSTORE_H #define V84_KEYSTORE_H +#include + struct v84_config { - char vault_url[512]; /* e.g. https://use.vault1984.com:1984 */ - char username[256]; /* e.g. johan@jongsma.me */ - char api_key[256]; /* API auth key (base64) */ - unsigned char l2_key[16]; /* L2 encryption key (16 bytes, derived) */ - int has_l2_key; + char vault_url[512]; /* e.g. https://use.vault1984.com:1984 */ + char agent_name[128]; /* e.g. claude-code-forge */ + unsigned char l2_key[16]; /* L2 encryption key (16 bytes) */ }; -/* Initialize config from provided values, write to ~/.vault1984/ */ -int keystore_init(const char *vault_url, const char *username, const char *api_key); +/* Initialize config: encrypt and write to ~/.config/v1984/config */ +int keystore_init(const char *vault_url, const char *agent_name, + const unsigned char *l2_key, size_t l2_key_len); -/* Load config from ~/.vault1984/ */ +/* Load config: read, verify signature, decrypt */ int keystore_load(struct v84_config *cfg); #endif diff --git a/cli/src/main.c b/cli/src/main.c index 3ea6032..1e17e15 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -1,5 +1,5 @@ /* - * vault1984 CLI — credential access for AI agents + * v1984 CLI — credential access for AI agents * Copyright (c) 2026 Vault1984. Elastic License 2.0. */ @@ -21,8 +21,10 @@ static void usage(void) { fprintf(stderr, "v1984 %s — credential access for AI agents\n" "\n" - "Usage:\n" - " v1984 init --vault --user --key \n" + "Setup:\n" + " v1984 config --vault --agent --l2 \n" + "\n" + "Commands:\n" " v1984 get [--json]\n" " v1984 list [filter] [--json]\n" " v1984 totp \n" @@ -34,441 +36,70 @@ static void usage(void) { " --version Show version\n" " --json Output as JSON\n" "\n" - "Examples:\n" - " v1984 init --vault https://use.vault1984.com:1984 --user johan@jongsma.me --key ABCDEF...\n" - " v1984 get twitter.com\n" - " v1984 totp twitter.com\n" - " v1984 list\n" - " v1984 test-totp JBSWY3DPEHPK3PXP\n", + "The config command stores vault URL, agent name, and L2 key\n" + "in an encrypted local config file. Run once per agent.\n", VERSION); } -/* --- URL builder --- */ +/* --- URL + auth helpers --- */ static void build_url(char *buf, size_t len, const struct v84_config *cfg, const char *path) { snprintf(buf, len, "%s%s", cfg->vault_url, path); } -/* --- command handlers --- */ - -static int cmd_init(int argc, char **argv) { - const char *vault = NULL; - const char *user = NULL; - const char *key = NULL; - - for (int i = 0; i < argc; i++) { - if (strcmp(argv[i], "--vault") == 0 && i + 1 < argc) { - vault = argv[++i]; - } else if (strcmp(argv[i], "--user") == 0 && i + 1 < argc) { - user = argv[++i]; - } else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) { - key = argv[++i]; - } - } - - if (!vault || !user || !key) { - fprintf(stderr, "error: --vault, --user, and --key are required\n"); - fprintf(stderr, "usage: v1984 init --vault --user --key \n"); - return 1; - } - - return keystore_init(vault, user, key); +static void get_bearer(const struct v84_config *cfg, char *buf, size_t len) { + base64_encode(cfg->l2_key, 16, buf, len); } -static int cmd_list(int argc, char **argv) { - int json_output = 0; - const char *filter = NULL; - - for (int i = 0; i < argc; i++) { - if (strcmp(argv[i], "--json") == 0) { - json_output = 1; - } else if (argv[i][0] != '-') { - filter = argv[i]; - } - } - - struct v84_config cfg; - if (keystore_load(&cfg) != 0) return 1; - - char url[1024]; - if (filter) { - char encoded_filter[512]; - char path[768]; - url_encode(filter, encoded_filter, sizeof(encoded_filter)); - snprintf(path, sizeof(path), "/api/search?q=%s", encoded_filter); - build_url(url, sizeof(url), &cfg, path); - } else { - build_url(url, sizeof(url), &cfg, "/api/entries"); - } - - struct v84_response resp; - if (http_get(url, cfg.api_key, &resp) != 0) { - fprintf(stderr, "error: request failed\n"); - return 1; - } - - if (resp.status != 200) { - fprintf(stderr, "error: server returned %d\n", resp.status); - free(resp.body); - return 1; - } - - if (json_output) { - printf("%s\n", resp.body); - free(resp.body); - return 0; - } - - /* parse and format */ - cJSON *root = cJSON_Parse(resp.body); - free(resp.body); - if (!root) { - fprintf(stderr, "error: invalid JSON response\n"); - return 1; - } - - /* handle both array (entries list) and object with entries key */ - cJSON *entries = root; - if (!cJSON_IsArray(root)) { - entries = cJSON_GetObjectItem(root, "entries"); - if (!entries) entries = root; - } - - cJSON *entry; - cJSON_ArrayForEach(entry, entries) { - const char *eid = cJSON_GetStringValue(cJSON_GetObjectItem(entry, "entry_id")); - const char *type = cJSON_GetStringValue(cJSON_GetObjectItem(entry, "type")); - const char *title = cJSON_GetStringValue(cJSON_GetObjectItem(entry, "title")); - printf("%s\t%s\t%s\n", - eid ? eid : "?", - type ? type : "?", - title ? title : "?"); - } - - cJSON_Delete(root); - return 0; -} - -static int cmd_get(int argc, char **argv) { - int json_output = 0; - const char *query = NULL; - - for (int i = 0; i < argc; i++) { - if (strcmp(argv[i], "--json") == 0) { - json_output = 1; - } else if (argv[i][0] != '-') { - query = argv[i]; - } - } - - if (!query) { - fprintf(stderr, "error: query required\n"); - fprintf(stderr, "usage: vault1984 get [--json]\n"); - return 1; - } - - struct v84_config cfg; - if (keystore_load(&cfg) != 0) return 1; - - /* search first */ - char url[1024]; - char encoded_query[512]; - char path[768]; - url_encode(query, encoded_query, sizeof(encoded_query)); - snprintf(path, sizeof(path), "/api/search?q=%s", encoded_query); - build_url(url, sizeof(url), &cfg, path); - - struct v84_response resp; - if (http_get(url, cfg.api_key, &resp) != 0) { - fprintf(stderr, "error: search request failed\n"); - return 1; - } - - if (resp.status != 200) { - fprintf(stderr, "error: server returned %d\n", resp.status); - free(resp.body); - return 1; - } - - cJSON *results = cJSON_Parse(resp.body); - free(resp.body); - if (!results) { - fprintf(stderr, "error: invalid JSON\n"); - return 1; - } - - /* handle array or object wrapper */ - cJSON *arr = results; - if (!cJSON_IsArray(results)) { - arr = cJSON_GetObjectItem(results, "entries"); - if (!arr) arr = results; - } - - cJSON *first = cJSON_GetArrayItem(arr, 0); - if (!first) { - fprintf(stderr, "error: no results for '%s'\n", query); - cJSON_Delete(results); - return 1; - } - - const char *entry_id = cJSON_GetStringValue(cJSON_GetObjectItem(first, "entry_id")); - if (!entry_id) { - fprintf(stderr, "error: result missing entry_id\n"); - cJSON_Delete(results); - return 1; - } - - /* fetch full entry */ - snprintf(path, sizeof(path), "/api/entries/%s", entry_id); - build_url(url, sizeof(url), &cfg, path); - cJSON_Delete(results); - - if (http_get(url, cfg.api_key, &resp) != 0) { - fprintf(stderr, "error: fetch request failed\n"); - return 1; - } - - if (resp.status != 200) { - fprintf(stderr, "error: server returned %d\n", resp.status); - free(resp.body); - return 1; - } - - if (json_output) { - printf("%s\n", resp.body); - free(resp.body); - return 0; - } - - cJSON *entry = cJSON_Parse(resp.body); - free(resp.body); - if (!entry) { - fprintf(stderr, "error: invalid JSON\n"); - return 1; - } - - /* print fields from vault_data */ - cJSON *data = cJSON_GetObjectItem(entry, "data"); - if (!data) data = cJSON_GetObjectItem(entry, "vault_data"); - if (data) { - cJSON *fields = cJSON_GetObjectItem(data, "fields"); - cJSON *field; - cJSON_ArrayForEach(field, fields) { - const char *label = cJSON_GetStringValue(cJSON_GetObjectItem(field, "label")); - const char *value = cJSON_GetStringValue(cJSON_GetObjectItem(field, "value")); - cJSON *tier = cJSON_GetObjectItem(field, "tier"); - int tier_val = tier ? tier->valueint : 1; - - if (!label) continue; - - if (tier_val >= 3) { - printf("%s: [L3 -- requires hardware key]\n", label); - } else if (tier_val == 2 && (!value || strlen(value) == 0)) { - printf("%s: [L2 -- encrypted]\n", label); - } else { - printf("%s: %s\n", label, value ? value : ""); - } - } - - /* also print URLs if present */ - cJSON *urls = cJSON_GetObjectItem(data, "urls"); - if (urls && cJSON_GetArraySize(urls) > 0) { - cJSON *u; - cJSON_ArrayForEach(u, urls) { - const char *uv = cJSON_GetStringValue(u); - if (uv) printf("url: %s\n", uv); - } - } - - /* notes */ - const char *notes = cJSON_GetStringValue(cJSON_GetObjectItem(data, "notes")); - if (notes && strlen(notes) > 0) { - printf("notes: %s\n", notes); - } - } - - cJSON_Delete(entry); - return 0; -} - -static int cmd_totp(int argc, char **argv) { - const char *query = NULL; - - for (int i = 0; i < argc; i++) { - if (argv[i][0] != '-') { - query = argv[i]; - } - } - - if (!query) { - fprintf(stderr, "error: query required\n"); - fprintf(stderr, "usage: vault1984 totp \n"); - return 1; - } - - struct v84_config cfg; - if (keystore_load(&cfg) != 0) return 1; - - /* search for the entry first */ - char url[1024]; - char encoded_query[512]; - char path[768]; - url_encode(query, encoded_query, sizeof(encoded_query)); - snprintf(path, sizeof(path), "/api/search?q=%s", encoded_query); - build_url(url, sizeof(url), &cfg, path); - - struct v84_response resp; - if (http_get(url, cfg.api_key, &resp) != 0) { - fprintf(stderr, "error: search request failed\n"); - return 1; - } - - if (resp.status != 200) { - fprintf(stderr, "error: server returned %d\n", resp.status); - free(resp.body); - return 1; - } - - cJSON *results = cJSON_Parse(resp.body); - free(resp.body); - if (!results) { - fprintf(stderr, "error: invalid JSON\n"); - return 1; - } - - cJSON *arr = results; - if (!cJSON_IsArray(results)) { - arr = cJSON_GetObjectItem(results, "entries"); - if (!arr) arr = results; - } - - cJSON *first = cJSON_GetArrayItem(arr, 0); - if (!first) { - fprintf(stderr, "error: no results for '%s'\n", query); - cJSON_Delete(results); - return 1; - } - - const char *entry_id = cJSON_GetStringValue(cJSON_GetObjectItem(first, "entry_id")); - if (!entry_id) { - fprintf(stderr, "error: result missing entry_id\n"); - cJSON_Delete(results); - return 1; - } - - /* fetch TOTP code */ - snprintf(path, sizeof(path), "/api/ext/totp/%s", entry_id); - build_url(url, sizeof(url), &cfg, path); - cJSON_Delete(results); - - if (http_get(url, cfg.api_key, &resp) != 0) { - fprintf(stderr, "error: TOTP request failed\n"); - return 1; - } - - if (resp.status != 200) { - fprintf(stderr, "error: server returned %d\n", resp.status); - free(resp.body); - return 1; - } - - cJSON *totp = cJSON_Parse(resp.body); - free(resp.body); - if (!totp) { - fprintf(stderr, "error: invalid JSON\n"); - return 1; - } - - /* check if L2/L3 locked */ - cJSON *l2 = cJSON_GetObjectItem(totp, "l2"); - if (l2 && cJSON_IsTrue(l2)) { - fprintf(stderr, "error: TOTP seed is L3-locked (requires hardware key)\n"); - cJSON_Delete(totp); - return 1; - } - - const char *code = cJSON_GetStringValue(cJSON_GetObjectItem(totp, "code")); - if (!code) { - fprintf(stderr, "error: no TOTP code in response\n"); - cJSON_Delete(totp); - return 1; - } - - printf("%s\n", code); - cJSON_Delete(totp); - return 0; -} +/* --- test commands --- */ static int cmd_test_crypto(void) { - fprintf(stderr, "vault1984: crypto self-test\n"); + fprintf(stderr, "v1984: crypto self-test\n"); - /* First: test BearSSL primitives directly in C */ + /* BearSSL AES-128-GCM roundtrip */ { fprintf(stderr, " [C] AES-128-GCM roundtrip... "); unsigned char key[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; unsigned char nonce[12] = {0}; unsigned char pt[] = "hello world test"; - unsigned char buf[sizeof(pt) + 16]; /* pt + tag */ + unsigned char buf[sizeof(pt) + 16]; unsigned char tag[16]; - memcpy(buf, pt, sizeof(pt)); br_aes_ct64_ctr_keys aes_ctx; br_aes_ct64_ctr_init(&aes_ctx, key, 16); - br_gcm_context gcm; br_gcm_init(&gcm, &aes_ctx.vtable, br_ghash_ctmul64); br_gcm_reset(&gcm, nonce, 12); br_gcm_flip(&gcm); - br_gcm_run(&gcm, 1, buf, sizeof(pt)); /* encrypt */ + br_gcm_run(&gcm, 1, buf, sizeof(pt)); br_gcm_get_tag(&gcm, tag); - /* decrypt */ br_gcm_reset(&gcm, nonce, 12); br_gcm_flip(&gcm); - br_gcm_run(&gcm, 0, buf, sizeof(pt)); /* decrypt */ - - if (!br_gcm_check_tag(&gcm, tag)) { - fprintf(stderr, "FAIL (tag)\n"); - return 1; - } - if (memcmp(buf, pt, sizeof(pt)) != 0) { - fprintf(stderr, "FAIL (data)\n"); - return 1; + br_gcm_run(&gcm, 0, buf, sizeof(pt)); + if (!br_gcm_check_tag(&gcm, tag) || memcmp(buf, pt, sizeof(pt)) != 0) { + fprintf(stderr, "FAIL\n"); return 1; } fprintf(stderr, "OK\n"); } - /* Init QuickJS + load crypto.js */ - if (jsbridge_init() != 0) { - fprintf(stderr, "FAIL: jsbridge_init\n"); - return 1; - } + if (jsbridge_init() != 0) { fprintf(stderr, "FAIL: jsbridge_init\n"); return 1; } - /* Test L2 encrypt → decrypt roundtrip */ - unsigned char test_key[16] = { - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 - }; - const char *entry_id = "abcdef0123456789"; - const char *label = "password"; - const char *plaintext = "s3cret-p@ssw0rd!"; + /* JS crypto tests */ + unsigned char test_key[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; - /* Test base64 roundtrip via JS */ - { + { /* base64 roundtrip */ char *r = jsbridge_eval( "var orig = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);" "var b64 = native_base64_encode(orig);" "var back = native_base64_decode(b64);" "var ok = (orig.length === back.length);" "for (var i=0; i\n\n"); - fprintf(stderr, "example: v1984 test-totp JBSWY3DPEHPK3PXP\n"); - fprintf(stderr, " 482901 expires in 17s\n"); + fprintf(stderr, "usage: v1984 test-totp \n"); return 1; } if (jsbridge_init() != 0) { fprintf(stderr, "error: crypto init failed\n"); return 1; } char *code = jsbridge_totp(argv[2]); if (code) { - long now = (long)time(NULL); - int remaining = 30 - (int)(now % 30); + int remaining = 30 - (int)(time(NULL) % 30); printf("%s expires in %ds\n", code, remaining); free(code); - } - else { fprintf(stderr, "error: TOTP generation failed\n"); } + } else { fprintf(stderr, "error: TOTP failed\n"); } jsbridge_cleanup(); return code ? 0 : 1; } + /* Config command — one-time setup */ + if (strcmp(cmd, "config") == 0) { + const char *vault = NULL, *agent = NULL, *l2_b64 = NULL; + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--vault") == 0 && i + 1 < argc) vault = argv[++i]; + else if (strcmp(argv[i], "--agent") == 0 && i + 1 < argc) agent = argv[++i]; + else if (strcmp(argv[i], "--l2") == 0 && i + 1 < argc) l2_b64 = argv[++i]; + } + if (!vault || !agent || !l2_b64) { + fprintf(stderr, "usage: v1984 config --vault --agent --l2 \n"); + return 1; + } + unsigned char l2_key[32]; + size_t l2_len = 0; + if (base64_decode(l2_b64, l2_key, sizeof(l2_key), &l2_len) != 0 || l2_len != 16) { + fprintf(stderr, "error: L2 key must be 16 bytes (24 base64 chars)\n"); + return 1; + } + int ret = keystore_init(vault, agent, l2_key, l2_len); + memset(l2_key, 0, sizeof(l2_key)); + return ret; + } + + /* Commands that need config */ + struct v84_config cfg; + if (keystore_load(&cfg) != 0) return 1; + char bearer[32]; + get_bearer(&cfg, bearer, sizeof(bearer)); + + int json_output = 0; + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--json") == 0) json_output = 1; + } + + if (strcmp(cmd, "list") == 0) { + const char *filter = NULL; + for (int i = 2; i < argc; i++) { + if (argv[i][0] != '-') { filter = argv[i]; break; } + } + + char url[1024]; + if (filter) { + char encoded[512], path[768]; + url_encode(filter, encoded, sizeof(encoded)); + snprintf(path, sizeof(path), "/api/search?q=%s", encoded); + build_url(url, sizeof(url), &cfg, path); + } else { + build_url(url, sizeof(url), &cfg, "/api/entries"); + } + + struct v84_response resp; + if (http_get(url, bearer, &resp) != 0) { fprintf(stderr, "error: request failed\n"); return 1; } + if (resp.status != 200) { fprintf(stderr, "error: server returned %d\n", resp.status); free(resp.body); return 1; } + if (json_output) { printf("%s\n", resp.body); free(resp.body); return 0; } + + cJSON *root = cJSON_Parse(resp.body); free(resp.body); + if (!root) { fprintf(stderr, "error: invalid JSON\n"); return 1; } + cJSON *entries = cJSON_IsArray(root) ? root : cJSON_GetObjectItem(root, "entries"); + if (!entries) entries = root; + cJSON *entry; + cJSON_ArrayForEach(entry, entries) { + printf("%s\t%s\t%s\n", + cJSON_GetStringValue(cJSON_GetObjectItem(entry, "entry_id")) ?: "?", + cJSON_GetStringValue(cJSON_GetObjectItem(entry, "type")) ?: "?", + cJSON_GetStringValue(cJSON_GetObjectItem(entry, "title")) ?: "?"); + } + cJSON_Delete(root); + return 0; + } + + if (strcmp(cmd, "get") == 0 || strcmp(cmd, "totp") == 0) { + int is_totp = (strcmp(cmd, "totp") == 0); + const char *query = NULL; + for (int i = 2; i < argc; i++) { + if (argv[i][0] != '-') { query = argv[i]; break; } + } + if (!query) { fprintf(stderr, "error: query required\nusage: v1984 %s \n", cmd); return 1; } + + /* Search */ + char url[1024], encoded[512], path[768]; + url_encode(query, encoded, sizeof(encoded)); + snprintf(path, sizeof(path), "/api/search?q=%s", encoded); + build_url(url, sizeof(url), &cfg, path); + + struct v84_response resp; + if (http_get(url, bearer, &resp) != 0) { fprintf(stderr, "error: search failed\n"); return 1; } + if (resp.status != 200) { fprintf(stderr, "error: server returned %d\n", resp.status); free(resp.body); return 1; } + + cJSON *results = cJSON_Parse(resp.body); free(resp.body); + if (!results) { fprintf(stderr, "error: invalid JSON\n"); return 1; } + cJSON *arr = cJSON_IsArray(results) ? results : cJSON_GetObjectItem(results, "entries"); + if (!arr) arr = results; + cJSON *first = cJSON_GetArrayItem(arr, 0); + if (!first) { fprintf(stderr, "error: no results for '%s'\n", query); cJSON_Delete(results); return 1; } + const char *entry_id = cJSON_GetStringValue(cJSON_GetObjectItem(first, "entry_id")); + if (!entry_id) { fprintf(stderr, "error: missing entry_id\n"); cJSON_Delete(results); return 1; } + + if (is_totp) { + snprintf(path, sizeof(path), "/api/ext/totp/%s", entry_id); + build_url(url, sizeof(url), &cfg, path); + cJSON_Delete(results); + if (http_get(url, bearer, &resp) != 0) { fprintf(stderr, "error: TOTP request failed\n"); return 1; } + if (resp.status != 200) { fprintf(stderr, "error: server returned %d\n", resp.status); free(resp.body); return 1; } + cJSON *totp = cJSON_Parse(resp.body); free(resp.body); + if (!totp) { fprintf(stderr, "error: invalid JSON\n"); return 1; } + const char *code = cJSON_GetStringValue(cJSON_GetObjectItem(totp, "code")); + if (!code) { fprintf(stderr, "error: no TOTP code\n"); cJSON_Delete(totp); return 1; } + printf("%s\n", code); + cJSON_Delete(totp); + } else { + snprintf(path, sizeof(path), "/api/entries/%s", entry_id); + build_url(url, sizeof(url), &cfg, path); + cJSON_Delete(results); + if (http_get(url, bearer, &resp) != 0) { fprintf(stderr, "error: fetch failed\n"); return 1; } + if (resp.status != 200) { fprintf(stderr, "error: server returned %d\n", resp.status); free(resp.body); return 1; } + if (json_output) { printf("%s\n", resp.body); free(resp.body); return 0; } + + cJSON *entry = cJSON_Parse(resp.body); free(resp.body); + if (!entry) { fprintf(stderr, "error: invalid JSON\n"); return 1; } + cJSON *data = cJSON_GetObjectItem(entry, "data"); + if (!data) data = cJSON_GetObjectItem(entry, "vault_data"); + if (data) { + cJSON *fields = cJSON_GetObjectItem(data, "fields"); + cJSON *field; + cJSON_ArrayForEach(field, fields) { + const char *label = cJSON_GetStringValue(cJSON_GetObjectItem(field, "label")); + const char *value = cJSON_GetStringValue(cJSON_GetObjectItem(field, "value")); + cJSON *tier = cJSON_GetObjectItem(field, "tier"); + int tier_val = tier ? tier->valueint : 1; + if (!label) continue; + if (tier_val >= 3) printf("%s: [L3 -- requires hardware key]\n", label); + else printf("%s: %s\n", label, value ? value : ""); + } + } + cJSON_Delete(entry); + } + return 0; + } + fprintf(stderr, "error: unknown command '%s'\n", cmd); usage(); return 1; diff --git a/cli/v1984 b/cli/v1984 index 31eaf41..67cc3f2 100755 Binary files a/cli/v1984 and b/cli/v1984 differ