Files
MetonaSqlark/tests/engine/aria-encryption.test.ts
T
thzxx 24b3ca1079
CI / test (20.x) (push) Successful in 10m53s
CI / test (22.x) (push) Successful in 10m47s
CI / test (24.x) (push) Successful in 10m42s
CI / e2e (push) Successful in 10m27s
CI / test (18.x) (push) Successful in 11m1s
release: v0.6.0 — 完全移除 IndexedDB,自研 KVStore 事务存储引擎(多key原子写/快照日志恢复/CRC自愈)+ KVStoreEngine + 旧库迁移工具 + 10万级压力验证 + 崩溃注入e2e
2026-08-10 13:56:04 +08:00

353 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 { installOPFSMock } from '../helpers/opfs-mock';
beforeEach(() => { installOPFSMock(new Map()); });
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(new TextDecoder().decode(back)).toBe('hello encrypted world');
// 底层是密文(非明文)
const raw = await inner.read('k1');
const rawStr = new TextDecoder().decode(raw);
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(new TextDecoder().decode(await backend.read('a'))).toBe('AAA');
expect(new TextDecoder().decode(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(new TextDecoder().decode(await backend.read('x'))).toBe('same plaintext');
expect(new TextDecoder().decode(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(new TextDecoder().decode(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_ERRORGCM 认证失败)', 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<string, unknown>[];
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();
});
});