// Sealing to a public key, against vectors/contra-v1.json ("handoff"). // // The device handoff is the one place a secret travels through the server, so // the JS half of it is pinned to the same bytes the Go half is — generated by // neither, in vectors/gen/gen.c against OpenSSL, itself checked against // RFC 5903 before it may emit anything. import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const mod = (name) => join(here, "..", "internal", "contra", "static", "crypto", name); const { toHex, fromHex } = await import(mod("bytes.js")); const { deriveP256 } = await import(mod("p256.js")); const { aad, OBJECT_HANDOFF } = await import(mod("sealed.js")); const ecies = await import(mod("ecies.js")); const V = JSON.parse(readFileSync(join(here, "..", "vectors", "contra-v1.json"), "utf8")); const H = V.handoff; test("handoff: both keypairs derive from their published seeds", async () => { for (const [name, seed, scalar, pub] of [ ["recipient", H.recipient_seed_hex, H.recipient_scalar_hex, H.recipient_public_hex], ["ephemeral", H.ephemeral_seed_hex, H.ephemeral_scalar_hex, H.ephemeral_public_hex], ]) { const key = await deriveP256(fromHex(seed)); assert.equal(toHex(key.scalar), scalar, `${name} scalar`); assert.equal(toHex(key.publicKey), pub, `${name} public key`); } }); // Both directions. A one-sided check would pass on something that is not // actually Diffie-Hellman. test("handoff: ECDH agrees from both sides", async () => { const fwd = await ecies.ecdh(fromHex(H.ephemeral_scalar_hex), fromHex(H.recipient_public_hex)); assert.equal(toHex(fwd), H.shared_secret_hex, "sender side"); const back = await ecies.ecdh(fromHex(H.recipient_scalar_hex), fromHex(H.ephemeral_public_hex)); assert.equal(toHex(back), H.shared_secret_hex, "recipient side"); }); test("handoff: the derived key, the AAD and the sealed bytes", async () => { const key = await ecies.eciesKey( fromHex(H.ephemeral_public_hex), fromHex(H.recipient_public_hex), fromHex(H.shared_secret_hex)); assert.equal(toHex(key), H.key_hex, "handoff key"); const a = aad(fromHex(H.account_id_hex), H.object_type, fromHex(H.recipient_public_hex), H.version); assert.equal(toHex(a), H.aad_hex, "handoff AAD"); const { seal } = await import(mod("sealed.js")); const sealed = await seal(key, fromHex(H.nonce_hex), a, fromHex(H.plaintext_hex)); assert.equal(toHex(sealed.slice(0, -16)), H.ciphertext_hex, "ciphertext"); assert.equal(toHex(sealed.slice(-16)), H.tag_hex, "tag"); }); // sealTo mints its own ephemeral key and nonce, so it cannot be checked // against fixed bytes. What can be checked is the property that matters. test("handoff: an envelope opens for its device and for nobody else", async () => { const recipientPublic = fromHex(H.recipient_public_hex); const recipientScalar = fromHex(H.recipient_scalar_hex); const accountKey = fromHex(H.plaintext_hex); const a = aad(fromHex(H.account_id_hex), OBJECT_HANDOFF, recipientPublic, 1); const envelope = await ecies.sealTo(recipientPublic, a, accountKey); assert.equal(envelope.length, 65 + 12 + accountKey.length + 16, "envelope length"); const got = await ecies.openFrom(recipientScalar, recipientPublic, a, envelope); assert.equal(toHex(got), H.plaintext_hex, "round trip"); const other = await deriveP256(fromHex(H.ephemeral_seed_hex)); await assert.rejects( () => ecies.openFrom(other.scalar, other.publicKey, a, envelope), "a different device must not open an envelope sealed to another"); // The AAD names the recipient, so the server cannot relay one envelope on. const wrong = aad(fromHex(H.account_id_hex), OBJECT_HANDOFF, other.publicKey, 1); await assert.rejects( () => ecies.openFrom(recipientScalar, recipientPublic, wrong, envelope), "an envelope must not open under an AAD naming a different device"); }); test("handoff: the Go and JS halves seal for each other", async () => { // A fresh ephemeral pair from the browser side, opened with the scalar it // kept — the exact shape the fresh device runs. const device = await ecies.newEphemeral(); assert.equal(device.publicKey.length, 65); assert.equal(device.publicKey[0], 0x04); const a = aad(fromHex(H.account_id_hex), OBJECT_HANDOFF, device.publicKey, 1); const envelope = await ecies.sealTo(device.publicKey, a, fromHex(H.plaintext_hex)); const got = await ecies.openFrom(device.scalar, device.publicKey, a, envelope); assert.equal(toHex(got), H.plaintext_hex); }); test("handoff: a truncated envelope is refused before any key work", async () => { const recipientPublic = fromHex(H.recipient_public_hex); const a = aad(fromHex(H.account_id_hex), OBJECT_HANDOFF, recipientPublic, 1); await assert.rejects( () => ecies.openFrom(fromHex(H.recipient_scalar_hex), recipientPublic, a, new Uint8Array(40)), /too short/); });