chore: auto-commit uncommitted changes

This commit is contained in:
James 2026-03-17 06:02:13 -04:00
parent 23b0a8d9ca
commit 474e084f6d
9 changed files with 134 additions and 72 deletions

View File

@ -45,8 +45,10 @@ make status # check what's running
- **FIPS 140-3** via `GOEXPERIMENT=boringcrypto` — never build without it
- **Zero-knowledge** — server never sees plaintext credentials
- **WebAuthn L2** — hardware key support (in progress)
- **WebAuthn PRF** — hardware key derives master secret; L2 (16 bytes) for agents, L3 (32 bytes) for humans only
- No logging of credential content, ever
- **Registration = unlocked.** Passkey registration MUST derive and store the master key. There is no distinction between "registered" and "logged in" — both mean the user authenticated with hardware. The vault is immediately usable after registration, no second tap.
- **No "simplest fix" shortcuts.** This is a world-class security product. Every flow must be correct by design, not patched after the fact.
## Current Status (Mar 2026)

View File

@ -16,7 +16,7 @@ WEB_DIR := website
CLI_DIR := cli
APP_BIN := $(APP_DIR)/vault1984
WEB_BIN := $(WEB_DIR)/vault1984-web
CLI_BIN := $(CLI_DIR)/v1984
CLI_BIN := $(CLI_DIR)/vault1984-cli
APP_ENTRY := ./cmd/vault1984
WEB_ENTRY := .

View File

@ -35,7 +35,7 @@ CJSON_DIR := $(VENDOR_DIR)/cjson
CRYPTO_DIR := ../crypto
# Output binary
BIN := v1984
BIN := vault1984-cli
# --- Source files ---

Binary file not shown.

Binary file not shown.

View File

@ -35,7 +35,7 @@
#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 char CONFIG_SEED[] = "vault1984-l2-";
static const unsigned char CONFIG_MAGIC[4] = { 'V', '1', '9', 0x01 };
/* Derive 16-byte config encryption key from seed */

View File

@ -1,5 +1,5 @@
/*
* v1984 CLI credential access for AI agents
* vault1984-cli CLI credential access for AI agents
* Copyright (c) 2026 Vault1984. Elastic License 2.0.
*/
@ -19,25 +19,20 @@
static void usage(void) {
fprintf(stderr,
"v1984 %s — credential access for AI agents\n"
"vault1984-cli %s — credential access for AI agents\n"
"\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"
" v1984 test-totp <base32-seed>\n"
" v1984 test-crypto\n"
"Usage:\n"
" vault1984-cli --token <token> get <query>\n"
" vault1984-cli --token <token> list [filter]\n"
" vault1984-cli --token <token> totp <query>\n"
" vault1984-cli test-totp <base32-seed>\n"
" vault1984-cli test-crypto\n"
"\n"
"Options:\n"
" --help Show this help\n"
" --version Show version\n"
" --token <t> Agent token (from vault1984 web UI)\n"
" --json Output as JSON\n"
"\n"
"The config command stores vault URL, agent name, and L2 key\n"
"in an encrypted local config file. Run once per agent.\n",
" --help Show this help\n"
" --version Show version\n",
VERSION);
}
@ -54,7 +49,7 @@ static void get_bearer(const struct v84_config *cfg, char *buf, size_t len) {
/* --- test commands --- */
static int cmd_test_crypto(void) {
fprintf(stderr, "v1984: crypto self-test\n");
fprintf(stderr, "vault1984-cli: crypto self-test\n");
/* BearSSL AES-128-GCM roundtrip */
{
@ -180,11 +175,11 @@ int main(int argc, char **argv) {
/* 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, "--version") == 0) { printf("vault1984-cli %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, "usage: v1984 test-totp <base32-seed>\n");
fprintf(stderr, "usage: vault1984-cli test-totp <base32-seed>\n");
return 1;
}
if (jsbridge_init() != 0) { fprintf(stderr, "error: crypto init failed\n"); return 1; }
@ -198,43 +193,94 @@ int main(int argc, char **argv) {
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];
/* Parse --token and find the command */
const char *token_b64 = NULL;
const char *command = NULL;
int cmd_start = 0;
int json_output = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--token") == 0 && i + 1 < argc) {
token_b64 = argv[++i];
} else if (strcmp(argv[i], "--json") == 0) {
json_output = 1;
} else if (argv[i][0] != '-' && !command) {
command = argv[i];
cmd_start = i + 1;
}
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 */
if (!command) { fprintf(stderr, "error: no command\n"); usage(); return 1; }
if (!token_b64) { fprintf(stderr, "error: --token required\n"); usage(); return 1; }
/* Decode token: base64url → AES-128-GCM encrypted blob → decrypt → split */
if (jsbridge_init() != 0) { fprintf(stderr, "error: crypto init failed\n"); return 1; }
/* Decrypt token via JS (same key derivation as browser) */
char js_code[2048];
snprintf(js_code, sizeof(js_code),
"(function() {"
" var enc = new TextEncoder();"
" var seed = enc.encode('vault1984-l2-');"
" var encKey = native_hkdf_sha256(seed, null, enc.encode('token'), 16);"
" var ct = native_base64_decode('%s');"
" var pt = native_aes_gcm_decrypt_blob(encKey, ct);"
" return native_base64_encode(pt);"
"})()", token_b64);
char *decrypted_b64 = jsbridge_eval(js_code);
if (!decrypted_b64) {
fprintf(stderr, "error: token decryption failed (invalid or corrupted token)\n");
jsbridge_cleanup();
return 1;
}
unsigned char token_raw[512];
size_t token_len = 0;
if (base64_decode(decrypted_b64, token_raw, sizeof(token_raw), &token_len) != 0 || token_len < 3) {
fprintf(stderr, "error: invalid token payload\n");
free(decrypted_b64);
jsbridge_cleanup();
return 1;
}
free(decrypted_b64);
/* Split on null bytes: host \0 agent_name \0 l2_key_16_bytes */
char *vault_host = (char *)token_raw;
char *sep1 = memchr(token_raw, '\0', token_len);
if (!sep1 || (size_t)(sep1 - (char *)token_raw) >= token_len - 1) {
fprintf(stderr, "error: malformed token\n");
jsbridge_cleanup();
return 1;
}
char *agent_name = sep1 + 1;
char *sep2 = memchr(agent_name, '\0', token_len - (size_t)(agent_name - (char *)token_raw));
if (!sep2) {
fprintf(stderr, "error: malformed token\n");
jsbridge_cleanup();
return 1;
}
unsigned char *l2_key = (unsigned char *)(sep2 + 1);
size_t l2_len = token_len - (size_t)(l2_key - token_raw);
if (l2_len != 16) {
fprintf(stderr, "error: invalid L2 key in token (%zu bytes, need 16)\n", l2_len);
jsbridge_cleanup();
return 1;
}
/* Build config from token */
struct v84_config cfg;
if (keystore_load(&cfg) != 0) return 1;
memset(&cfg, 0, sizeof(cfg));
snprintf(cfg.vault_url, sizeof(cfg.vault_url), "https://%s:1984", vault_host);
snprintf(cfg.agent_name, sizeof(cfg.agent_name), "%s", agent_name);
memcpy(cfg.l2_key, l2_key, 16);
/* L1 = first 8 bytes of L2 key, used as Bearer auth */
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) {
if (strcmp(command, "list") == 0) {
const char *filter = NULL;
for (int i = 2; i < argc; i++) {
for (int i = cmd_start; i < argc; i++) {
if (argv[i][0] != '-') { filter = argv[i]; break; }
}
@ -268,13 +314,13 @@ int main(int argc, char **argv) {
return 0;
}
if (strcmp(cmd, "get") == 0 || strcmp(cmd, "totp") == 0) {
int is_totp = (strcmp(cmd, "totp") == 0);
if (strcmp(command, "get") == 0 || strcmp(command, "totp") == 0) {
int is_totp = (strcmp(command, "totp") == 0);
const char *query = NULL;
for (int i = 2; i < argc; i++) {
for (int i = cmd_start; 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; }
if (!query) { fprintf(stderr, "error: query required\nusage: vault1984-cli --token TOKEN %s <query>\n", command); return 1; }
/* Search */
char url[1024], encoded[512], path[768];
@ -337,7 +383,7 @@ int main(int argc, char **argv) {
return 0;
}
fprintf(stderr, "error: unknown command '%s'\n", cmd);
fprintf(stderr, "error: unknown command '%s'\n", command);
usage();
return 1;
}

BIN
cli/vault1984-cli Executable file

Binary file not shown.

View File

@ -121,27 +121,31 @@ function hkdf_sha256(ikm, salt, info, length) {
}
}
/* --- L2 field encryption/decryption --- */
/* --- Field encryption/decryption --- */
/**
* Encrypt a field value with L2 key (AES-128-GCM).
* In QuickJS: returns base64 string (synchronous).
* In browser: returns Promise<string>.
* Encrypt a field value.
* Key length determines tier: 16 bytes = L2 (AES-128), 32 bytes = L3 (AES-256).
* @param {Uint8Array} key - 16 or 32 bytes
* @param {string} field_label - field label (for per-field key derivation)
* @param {string} plaintext - field value to encrypt
* @returns {string|Promise<string>} base64-encoded ciphertext
*/
function l2_encrypt_field(l2_key, entry_id, field_label, plaintext) {
var info_str = 'vault1984-l2-' + entry_id + '-' + field_label;
function encrypt_field(key, field_label, plaintext) {
var info_str = 'vault1984-field-' + field_label;
var key_len = key.length;
if (IS_BROWSER) {
var enc = new TextEncoder();
var info = enc.encode(info_str);
return hkdf_sha256(l2_key, null, info, 16).then(function(field_key) {
return hkdf_sha256(key, null, info, key_len).then(function(field_key) {
return aes_gcm_encrypt(field_key, enc.encode(plaintext));
}).then(function(ct) {
return uint8_to_base64(ct);
});
} else {
var info = native_encode_utf8(info_str);
var field_key = native_hkdf_sha256(l2_key, null, info, 16);
var field_key = native_hkdf_sha256(key, null, info, key_len);
var pt_bytes = native_encode_utf8(plaintext);
var ct = native_aes_gcm_encrypt(field_key, pt_bytes);
return native_base64_encode(ct);
@ -149,18 +153,22 @@ function l2_encrypt_field(l2_key, entry_id, field_label, plaintext) {
}
/**
* Decrypt a field value with L2 key (AES-128-GCM).
* In QuickJS: returns plaintext string (synchronous).
* In browser: returns Promise<string>.
* Decrypt a field value.
* Key length determines tier: 16 bytes = L2, 32 bytes = L3.
* @param {Uint8Array} key - 16 or 32 bytes
* @param {string} field_label - field label
* @param {string} ciphertext_b64 - base64-encoded ciphertext
* @returns {string|Promise<string>} plaintext
*/
function l2_decrypt_field(l2_key, entry_id, field_label, ciphertext_b64) {
var info_str = 'vault1984-l2-' + entry_id + '-' + field_label;
function decrypt_field(key, field_label, ciphertext_b64) {
var info_str = 'vault1984-field-' + field_label;
var key_len = key.length;
if (IS_BROWSER) {
var enc = new TextEncoder();
var dec = new TextDecoder();
var info = enc.encode(info_str);
return hkdf_sha256(l2_key, null, info, 16).then(function(field_key) {
return hkdf_sha256(key, null, info, key_len).then(function(field_key) {
var ct = base64_to_uint8(ciphertext_b64);
return aes_gcm_decrypt(field_key, ct);
}).then(function(pt) {
@ -168,19 +176,25 @@ function l2_decrypt_field(l2_key, entry_id, field_label, ciphertext_b64) {
});
} else {
var info = native_encode_utf8(info_str);
var field_key = native_hkdf_sha256(l2_key, null, info, 16);
var field_key = native_hkdf_sha256(key, null, info, key_len);
var ct = native_base64_decode(ciphertext_b64);
var pt = native_aes_gcm_decrypt_blob(field_key, ct);
return native_decode_utf8(pt);
}
}
/* Backward compat aliases */
function l2_encrypt_field(key, entry_id, label, pt) { return encrypt_field(key, label, pt); }
function l2_decrypt_field(key, entry_id, label, ct) { return decrypt_field(key, label, ct); }
/* Export for both environments */
if (typeof globalThis.vault1984 === 'undefined') globalThis.vault1984 = {};
globalThis.vault1984.crypto = {
aes_gcm_encrypt: aes_gcm_encrypt,
aes_gcm_decrypt: aes_gcm_decrypt,
hkdf_sha256: hkdf_sha256,
encrypt_field: encrypt_field,
decrypt_field: decrypt_field,
l2_encrypt_field: l2_encrypt_field,
l2_decrypt_field: l2_decrypt_field,
uint8_to_base64: uint8_to_base64,