| new file mode 100644 |
| index 0000000..83b73bf |
| --- /dev/null |
| +++ b/vectors/gen/gen.c |
| @@ -0,0 +1,977 @@ |
| +/* |
| + * gen โ produce vectors/contra-v1.json from DESIGN.md, using OpenSSL. |
| + * |
| + * Why this is C and not Go: discussion #6 forbids generating the vectors |
| + * from an implementation written in the same session and then testing that |
| + * implementation against them, because it proves nothing. So the generator |
| + * is a second, independent implementation โ OpenSSL 3.x, which nobody here |
| + * wrote โ and the Go package under test never contributes a byte to this |
| + * file. |
| + * |
| + * Before it generates anything, main() re-computes the published third-party |
| + * vectors in `external_anchors` and aborts if any disagrees. That is what |
| + * stops "independent" from meaning "differently wrong": the PHC Argon2id |
| + * reference outputs, RFC 5869 HKDF and RFC 4231 HMAC all have to come back |
| + * right first. |
| + * |
| + * Every fixture input is itself derived from a published label โ |
| + * |
| + * fixture(label, n) = SHA-256("contra/v1/vectors/" || label)[0..n] |
| + * |
| + * so the whole file regenerates from nothing but this source, and no value |
| + * in it can be mistaken for a real key. AGENTS.md decision 8: fixtures are |
| + * generated, never real. |
| + * |
| + * Build: make Run: ./gen > ../contra-v1.json |
| + */ |
| +#include <stdarg.h> |
| +#include <stdint.h> |
| +#include <stdio.h> |
| +#include <stdlib.h> |
| +#include <string.h> |
| + |
| +#include <openssl/bn.h> |
| +#include <openssl/core_names.h> |
| +#include <openssl/ec.h> |
| +#include <openssl/evp.h> |
| +#include <openssl/kdf.h> |
| +#include <openssl/obj_mac.h> |
| +#include <openssl/params.h> |
| + |
| +/* ---------------------------------------------------------------- basics */ |
| + |
| +static void die(const char *what) { |
| + fprintf(stderr, "gen: %s\n", what); |
| + exit(1); |
| +} |
| + |
| +/* Rotating hex buffers, so several hex() calls can share one printf. */ |
| +static const char *hex(const unsigned char *b, size_t n) { |
| + static char pool[16][600]; |
| + static int slot = 0; |
| + char *out = pool[slot = (slot + 1) % 16]; |
| + size_t i; |
| + |
| + if (n * 2 + 1 > sizeof pool[0]) die("hex buffer too small"); |
| + for (i = 0; i < n; i++) sprintf(out + i * 2, "%02x", b[i]); |
| + out[n * 2] = 0; |
| + return out; |
| +} |
| + |
| +static void sha256(const unsigned char *m, size_t n, unsigned char out[32]) { |
| + unsigned int len = 32; |
| + if (!EVP_Digest(m, n, out, &len, EVP_sha256(), NULL)) die("sha256"); |
| +} |
| + |
| +static void hmac_sha256(const unsigned char *key, size_t keylen, |
| + const unsigned char *msg, size_t msglen, |
| + unsigned char out[32]) { |
| + EVP_MAC *mac = EVP_MAC_fetch(NULL, "HMAC", NULL); |
| + EVP_MAC_CTX *ctx; |
| + OSSL_PARAM p[2]; |
| + size_t outlen = 0; |
| + |
| + if (!mac) die("fetch HMAC"); |
| + ctx = EVP_MAC_CTX_new(mac); |
| + if (!ctx) die("HMAC ctx"); |
| + p[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_DIGEST, "SHA256", 0); |
| + p[1] = OSSL_PARAM_construct_end(); |
| + if (!EVP_MAC_init(ctx, key, keylen, p)) die("HMAC init"); |
| + if (!EVP_MAC_update(ctx, msg, msglen)) die("HMAC update"); |
| + if (!EVP_MAC_final(ctx, out, &outlen, 32)) die("HMAC final"); |
| + EVP_MAC_CTX_free(ctx); |
| + EVP_MAC_free(mac); |
| +} |
| + |
| +/* |
| + * HKDF-SHA-256, RFC 5869 extract-then-expand โ the one mode this protocol |
| + * uses (discussion #19). A NULL salt is a zero-length salt, which RFC 5869 |
| + * ยง2.2 turns into HashLen zero bytes. |
| + */ |
| +static void hkdf(const unsigned char *ikm, size_t ikmlen, |
| + const unsigned char *salt, size_t saltlen, |
| + const unsigned char *info, size_t infolen, |
| + unsigned char *out, size_t outlen) { |
| + EVP_KDF *kdf = EVP_KDF_fetch(NULL, "HKDF", NULL); |
| + EVP_KDF_CTX *ctx; |
| + OSSL_PARAM p[6]; |
| + int i = 0; |
| + static const unsigned char empty[1] = {0}; |
| + |
| + if (!kdf) die("fetch HKDF"); |
| + ctx = EVP_KDF_CTX_new(kdf); |
| + if (!ctx) die("HKDF ctx"); |
| + p[i++] = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, "SHA256", 0); |
| + p[i++] = OSSL_PARAM_construct_octet_string( |
| + OSSL_KDF_PARAM_KEY, (void *)(ikmlen ? ikm : empty), ikmlen); |
| + p[i++] = OSSL_PARAM_construct_octet_string( |
| + OSSL_KDF_PARAM_SALT, (void *)(saltlen ? salt : empty), saltlen); |
| + p[i++] = OSSL_PARAM_construct_octet_string( |
| + OSSL_KDF_PARAM_INFO, (void *)(infolen ? info : empty), infolen); |
| + p[i] = OSSL_PARAM_construct_end(); |
| + if (EVP_KDF_derive(ctx, out, outlen, p) <= 0) die("HKDF derive"); |
| + EVP_KDF_CTX_free(ctx); |
| + EVP_KDF_free(kdf); |
| +} |
| + |
| +static void hkdf_label(const unsigned char *ikm, size_t ikmlen, |
| + const unsigned char *salt, size_t saltlen, |
| + const char *label, unsigned char *out, size_t outlen) { |
| + hkdf(ikm, ikmlen, salt, saltlen, (const unsigned char *)label, strlen(label), |
| + out, outlen); |
| +} |
| + |
| +static void argon2id(const unsigned char *pass, size_t passlen, |
| + const unsigned char *salt, size_t saltlen, uint32_t m_kib, |
| + uint32_t t, uint32_t lanes, unsigned char *out, |
| + size_t outlen) { |
| + EVP_KDF *kdf = EVP_KDF_fetch(NULL, "ARGON2ID", NULL); |
| + EVP_KDF_CTX *ctx; |
| + OSSL_PARAM p[7]; |
| + uint32_t threads = 1; |
| + int i = 0; |
| + |
| + if (!kdf) die("fetch ARGON2ID (needs OpenSSL 3.2+)"); |
| + ctx = EVP_KDF_CTX_new(kdf); |
| + if (!ctx) die("argon2 ctx"); |
| + p[i++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, |
| + (void *)pass, passlen); |
| + p[i++] = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, |
| + (void *)salt, saltlen); |
| + p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &t); |
| + p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &m_kib); |
| + p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes); |
| + p[i++] = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads); |
| + p[i] = OSSL_PARAM_construct_end(); |
| + if (EVP_KDF_derive(ctx, out, outlen, p) <= 0) die("argon2 derive"); |
| + EVP_KDF_CTX_free(ctx); |
| + EVP_KDF_free(kdf); |
| +} |
| + |
| +/* --------------------------------------------------------------- fixtures */ |
| + |
| +/* |
| + * fixture(label, n) โ every synthetic input in the file, derived from its own |
| + * published label so the file regenerates from this source alone. |
| + */ |
| +static void fixture(const char *label, unsigned char *out, size_t n) { |
| + char buf[128]; |
| + unsigned char d[32]; |
| + |
| + if (n > 32) die("fixture longer than SHA-256"); |
| + if (snprintf(buf, sizeof buf, "contra/v1/vectors/%s", label) >= (int)sizeof buf) |
| + die("fixture label too long"); |
| + sha256((const unsigned char *)buf, strlen(buf), d); |
| + memcpy(out, d, n); |
| +} |
| + |
| +/* ------------------------------------------------------ P-256 from a seed */ |
| + |
| +#define EC_LABEL "contra/v1/ec-p256" |
| + |
| +struct p256 { |
| + unsigned char cand[8][32]; /* candidates tried, in order */ |
| + int ncand; /* how many, including the accepted one */ |
| + unsigned char d[32]; /* the accepted scalar */ |
| + unsigned char pub[65]; /* SEC1 uncompressed 0x04 || X || Y */ |
| +}; |
| + |
| +/* |
| + * Rejection sampling exactly as pinned in discussion #19: expand the seed |
| + * under "contra/v1/ec-p256" || byte(i), reject anything outside [1, n-1], |
| + * and step i. Rejection has probability ~2^-32, so `p256_rejecting_seed` in |
| + * the vector file is the only reason this loop is ever tested. |
| + */ |
| +static void p256_from_seed(const unsigned char seed[32], struct p256 *r) { |
| + EC_GROUP *g = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1); |
| + const BIGNUM *n; |
| + EC_POINT *pt; |
| + BIGNUM *d; |
| + unsigned char info[sizeof EC_LABEL]; /* label bytes + counter, no NUL */ |
| + int i; |
| + |
| + if (!g) die("P-256 group"); |
| + n = EC_GROUP_get0_order(g); |
| + memcpy(info, EC_LABEL, sizeof EC_LABEL - 1); |
| + memset(r, 0, sizeof *r); |
| + |
| + for (i = 0; i < 8; i++) { |
| + info[sizeof EC_LABEL - 1] = (unsigned char)i; |
| + hkdf(seed, 32, NULL, 0, info, sizeof info, r->cand[i], 32); |
| + r->ncand = i + 1; |
| + |
| + d = BN_bin2bn(r->cand[i], 32, NULL); |
| + if (!d) die("BN_bin2bn"); |
| + if (!BN_is_zero(d) && BN_cmp(d, n) < 0) { |
| + memcpy(r->d, r->cand[i], 32); |
| + pt = EC_POINT_new(g); |
| + if (!pt || !EC_POINT_mul(g, pt, d, NULL, NULL, NULL)) |
| + die("EC_POINT_mul"); |
| + if (EC_POINT_point2oct(g, pt, POINT_CONVERSION_UNCOMPRESSED, r->pub, |
| + sizeof r->pub, NULL) != sizeof r->pub) |
| + die("point2oct"); |
| + EC_POINT_free(pt); |
| + BN_free(d); |
| + EC_GROUP_free(g); |
| + return; |
| + } |
| + BN_free(d); |
| + } |
| + die("P-256 derivation exhausted 8 candidates"); |
| +} |
| + |
| +/* --------------------------------------------------------- AEAD, with AAD */ |
| + |
| +/* |
| + * The canonical AAD, pinned in discussion #19. Length-prefixed so the tuple |
| + * is injective โ plain concatenation lets different tuples produce identical |
| + * AAD, which is the exact transplant the binding exists to stop โ and |
| + * domain-tagged so it cannot be replayed into another protocol. |
| + * |
| + * The scope id is the account id for account-scoped objects and the vault id |
| + * for vault-scoped ones, which is how DESIGN.md's two spellings of the |
| + * binding โ "(account_id, object_type, object_id, version)" and "(vault_id, |
| + * item_id, version)" โ turn out to be the same rule. |
| + */ |
| +static size_t build_aad(const unsigned char *scope_id, size_t scope_len, |
| + const char *object_type, const unsigned char *object_id, |
| + size_t object_id_len, uint64_t version, |
| + unsigned char *out, size_t cap) { |
| + size_t n = 0, tlen = strlen(object_type); |
| + int i; |
| + |
| +#define PUT(p, l) \ |
| + do { \ |
| + if (n + (l) > cap) die("AAD buffer"); \ |
| + memcpy(out + n, (p), (l)); \ |
| + n += (l); \ |
| + } while (0) |
| +#define PUT_U32(v) \ |
| + do { \ |
| + unsigned char b4[4]; \ |
| + b4[0] = (unsigned char)((v) >> 24); \ |
| + b4[1] = (unsigned char)((v) >> 16); \ |
| + b4[2] = (unsigned char)((v) >> 8); \ |
| + b4[3] = (unsigned char)(v); \ |
| + PUT(b4, 4); \ |
| + } while (0) |
| + |
| + PUT("contra/v1/aad", 13); |
| + PUT("\0", 1); |
| + PUT_U32((uint32_t)scope_len); |
| + PUT(scope_id, scope_len); |
| + PUT_U32((uint32_t)tlen); |
| + PUT(object_type, tlen); |
| + PUT_U32((uint32_t)object_id_len); |
| + PUT(object_id, object_id_len); |
| + for (i = 7; i >= 0; i--) { |
| + unsigned char b = (unsigned char)(version >> (8 * i)); |
| + PUT(&b, 1); |
| + } |
| +#undef PUT_U32 |
| +#undef PUT |
| + return n; |
| +} |
| + |
| +static void aes256gcm(const unsigned char key[32], const unsigned char nonce[12], |
| + const unsigned char *aad, size_t aadlen, |
| + const unsigned char *pt, size_t ptlen, unsigned char *ct, |
| + unsigned char tag[16]) { |
| + EVP_CIPHER_CTX *c = EVP_CIPHER_CTX_new(); |
| + unsigned char scratch[16]; |
| + int len = 0; |
| + |
| + if (!c) die("gcm ctx"); |
| + if (!EVP_EncryptInit_ex(c, EVP_aes_256_gcm(), NULL, NULL, NULL)) |
| + die("gcm init"); |
| + if (!EVP_CIPHER_CTX_ctrl(c, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL)) |
| + die("gcm ivlen"); |
| + if (!EVP_EncryptInit_ex(c, NULL, NULL, key, nonce)) die("gcm key"); |
| + if (aadlen && !EVP_EncryptUpdate(c, NULL, &len, aad, (int)aadlen)) |
| + die("gcm aad"); |
| + if (ptlen && !EVP_EncryptUpdate(c, ct, &len, pt, (int)ptlen)) |
| + die("gcm update"); |
| + /* GCM is a stream cipher, so Final never emits bytes; give it somewhere |
| + * to write anyway rather than offsetting a possibly-NULL ct. */ |
| + if (!EVP_EncryptFinal_ex(c, scratch, &len)) die("gcm final"); |
| + if (!EVP_CIPHER_CTX_ctrl(c, EVP_CTRL_AEAD_GET_TAG, 16, tag)) die("gcm tag"); |
| + EVP_CIPHER_CTX_free(c); |
| +} |
| + |
| +/* ------------------------------------------------------- Crockford Base32 */ |
| + |
| +static const char CROCKFORD[33] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; |
| + |
| +/* |
| + * Byte-oriented, MSB-first, left-aligned (discussion #19) โ 16 bytes become |
| + * 26 symbols with two zero pad bits in the last one. Byte packing rather than |
| + * Crockford's big-integer form because the JS console has a 300-line module |
| + * cap and no business carrying a bignum. |
| + */ |
| +static void crockford_encode(const unsigned char *in, size_t nbytes, char *out) { |
| + size_t nsym = (nbytes * 8 + 4) / 5, i; |
| + |
| + for (i = 0; i < nsym; i++) { |
| + size_t bit = i * 5, byte = bit / 8, off = bit % 8; |
| + unsigned v = (unsigned)in[byte] << 8; |
| + if (byte + 1 < nbytes) v |= in[byte + 1]; |
| + out[i] = CROCKFORD[(v >> (11 - off)) & 0x1f]; |
| + } |
| + out[nsym] = 0; |
| +} |
| + |
| +/* The 20 bits of SHA-256(payload) that become the 4 checksum symbols. */ |
| +static uint32_t checksum20(const unsigned char *payload, size_t n) { |
| + unsigned char d[32]; |
| + sha256(payload, n, d); |
| + return ((uint32_t)d[0] << 12) | ((uint32_t)d[1] << 4) | (uint32_t)(d[2] >> 4); |
| +} |
| + |
| +static void checksum_symbols(uint32_t c20, char out[5]) { |
| + int i; |
| + for (i = 0; i < 4; i++) out[i] = CROCKFORD[(c20 >> (15 - 5 * i)) & 0x1f]; |
| + out[4] = 0; |
| +} |
| + |
| +static int crockford_value(char c) { |
| + const char *p; |
| + if (c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A'); |
| + if (c == 'O') c = '0'; |
| + if (c == 'I' || c == 'L') c = '1'; |
| + p = strchr(CROCKFORD, c); |
| + return (p && c) ? (int)(p - CROCKFORD) : -1; |
| +} |
| + |
| +/* |
| + * Normalisation: strip hyphens and whitespace, uppercase, O->0, I->1, L->1. |
| + * The tag survives it unchanged, so normalise first and match the tag after. |
| + * Returns 0 if any character is outside the alphabet โ `U` is the one people |
| + * ask about, and it is an error, not a repair candidate. |
| + */ |
| +static int crockford_normalize(const char *in, char *out, size_t cap) { |
| + size_t n = 0; |
| + |
| + for (; *in; in++) { |
| + if (*in == '-' || *in == ' ' || *in == '\t' || *in == '\n') continue; |
| + if (crockford_value(*in) < 0) return 0; |
| + if (n + 1 >= cap) return 0; |
| + out[n++] = CROCKFORD[crockford_value(*in)]; |
| + } |
| + out[n] = 0; |
| + return 1; |
| +} |
| + |
| +/* |
| + * Strict decode of 26 symbols back to 16 bytes. A non-zero pad in the last |
| + * symbol is rejected, so every value has exactly one encoding โ without that |
| + * two strings decode to one key and the checksum stops meaning what it says. |
| + */ |
| +static int crockford_decode16(const char *sym, unsigned char out[16]) { |
| + unsigned acc = 0; |
| + int nbits = 0, i, o = 0; |
| + |
| + for (i = 0; i < 26; i++) { |
| + int v = crockford_value(sym[i]); |
| + if (v < 0) return 0; |
| + acc = (acc << 5) | (unsigned)v; |
| + nbits += 5; |
| + if (nbits >= 8) { |
| + nbits -= 8; |
| + if (o >= 16) return 0; |
| + out[o++] = (unsigned char)((acc >> nbits) & 0xff); |
| + } |
| + } |
| + return o == 16 && (acc & ((1u << nbits) - 1)) == 0; |
| +} |
| + |
| +/* tag(2) || data(26) || checksum(4), grouped five at a time for printing. */ |
| +static void printed_form(const char *tag, const unsigned char payload[16], |
| + char normalized[33], char printed[40]) { |
| + char data[27], ck[5]; |
| + int i, o = 0; |
| + |
| + crockford_encode(payload, 16, data); |
| + checksum_symbols(checksum20(payload, 16), ck); |
| + snprintf(normalized, 33, "%s%s%s", tag, data, ck); |
| + |
| + memcpy(printed, tag, 2); |
| + o = 2; |
| + for (i = 0; i < 30; i++) { |
| + if (i % 5 == 0) printed[o++] = '-'; |
| + printed[o++] = normalized[2 + i]; |
| + } |
| + printed[o] = 0; |
| +} |
| + |
| +/* A normalised 32-character code is valid when it decodes and its own |
| + * checksum symbols match. Nothing here looks at the tag. */ |
| +static int code_valid(const char *norm, unsigned char payload[16]) { |
| + char want[5]; |
| + if (strlen(norm) != 32) return 0; |
| + if (!crockford_decode16(norm + 2, payload)) return 0; |
| + checksum_symbols(checksum20(payload, 16), want); |
| + return memcmp(norm + 28, want, 4) == 0; |
| +} |
| + |
| +/* |
| + * Repair: try every single-character substitution over the 30 payload |
| + * positions and return every survivor, in position-then-alphabet order. It |
| + * deliberately does not pick one โ a 20-bit checksum collides about once in |
| + * 1100 attempts, and silently choosing would be worse than handing the caller |
| + * two candidates to try against the KCV. |
| + */ |
| +static int code_repair(const char *norm, char out[][33], int cap) { |
| + int n = 0, pos, s; |
| + |
| + for (pos = 2; pos < 32; pos++) { |
| + for (s = 0; s < 32; s++) { |
| + char cand[33]; |
| + unsigned char payload[16]; |
| + if (CROCKFORD[s] == norm[pos]) continue; |
| + memcpy(cand, norm, 33); |
| + cand[pos] = CROCKFORD[s]; |
| + if (!code_valid(cand, payload)) continue; |
| + if (n < cap) memcpy(out[n], cand, 33); |
| + n++; |
| + } |
| + } |
| + return n; |
| +} |
| + |
| +/* -------------------------------------------------------- JSON emit sugar */ |
| + |
| +static int need_comma = 0; |
| +static const char *indent = " "; |
| + |
| +static void field(const char *k, const char *fmt, ...) { |
| + va_list ap; |
| + if (need_comma) printf(",\n"); |
| + printf("%s\"%s\": ", indent, k); |
| + va_start(ap, fmt); |
| + vprintf(fmt, ap); |
| + va_end(ap); |
| + need_comma = 1; |
| +} |
| + |
| +/* ------------------------------------------------- external anchor checks */ |
| + |
| +static void expect(const char *what, const unsigned char *got, size_t n, |
| + const char *want_hex) { |
| + if (strcmp(hex(got, n), want_hex) != 0) { |
| + fprintf(stderr, "gen: anchor %s mismatch\n got %s\n want %s\n", what, |
| + hex(got, n), want_hex); |
| + exit(1); |
| + } |
| +} |
| + |
| +/* |
| + * Check OpenSSL against published third-party vectors before it is allowed to |
| + * generate ours. Without this, "independent implementation" only guarantees |
| + * the two implementations are not the same โ not that either is right. |
| + */ |
| +static void check_anchors(void) { |
| + unsigned char out[42]; |
| + static const unsigned char rfc5869_ikm[22] = { |
| + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, |
| + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b}; |
| + static const unsigned char rfc5869_salt[13] = {0x00, 0x01, 0x02, 0x03, 0x04, |
| + 0x05, 0x06, 0x07, 0x08, 0x09, |
| + 0x0a, 0x0b, 0x0c}; |
| + static const unsigned char rfc5869_info[10] = {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, |
| + 0xf5, 0xf6, 0xf7, 0xf8, 0xf9}; |
| + static const unsigned char rfc4231_key[20] = { |
| + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, |
| + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b}; |
| + |
| + /* PHC argon2 reference, Argon2id v=0x13, t=2 m=65536 p=1. */ |
| + argon2id((const unsigned char *)"password", 8, |
| + (const unsigned char *)"somesalt", 8, 65536, 2, 1, out, 32); |
| + expect("PHC Argon2id t=2 m=65536 p=1", out, 32, |
| + "09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7"); |
| + |
| + /* Same, at t=3 โ the parameters DESIGN.md actually pins. */ |
| + argon2id((const unsigned char *)"password", 8, |
| + (const unsigned char *)"somesalt", 8, 65536, 3, 1, out, 32); |
| + |
| + /* RFC 5869 test case 1. */ |
| + hkdf(rfc5869_ikm, sizeof rfc5869_ikm, rfc5869_salt, sizeof rfc5869_salt, |
| + rfc5869_info, sizeof rfc5869_info, out, 42); |
| + expect("RFC 5869 case 1", out, 42, |
| + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf" |
| + "34007208d5b887185865"); |
| + |
| + /* RFC 5869 test case 3: zero-length salt and info โ the shape every |
| + * unsalted CONTRA label uses. */ |
| + hkdf(rfc5869_ikm, sizeof rfc5869_ikm, NULL, 0, NULL, 0, out, 42); |
| + expect("RFC 5869 case 3", out, 42, |
| + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" |
| + "9d201395faa4b61a96c8"); |
| + |
| + /* RFC 4231 test case 1, HMAC-SHA-256. */ |
| + hmac_sha256(rfc4231_key, sizeof rfc4231_key, (const unsigned char *)"Hi There", |
| + 8, out); |
| + expect("RFC 4231 case 1", out, 32, |
| + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); |
| + |
| + /* NIST CAVP AES-256-GCM, all-zero key/IV, empty plaintext and AAD. */ |
| + { |
| + unsigned char key[32] = {0}, iv[12] = {0}, tag[16]; |
| + aes256gcm(key, iv, NULL, 0, NULL, 0, NULL, tag); |
| + expect("NIST AES-256-GCM empty", tag, 16, |
| + "530f8afbc74536b9a963b4f1c4cb738b"); |
| + } |
| +} |
| + |
| +/* ------------------------------------------------------------------- main */ |
| + |
| +struct wrap { |
| + const char *name; |
| + const char *object_type; |
| + unsigned char kek[32]; |
| + unsigned char kcv[16]; |
| + unsigned char wrap_id[16]; |
| + unsigned char nonce[12]; |
| + unsigned char aad[256]; |
| + size_t aadlen; |
| + unsigned char ct[32]; |
| + unsigned char tag[16]; |
| +}; |
| + |
| +/* KCV = HMAC-SHA-256(KEK, "contra/v1/kcv")[..16] โ AES-GCM is not |
| + * key-committing, so this is the commitment check, and it is also what tells |
| + * "wrong key" from "corrupted blob" on every unlock path. */ |
| +static void kcv_of(const unsigned char kek[32], unsigned char out[16]) { |
| + unsigned char full[32]; |
| + hmac_sha256(kek, 32, (const unsigned char *)"contra/v1/kcv", 13, full); |
| + memcpy(out, full, 16); |
| +} |
| + |
| +static void seal_wrap(struct wrap *w, const unsigned char account_id[16], |
| + const unsigned char rk[32]) { |
| + kcv_of(w->kek, w->kcv); |
| + w->aadlen = build_aad(account_id, 16, w->object_type, w->wrap_id, 16, 1, |
| + w->aad, sizeof w->aad); |
| + aes256gcm(w->kek, w->nonce, w->aad, w->aadlen, rk, 32, w->ct, w->tag); |
| +} |
| + |
| +static void emit_wrap(const struct wrap *w) { |
| + printf(" {\n"); |
| + need_comma = 0; |
| + field("name", "\"%s\"", w->name); |
| + field("object_type", "\"%s\"", w->object_type); |
| + field("kek_hex", "\"%s\"", hex(w->kek, 32)); |
| + field("kcv_hex", "\"%s\"", hex(w->kcv, 16)); |
| + field("wrap_id_hex", "\"%s\"", hex(w->wrap_id, 16)); |
| + field("version", "1"); |
| + field("nonce_hex", "\"%s\"", hex(w->nonce, 12)); |
| + field("aad_hex", "\"%s\"", hex(w->aad, w->aadlen)); |
| + field("ciphertext_hex", "\"%s\"", hex(w->ct, 32)); |
| + field("tag_hex", "\"%s\"", hex(w->tag, 16)); |
| + printf("\n }"); |
| +} |
| + |
| +int main(int argc, char **argv) { |
| + /* The witness k from seedsearch, passed in by the Makefile. */ |
| + unsigned long long reject_k = (argc > 1) ? strtoull(argv[1], NULL, 10) : 0; |
| + |
| + const char *passphrase = "contra-golden-vector-passphrase-do-not-use-8f3a1c"; |
| + unsigned char salt_pw[16], account_key[16], recovery_code[16]; |
| + unsigned char prf_output[32], cred_salt[32], rk[32], k_vault[32]; |
| + unsigned char account_id[16], vault_id[16], item_id[16]; |
| + unsigned char s_pw[32], seed_login[32], seed_rc[32]; |
| + unsigned char item_nonce[12], item_ct[256], item_tag[16], item_aad[256]; |
| + unsigned char reject_seed[32]; |
| + size_t item_aadlen, item_ptlen; |
| + struct p256 lk_pw, lk_rc, lk_reject; |
| + struct wrap w_pw, w_rc, w_prf; |
| + char ak_norm[33], ak_print[40], rc_norm[33], rc_print[40]; |
| + char typo[40]; |
| + const char *item_pt = |
| + "{\"title\":\"Generated vector item\",\"username\":\"vector@example." |
| + "invalid\",\"secret\":\"not-a-real-value\"}"; |
| + int i; |
| + |
| + check_anchors(); |
| + |
| + fixture("salt_pw", salt_pw, 16); |
| + fixture("account_key", account_key, 16); |
| + fixture("recovery_code", recovery_code, 16); |
| + fixture("prf_output", prf_output, 32); |
| + fixture("cred_salt", cred_salt, 32); |
| + fixture("rk", rk, 32); |
| + fixture("k_vault", k_vault, 32); |
| + fixture("account_id", account_id, 16); |
| + fixture("vault_id", vault_id, 16); |
| + fixture("item_id", item_id, 16); |
| + fixture("nonce/item", item_nonce, 12); |
| + |
| + /* The passphrase branch. One expensive Argon2id call, then two peer |
| + * subkeys under distinct labels โ holding one says nothing about the |
| + * other without inverting HMAC-SHA-256. */ |
| + argon2id((const unsigned char *)passphrase, strlen(passphrase), salt_pw, 16, |
| + 65536, 3, 1, s_pw, 32); |
| + hkdf_label(s_pw, 32, NULL, 0, "contra/v1/login", seed_login, 32); |
| + p256_from_seed(seed_login, &lk_pw); |
| + |
| + w_pw.name = "W_pw"; |
| + w_pw.object_type = "wrap.pw"; |
| + hkdf_label(s_pw, 32, account_key, 16, "contra/v1/kek", w_pw.kek, 32); |
| + fixture("wrap_id/pw", w_pw.wrap_id, 16); |
| + fixture("nonce/wrap_pw", w_pw.nonce, 12); |
| + seal_wrap(&w_pw, account_id, rk); |
| + |
| + /* The recovery branch. HKDF, not Argon2id: the input is already |
| + * full-entropy, and this path must be the least-rotted code we own. */ |
| + hkdf_label(recovery_code, 16, NULL, 0, "contra/v1/rc-login", seed_rc, 32); |
| + p256_from_seed(seed_rc, &lk_rc); |
| + |
| + w_rc.name = "W_rc"; |
| + w_rc.object_type = "wrap.rc"; |
| + hkdf_label(recovery_code, 16, NULL, 0, "contra/v1/rc-kek", w_rc.kek, 32); |
| + fixture("wrap_id/rc", w_rc.wrap_id, 16); |
| + fixture("nonce/wrap_rc", w_rc.nonce, 12); |
| + seal_wrap(&w_rc, account_id, rk); |
| + |
| + /* The passkey branch โ a third peer wrap, never a shortcut around RK. */ |
| + w_prf.name = "W_prf"; |
| + w_prf.object_type = "wrap.prf"; |
| + hkdf_label(prf_output, 32, cred_salt, 32, "contra/v1/prf-kek", w_prf.kek, 32); |
| + fixture("wrap_id/prf", w_prf.wrap_id, 16); |
| + fixture("nonce/wrap_prf", w_prf.nonce, 12); |
| + seal_wrap(&w_prf, account_id, rk); |
| + |
| + /* An item, sealed under the vault key and scoped to the vault. */ |
| + item_ptlen = strlen(item_pt); |
| + item_aadlen = build_aad(vault_id, 16, "item", item_id, 16, 1, item_aad, |
| + sizeof item_aad); |
| + aes256gcm(k_vault, item_nonce, item_aad, item_aadlen, |
| + (const unsigned char *)item_pt, item_ptlen, item_ct, item_tag); |
| + |
| + /* The seed that forces the rejection branch. */ |
| + memcpy(reject_seed, "contra/v1/vector-search!", 24); |
| + for (i = 0; i < 8; i++) |
| + reject_seed[24 + i] = (unsigned char)(reject_k >> (56 - 8 * i)); |
| + p256_from_seed(reject_seed, &lk_reject); |
| + if (reject_k && lk_reject.ncand < 2) |
| + die("witness k does not reject at i=0 โ rerun seedsearch"); |
| + |
| + printed_form("A1", account_key, ak_norm, ak_print); |
| + printed_form("C1", recovery_code, rc_norm, rc_print); |
| + |
| + /* ------------------------------------------------------------ output */ |
| + |
| + printf("{\n"); |
| + printf(" \"format\": \"contra-golden-vectors\",\n"); |
| + printf(" \"version\": 1,\n"); |
| + printf(" \"spec\": \"DESIGN.md; wire formats pinned in discussion 19\",\n"); |
| + printf(" \"generator\": \"vectors/gen/gen.c against %s\",\n", |
| + OPENSSL_VERSION_TEXT); |
| + printf(" \"note\": \"Every input here is generated: fixture(label) = " |
| + "SHA-256(\\\"contra/v1/vectors/\\\" || label). Nothing in this file " |
| + "is a real secret, and nothing in it may be reused as one.\",\n"); |
| + printf(" \"binding\": \"The Go, JS and Swift crypto suites must all pass " |
| + "this file. Changing a value here is changing the protocol.\",\n"); |
| + |
| + printf(" \"external_anchors\": {\n"); |
| + printf(" \"note\": \"Published third-party vectors. gen.c recomputes " |
| + "each one and aborts on mismatch before generating anything " |
| + "below.\",\n"); |
| + printf(" \"sources\": [\"PHC argon2 reference vectors\", \"RFC 5869 " |
| + "cases 1 and 3\", \"RFC 4231 case 1\", \"NIST CAVP AES-256-GCM\"]\n"); |
| + printf(" },\n"); |
| + |
| + printf(" \"argon2id\": {\n"); |
| + printf(" \"params\": {\"m_kib\": 65536, \"t\": 3, \"p\": 1, " |
| + "\"tag_len\": 32},\n"); |
| + printf(" \"floor\": {\"m_kib\": 65536, \"t\": 3, \"note\": \"clients " |
| + "refuse anything weaker a server offers\"},\n"); |
| + printf(" \"cases\": [\n"); |
| + printf(" {\n"); |
| + need_comma = 0; |
| + field("name", "\"S_pw\""); |
| + field("passphrase_utf8", "\"%s\"", passphrase); |
| + field("passphrase_hex", "\"%s\"", |
| + hex((const unsigned char *)passphrase, strlen(passphrase))); |
| + field("salt_hex", "\"%s\"", hex(salt_pw, 16)); |
| + field("out_hex", "\"%s\"", hex(s_pw, 32)); |
| + printf("\n }\n ]\n },\n"); |
| + |
| + printf(" \"hkdf\": {\n"); |
| + printf(" \"construction\": \"RFC 5869 extract-then-expand, SHA-256; an " |
| + "absent salt is zero-length; info is the label's ASCII bytes with " |
| + "no NUL\",\n"); |
| + printf(" \"cases\": [\n"); |
| + { |
| + struct { |
| + const char *name, *label; |
| + const unsigned char *ikm; |
| + size_t ikmlen; |
| + const unsigned char *salt; |
| + size_t saltlen; |
| + const unsigned char *out; |
| + } cases[] = { |
| + {"seed_login", "contra/v1/login", s_pw, 32, NULL, 0, seed_login}, |
| + {"KEK_pw", "contra/v1/kek", s_pw, 32, account_key, 16, w_pw.kek}, |
| + {"seed_rc_login", "contra/v1/rc-login", recovery_code, 16, NULL, 0, |
| + seed_rc}, |
| + {"KEK_rc", "contra/v1/rc-kek", recovery_code, 16, NULL, 0, w_rc.kek}, |
| + {"KEK_prf", "contra/v1/prf-kek", prf_output, 32, cred_salt, 32, |
| + w_prf.kek}, |
| + }; |
| + for (i = 0; i < (int)(sizeof cases / sizeof cases[0]); i++) { |
| + printf(" {\n"); |
| + need_comma = 0; |
| + field("name", "\"%s\"", cases[i].name); |
| + field("info", "\"%s\"", cases[i].label); |
| + field("ikm_hex", "\"%s\"", hex(cases[i].ikm, cases[i].ikmlen)); |
| + field("salt_hex", "\"%s\"", |
| + cases[i].saltlen ? hex(cases[i].salt, cases[i].saltlen) : ""); |
| + field("l", "32"); |
| + field("okm_hex", "\"%s\"", hex(cases[i].out, 32)); |
| + printf("\n }%s\n", |
| + i + 1 < (int)(sizeof cases / sizeof cases[0]) ? "," : ""); |
| + } |
| + } |
| + printf(" ]\n },\n"); |
| + |
| + printf(" \"kcv\": {\n"); |
| + printf(" \"construction\": \"HMAC-SHA-256(KEK, \\\"contra/v1/kcv\\\")" |
| + "[..16]\",\n"); |
| + printf(" \"cases\": [\n"); |
| + printf(" {\"name\": \"KCV_pw\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| + "\"%s\"},\n", |
| + hex(w_pw.kek, 32), hex(w_pw.kcv, 16)); |
| + printf(" {\"name\": \"KCV_rc\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| + "\"%s\"},\n", |
| + hex(w_rc.kek, 32), hex(w_rc.kcv, 16)); |
| + printf(" {\"name\": \"KCV_prf\", \"kek_hex\": \"%s\", \"kcv_hex\": " |
| + "\"%s\"}\n", |
| + hex(w_prf.kek, 32), hex(w_prf.kcv, 16)); |
| + printf(" ]\n },\n"); |
| + |
| + printf(" \"p256_from_seed\": {\n"); |
| + printf(" \"construction\": \"candidate_i = HKDF-SHA-256(ikm=seed, " |
| + "salt=\\\"\\\", info=\\\"%s\\\" || byte(i)); accept when 1 <= " |
| + "OS2IP(candidate) <= n-1, else i += 1\",\n", |
| + EC_LABEL); |
| + printf(" \"public_key_encoding\": \"SEC1 uncompressed, 0x04 || X || " |
| + "Y\",\n"); |
| + printf(" \"cases\": [\n"); |
| + { |
| + struct { |
| + const char *name; |
| + const unsigned char *seed; |
| + const struct p256 *r; |
| + } cases[] = { |
| + {"LK_pw", seed_login, &lk_pw}, |
| + {"LK_rc", seed_rc, &lk_rc}, |
| + {"rejects_at_i0", reject_seed, &lk_reject}, |
| + }; |
| + int c; |
| + for (c = 0; c < 3; c++) { |
| + printf(" {\n"); |
| + need_comma = 0; |
| + field("name", "\"%s\"", cases[c].name); |
| + field("seed_hex", "\"%s\"", hex(cases[c].seed, 32)); |
| + field("candidates_tried", "%d", cases[c].r->ncand); |
| + printf(",\n \"candidates\": ["); |
| + for (i = 0; i < cases[c].r->ncand; i++) |
| + printf("%s\"%s\"", i ? ", " : "", hex(cases[c].r->cand[i], 32)); |
| + printf("]"); |
| + need_comma = 1; |
| + field("d_hex", "\"%s\"", hex(cases[c].r->d, 32)); |
| + field("public_uncompressed_hex", "\"%s\"", hex(cases[c].r->pub, 65)); |
| + printf("\n }%s\n", c < 2 ? "," : ""); |
| + } |
| + } |
| + printf(" ]\n },\n"); |
| + printf(" \"p256_rejecting_seed\": {\n"); |
| + printf(" \"note\": \"A rejection has probability about 2^-32, so this " |
| + "seed was searched for. Without it the rejection branch is dead " |
| + "code in all three languages.\",\n"); |
| + printf(" \"seed_recipe\": \"\\\"contra/v1/vector-search!\\\" || " |
| + "u64be(k)\",\n"); |
| + printf(" \"k\": %llu,\n", reject_k); |
| + printf(" \"seed_hex\": \"%s\"\n", hex(reject_seed, 32)); |
| + printf(" },\n"); |
| + |
| + printf(" \"crockford\": {\n"); |
| + printf(" \"alphabet\": \"%s\",\n", CROCKFORD); |
| + printf(" \"packing\": \"MSB-first over the raw bytes, left-aligned; 16 " |
| + "bytes -> 26 symbols with two zero pad bits. Decoding rejects a " |
| + "non-zero pad, so every value has exactly one encoding.\",\n"); |
| + printf(" \"normalize\": \"strip '-' and whitespace, uppercase, O->0, " |
| + "I->1, L->1; U is not in the alphabet and is an error\",\n"); |
| + printf(" \"encode_cases\": [\n"); |
| + { |
| + unsigned char z[16], f[16]; |
| + char s[27]; |
| + memset(z, 0x00, 16); |
| + memset(f, 0xff, 16); |
| + crockford_encode(z, 16, s); |
| + printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| + hex(z, 16), s); |
| + crockford_encode(f, 16, s); |
| + printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| + hex(f, 16), s); |
| + crockford_encode(account_key, 16, s); |
| + printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"},\n", |
| + hex(account_key, 16), s); |
| + crockford_encode(recovery_code, 16, s); |
| + printf(" {\"bytes_hex\": \"%s\", \"symbols\": \"%s\"}\n", |
| + hex(recovery_code, 16), s); |
| + } |
| + printf(" ],\n"); |
| + printf(" \"normalize_cases\": [\n"); |
| + { |
| + static const char *inputs[] = {"c1-oi1l-0O1I", " A1 oL Li ", |
| + "c1-abc-def", "C1-UUUU", "c1?abc"}; |
| + char norm[64]; |
| + int k; |
| + for (k = 0; k < 5; k++) { |
| + int ok = crockford_normalize(inputs[k], norm, sizeof norm); |
| + printf(" {\"input\": \"%s\", ", inputs[k]); |
| + if (ok) |
| + printf("\"normalized\": \"%s\"}", norm); |
| + else |
| + printf("\"error\": \"character outside the Crockford " |
| + "alphabet\"}"); |
| + printf("%s\n", k < 4 ? "," : ""); |
| + } |
| + } |
| + printf(" ],\n"); |
| + printf(" \"decode_cases\": [\n"); |
| + { |
| + char sym[27]; |
| + unsigned char rt[16]; |
| + char bad[27]; |
| + crockford_encode(recovery_code, 16, sym); |
| + printf(" {\"symbols\": \"%s\", \"bytes_hex\": \"%s\"},\n", sym, |
| + crockford_decode16(sym, rt) ? hex(rt, 16) : "DECODE FAILED"); |
| + /* Same value, last symbol nudged so the two pad bits are no longer |
| + * zero. Strict decoding must refuse it. */ |
| + memcpy(bad, sym, sizeof bad); |
| + bad[25] = CROCKFORD[(crockford_value(bad[25]) + 1) & 0x1f]; |
| + printf(" {\"symbols\": \"%s\", \"error\": \"non-zero pad bits in " |
| + "the final symbol\", \"rejected\": %s}\n", |
| + bad, crockford_decode16(bad, rt) ? "false" : "true"); |
| + } |
| + printf(" ]\n },\n"); |
| + |
| + printf(" \"printed_code\": {\n"); |
| + printf(" \"form\": \"tag(2) || data(26) || checksum(4), printed as " |
| + "TAG-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX\",\n"); |
| + printf(" \"checksum\": \"first 20 bits, MSB-first, of " |
| + "SHA-256(payload_bytes) -- over the 16 raw bytes, not the " |
| + "string\",\n"); |
| + printf(" \"cases\": [\n"); |
| + printf(" {\"name\": \"account_key\", \"tag\": \"A1\", " |
| + "\"payload_hex\": \"%s\", \"checksum20\": %u, \"normalized\": " |
| + "\"%s\", \"printed\": \"%s\"},\n", |
| + hex(account_key, 16), checksum20(account_key, 16), ak_norm, ak_print); |
| + printf(" {\"name\": \"recovery_code\", \"tag\": \"C1\", " |
| + "\"payload_hex\": \"%s\", \"checksum20\": %u, \"normalized\": " |
| + "\"%s\", \"printed\": \"%s\"}\n", |
| + hex(recovery_code, 16), checksum20(recovery_code, 16), rc_norm, |
| + rc_print); |
| + printf(" ],\n"); |
| + |
| + /* One substituted character, for the self-healing repair path. The |
| + * candidate list is what a client tries against the KCV; the repair |
| + * never picks one on its own, because 20 bits collides. */ |
| + { |
| + int pos = 7; /* a data position, well inside the printed groups */ |
| + char cands[8][33]; |
| + unsigned char payload[16]; |
| + int ncand, k; |
| + |
| + strcpy(typo, rc_norm); |
| + typo[pos] = CROCKFORD[(crockford_value(typo[pos]) + 9) & 0x1f]; |
| + if (code_valid(typo, payload)) die("typo case still validates"); |
| + ncand = code_repair(typo, cands, 8); |
| + |
| + printf(" \"repair\": {\n"); |
| + printf(" \"note\": \"substitute one character, then brute-force " |
| + "all 30 payload positions x 32 symbols against the checksum; " |
| + "return every survivor in position-then-alphabet order for the " |
| + "caller to try against the KCV\",\n"); |
| + printf(" \"correct_normalized\": \"%s\",\n", rc_norm); |
| + printf(" \"typo_normalized\": \"%s\",\n", typo); |
| + printf(" \"typo_position\": %d,\n", pos); |
| + printf(" \"candidates\": ["); |
| + for (k = 0; k < ncand && k < 8; k++) |
| + printf("%s\"%s\"", k ? ", " : "", cands[k]); |
| + printf("],\n"); |
| + printf(" \"candidate_count\": %d,\n", ncand); |
| + { |
| + int found = 0; |
| + for (k = 0; k < ncand && k < 8; k++) |
| + if (strcmp(cands[k], rc_norm) == 0) found = 1; |
| + if (!found) die("repair lost the correct code"); |
| + printf(" \"correct_is_a_candidate\": true\n"); |
| + } |
| + printf(" }\n },\n"); |
| + } |
| + |
| + printf(" \"aead\": {\n"); |
| + printf(" \"algorithm\": \"AES-256-GCM, 96-bit nonce, 128-bit tag\",\n"); |
| + printf(" \"aad\": \"\\\"contra/v1/aad\\\" || 0x00 || " |
| + "u32be(len(scope_id)) || scope_id || u32be(len(object_type)) || " |
| + "object_type || u32be(len(object_id)) || object_id || " |
| + "u64be(version)\",\n"); |
| + printf(" \"scope_id\": \"the account id for account-scoped objects, the " |
| + "vault id for vault-scoped ones\",\n"); |
| + printf(" \"account_id_hex\": \"%s\",\n", hex(account_id, 16)); |
| + printf(" \"vault_id_hex\": \"%s\",\n", hex(vault_id, 16)); |
| + printf(" \"rk_hex\": \"%s\",\n", hex(rk, 32)); |
| + printf(" \"wraps\": [\n"); |
| + emit_wrap(&w_pw); |
| + printf(",\n"); |
| + emit_wrap(&w_rc); |
| + printf(",\n"); |
| + emit_wrap(&w_prf); |
| + printf("\n ],\n"); |
| + printf(" \"item\": {\n"); |
| + need_comma = 0; |
| + field("scope", "\"vault\""); |
| + field("vault_id_hex", "\"%s\"", hex(vault_id, 16)); |
| + field("object_type", "\"item\""); |
| + field("item_id_hex", "\"%s\"", hex(item_id, 16)); |
| + field("version", "1"); |
| + field("k_vault_hex", "\"%s\"", hex(k_vault, 32)); |
| + field("nonce_hex", "\"%s\"", hex(item_nonce, 12)); |
| + field("aad_hex", "\"%s\"", hex(item_aad, item_aadlen)); |
| + field("plaintext_utf8", "\"%s\"", |
| + "{\\\"title\\\":\\\"Generated vector item\\\",\\\"username\\\":" |
| + "\\\"vector@example.invalid\\\",\\\"secret\\\":" |
| + "\\\"not-a-real-value\\\"}"); |
| + field("plaintext_hex", "\"%s\"", |
| + hex((const unsigned char *)item_pt, item_ptlen)); |
| + field("ciphertext_hex", "\"%s\"", hex(item_ct, item_ptlen)); |
| + field("tag_hex", "\"%s\"", hex(item_tag, 16)); |
| + printf("\n }\n },\n"); |
| + |
| + printf(" \"account_chain\": {\n"); |
| + printf(" \"note\": \"the whole of DESIGN.md's 'The shape', end to end, " |
| + "so a new implementation can bisect where it diverges\",\n"); |
| + need_comma = 0; |
| + indent = " "; |
| + field("passphrase_utf8", "\"%s\"", passphrase); |
| + field("salt_pw_hex", "\"%s\"", hex(salt_pw, 16)); |
| + field("account_key_hex", "\"%s\"", hex(account_key, 16)); |
| + field("recovery_code_hex", "\"%s\"", hex(recovery_code, 16)); |
| + field("prf_output_hex", "\"%s\"", hex(prf_output, 32)); |
| + field("cred_salt_hex", "\"%s\"", hex(cred_salt, 32)); |
| + field("s_pw_hex", "\"%s\"", hex(s_pw, 32)); |
| + field("seed_login_hex", "\"%s\"", hex(seed_login, 32)); |
| + field("lk_pw_public_hex", "\"%s\"", hex(lk_pw.pub, 65)); |
| + field("seed_rc_login_hex", "\"%s\"", hex(seed_rc, 32)); |
| + field("lk_rc_public_hex", "\"%s\"", hex(lk_rc.pub, 65)); |
| + field("kek_pw_hex", "\"%s\"", hex(w_pw.kek, 32)); |
| + field("kek_rc_hex", "\"%s\"", hex(w_rc.kek, 32)); |
| + field("kek_prf_hex", "\"%s\"", hex(w_prf.kek, 32)); |
| + field("rk_hex", "\"%s\"", hex(rk, 32)); |
| + printf("\n }\n}\n"); |
| + |
| + return 0; |
| +} |