/** * AriaEngine — 全库 AES-256-GCM 加密测试 * * 覆盖: * 1. EncryptedBackend 单元:读写往返 / writeMany / delete / clear / 密文落盘验证 * 2. AriaEngine 集成:密码往返(open → 写入 → close → 同密码重开数据完整) * 3. 错误密码 → ARIA_DECRYPT_ERROR * 4. 明文旧库 + 加密配置 → ARIA_ENCRYPT_CONFIG_ERROR * 5. 加密库 + 无密码 → ARIA_ENCRYPT_REQUIRED * 6. clearAll 后可换新密码重建 * 7. 元数据(迁移版本)加密往返 */ import { AriaEngine } from '../../src/engine/aria/index'; import { EncryptedBackend } from '../../src/engine/aria/store/encrypted_backend'; import { MemoryBackend } from '../../src/engine/aria/store/backend'; import { createSchema } from '../../src/table/schema'; import { resetOPFSMock } from '../helpers/storage-harness'; import { decode as decodeBytes } from '../helpers/assertions'; beforeEach(() => { resetOPFSMock(); }); let idbCounter = 0; function uniqueDB(): string { return `enc-${Date.now()}-${++idbCounter}-${Math.random().toString(36).slice(2, 8)}`; } const SCHEMA = () => createSchema('users', { id: { type: 'string', primaryKey: true }, name: { type: 'string' }, secret: { type: 'string' }, }); // =================================================================== // EncryptedBackend 单元 // =================================================================== describe('AriaEngine — EncryptedBackend 单元', () => { it('open 创建 keymeta,读写往返完整', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 's3cret-password'); await backend.open('enc-unit-1'); // keymeta 已创建 const keymeta = await inner.read('__aria_keymeta'); expect(keymeta).not.toBeNull(); // 读写往返 const payload = new TextEncoder().encode('hello encrypted world').buffer; await backend.write('k1', payload); const back = await backend.read('k1'); expect(back).not.toBeNull(); expect(decodeBytes(back)).toBe('hello encrypted world'); // 底层是密文(非明文) const raw = await inner.read('k1'); const rawStr = decodeBytes(raw, 'ciphertext'); expect(rawStr).not.toContain('hello encrypted world'); await backend.close(); }); it('writeMany / deleteMany 往返', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-2'); const enc = (s: string) => new TextEncoder().encode(s).buffer; await backend.writeMany({ a: enc('AAA'), b: enc('BBB'), c: enc('CCC') }); expect(decodeBytes(await backend.read('a'))).toBe('AAA'); expect(decodeBytes(await backend.read('c'))).toBe('CCC'); await backend.deleteMany(['a', 'c']); expect(await backend.exists('a')).toBe(false); expect(await backend.exists('b')).toBe(true); await backend.delete('b'); expect(await backend.exists('b')).toBe(false); expect(await backend.listKeys()).toEqual(['__aria_keymeta']); await backend.close(); }); it('每个 value 独立随机 IV(相同明文两次加密密文不同)', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-3'); const payload = new TextEncoder().encode('same plaintext').buffer; await backend.write('x', payload); await backend.write('y', payload); const rawX = await inner.read('x'); const rawY = await inner.read('y'); // Jest toEqual 对 ArrayBuffer 不比较内容 → 字节级比较 expect(new Uint8Array(rawX as ArrayBuffer)).not.toEqual(new Uint8Array(rawY as ArrayBuffer)); // 但 IV 前 12 字节后的长度一致(密文长度 = 明文 + GCM tag 16B) expect((rawX as ArrayBuffer).byteLength).toBe((rawY as ArrayBuffer).byteLength); // 但解密一致 expect(decodeBytes(await backend.read('x'))).toBe('same plaintext'); expect(decodeBytes(await backend.read('y'))).toBe('same plaintext'); await backend.close(); }); it('clear 清空全部数据但保留密钥元数据(库身份保留)', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-4'); await backend.write('k', new TextEncoder().encode('v').buffer); expect((await backend.listKeys()).length).toBe(2); await backend.clear(); // 数据清空,keymeta 保留 expect(await backend.listKeys()).toEqual(['__aria_keymeta']); expect(await backend.exists('k')).toBe(false); await backend.close(); // 重新打开:keymeta 仍在,同密码可用 const backend2 = new EncryptedBackend(inner, 'pw'); await backend2.open('enc-unit-4'); await backend2.write('k2', new TextEncoder().encode('v2').buffer); expect(decodeBytes(await backend2.read('k2'))).toBe('v2'); // 换密码被拒(keymeta 与旧密码绑定)——注意:MemoryBackend close 清空 store, // 因此在 close 前验证(backend2 仍持有 inner) const backend3 = new EncryptedBackend(inner, 'other-pw'); await expect(backend3.open('enc-unit-4')).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' }); await backend2.close(); }); it('明文旧库(有数据无 keymeta)+ 加密 → ARIA_ENCRYPT_CONFIG_ERROR', async () => { const inner = new MemoryBackend(); await inner.open('enc-unit-5'); await inner.write('__aria_schemas', new TextEncoder().encode('{}').buffer); const backend = new EncryptedBackend(inner, 'pw'); await expect(backend.open('enc-unit-5')).rejects.toMatchObject({ code: 'ARIA_ENCRYPT_CONFIG_ERROR' }); }); it('空密码配置直接抛错', () => { const inner = new MemoryBackend(); try { new EncryptedBackend(inner, ''); fail('should throw'); } catch (error) { expect((error as { code?: string }).code).toBe('ARIA_ENCRYPT_CONFIG_ERROR'); } }); it('getInner / isCryptoReady 访问器', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-6'); expect(backend.getInner()).toBe(inner); expect(backend.isCryptoReady()).toBe(true); await backend.close(); expect(backend.isCryptoReady()).toBe(false); }); it('密文块被篡改 → 读取抛 ARIA_DECRYPT_ERROR(GCM 认证失败)', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-7'); await backend.write('k', new TextEncoder().encode('integrity-check').buffer); // 篡改密文一个字节(绕过 IV 区) const raw = (await inner.read('k')) as ArrayBuffer; const buf = new Uint8Array(raw); buf[16] ^= 0xff; await inner.write('k', buf.buffer as ArrayBuffer); await expect(backend.read('k')).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' }); await backend.close(); }); it('过短密文块 → 抛 ARIA_DECRYPT_ERROR', async () => { const inner = new MemoryBackend(); const backend = new EncryptedBackend(inner, 'pw'); await backend.open('enc-unit-8'); await inner.write('short', new Uint8Array(4).buffer); // 不足 IV(12) + tag await expect(backend.read('short')).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' }); await backend.close(); }); }); // =================================================================== // AriaEngine 集成 — 加密往返 // =================================================================== describe('AriaEngine — 全库加密(集成)', () => { it('加密库:写入 → close → 同密码重开数据完整', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'master-pass' } }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.insert('users', [ { id: '1', name: 'Alice', secret: 'top-secret-a' }, { id: '2', name: 'Bob', secret: 'top-secret-b' }, ]); await engine.setMeta('__metona_version', '3'); await engine.close(); // 重开(同密码) const engine2 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'master-pass' } }); await engine2.open(dbName, 1); const rows = await engine2.find('users', { table: 'users' }); expect(rows).toHaveLength(2); const byId = Object.fromEntries(rows.map((r) => [r.id, r])); expect(byId['1'].secret).toBe('top-secret-a'); expect(byId['2'].secret).toBe('top-secret-b'); expect(await engine2.getMeta('__metona_version')).toBe('3'); await engine2.close(); }); it('加密库:错误密码重开 → ARIA_DECRYPT_ERROR', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'right-pass' } }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.insert('users', [{ id: '1', name: 'A', secret: 'x' }]); await engine.close(); const engine2 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'wrong-pass' } }); await expect(engine2.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' }); }); it('加密库:无密码打开 → ARIA_ENCRYPT_REQUIRED', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.close(); const engine2 = new AriaEngine({ storageBackend: 'opfs' }); await expect(engine2.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_ENCRYPT_REQUIRED' }); }); it('明文库:加密配置打开 → ARIA_ENCRYPT_CONFIG_ERROR', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs' }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.insert('users', [{ id: '1', name: 'A', secret: 'plain' }]); await engine.close(); const engine2 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } }); await expect(engine2.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_ENCRYPT_CONFIG_ERROR' }); }); it('加密库:底层存储全部为密文(SSTable/Schema/WAL 均不可见明文)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' } }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.insert('users', [{ id: '1', name: 'Alice', secret: 'needle-in-cipher' }]); await (engine as any).lsm.flush(); // 强制落盘后 WAL 仍有记录(checkpoint 截断前)→ 全量扫描检查 const backend = (engine as any).backend as EncryptedBackend; const inner = backend.getInner(); const keys = await inner.listKeys(); expect(keys.length).toBeGreaterThan(0); // 任何一个底层 key 的内容都不包含明文数据(需解密才可见) for (const key of keys) { if (key === '__aria_keymeta') continue; const raw = await inner.read(key); const str = new TextDecoder().decode(raw as ArrayBuffer); expect(str).not.toContain('needle-in-cipher'); expect(str).not.toContain('Alice'); expect(str).not.toContain('users'); } await engine.close(); }); it('加密 + 压缩组合:往返完整(压缩层先压,加密层后加密)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', compression: true, encryption: { password: 'pw-compress' }, memtableSizeThreshold: 64 * 1024 * 1024, }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); const rows = [] as Record[]; for (let i = 0; i < 200; i++) { rows.push({ id: `u-${i}`, name: `User${i}`, secret: '这是需要加密的敏感内容'.repeat(5) }); } await engine.insert('users', rows); await (engine as any).lsm.flush(); await engine.close(); const engine2 = new AriaEngine({ storageBackend: 'opfs', compression: true, encryption: { password: 'pw-compress' }, }); await engine2.open(dbName, 1); expect(await engine2.count('users')).toBe(200); const one = await engine2.find('users', { table: 'users', where: { id: 'u-42' } }); expect(one[0].secret).toBe('这是需要加密的敏感内容'.repeat(5)); await engine2.close(); }); it('加密库:clearAll 保留密钥(库身份),同密码重开重建,新密码被拒', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'old-pw' } }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); await engine.insert('users', [{ id: '1', name: 'A', secret: 'x' }]); await engine.clearAll(); await engine.close(); // clearAll 保留 keymeta → 换新密码会被拒绝 const engineWrong = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'new-pw' } }); await expect(engineWrong.open(dbName, 1)).rejects.toMatchObject({ code: 'ARIA_DECRYPT_ERROR' }); // 同密码重开可重建 const engine2 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'old-pw' } }); await engine2.open(dbName, 1); await engine2.createTable(SCHEMA()); await engine2.insert('users', [{ id: '1', name: 'A', secret: 'new' }]); expect(await engine2.count('users')).toBe(1); await engine2.close(); }); it('加密库:WAL 崩溃恢复路径可用(含加密 WAL)', async () => { const dbName = uniqueDB(); const engine = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' }, walSyncMode: 'full', checkpointInterval: 100000, }); await engine.open(dbName, 1); await engine.createTable(SCHEMA()); // 写入后不 flush 不 checkpoint,直接"崩溃"(close 会 checkpoint,这里模拟崩溃:直接断 backend) for (let i = 0; i < 10; i++) { await engine.insert('users', [{ id: `w-${i}`, name: `W${i}`, secret: `s${i}` }]); } // 模拟崩溃:不 close,直接断开(真实崩溃时 WAL 留在存储中) await (engine as any).backend.close(); (engine as any).opened = false; // 重开:WAL 重放(加密 WAL 解密后解析) const engine2 = new AriaEngine({ storageBackend: 'opfs', encryption: { password: 'pw' }, walSyncMode: 'full', checkpointInterval: 100000, }); await engine2.open(dbName, 1); const rows = await engine2.find('users', { table: 'users' }); expect(rows).toHaveLength(10); await engine2.close(); }); });