chore: auto-commit uncommitted changes
This commit is contained in:
parent
ec2eb2ee71
commit
2420c964b9
Binary file not shown.
Binary file not shown.
Binary file not shown.
170
cli/src/http.c
170
cli/src/http.c
|
|
@ -34,6 +34,141 @@ typedef int socklen_t;
|
||||||
|
|
||||||
#include "bearssl.h"
|
#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 --- */
|
/* --- socket connect --- */
|
||||||
|
|
||||||
static SOCKET tcp_connect(const char *host, const char *port) {
|
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) --- */
|
/* --- HTTP GET (plain) --- */
|
||||||
|
|
||||||
static int http_get_plain(const struct parsed_url *pu, const char *bearer_token,
|
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);
|
SOCKET fd = tcp_connect(pu->host, pu->port);
|
||||||
if (fd == INVALID_SOCKET) return -1;
|
if (fd == INVALID_SOCKET) return -1;
|
||||||
|
|
||||||
char request[2048];
|
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),
|
int reqlen = snprintf(request, sizeof(request),
|
||||||
"GET %s HTTP/1.1\r\n"
|
"GET %s HTTP/1.1\r\n"
|
||||||
"Host: %s\r\n"
|
"Host: %s\r\n"
|
||||||
"Authorization: Bearer %s\r\n"
|
"Authorization: Bearer %s\r\n"
|
||||||
|
"%s"
|
||||||
"Connection: close\r\n"
|
"Connection: close\r\n"
|
||||||
"Accept: application/json\r\n"
|
"Accept: application/json\r\n"
|
||||||
"\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) {
|
if (plain_send_all(fd, request, (size_t)reqlen) != 0) {
|
||||||
fprintf(stderr, "error: send failed\n");
|
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) --- */
|
/* --- HTTPS GET (BearSSL TLS) --- */
|
||||||
|
|
||||||
static int http_get_tls(const struct parsed_url *pu, const char *bearer_token,
|
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);
|
SOCKET fd = tcp_connect(pu->host, pu->port);
|
||||||
if (fd == INVALID_SOCKET) return -1;
|
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];
|
unsigned char iobuf[BR_SSL_BUFSIZE_BIDI];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO: load system CA bundle for production certificate validation.
|
* Load system CA certificates for TLS validation.
|
||||||
* For now: no trust anchors = no 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_rsa(&xc, &br_rsa_i31_pkcs1_vrfy);
|
||||||
br_x509_minimal_set_ecdsa(&xc, &br_ec_prime_i31, &br_ecdsa_i31_vrfy_asn1);
|
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_engine_set_buffer(&sc.eng, iobuf, sizeof(iobuf), 1);
|
||||||
br_ssl_client_reset(&sc, pu->host, 0);
|
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);
|
br_sslio_init(&ioc, &sc.eng, sock_read, &fd, sock_write, &fd);
|
||||||
|
|
||||||
char request[2048];
|
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),
|
int reqlen = snprintf(request, sizeof(request),
|
||||||
"GET %s HTTP/1.1\r\n"
|
"GET %s HTTP/1.1\r\n"
|
||||||
"Host: %s\r\n"
|
"Host: %s\r\n"
|
||||||
"Authorization: Bearer %s\r\n"
|
"Authorization: Bearer %s\r\n"
|
||||||
|
"%s"
|
||||||
"Connection: close\r\n"
|
"Connection: close\r\n"
|
||||||
"Accept: application/json\r\n"
|
"Accept: application/json\r\n"
|
||||||
"\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) {
|
if (br_sslio_write_all(&ioc, request, (size_t)reqlen) != 0) {
|
||||||
fprintf(stderr, "error: TLS write failed\n");
|
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 --- */
|
/* --- 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));
|
memset(resp, 0, sizeof(*resp));
|
||||||
|
|
||||||
struct parsed_url pu;
|
struct parsed_url pu;
|
||||||
if (parse_url(url, &pu) != 0) return -1;
|
if (parse_url(url, &pu) != 0) return -1;
|
||||||
|
|
||||||
if (pu.use_tls) {
|
if (pu.use_tls) {
|
||||||
return http_get_tls(&pu, bearer_token, resp);
|
return http_get_tls(&pu, bearer_token, agent_name, resp);
|
||||||
} else {
|
} else {
|
||||||
return http_get_plain(&pu, bearer_token, resp);
|
return http_get_plain(&pu, bearer_token, agent_name, resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ struct v84_response {
|
||||||
size_t body_len;
|
size_t body_len;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Perform HTTPS GET with Bearer auth. Returns 0 on success, -1 on error. */
|
/* Perform HTTPS GET with Bearer auth + optional X-Agent header.
|
||||||
int http_get(const char *url, const char *bearer_token, struct v84_response *resp);
|
* 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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -503,43 +503,37 @@ char *jsbridge_eval(const char *code) {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len,
|
char *jsbridge_encrypt_field(const unsigned char *key, size_t key_len,
|
||||||
const char *entry_id, const char *field_label,
|
const char *field_label, const char *plaintext) {
|
||||||
const char *plaintext) {
|
|
||||||
if (!ctx) return NULL;
|
if (!ctx) return NULL;
|
||||||
|
|
||||||
/* Call vault1984.crypto.l2_encrypt_field(key, entry_id, label, plaintext) */
|
|
||||||
JSValue global = JS_GetGlobalObject(ctx);
|
JSValue global = JS_GetGlobalObject(ctx);
|
||||||
JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
|
JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
|
||||||
JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto");
|
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 key_arr = js_new_uint8array(ctx, key, key_len);
|
||||||
JSValue args[4] = {
|
JSValue args[3] = {
|
||||||
key_arr,
|
key_arr,
|
||||||
JS_NewString(ctx, entry_id),
|
|
||||||
JS_NewString(ctx, field_label),
|
JS_NewString(ctx, field_label),
|
||||||
JS_NewString(ctx, plaintext)
|
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;
|
char *out = NULL;
|
||||||
if (!JS_IsException(result)) {
|
if (!JS_IsException(result)) {
|
||||||
const char *str = JS_ToCString(ctx, result);
|
const char *str = JS_ToCString(ctx, result);
|
||||||
if (str) {
|
if (str) { out = strdup(str); JS_FreeCString(ctx, str); }
|
||||||
out = strdup(str);
|
|
||||||
JS_FreeCString(ctx, str);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
JSValue exc = JS_GetException(ctx);
|
JSValue exc = JS_GetException(ctx);
|
||||||
const char *msg = JS_ToCString(ctx, exc);
|
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);
|
if (msg) JS_FreeCString(ctx, msg);
|
||||||
JS_FreeValue(ctx, exc);
|
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, result);
|
||||||
JS_FreeValue(ctx, fn);
|
JS_FreeValue(ctx, fn);
|
||||||
JS_FreeValue(ctx, crypto_obj);
|
JS_FreeValue(ctx, crypto_obj);
|
||||||
|
|
@ -548,42 +542,37 @@ char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len,
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *jsbridge_l2_decrypt(const unsigned char *l2_key, size_t key_len,
|
char *jsbridge_decrypt_field(const unsigned char *key, size_t key_len,
|
||||||
const char *entry_id, const char *field_label,
|
const char *field_label, const char *ciphertext_b64) {
|
||||||
const char *ciphertext_b64) {
|
|
||||||
if (!ctx) return NULL;
|
if (!ctx) return NULL;
|
||||||
|
|
||||||
JSValue global = JS_GetGlobalObject(ctx);
|
JSValue global = JS_GetGlobalObject(ctx);
|
||||||
JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
|
JSValue v84 = JS_GetPropertyStr(ctx, global, "vault1984");
|
||||||
JSValue crypto_obj = JS_GetPropertyStr(ctx, v84, "crypto");
|
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 key_arr = js_new_uint8array(ctx, key, key_len);
|
||||||
JSValue args[4] = {
|
JSValue args[3] = {
|
||||||
key_arr,
|
key_arr,
|
||||||
JS_NewString(ctx, entry_id),
|
|
||||||
JS_NewString(ctx, field_label),
|
JS_NewString(ctx, field_label),
|
||||||
JS_NewString(ctx, ciphertext_b64)
|
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;
|
char *out = NULL;
|
||||||
if (!JS_IsException(result)) {
|
if (!JS_IsException(result)) {
|
||||||
const char *str = JS_ToCString(ctx, result);
|
const char *str = JS_ToCString(ctx, result);
|
||||||
if (str) {
|
if (str) { out = strdup(str); JS_FreeCString(ctx, str); }
|
||||||
out = strdup(str);
|
|
||||||
JS_FreeCString(ctx, str);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
JSValue exc = JS_GetException(ctx);
|
JSValue exc = JS_GetException(ctx);
|
||||||
const char *msg = JS_ToCString(ctx, exc);
|
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);
|
if (msg) JS_FreeCString(ctx, msg);
|
||||||
JS_FreeValue(ctx, exc);
|
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, result);
|
||||||
JS_FreeValue(ctx, fn);
|
JS_FreeValue(ctx, fn);
|
||||||
JS_FreeValue(ctx, crypto_obj);
|
JS_FreeValue(ctx, crypto_obj);
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,20 @@ int jsbridge_init(void);
|
||||||
void jsbridge_cleanup(void);
|
void jsbridge_cleanup(void);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Encrypt a field value using L2 key.
|
* Encrypt a field value. Key length determines tier (8=L1, 16=L2, 32=L3).
|
||||||
* Returns base64-encoded ciphertext (caller frees).
|
* Uses crypto.js encrypt_field() — the single source of truth.
|
||||||
* Returns NULL on error.
|
* Returns base64-encoded ciphertext (caller frees). NULL on error.
|
||||||
*/
|
*/
|
||||||
char *jsbridge_l2_encrypt(const unsigned char *l2_key, size_t key_len,
|
char *jsbridge_encrypt_field(const unsigned char *key, size_t key_len,
|
||||||
const char *entry_id, const char *field_label,
|
const char *field_label, const char *plaintext);
|
||||||
const char *plaintext);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Decrypt a field value using L2 key.
|
* Decrypt a field value. Key length determines tier (8=L1, 16=L2, 32=L3).
|
||||||
* Returns plaintext string (caller frees).
|
* Uses crypto.js decrypt_field() — the single source of truth.
|
||||||
* Returns NULL on error.
|
* Returns plaintext string (caller frees). NULL on error.
|
||||||
*/
|
*/
|
||||||
char *jsbridge_l2_decrypt(const unsigned char *l2_key, size_t key_len,
|
char *jsbridge_decrypt_field(const unsigned char *key, size_t key_len,
|
||||||
const char *entry_id, const char *field_label,
|
const char *field_label, const char *ciphertext_b64);
|
||||||
const char *ciphertext_b64);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generate TOTP code from a base32-encoded seed.
|
* Generate TOTP code from a base32-encoded seed.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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 --- */
|
/* --- test commands --- */
|
||||||
|
|
@ -126,11 +126,11 @@ static int cmd_test_crypto(void) {
|
||||||
|
|
||||||
/* L2 encrypt/decrypt via C API */
|
/* L2 encrypt/decrypt via C API */
|
||||||
fprintf(stderr, " encrypting: \"s3cret-p@ssw0rd!\"\n");
|
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; }
|
if (!ct) { fprintf(stderr, "FAIL: l2_encrypt\n"); jsbridge_cleanup(); return 1; }
|
||||||
fprintf(stderr, " ciphertext: %s\n", ct);
|
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);
|
free(ct);
|
||||||
if (!pt || strcmp(pt, "s3cret-p@ssw0rd!") != 0) {
|
if (!pt || strcmp(pt, "s3cret-p@ssw0rd!") != 0) {
|
||||||
fprintf(stderr, "FAIL: l2_decrypt\n"); free(pt); jsbridge_cleanup(); return 1;
|
fprintf(stderr, "FAIL: l2_decrypt\n"); free(pt); jsbridge_cleanup(); return 1;
|
||||||
|
|
@ -139,10 +139,10 @@ static int cmd_test_crypto(void) {
|
||||||
free(pt);
|
free(pt);
|
||||||
|
|
||||||
/* Wrong key rejection */
|
/* 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) {
|
if (ct2) {
|
||||||
unsigned char wrong[16] = {0};
|
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; }
|
if (bad) { fprintf(stderr, "FAIL: wrong key accepted\n"); free(bad); free(ct2); jsbridge_cleanup(); return 1; }
|
||||||
fprintf(stderr, " wrong key rejection: OK\n");
|
fprintf(stderr, " wrong key rejection: OK\n");
|
||||||
free(ct2);
|
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, "--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, "--version") == 0) { printf("vault1984-cli %s\n", VERSION); return 0; }
|
||||||
if (strcmp(cmd, "test-crypto") == 0) { return cmd_test_crypto(); }
|
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 (strcmp(cmd, "test-totp") == 0) {
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "usage: vault1984-cli test-totp <base32-seed>\n");
|
fprintf(stderr, "usage: vault1984-cli test-totp <base32-seed>\n");
|
||||||
|
|
@ -220,9 +254,8 @@ int main(int argc, char **argv) {
|
||||||
char js_code[2048];
|
char js_code[2048];
|
||||||
snprintf(js_code, sizeof(js_code),
|
snprintf(js_code, sizeof(js_code),
|
||||||
"(function() {"
|
"(function() {"
|
||||||
" var enc = new TextEncoder();"
|
" var seed = native_encode_utf8('vault1984-l2-');"
|
||||||
" var seed = enc.encode('vault1984-l2-');"
|
" var encKey = native_hkdf_sha256(seed, null, native_encode_utf8('token'), 16);"
|
||||||
" var encKey = native_hkdf_sha256(seed, null, enc.encode('token'), 16);"
|
|
||||||
" var ct = native_base64_decode('%s');"
|
" var ct = native_base64_decode('%s');"
|
||||||
" var pt = native_aes_gcm_decrypt_blob(encKey, ct);"
|
" var pt = native_aes_gcm_decrypt_blob(encKey, ct);"
|
||||||
" return native_base64_encode(pt);"
|
" return native_base64_encode(pt);"
|
||||||
|
|
@ -295,7 +328,7 @@ int main(int argc, char **argv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct v84_response resp;
|
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 (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; }
|
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);
|
build_url(url, sizeof(url), &cfg, path);
|
||||||
|
|
||||||
struct v84_response resp;
|
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; }
|
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);
|
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);
|
snprintf(path, sizeof(path), "/api/ext/totp/%s", entry_id);
|
||||||
build_url(url, sizeof(url), &cfg, path);
|
build_url(url, sizeof(url), &cfg, path);
|
||||||
cJSON_Delete(results);
|
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; }
|
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);
|
cJSON *totp = cJSON_Parse(resp.body); free(resp.body);
|
||||||
if (!totp) { fprintf(stderr, "error: invalid JSON\n"); return 1; }
|
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);
|
snprintf(path, sizeof(path), "/api/entries/%s", entry_id);
|
||||||
build_url(url, sizeof(url), &cfg, path);
|
build_url(url, sizeof(url), &cfg, path);
|
||||||
cJSON_Delete(results);
|
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 (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; }
|
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) {
|
cJSON_ArrayForEach(field, fields) {
|
||||||
const char *label = cJSON_GetStringValue(cJSON_GetObjectItem(field, "label"));
|
const char *label = cJSON_GetStringValue(cJSON_GetObjectItem(field, "label"));
|
||||||
const char *value = cJSON_GetStringValue(cJSON_GetObjectItem(field, "value"));
|
const char *value = cJSON_GetStringValue(cJSON_GetObjectItem(field, "value"));
|
||||||
cJSON *tier = cJSON_GetObjectItem(field, "tier");
|
cJSON *tier_j = cJSON_GetObjectItem(field, "tier");
|
||||||
int tier_val = tier ? tier->valueint : 1;
|
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 (!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);
|
cJSON_Delete(entry);
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -131,21 +131,37 @@ function hkdf_sha256(ikm, salt, info, length) {
|
||||||
* @param {string} plaintext - field value to encrypt
|
* @param {string} plaintext - field value to encrypt
|
||||||
* @returns {string|Promise<string>} base64-encoded ciphertext
|
* @returns {string|Promise<string>} 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) {
|
function encrypt_field(key, field_label, plaintext) {
|
||||||
var info_str = 'vault1984-field-' + field_label;
|
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) {
|
if (IS_BROWSER) {
|
||||||
var enc = new TextEncoder();
|
var enc = new TextEncoder();
|
||||||
var info = enc.encode(info_str);
|
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));
|
return aes_gcm_encrypt(field_key, enc.encode(plaintext));
|
||||||
}).then(function(ct) {
|
}).then(function(ct) {
|
||||||
return uint8_to_base64(ct);
|
return uint8_to_base64(ct);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var info = native_encode_utf8(info_str);
|
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 pt_bytes = native_encode_utf8(plaintext);
|
||||||
var ct = native_aes_gcm_encrypt(field_key, pt_bytes);
|
var ct = native_aes_gcm_encrypt(field_key, pt_bytes);
|
||||||
return native_base64_encode(ct);
|
return native_base64_encode(ct);
|
||||||
|
|
@ -162,13 +178,14 @@ function encrypt_field(key, field_label, plaintext) {
|
||||||
*/
|
*/
|
||||||
function decrypt_field(key, field_label, ciphertext_b64) {
|
function decrypt_field(key, field_label, ciphertext_b64) {
|
||||||
var info_str = 'vault1984-field-' + field_label;
|
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) {
|
if (IS_BROWSER) {
|
||||||
var enc = new TextEncoder();
|
var enc = new TextEncoder();
|
||||||
var dec = new TextDecoder();
|
var dec = new TextDecoder();
|
||||||
var info = enc.encode(info_str);
|
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);
|
var ct = base64_to_uint8(ciphertext_b64);
|
||||||
return aes_gcm_decrypt(field_key, ct);
|
return aes_gcm_decrypt(field_key, ct);
|
||||||
}).then(function(pt) {
|
}).then(function(pt) {
|
||||||
|
|
@ -176,7 +193,7 @@ function decrypt_field(key, field_label, ciphertext_b64) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
var info = native_encode_utf8(info_str);
|
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 ct = native_base64_decode(ciphertext_b64);
|
||||||
var pt = native_aes_gcm_decrypt_blob(field_key, ct);
|
var pt = native_aes_gcm_decrypt_blob(field_key, ct);
|
||||||
return native_decode_utf8(pt);
|
return native_decode_utf8(pt);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue