import { describe, expect, it } from 'vitest'; import { fromBase64Url, toBase64Url } from '../src/bytes.js'; import { IntegrityError, buildLink, parseLink, parseLogRecord, type ItemEvent, } from '../src/index.js'; import { deriveChannelKeys, deriveItemKey, deriveOwnerToken, deriveRawChannelSecrets, deriveRawItemKey, deriveRawOwnerToken, frameAad, frameIv, padPlaintext, parseEnvelope, sealFrame, sha256, tierFor, unpadPlaintext, encodeEnvelope, } from '../src/internal.js'; import { openFrame } from '../src/frame.js'; import { framesOf, h, inputs, openVectorSession, toHex, vectors } from './helpers.js'; /** * 这些向量由 test-vectors/generate.mjs 用 node:crypto 独立生成(不引用本包), * 服务端的 C# 测试用 .NET 的密码学实现再复现一遍。 * 三套互不相干的实现一致,才说明 protocol.md 没有歧义。 */ describe('密钥派生', () => { it('原始字节与向量一致', async () => { const secrets = await deriveRawChannelSecrets(inputs.channelId, inputs.k); expect(toHex(secrets.kEnc)).toBe(vectors.kdf.k_enc); expect(toHex(secrets.tRead)).toBe(vectors.kdf.t_read); expect(toHex(secrets.tWrite)).toBe(vectors.kdf.t_write); expect(toHex(secrets.kLabel)).toBe(vectors.kdf.k_label); expect(toHex(await deriveRawOwnerToken(inputs.channelId, inputs.a))).toBe(vectors.kdf.t_owner); for (const item of vectors.item_keys) { expect(toHex(await deriveRawItemKey(secrets.kEnc, h(item.item_id)))).toBe(item.k_item); } }); it('生产路径(不可导出的 CryptoKey)得到同样的令牌', async () => { const keys = await deriveChannelKeys(inputs.channelId, inputs.k); expect(toHex(keys.readToken)).toBe(vectors.kdf.t_read); expect(toHex(keys.writeToken)).toBe(vectors.kdf.t_write); expect(toHex(await deriveOwnerToken(inputs.channelId, inputs.a))).toBe(vectors.kdf.t_owner); }); it('生产路径里的密钥确实不可导出', async () => { const keys = await deriveChannelKeys(inputs.channelId, inputs.k); const itemKey = await deriveItemKey(keys.encRoot, inputs.itemA); expect(itemKey.extractable).toBe(false); expect(keys.encRoot.extractable).toBe(false); expect(keys.labelKey.extractable).toBe(false); await expect(crypto.subtle.exportKey('raw', itemKey)).rejects.toThrow(); }); it('令牌哈希与向量一致', async () => { expect(toHex(await sha256(h(vectors.kdf.t_read)))).toBe(vectors.kdf.read_hash); expect(toHex(await sha256(h(vectors.kdf.t_write)))).toBe(vectors.kdf.write_hash); expect(toHex(await sha256(h(vectors.kdf.t_owner)))).toBe(vectors.kdf.owner_hash); expect(toHex(await sha256(h(vectors.kdf.raw_write_token)))).toBe(vectors.kdf.raw_write_hash); expect(toHex(await sha256(inputs.endpointSecret))).toBe(vectors.kdf.endpoint_hash); }); }); describe('IV 与 AAD', () => { it('IV = 8 字节零 ‖ frame_index', () => { for (const v of vectors.iv) expect(toHex(frameIv(v.index))).toBe(v.iv); }); it('AAD = "WD1" ‖ channel_id ‖ 头部前 22 字节', () => { for (const c of vectors.aad) { // 只含头部的封装(payload_len = 0),AAD 只取前 22 字节 const header = encodeEnvelope(c.flags, h(c.item_id), c.index, new Uint8Array(0)); expect(toHex(frameAad(inputs.channelId, header))).toBe(c.aad); } }); }); describe('padding', () => { it('每个档位边界的明文长度与向量一致', () => { for (const p of vectors.padding) { expect(tierFor(4 + p.data_len)).toBe(p.plaintext_len); const padded = padPlaintext(new Uint8Array(p.data_len)); expect(padded.length).toBe(p.plaintext_len); expect(unpadPlaintext(padded).length).toBe(p.data_len); } }); it('拒绝非零填充、越界长度和非档位长度', () => { const padded = padPlaintext(new Uint8Array(10)); const dirty = padded.slice(); dirty[dirty.length - 1] = 1; expect(() => unpadPlaintext(dirty)).toThrowError(IntegrityError); const lying = padded.slice(); lying.set([0xff, 0xff, 0xff, 0xff], 0); expect(() => unpadPlaintext(lying)).toThrowError(IntegrityError); expect(() => unpadPlaintext(new Uint8Array(1000))).toThrowError(IntegrityError); // 不是档位 expect(() => unpadPlaintext(new Uint8Array(2))).toThrowError(IntegrityError); }); it('超过单帧上限时拒绝', () => { expect(() => padPlaintext(new Uint8Array(1048573))).toThrow(RangeError); expect(padPlaintext(new Uint8Array(1048572)).length).toBe(1048576); }); }); describe('单帧加解密', () => { for (const f of vectors.frames) { it(`${f.name}:密文逐字节一致,并能解回原文`, async () => { const keys = await deriveChannelKeys(inputs.channelId, inputs.k); const itemKey = await deriveItemKey(keys.encRoot, h(f.item_id)); const sealed = await sealFrame(itemKey, inputs.channelId, { flags: f.flags, itemId: h(f.item_id), index: f.index, data: h(f.data), }); expect(toHex(sealed)).toBe(f.envelope); const opened = await openFrame(itemKey, inputs.channelId, parseEnvelope(h(f.envelope))); expect(toHex(opened)).toBe(f.data); }); } }); describe('条目分帧', () => { for (const item of vectors.items) { it(`${item.name}:分帧与 GATED 决策和向量一致`, async () => { const session = await openVectorSession(); const m = item.meta; const { kind, name, mime, size } = m; const plan = session.encryptBytes( h(item.content), { kind, ...(name !== undefined && { name }), ...(mime !== undefined && { mime }), ...(size !== undefined && { size }) }, { itemId: h(item.item_id), ts: m.ts, ...(m.dev !== undefined && { dev: m.dev }), snapshot: item.snapshot, gated: item.force_gated ? true : 'auto', maxFrameData: item.max_frame_data, }, ); const frames = await framesOf(plan); expect(frames.map((f) => toHex(f.bytes))).toEqual(item.frames); expect(plan.gated).toBe(item.gated); expect(frames.map((f) => f.final)).toEqual(frames.map((_, i) => i === frames.length - 1)); }); } it('边界:恰好放得进第 0 帧是单帧,多一个字节就变成两帧 GATED', () => { const fits = vectors.items.find((i: { name: string }) => i.name === 'boundary_fits_exactly'); const over = vectors.items.find((i: { name: string }) => i.name === 'boundary_one_byte_over'); expect([fits.gated, fits.frames.length]).toEqual([false, 1]); expect([over.gated, over.frames.length]).toEqual([true, 2]); }); }); describe('raw 帧', () => { it('封装与向量一致', () => { for (const f of vectors.raw_frames) { const built = encodeEnvelope(f.flags, h(f.item_id), f.index, h(f.payload)); expect(toHex(built)).toBe(f.envelope); } }); }); describe('设备名', () => { it('固定 IV 下的密文一致,并能解回', async () => { const session = await openVectorSession(); for (const l of vectors.labels) { const sealed = await session.sealLabel(l.name, { iv: h(l.iv) }); expect(toHex(sealed)).toBe(l.blob); expect(sealed.length).toBeLessThanOrEqual(256); expect(await session.openLabel(sealed, session.endpointHash)).toBe(l.name); } }); it('换一个终端的哈希就解不开(AAD 绑定终端)', async () => { const session = await openVectorSession(); const sealed = await session.sealLabel('我的 iPhone'); await expect(session.openLabel(sealed, new Uint8Array(32))).rejects.toThrowError(IntegrityError); }); it('超过 228 字节 UTF-8 时拒绝', async () => { const session = await openVectorSession(); await expect(session.sealLabel('x'.repeat(229))).rejects.toThrow(); await expect(session.sealLabel('x'.repeat(228))).resolves.toHaveLength(256); }); }); describe('链接', () => { it('合法链接解析结果与向量一致', () => { for (const v of vectors.links.valid) { const link = parseLink(v.url); expect(link.id).toBe(v.id); expect(link.mode).toBe(v.mode); expect(link.role).toBe(v.role); expect(link.k && toHex(link.k)).toBe(v.k); expect(link.a && toHex(link.a)).toBe(v.a); expect(link.w && toHex(link.w)).toBe(v.w); } }); it('非法链接全部被拒绝', () => { for (const v of vectors.links.invalid) { expect(() => parseLink(v.url), v.name).toThrow(); } }); it('生成的链接能被解析回原值', () => { const url = buildLink('https://app.example.test/', { id: inputs.channelIdB64, k: inputs.k, a: inputs.a }); expect(url).toBe(`https://app.example.test/c/${inputs.channelIdB64}#k=${toBase64Url(inputs.k)}&a=${toBase64Url(inputs.a)}`); const back = parseLink(url); expect(toHex(back.k!)).toBe(toHex(inputs.k)); expect(back.role).toBe('owner'); }); }); describe('日志记录', () => { it('解析结果与向量一致', () => { for (const r of vectors.log_records) { const rec = parseLogRecord(h(r.hex)); const p = r.parsed; expect(rec.kind).toBe(p.kind); expect(rec.seq).toBe(p.seq); if (rec.kind === 'frame') expect(toHex(rec.envelope)).toBe(p.envelope); if (rec.kind === 'tombstone') { expect(toHex(fromBase64Url(rec.itemId)!)).toBe(p.item_id); expect(['deleted', 'expired', 'evicted', 'download_limit', 'superseded'][p.reason]).toBe(rec.reason); } if (rec.kind === 'announce') { expect(toHex(fromBase64Url(rec.itemId)!)).toBe(p.item_id); expect(rec.flags).toBe(p.flags); expect(rec.createdAt).toBe(p.created_at); } } }); it('损坏的记录被拒绝', () => { for (const r of vectors.log_invalid) { expect(() => parseLogRecord(h(r.hex)), r.name).toThrowError(IntegrityError); } }); }); describe('负例:必须被接收方拒绝', () => { for (const n of vectors.negative) { it(`${n.name} → ${n.expect}`, async () => { const envelope = h(n.envelope); const channelId = n.channel_id ? h(n.channel_id) : inputs.channelId; if (n.expect === 'bad_envelope') { expect(() => parseEnvelope(envelope)).toThrowError(IntegrityError); return; } const env = parseEnvelope(envelope); const keys = await deriveChannelKeys(channelId === inputs.channelId ? inputs.channelId : inputs.channelId, inputs.k); // 注意:wrong_channel 用的是「错误的 channel_id 参与 AAD」,密钥仍由真实频道派生 const itemKey = await deriveItemKey(keys.encRoot, env.itemId); const failure = await openFrame(itemKey, channelId, env).then( () => null, (e: unknown) => e, ); expect(failure).toBeInstanceOf(IntegrityError); expect((failure as IntegrityError).reason).toBe(n.expect); }); } it('装配器对每个负例都产生 rejected 事件,而不是抛异常或产出内容', async () => { const session = await openVectorSession(); const reader = session.createItemReader(); // 这两个负例在到达解密之前就违反了结构规则(非 GATED 条目必须是单帧且带 FINAL), // 装配器先以 bad_envelope 拒绝;直接 openFrame 时则是 auth_failed(上面已断言) const structural = new Set(['clear_final_flag', 'change_frame_index']); for (const n of vectors.negative) { if (n.channel_id) continue; // 这一条是换了会话的 channel_id,装配器视角下等同于正常帧 const events: ItemEvent[] = await reader.pushFrame(h(n.envelope)); expect(events, n.name).toHaveLength(1); const ev = events[0]!; expect(ev.type, n.name).toBe('rejected'); if (ev.type === 'rejected') expect(ev.reason, n.name).toBe(structural.has(n.name) ? 'bad_envelope' : n.expect); } }); });