diff --git a/cli/build/src/http.o b/cli/build/src/http.o index 28a2876..146ec03 100644 Binary files a/cli/build/src/http.o and b/cli/build/src/http.o differ diff --git a/cli/build/src/jsbridge.o b/cli/build/src/jsbridge.o index a0cf82a..6d55538 100644 Binary files a/cli/build/src/jsbridge.o and b/cli/build/src/jsbridge.o differ diff --git a/cli/build/src/main.o b/cli/build/src/main.o index 91a5d08..5378373 100644 Binary files a/cli/build/src/main.o and b/cli/build/src/main.o differ diff --git a/cli/src/http.c b/cli/src/http.c index e9c7264..34a66a6 100644 --- a/cli/src/http.c +++ b/cli/src/http.c @@ -34,6 +34,141 @@ typedef int socklen_t; #include "bearssl.h" +/* --- System CA trust anchor loading --- */ + +/* + * Load PEM CA certificates from system trust store. + * Parses each certificate, extracts DN + public key → br_x509_trust_anchor. + */ + +struct pem_accum { + unsigned char *buf; + size_t len; + size_t cap; +}; + +static void pem_accum_push(void *dest_ctx, const void *src, size_t len) { + struct pem_accum *a = (struct pem_accum *)dest_ctx; + if (a->len + len > a->cap) { + a->cap = (a->len + len) * 2; + a->buf = realloc(a->buf, a->cap); + } + memcpy(a->buf + a->len, src, len); + a->len += len; +} + +/* DN callback for x509 decoder */ +struct dn_accum { + unsigned char *buf; + size_t len; + size_t cap; +}; + +static void dn_push(void *dest_ctx, const void *src, size_t len) { + struct dn_accum *a = (struct dn_accum *)dest_ctx; + if (a->len + len > a->cap) { + a->cap = (a->len + len) * 2; + a->buf = realloc(a->buf, a->cap); + } + memcpy(a->buf + a->len, src, len); + a->len += len; +} + +static void load_system_tas(br_x509_trust_anchor **out_tas, size_t *out_count) { + *out_tas = NULL; + *out_count = 0; + + static const char *ca_paths[] = { + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/cert.pem", + "/usr/local/share/certs/ca-root-nss.crt", + NULL + }; + + FILE *f = NULL; + for (int i = 0; ca_paths[i]; i++) { + f = fopen(ca_paths[i], "r"); + if (f) break; + } + if (!f) return; + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + if (fsize <= 0 || fsize > 4 * 1024 * 1024) { fclose(f); return; } + + char *pem = malloc((size_t)fsize); + size_t nr = fread(pem, 1, (size_t)fsize, f); + fclose(f); + + /* Parse PEM → DER certificates → trust anchors */ + size_t ta_cap = 256; + br_x509_trust_anchor *tas = calloc(ta_cap, sizeof(br_x509_trust_anchor)); + size_t ta_count = 0; + + br_pem_decoder_context pc; + br_pem_decoder_init(&pc); + + struct pem_accum der = { malloc(8192), 0, 8192 }; + int in_cert = 0; + + size_t pos = 0; + while (pos < nr) { + size_t pushed = br_pem_decoder_push(&pc, pem + pos, nr - pos); + pos += pushed; + + int event = br_pem_decoder_event(&pc); + if (event == BR_PEM_BEGIN_OBJ) { + in_cert = (strcmp(br_pem_decoder_name(&pc), "CERTIFICATE") == 0); + der.len = 0; + if (in_cert) br_pem_decoder_setdest(&pc, pem_accum_push, &der); + } else if (event == BR_PEM_END_OBJ && in_cert && der.len > 0) { + /* Decode X.509 certificate */ + struct dn_accum dn = { malloc(512), 0, 512 }; + br_x509_decoder_context dc; + br_x509_decoder_init(&dc, dn_push, &dn); + br_x509_decoder_push(&dc, der.buf, der.len); + + if (br_x509_decoder_last_error(&dc) == 0 && br_x509_decoder_isCA(&dc)) { + br_x509_pkey *pk = br_x509_decoder_get_pkey(&dc); + if (pk && ta_count < ta_cap) { + br_x509_trust_anchor *ta = &tas[ta_count]; + ta->dn.data = dn.buf; + ta->dn.len = dn.len; + ta->flags = BR_X509_TA_CA; + ta->pkey = *pk; + + /* Deep copy key data (decoder buffer will be reused) */ + if (pk->key_type == BR_KEYTYPE_RSA) { + unsigned char *n = malloc(pk->key.rsa.nlen); + unsigned char *e = malloc(pk->key.rsa.elen); + memcpy(n, pk->key.rsa.n, pk->key.rsa.nlen); + memcpy(e, pk->key.rsa.e, pk->key.rsa.elen); + ta->pkey.key.rsa.n = n; + ta->pkey.key.rsa.e = e; + } else if (pk->key_type == BR_KEYTYPE_EC) { + unsigned char *q = malloc(pk->key.ec.qlen); + memcpy(q, pk->key.ec.q, pk->key.ec.qlen); + ta->pkey.key.ec.q = q; + } + ta_count++; + dn.buf = NULL; /* ownership transferred */ + } + } + free(dn.buf); + in_cert = 0; + } else if (event == BR_PEM_ERROR) { + in_cert = 0; + } + } + + free(pem); + free(der.buf); + *out_tas = tas; + *out_count = ta_count; +} + /* --- socket connect --- */ static SOCKET tcp_connect(const char *host, const char *port) { @@ -227,19 +362,23 @@ static int parse_response(char *resp_buf, size_t resp_len, struct v84_response * /* --- HTTP GET (plain) --- */ static int http_get_plain(const struct parsed_url *pu, const char *bearer_token, - struct v84_response *resp) { + const char *agent_name, struct v84_response *resp) { SOCKET fd = tcp_connect(pu->host, pu->port); if (fd == INVALID_SOCKET) return -1; char request[2048]; + char agent_hdr[256] = ""; + if (agent_name && agent_name[0]) + snprintf(agent_hdr, sizeof(agent_hdr), "X-Agent: %s\r\n", agent_name); int reqlen = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Authorization: Bearer %s\r\n" + "%s" "Connection: close\r\n" "Accept: application/json\r\n" "\r\n", - pu->path, pu->host, bearer_token); + pu->path, pu->host, bearer_token, agent_hdr); if (plain_send_all(fd, request, (size_t)reqlen) != 0) { fprintf(stderr, "error: send failed\n"); @@ -262,7 +401,7 @@ static int http_get_plain(const struct parsed_url *pu, const char *bearer_token, /* --- HTTPS GET (BearSSL TLS) --- */ static int http_get_tls(const struct parsed_url *pu, const char *bearer_token, - struct v84_response *resp) { + const char *agent_name, struct v84_response *resp) { SOCKET fd = tcp_connect(pu->host, pu->port); if (fd == INVALID_SOCKET) return -1; @@ -271,14 +410,18 @@ static int http_get_tls(const struct parsed_url *pu, const char *bearer_token, unsigned char iobuf[BR_SSL_BUFSIZE_BIDI]; /* - * TODO: load system CA bundle for production certificate validation. - * For now: no trust anchors = no validation. + * Load system CA certificates for TLS validation. + * Falls back to no-validation mode if CAs can't be loaded. */ - br_x509_minimal_init(&xc, &br_sha256_vtable, NULL, 0); + size_t ta_count = 0; + br_x509_trust_anchor *tas = NULL; + load_system_tas(&tas, &ta_count); + + br_x509_minimal_init(&xc, &br_sha256_vtable, tas, ta_count); br_x509_minimal_set_rsa(&xc, &br_rsa_i31_pkcs1_vrfy); br_x509_minimal_set_ecdsa(&xc, &br_ec_prime_i31, &br_ecdsa_i31_vrfy_asn1); - br_ssl_client_init_full(&sc, &xc, NULL, 0); + br_ssl_client_init_full(&sc, &xc, tas, ta_count); br_ssl_engine_set_buffer(&sc.eng, iobuf, sizeof(iobuf), 1); br_ssl_client_reset(&sc, pu->host, 0); @@ -286,14 +429,18 @@ static int http_get_tls(const struct parsed_url *pu, const char *bearer_token, br_sslio_init(&ioc, &sc.eng, sock_read, &fd, sock_write, &fd); char request[2048]; + char agent_hdr[256] = ""; + if (agent_name && agent_name[0]) + snprintf(agent_hdr, sizeof(agent_hdr), "X-Agent: %s\r\n", agent_name); int reqlen = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Authorization: Bearer %s\r\n" + "%s" "Connection: close\r\n" "Accept: application/json\r\n" "\r\n", - pu->path, pu->host, bearer_token); + pu->path, pu->host, bearer_token, agent_hdr); if (br_sslio_write_all(&ioc, request, (size_t)reqlen) != 0) { fprintf(stderr, "error: TLS write failed\n"); @@ -328,15 +475,16 @@ static int http_get_tls(const struct parsed_url *pu, const char *bearer_token, /* --- public API --- */ -int http_get(const char *url, const char *bearer_token, struct v84_response *resp) { +int http_get(const char *url, const char *bearer_token, const char *agent_name, + struct v84_response *resp) { memset(resp, 0, sizeof(*resp)); struct parsed_url pu; if (parse_url(url, &pu) != 0) return -1; if (pu.use_tls) { - return http_get_tls(&pu, bearer_token, resp); + return http_get_tls(&pu, bearer_token, agent_name, resp); } else { - return http_get_plain(&pu, bearer_token, resp); + return http_get_plain(&pu, bearer_token, agent_name, resp); } } diff --git a/cli/src/http.h b/cli/src/http.h index ef8ff1e..dc086f6 100644 --- a/cli/src/http.h +++ b/cli/src/http.h @@ -13,7 +13,9 @@ struct v84_response { size_t body_len; }; -/* Perform HTTPS GET with Bearer auth. Returns 0 on success, -1 on error. */ -int http_get(const char *url, const char *bearer_token, struct v84_response *resp); +/* Perform HTTPS GET with Bearer auth + optional X-Agent header. + * agent_name can be NULL to omit the header. */ +int http_get(const char *url, const char *bearer_token, const char *agent_name, + struct v84_response *resp); #endif diff --git a/cli/src/jsbridge.c b/cli/src/jsbridge.c index 850d5ea..e72b51b 100644 --- a/cli/src/jsbridge.c +++ b/cli/src/jsbridge.c @@ -503,43 +503,37 @@ char *jsbridge_eval(const char *code) { return out; } -char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len, - const char *entry_id, const char *field_label, - const char *plaintext) { +char *jsbridge_encrypt_field(const unsigned char *key, size_t key_len, + const char *field_label, const char *plaintext) { if (!ctx) return NULL; - /* Call vault1984.crypto.l2_encrypt_field(key, entry_id, label, plaintext) */ JSValue global = JS_GetGlobalObject(ctx); JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984"); JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto"); - JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "l2_encrypt_field"); + JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "encrypt_field"); - JSValue key_arr = js_new_uint8array(ctx, l2_key, key_len); - JSValue args[4] = { + JSValue key_arr = js_new_uint8array(ctx, key, key_len); + JSValue args[3] = { key_arr, - JS_NewString(ctx, entry_id), JS_NewString(ctx, field_label), JS_NewString(ctx, plaintext) }; - JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 4, args); + JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 3, args); char *out = NULL; if (!JS_IsException(result)) { const char *str = JS_ToCString(ctx, result); - if (str) { - out = strdup(str); - JS_FreeCString(ctx, str); - } + if (str) { out = strdup(str); JS_FreeCString(ctx, str); } } else { JSValue exc = JS_GetException(ctx); const char *msg = JS_ToCString(ctx, exc); - fprintf(stderr, "error: l2_encrypt: %s\n", msg ? msg : "unknown"); + fprintf(stderr, "error: encrypt_field: %s\n", msg ? msg : "unknown"); if (msg) JS_FreeCString(ctx, msg); JS_FreeValue(ctx, exc); } - for (int i = 0; i < 4; i++) JS_FreeValue(ctx, args[i]); + for (int i = 0; i < 3; i++) JS_FreeValue(ctx, args[i]); JS_FreeValue(ctx, result); JS_FreeValue(ctx, fn); JS_FreeValue(ctx, crypto_obj); @@ -548,42 +542,37 @@ char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len, return out; } -char *jsbridge_l2_decrypt(const unsigned char *l2_key, size_t key_len, - const char *entry_id, const char *field_label, - const char *ciphertext_b64) { +char *jsbridge_decrypt_field(const unsigned char *key, size_t key_len, + const char *field_label, const char *ciphertext_b64) { if (!ctx) return NULL; JSValue global = JS_GetGlobalObject(ctx); JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984"); JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto"); - JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "l2_decrypt_field"); + JSValue fn = JS_GetPropertyStr(ctx, crypto_obj, "decrypt_field"); - JSValue key_arr = js_new_uint8array(ctx, l2_key, key_len); - JSValue args[4] = { + JSValue key_arr = js_new_uint8array(ctx, key, key_len); + JSValue args[3] = { key_arr, - JS_NewString(ctx, entry_id), JS_NewString(ctx, field_label), JS_NewString(ctx, ciphertext_b64) }; - JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 4, args); + JSValue result = JS_Call(ctx, fn, JS_UNDEFINED, 3, args); char *out = NULL; if (!JS_IsException(result)) { const char *str = JS_ToCString(ctx, result); - if (str) { - out = strdup(str); - JS_FreeCString(ctx, str); - } + if (str) { out = strdup(str); JS_FreeCString(ctx, str); } } else { JSValue exc = JS_GetException(ctx); const char *msg = JS_ToCString(ctx, exc); - fprintf(stderr, "error: l2_decrypt: %s\n", msg ? msg : "unknown"); + fprintf(stderr, "error: decrypt_field: %s\n", msg ? msg : "unknown"); if (msg) JS_FreeCString(ctx, msg); JS_FreeValue(ctx, exc); } - for (int i = 0; i < 4; i++) JS_FreeValue(ctx, args[i]); + for (int i = 0; i < 3; i++) JS_FreeValue(ctx, args[i]); JS_FreeValue(ctx, result); JS_FreeValue(ctx, fn); JS_FreeValue(ctx, crypto_obj); diff --git a/cli/src/jsbridge.h b/cli/src/jsbridge.h index 5fd25d7..b896754 100644 --- a/cli/src/jsbridge.h +++ b/cli/src/jsbridge.h @@ -15,22 +15,20 @@ int jsbridge_init(void); void jsbridge_cleanup(void); /* - * Encrypt a field value using L2 key. - * Returns base64-encoded ciphertext (caller frees). - * Returns NULL on error. + * Encrypt a field value. Key length determines tier (8=L1, 16=L2, 32=L3). + * Uses crypto.js encrypt_field() — the single source of truth. + * Returns base64-encoded ciphertext (caller frees). NULL on error. */ -char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len, - const char *entry_id, const char *field_label, - const char *plaintext); +char *jsbridge_encrypt_field(const unsigned char *key, size_t key_len, + const char *field_label, const char *plaintext); /* - * Decrypt a field value using L2 key. - * Returns plaintext string (caller frees). - * Returns NULL on error. + * Decrypt a field value. Key length determines tier (8=L1, 16=L2, 32=L3). + * Uses crypto.js decrypt_field() — the single source of truth. + * Returns plaintext string (caller frees). NULL on error. */ -char *jsbridge_l2_decrypt(const unsigned char *l2_key, size_t key_len, - const char *entry_id, const char *field_label, - const char *ciphertext_b64); +char *jsbridge_decrypt_field(const unsigned char *key, size_t key_len, + const char *field_label, const char *ciphertext_b64); /* * Generate TOTP code from a base32-encoded seed. diff --git a/cli/src/main.c b/cli/src/main.c index 5863e21..3f55c17 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -43,7 +43,7 @@ static void build_url(char *buf, size_t len, const struct v84_config *cfg, const } static void get_bearer(const struct v84_config *cfg, char *buf, size_t len) { - base64_encode(cfg->l2_key, 16, buf, len); + base64_encode(cfg->l2_key, 8, buf, len); /* L1 = first 8 bytes */ } /* --- test commands --- */ @@ -126,11 +126,11 @@ static int cmd_test_crypto(void) { /* 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!"); + char *ct = jsbridge_encrypt_field(test_key, 16, "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, "abcdef0123456789", "password", ct); + char *pt = jsbridge_decrypt_field(test_key, 16, "password", ct); free(ct); if (!pt || strcmp(pt, "s3cret-p@ssw0rd!") != 0) { fprintf(stderr, "FAIL: l2_decrypt\n"); free(pt); jsbridge_cleanup(); return 1; @@ -139,10 +139,10 @@ static int cmd_test_crypto(void) { free(pt); /* Wrong key rejection */ - char *ct2 = jsbridge_l2_encrypt(test_key, 16, "abcdef0123456789", "pw", "test"); + char *ct2 = jsbridge_encrypt_field(test_key, 16, "pw", "test"); if (ct2) { unsigned char wrong[16] = {0}; - char *bad = jsbridge_l2_decrypt(wrong, 16, "abcdef0123456789", "pw", ct2); + char *bad = jsbridge_decrypt_field(wrong, 16, "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(ct2); @@ -177,6 +177,40 @@ int main(int argc, char **argv) { if (strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { usage(); 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-roundtrip") == 0) { + if (jsbridge_init() != 0) { fprintf(stderr, "error: crypto init failed\n"); return 1; } + char *r = jsbridge_eval( + "var R = [];" + "function t(kb, label, pt) {" + " var k = new Uint8Array(kb);" + " var d = kb.length + 'B key, label=' + label;" + " try {" + " var ct = vault1984.crypto.encrypt_field(k, label, pt);" + " var p2 = vault1984.crypto.decrypt_field(k, label, ct);" + " R.push(d + ': ' + (p2 === pt ? 'PASS' : 'FAIL got=' + p2));" + " } catch(e) { R.push(d + ': ERROR ' + e.message); }" + "}" + /* 8-byte key */ + "t([1,2,3,4,5,6,7,8], 'username', 'johanj');" + /* 16-byte key (L2) */ + "t([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], 'password', 's3cret!');" + "t([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], 'totp', 'JBSWY3DPEHPK3PXP');" + "t([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], 'Number', '5452120017212208');" + /* 32-byte key (L3) */ + "t([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32], 'passport', 'NL12345678');" + "t([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32], 'CVV', '755');" + /* wrong key must fail */ + "var k1=new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);" + "var k2=new Uint8Array([99,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]);" + "try{var c=vault1984.crypto.encrypt_field(k1,'x','s');vault1984.crypto.decrypt_field(k2,'x',c);R.push('wrong-key: FAIL')}catch(e){R.push('wrong-key rejection: PASS')}" + /* wrong label must fail */ + "try{var c2=vault1984.crypto.encrypt_field(k1,'a','s');vault1984.crypto.decrypt_field(k1,'b',c2);R.push('wrong-label: FAIL')}catch(e){R.push('wrong-label rejection: PASS')}" + "R.join('\\n');" + ); + if (r) { printf("%s\n", r); free(r); } + jsbridge_cleanup(); + return (r && strstr(r, "FAIL") == NULL && strstr(r, "ERROR") == NULL) ? 0 : 1; + } if (strcmp(cmd, "test-totp") == 0) { if (argc < 3) { fprintf(stderr, "usage: vault1984-cli test-totp \n"); @@ -220,9 +254,8 @@ int main(int argc, char **argv) { 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 seed = native_encode_utf8('vault1984-l2-');" + " var encKey = native_hkdf_sha256(seed, null, native_encode_utf8('token'), 16);" " var ct = native_base64_decode('%s');" " var pt = native_aes_gcm_decrypt_blob(encKey, ct);" " return native_base64_encode(pt);" @@ -295,7 +328,7 @@ int main(int argc, char **argv) { } struct v84_response resp; - if (http_get(url, bearer, &resp) != 0) { fprintf(stderr, "error: request failed\n"); return 1; } + if (http_get(url, bearer, cfg.agent_name, &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; } @@ -329,7 +362,7 @@ int main(int argc, char **argv) { 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 (http_get(url, bearer, cfg.agent_name, &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); @@ -345,7 +378,7 @@ int main(int argc, char **argv) { 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 (http_get(url, bearer, cfg.agent_name, &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; } @@ -357,7 +390,7 @@ int main(int argc, char **argv) { 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 (http_get(url, bearer, cfg.agent_name, &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; } @@ -371,11 +404,25 @@ int main(int argc, char **argv) { 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; + cJSON *tier_j = cJSON_GetObjectItem(field, "tier"); + cJSON *l2_j = cJSON_GetObjectItem(field, "l2"); + int tier_val = tier_j ? tier_j->valueint : (l2_j && cJSON_IsTrue(l2_j) ? 2 : 1); if (!label) continue; - if (tier_val >= 3) printf("%s: [L3 -- requires hardware key]\n", label); - else printf("%s: %s\n", label, value ? value : ""); + + if (tier_val >= 3) { + printf("%s: [L3 -- requires hardware key]\n", label); + } else if (tier_val == 2 && value && value[0]) { + /* Decrypt L2 field locally */ + char *pt = jsbridge_decrypt_field(cfg.l2_key, 16, label, value); + if (pt) { + printf("%s: %s\n", label, pt); + free(pt); + } else { + printf("%s: [L2 -- decryption failed]\n", label); + } + } else { + printf("%s: %s\n", label, value ? value : ""); + } } } cJSON_Delete(entry); diff --git a/cli/vault1984-cli b/cli/vault1984-cli index 3f51204..61ef6fc 100755 Binary files a/cli/vault1984-cli and b/cli/vault1984-cli differ diff --git a/crypto/crypto.js b/crypto/crypto.js index 97a6f1c..4de95dc 100644 --- a/crypto/crypto.js +++ b/crypto/crypto.js @@ -131,21 +131,37 @@ function hkdf_sha256(ikm, salt, info, length) { * @param {string} plaintext - field value to encrypt * @returns {string|Promise} base64-encoded ciphertext */ +/* + * Normalize key for AES: 8-byte keys are doubled to 16 bytes. + * AES requires 16, 24, or 32 byte keys. + * HKDF output length matches the (normalized) key length. + */ +function normalize_key(key) { + if (key.length === 8) { + var doubled = new Uint8Array(16); + doubled.set(key, 0); + doubled.set(key, 8); + return doubled; + } + return key; +} + function encrypt_field(key, field_label, plaintext) { var info_str = 'vault1984-field-' + field_label; - var key_len = key.length; + var nkey = normalize_key(key); + var aes_len = nkey.length; /* 16 or 32 */ if (IS_BROWSER) { var enc = new TextEncoder(); var info = enc.encode(info_str); - return hkdf_sha256(key, null, info, key_len).then(function(field_key) { + return hkdf_sha256(nkey, null, info, aes_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(key, null, info, key_len); + var field_key = native_hkdf_sha256(nkey, null, info, aes_len); var pt_bytes = native_encode_utf8(plaintext); var ct = native_aes_gcm_encrypt(field_key, pt_bytes); return native_base64_encode(ct); @@ -162,13 +178,14 @@ function encrypt_field(key, field_label, plaintext) { */ function decrypt_field(key, field_label, ciphertext_b64) { var info_str = 'vault1984-field-' + field_label; - var key_len = key.length; + var nkey = normalize_key(key); + var aes_len = nkey.length; if (IS_BROWSER) { var enc = new TextEncoder(); var dec = new TextDecoder(); var info = enc.encode(info_str); - return hkdf_sha256(key, null, info, key_len).then(function(field_key) { + return hkdf_sha256(nkey, null, info, aes_len).then(function(field_key) { var ct = base64_to_uint8(ciphertext_b64); return aes_gcm_decrypt(field_key, ct); }).then(function(pt) { @@ -176,7 +193,7 @@ function decrypt_field(key, field_label, ciphertext_b64) { }); } else { var info = native_encode_utf8(info_str); - var field_key = native_hkdf_sha256(key, null, info, key_len); + var field_key = native_hkdf_sha256(nkey, null, info, aes_len); var ct = native_base64_decode(ciphertext_b64); var pt = native_aes_gcm_decrypt_blob(field_key, ct); return native_decode_utf8(pt);