chore: auto-commit uncommitted changes
This commit is contained in:
parent
acebec681d
commit
23b0a8d9ca
|
|
@ -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-name>/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
|
||||
|
|
@ -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
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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 <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "bearssl.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#define mkdir_700(p) _mkdir(p)
|
||||
#define mkdir_p(p) _mkdir(p)
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#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 <url> --user <email> --key <api_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 <url> --user <email> --key <api_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 <url> --user <email> --key <api_key>\n");
|
||||
fprintf(stderr, "error: config missing vault_url\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <stddef.h>
|
||||
|
||||
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
|
||||
|
|
|
|||
722
cli/src/main.c
722
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 <url> --user <email> --key <api_key>\n"
|
||||
"Setup:\n"
|
||||
" v1984 config --vault <url> --agent <name> --l2 <base64-key>\n"
|
||||
"\n"
|
||||
"Commands:\n"
|
||||
" v1984 get <query> [--json]\n"
|
||||
" v1984 list [filter] [--json]\n"
|
||||
" v1984 totp <query>\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 <url> --user <email> --key <api_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 <query> [--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 <query>\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<orig.length && ok; i++) ok = (orig[i] === back[i]);"
|
||||
"'base64 roundtrip: ' + (ok ? 'OK' : 'FAIL') + ' len=' + back.length + ' b64=' + b64;"
|
||||
);
|
||||
"'base64 roundtrip: ' + (ok ? 'OK' : 'FAIL');");
|
||||
if (r) { fprintf(stderr, " [JS] %s\n", r); free(r); }
|
||||
}
|
||||
|
||||
/* Test raw AES-GCM roundtrip via JS (no HKDF, no base64) */
|
||||
{
|
||||
{ /* raw AES-GCM */
|
||||
char *r = jsbridge_eval(
|
||||
"var k = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);"
|
||||
"var pt = native_encode_utf8('hello');"
|
||||
|
|
@ -476,183 +107,236 @@ static int cmd_test_crypto(void) {
|
|||
"var pt2 = native_aes_gcm_decrypt_blob(k, ct);"
|
||||
"var ok = (pt.length === pt2.length);"
|
||||
"for (var i=0; i<pt.length && ok; i++) ok = (pt[i] === pt2[i]);"
|
||||
"'raw AES-GCM: ' + (ok ? 'OK' : 'FAIL') + ' ctLen=' + ct.length + ' ptLen=' + pt2.length;"
|
||||
);
|
||||
"'raw AES-GCM: ' + (ok ? 'OK' : 'FAIL');");
|
||||
if (r) { fprintf(stderr, " [JS] %s\n", r); free(r); }
|
||||
}
|
||||
|
||||
/* Test HKDF determinism */
|
||||
{
|
||||
{ /* HKDF determinism */
|
||||
char *r = jsbridge_eval(
|
||||
"var ikm = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);"
|
||||
"var info = native_encode_utf8('test-info');"
|
||||
"var k1 = native_hkdf_sha256(ikm, null, info, 16);"
|
||||
"var k2 = native_hkdf_sha256(ikm, null, info, 16);"
|
||||
"var ok = true;"
|
||||
"for (var i=0; i<16; i++) ok = ok && (k1[i] === k2[i]);"
|
||||
"'HKDF determinism: ' + (ok ? 'OK' : 'FAIL');"
|
||||
);
|
||||
"var ok = true; for (var i=0; i<16; i++) ok = ok && (k1[i] === k2[i]);"
|
||||
"'HKDF determinism: ' + (ok ? 'OK' : 'FAIL');");
|
||||
if (r) { fprintf(stderr, " [JS] %s\n", r); free(r); }
|
||||
}
|
||||
|
||||
/* Test full l2 roundtrip via JS eval */
|
||||
{
|
||||
{ /* L2 field roundtrip */
|
||||
char *r = jsbridge_eval(
|
||||
"var k = new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);"
|
||||
"var ct = vault1984.crypto.l2_encrypt_field(k, 'abcdef01', 'pw', 'secret');"
|
||||
"var pt = vault1984.crypto.l2_decrypt_field(k, 'abcdef01', 'pw', ct);"
|
||||
"'l2 roundtrip: ' + (pt === 'secret' ? 'OK' : 'FAIL got=' + pt) + ' ct=' + ct;"
|
||||
);
|
||||
"'l2 roundtrip: ' + (pt === 'secret' ? 'OK' : 'FAIL');");
|
||||
if (r) { fprintf(stderr, " [JS] %s\n", r); free(r); }
|
||||
}
|
||||
|
||||
fprintf(stderr, " encrypting: \"%s\"\n", plaintext);
|
||||
|
||||
char *ct = jsbridge_l2_encrypt(test_key, 16, entry_id, label, plaintext);
|
||||
if (!ct) {
|
||||
fprintf(stderr, "FAIL: l2_encrypt returned NULL\n");
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
}
|
||||
/* L2 encrypt/decrypt via C API */
|
||||
fprintf(stderr, " encrypting: \"s3cret-p@ssw0rd!\"\n");
|
||||
char *ct = jsbridge_l2_encrypt(test_key, 16, "abcdef0123456789", "password", "s3cret-p@ssw0rd!");
|
||||
if (!ct) { fprintf(stderr, "FAIL: l2_encrypt\n"); jsbridge_cleanup(); return 1; }
|
||||
fprintf(stderr, " ciphertext: %s\n", ct);
|
||||
|
||||
char *pt = jsbridge_l2_decrypt(test_key, 16, entry_id, label, ct);
|
||||
char *pt = jsbridge_l2_decrypt(test_key, 16, "abcdef0123456789", "password", ct);
|
||||
free(ct);
|
||||
if (!pt) {
|
||||
fprintf(stderr, "FAIL: l2_decrypt returned NULL\n");
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
if (!pt || strcmp(pt, "s3cret-p@ssw0rd!") != 0) {
|
||||
fprintf(stderr, "FAIL: l2_decrypt\n"); free(pt); jsbridge_cleanup(); return 1;
|
||||
}
|
||||
fprintf(stderr, " decrypted: \"%s\"\n", pt);
|
||||
|
||||
if (strcmp(pt, plaintext) != 0) {
|
||||
fprintf(stderr, "FAIL: roundtrip mismatch\n");
|
||||
free(pt);
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
}
|
||||
free(pt);
|
||||
|
||||
/* Test with different field label (should produce different ciphertext) */
|
||||
char *ct1 = jsbridge_l2_encrypt(test_key, 16, entry_id, "password", "hello");
|
||||
char *ct2 = jsbridge_l2_encrypt(test_key, 16, entry_id, "username", "hello");
|
||||
if (ct1 && ct2 && strcmp(ct1, ct2) == 0) {
|
||||
fprintf(stderr, "FAIL: different labels produced same ciphertext\n");
|
||||
free(ct1); free(ct2);
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, " per-field key derivation: OK\n");
|
||||
free(ct1); free(ct2);
|
||||
|
||||
/* Test wrong key fails */
|
||||
char *ct3 = jsbridge_l2_encrypt(test_key, 16, entry_id, label, "test");
|
||||
if (ct3) {
|
||||
unsigned char wrong_key[16] = {0};
|
||||
char *bad = jsbridge_l2_decrypt(wrong_key, 16, entry_id, label, ct3);
|
||||
if (bad) {
|
||||
fprintf(stderr, "FAIL: wrong key should not decrypt\n");
|
||||
free(bad);
|
||||
free(ct3);
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
}
|
||||
/* Wrong key rejection */
|
||||
char *ct2 = jsbridge_l2_encrypt(test_key, 16, "abcdef0123456789", "pw", "test");
|
||||
if (ct2) {
|
||||
unsigned char wrong[16] = {0};
|
||||
char *bad = jsbridge_l2_decrypt(wrong, 16, "abcdef0123456789", "pw", ct2);
|
||||
if (bad) { fprintf(stderr, "FAIL: wrong key accepted\n"); free(bad); free(ct2); jsbridge_cleanup(); return 1; }
|
||||
fprintf(stderr, " wrong key rejection: OK\n");
|
||||
free(ct3);
|
||||
free(ct2);
|
||||
}
|
||||
|
||||
/* Test TOTP generation with RFC 6238 test vector */
|
||||
{
|
||||
/* RFC 6238 test: SHA1 key = "12345678901234567890" (ASCII), time=59, expect "287082" */
|
||||
char *code = jsbridge_eval(
|
||||
"vault1984.totp.generate_totp('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 59, 30, 6);"
|
||||
);
|
||||
if (code) {
|
||||
int ok = (strcmp(code, "287082") == 0);
|
||||
fprintf(stderr, " TOTP RFC6238 test vector: %s (got %s, expect 287082)\n",
|
||||
ok ? "OK" : "FAIL", code);
|
||||
free(code);
|
||||
if (!ok) { jsbridge_cleanup(); return 1; }
|
||||
} else {
|
||||
fprintf(stderr, " TOTP: FAIL (null)\n");
|
||||
jsbridge_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Test via C API */
|
||||
char *code2 = jsbridge_totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
|
||||
if (code2) {
|
||||
fprintf(stderr, " TOTP live code: %s (changes every 30s)\n", code2);
|
||||
free(code2);
|
||||
}
|
||||
/* TOTP RFC 6238 test vector */
|
||||
char *code = jsbridge_eval(
|
||||
"vault1984.totp.generate_totp('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 59, 30, 6);");
|
||||
if (!code || strcmp(code, "287082") != 0) {
|
||||
fprintf(stderr, "FAIL: TOTP (got %s, expect 287082)\n", code ? code : "null");
|
||||
free(code); jsbridge_cleanup(); return 1;
|
||||
}
|
||||
fprintf(stderr, " TOTP RFC6238: OK (%s)\n", code);
|
||||
free(code);
|
||||
|
||||
char *live = jsbridge_totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
|
||||
if (live) { fprintf(stderr, " TOTP live: %s\n", live); free(live); }
|
||||
|
||||
jsbridge_cleanup();
|
||||
fprintf(stderr, "PASS: all crypto tests passed\n");
|
||||
fprintf(stderr, "PASS: all tests passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- main --- */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) {
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
if (argc < 2) { usage(); return 1; }
|
||||
|
||||
const char *cmd = argv[1];
|
||||
|
||||
if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) {
|
||||
usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "--version") == 0 || strcmp(cmd, "-v") == 0) {
|
||||
printf("vault1984 %s\n", VERSION);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "init") == 0) {
|
||||
return cmd_init(argc - 2, argv + 2);
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "list") == 0) {
|
||||
return cmd_list(argc - 2, argv + 2);
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "get") == 0) {
|
||||
return cmd_get(argc - 2, argv + 2);
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "totp") == 0) {
|
||||
return cmd_totp(argc - 2, argv + 2);
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "test-crypto") == 0) {
|
||||
return cmd_test_crypto();
|
||||
}
|
||||
|
||||
/* No-config commands */
|
||||
if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { usage(); return 0; }
|
||||
if (strcmp(cmd, "--version") == 0) { printf("v1984 %s\n", VERSION); return 0; }
|
||||
if (strcmp(cmd, "test-crypto") == 0) { return cmd_test_crypto(); }
|
||||
if (strcmp(cmd, "test-totp") == 0) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Generate a TOTP code from a base32 seed.\n\n");
|
||||
fprintf(stderr, "usage: v1984 test-totp <base32-seed>\n\n");
|
||||
fprintf(stderr, "example: v1984 test-totp JBSWY3DPEHPK3PXP\n");
|
||||
fprintf(stderr, " 482901 expires in 17s\n");
|
||||
fprintf(stderr, "usage: v1984 test-totp <base32-seed>\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 <url> --agent <name> --l2 <base64-key>\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 <query>\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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue