chore: auto-commit uncommitted changes
This commit is contained in:
parent
23b0a8d9ca
commit
474e084f6d
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -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 := .
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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 */
|
||||
|
|
|
|||
152
cli/src/main.c
152
cli/src/main.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue