// Sharing, in the browser modules. // // The Go suite proves the API stores and returns the right rows. This proves // the thing that actually matters to a person: an owner seals a vault keyset // with the browser's own code, and a DIFFERENT account opens it with theirs. // // The network is a fake, because none of what is being tested is the network. import { test } from "node:test"; import assert from "node:assert/strict"; 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 { fromHex, randomBytes, toHex } = await import(mod("bytes.js")); const { newP256 } = await import(mod("p256.js")); const { publicKeyFor } = await import(mod("p256math.js")); const { concat } = await import(mod("bytes.js")); const sharing = await import(mod("sharing.js")); // A fake network that records the one POST sharing makes. function recorder() { const posts = []; return { posts, async get() { throw new Error("not used"); }, async post(path, body) { posts.push({ path, body }); return { epoch: 2 }; }, }; } async function anAccount() { const key = await newP256(); return { id: randomBytes(16), scalar: key.scalar, publicKey: key.publicKey, }; } test("an owner seals a keyset that a different account can open", async () => { const owner = await anAccount(); const guest = await anAccount(); const net = recorder(); const vault = { id: randomBytes(16), idHex: "", kVault: randomBytes(32), name: "Household", epoch: 1, owned: true, }; vault.idHex = toHex(vault.id); await sharing.share(net, vault, toHex(guest.id), toHex(guest.publicKey)); assert.equal(net.posts.length, 1); const body = net.posts[0].body; assert.equal(net.posts[0].path, "/api/members"); assert.equal(body.account_id_hex, toHex(guest.id)); // The recipient opens it with their own scalar — which is the whole claim. const opened = await sharing.openSharedKeyset( guest.scalar, guest.publicKey, guest.id, vault.id, { ephemeral_public_key_hex: body.ephemeral_public_key_hex, keyset: body.sealed, }); assert.equal(opened.k_vault, toHex(vault.kVault), "the member got the vault key"); assert.equal(opened.name, "Household", "the member got the vault name"); }); test("nobody else can open a member's wrap", async () => { const owner = await anAccount(); const guest = await anAccount(); const stranger = await anAccount(); const net = recorder(); const vault = { id: randomBytes(16), kVault: randomBytes(32), name: "Household", epoch: 1, owned: true, }; vault.idHex = toHex(vault.id); await sharing.share(net, vault, toHex(guest.id), toHex(guest.publicKey)); const body = net.posts[0].body; const row = { ephemeral_public_key_hex: body.ephemeral_public_key_hex, keyset: body.sealed, }; await assert.rejects( () => sharing.openSharedKeyset( stranger.scalar, stranger.publicKey, stranger.id, vault.id, row), "a stranger opened a wrap sealed to somebody else"); // And the member cannot have their own wrap replayed at another vault, or // under another member's name — the AAD names both. await assert.rejects( () => sharing.openSharedKeyset( guest.scalar, guest.publicKey, guest.id, randomBytes(16), row), "a member's wrap opened against a different vault"); await assert.rejects( () => sharing.openSharedKeyset( guest.scalar, guest.publicKey, randomBytes(16), vault.id, row), "a member's wrap opened under a different member's id"); }); // The drop-box private half is deliberately not shared: a member reads and // writes items without being able to open captures addressed to the vault. test("a member wrap carries the vault key and not the drop box", async () => { const guest = await anAccount(); const net = recorder(); const vault = { id: randomBytes(16), kVault: randomBytes(32), name: "Household", epoch: 1, owned: true, dropBoxPrivate: randomBytes(32), }; vault.idHex = toHex(vault.id); await sharing.share(net, vault, toHex(guest.id), toHex(guest.publicKey)); const body = net.posts[0].body; const opened = await sharing.openSharedKeyset( guest.scalar, guest.publicKey, guest.id, vault.id, { ephemeral_public_key_hex: body.ephemeral_public_key_hex, keyset: body.sealed }); assert.equal(opened.drop_box_private, undefined, "the drop-box private half was shared with a member"); }); // Only the owner. A member holding K_vault could otherwise compose a rotation // and evict the owner from their own vault. test("a member cannot share or rotate", async () => { const guest = await anAccount(); const net = recorder(); const notMine = { id: randomBytes(16), kVault: randomBytes(32), name: "Theirs", epoch: 1, owned: false, }; notMine.idHex = toHex(notMine.id); await assert.rejects( () => sharing.share(net, notMine, toHex(guest.id), toHex(guest.publicKey)), /only the owner/); await assert.rejects( () => sharing.rotate(net, {}, notMine, [], [], randomBytes(32)), /only the owner/); assert.equal(net.posts.length, 0, "a refused call still reached the network"); }); // Rotation re-seals every item under the new key and bumps every version, so // the old key opens nothing that remains. test("rotation re-seals every item under the new key", async () => { const owner = await anAccount(); const guest = await anAccount(); const net = recorder(); const vault = { id: randomBytes(16), kVault: randomBytes(32), name: "Household", epoch: 1, owned: true, }; vault.idHex = toHex(vault.id); const opened = { rk: randomBytes(32), accountId: owner.id }; const items = [ { id: randomBytes(16), idHex: "", version: 3, fields: { title: "One" } }, { id: randomBytes(16), idHex: "", version: 1, fields: { title: "Two" } }, ]; for (const it of items) it.idHex = toHex(it.id); const newKVault = randomBytes(32); const after = await sharing.rotate( net, opened, vault, items, [{ account_id_hex: toHex(guest.id), public_key_hex: toHex(guest.publicKey) }], newKVault); const body = net.posts[0].body; assert.equal(net.posts[0].path, "/api/vault/rotate"); assert.equal(body.expected_epoch, 1, "the rotation did not carry its CAS token"); assert.equal(body.items.length, 2, "not every item was re-sealed"); assert.equal(body.items[0].version, 4, "the version did not bump"); assert.equal(body.items[1].version, 2); assert.equal(body.members.length, 1); assert.equal(after.epoch, 2); // The retained member's new wrap opens, and carries the NEW key. const m = body.members[0]; const got = await sharing.openSharedKeyset( guest.scalar, guest.publicKey, guest.id, vault.id, { ephemeral_public_key_hex: m.ephemeral_public_key_hex, keyset: { nonce_hex: m.nonce_hex, ciphertext_hex: m.ciphertext_hex }, }); assert.equal(got.k_vault, toHex(newKVault), "a retained member got the old key"); });