- crypto.ts encryptPage/decryptPage 改传 TypedArray 视图(ArrayBuffer.isView 检查跨 realm 可靠)
- jest.setup.js structuredClone polyfill 用跨 realm toString 标签检查
(修复 fake-indexeddb 存储 ArrayBuffer 被 JSON 破坏成 {} 的问题)
- 新增 SSTable 加密真实路径测试(加密落盘→重载解密, 8 个测试)
- aria/v025 固定 db 名改随机(fake-indexeddb 真实持久化后防表残留冲突)
- Node 18/20/22/24 全矩阵 837 测试通过
158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
/**
|
|
* AriaEngine CryptoManager 加解密测试
|
|
* @module tests/engine/aria-crypto
|
|
*
|
|
* v0.2.6 补强:此前仅验证实例化,现在验证真实的加解密往返一致性。
|
|
*/
|
|
import 'fake-indexeddb/auto';
|
|
import {
|
|
CryptoManager,
|
|
initCrypto,
|
|
encryptPage,
|
|
decryptPage,
|
|
closeCrypto,
|
|
} from '../../src/engine/aria/crypto';
|
|
|
|
function toBytes(data: ArrayBuffer): number[] {
|
|
return Array.from(new Uint8Array(data));
|
|
}
|
|
|
|
describe('AriaEngine — CryptoManager', () => {
|
|
test('加解密往返一致', async () => {
|
|
const cm = new CryptoManager();
|
|
await cm.init('test-password');
|
|
expect(cm.enabled).toBe(true);
|
|
|
|
const original = new TextEncoder().encode('sensitive row data').buffer;
|
|
const { iv, data } = await cm.encryptPage(original);
|
|
// 密文应为乱码(与原文不同)
|
|
expect(toBytes(data)).not.toEqual(toBytes(original));
|
|
|
|
const decrypted = await cm.decryptPage(iv, data);
|
|
expect(toBytes(decrypted)).toEqual(toBytes(original));
|
|
cm.close();
|
|
expect(cm.enabled).toBe(false);
|
|
});
|
|
|
|
test('错误密码解密失败(密钥不同)', async () => {
|
|
const cm1 = new CryptoManager();
|
|
await cm1.init('correct-password');
|
|
const original = new TextEncoder().encode('top secret').buffer;
|
|
const { iv, data } = await cm1.encryptPage(original);
|
|
|
|
const cm2 = new CryptoManager();
|
|
await cm2.init('wrong-password');
|
|
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
|
|
|
|
cm1.close();
|
|
cm2.close();
|
|
});
|
|
|
|
test('不同 salt 派生不同密钥,解密互相失败', async () => {
|
|
const cm1 = new CryptoManager();
|
|
await cm1.init('pwd', new Uint8Array(16).fill(1));
|
|
const cm2 = new CryptoManager();
|
|
await cm2.init('pwd', new Uint8Array(16).fill(2));
|
|
|
|
const original = new TextEncoder().encode('salt matters').buffer;
|
|
const { iv, data } = await cm1.encryptPage(original);
|
|
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
|
|
|
|
cm1.close();
|
|
cm2.close();
|
|
});
|
|
|
|
test('未初始化时加密抛错', async () => {
|
|
const cm = new CryptoManager();
|
|
expect(cm.enabled).toBe(false);
|
|
const data = new TextEncoder().encode('x').buffer;
|
|
await expect(cm.encryptPage(data)).rejects.toThrow(/not initialized/);
|
|
});
|
|
|
|
test('不同实例互不影响(独立密钥状态)', async () => {
|
|
const cm1 = new CryptoManager();
|
|
await cm1.init('pwd-a');
|
|
const cm2 = new CryptoManager();
|
|
await cm2.init('pwd-b');
|
|
|
|
const original = new TextEncoder().encode('instance isolation').buffer;
|
|
const { iv, data } = await cm1.encryptPage(original);
|
|
await expect(cm2.decryptPage(iv, data)).rejects.toThrow();
|
|
|
|
// 各自解密自己的数据
|
|
const dec2orig = new TextEncoder().encode('two').buffer;
|
|
const enc2 = await cm2.encryptPage(dec2orig);
|
|
const dec2 = await cm2.decryptPage(enc2.iv, enc2.data);
|
|
expect(toBytes(dec2)).toEqual(toBytes(dec2orig));
|
|
|
|
cm1.close();
|
|
cm2.close();
|
|
});
|
|
|
|
test('全局兼容层往返一致', async () => {
|
|
await initCrypto('global-password');
|
|
const original = new TextEncoder().encode('global compat layer').buffer;
|
|
const { iv, data } = await encryptPage(original);
|
|
const decrypted = await decryptPage(iv, data);
|
|
expect(toBytes(decrypted)).toEqual(toBytes(original));
|
|
closeCrypto();
|
|
});
|
|
|
|
test('大块数据(接近页面大小)往返一致', async () => {
|
|
const cm = new CryptoManager();
|
|
await cm.init('page-size-test');
|
|
// 4KB 页面数据
|
|
const original = new Uint8Array(4096);
|
|
for (let i = 0; i < 4096; i++) original[i] = i % 251;
|
|
const { iv, data } = await cm.encryptPage(original.buffer);
|
|
const decrypted = await cm.decryptPage(iv, data);
|
|
expect(toBytes(decrypted)).toEqual(toBytes(original.buffer));
|
|
cm.close();
|
|
});
|
|
});
|
|
|
|
// ===================================================================
|
|
// 真实存储路径:SSTable 加密 → 持久化 → 重载解密(v0.3.2 CI 修复)
|
|
// ===================================================================
|
|
|
|
describe('AriaEngine — SSTable 加密存储真实路径', () => {
|
|
test('加密落盘后重载可完整解密', async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { AriaEngine } = require('../../src/engine/aria/index');
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const { createSchema } = require('../../src/table/schema');
|
|
const dbName = `enc-store-${Date.now()}`;
|
|
|
|
await initCrypto('engine-store-password');
|
|
|
|
const engine = new AriaEngine({
|
|
storageBackend: 'indexeddb',
|
|
walSyncMode: 'none',
|
|
} as any);
|
|
await engine.open(dbName, 1);
|
|
await engine.createTable(createSchema('t', {
|
|
id: { type: 'string', primaryKey: true },
|
|
v: { type: 'number' },
|
|
}));
|
|
await engine.insert('t', [{ id: '1', v: 42 }, { id: '2', v: 99 }]);
|
|
// 强制刷盘:SSTable 经加密保存(encryptPage 真实路径)
|
|
await (engine as any).lsm.flush();
|
|
await engine.close();
|
|
|
|
// 重载:SSTable 解密恢复
|
|
const engine2 = new AriaEngine({
|
|
storageBackend: 'indexeddb',
|
|
walSyncMode: 'none',
|
|
} as any);
|
|
await engine2.open(dbName, 1);
|
|
const rows = await engine2.find('t', { table: 't' });
|
|
expect(rows).toHaveLength(2);
|
|
const byId = Object.fromEntries(rows.map((r: any) => [r.id, r.v]));
|
|
expect(byId['1']).toBe(42);
|
|
expect(byId['2']).toBe(99);
|
|
await engine2.close();
|
|
|
|
closeCrypto();
|
|
});
|
|
});
|