UNPKG

indinis

Version:

A storage library using LSM trees for storage and B-trees for indices with MVCC support

138 lines (117 loc) 6.88 kB
// @tssrc/test/encryption_password_change.test.ts import { Indinis, IndinisOptions, StorageValue } from '../index'; import * as fs from 'fs'; import * as path from 'path'; import { rimraf } from 'rimraf'; const TEST_DATA_DIR_BASE = path.resolve(__dirname, '..', '..', '.test-data', 'indinis-pw-change-tests-v1'); const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const s = (obj: any): string => JSON.stringify(obj); interface PwChangeTestDoc { id: string; data: string; } describe('Indinis Password Change and KDF Iteration Persistence Test', () => { let testDataDir: string; const initialPassword = "InitialPassword123!"; const newPassword = "NewStrongerPassword456$"; const initialKdfIterations = 700000; // Custom high for initial const newKdfIterations = 800000; // Custom high for new const defaultKdfIterations = 600000; // Assumed C++ default const colPath = 'pw_change_docs'; let db: Indinis | null = null; const getOptions = (password?: string, kdfIters?: number): IndinisOptions => ({ checkpointIntervalSeconds: 5, sstableDataBlockUncompressedSizeKB: 8, // Small blocks encryptionOptions: { password: password, scheme: password ? "AES256_GCM_PBKDF2" : "NONE", kdfIterations: kdfIters, }, walOptions: { wal_directory: path.join(testDataDir, 'wal_pw_change') } }); jest.setTimeout(60000); // 1 minute beforeAll(async () => { await fs.promises.mkdir(TEST_DATA_DIR_BASE, { recursive: true }); }); afterAll(async () => { if (fs.existsSync(TEST_DATA_DIR_BASE)) await rimraf(TEST_DATA_DIR_BASE); }); beforeEach(async () => { const randomSuffix = `${Date.now()}-${Math.random().toString(36).substring(7)}`; testDataDir = path.join(TEST_DATA_DIR_BASE, `test-${randomSuffix}`); await fs.promises.mkdir(testDataDir, { recursive: true }); // WAL dir created by Indinis constructor console.log(`\n[PW-CHANGE TEST START] Using data directory: ${testDataDir}`); }); afterEach(async () => { if (db) { try { await db.close(); } catch (e) {} db = null; } await delay(200); if (fs.existsSync(testDataDir)) await rimraf(testDataDir, { maxRetries: 2, retryDelay: 500 }); }); const key1 = `${colPath}/doc1`; const doc1: PwChangeTestDoc = { id: "doc1", data: "Data under initial password" }; it('should allow password change and persist KDF iterations', async () => { // 1. Create DB with initial password and KDF iterations console.log(`--- Creating DB with initial password and KDF iterations: ${initialKdfIterations} ---`); db = new Indinis(testDataDir, getOptions(initialPassword, initialKdfIterations)); await delay(100); await db.transaction(async tx => { await tx.set(key1, s(doc1)); }); console.log(" Initial data written."); await db.close(); db = null; await delay(200); // 2. Reopen with initial password and KDF iterations - verify data console.log("--- Reopening with initial password and KDF iterations ---"); db = new Indinis(testDataDir, getOptions(initialPassword, initialKdfIterations)); await delay(100); await db.transaction(async tx => { const r = await tx.get(key1); expect(r).not.toBeNull(); expect(JSON.parse(r as string).data).toEqual(doc1.data); }); console.log(" Data verified with initial credentials."); // 3. Change password console.log(`--- Changing password to new password with KDF iterations: ${newKdfIterations} ---`); const changeSuccess = await db.changeDatabasePassword(initialPassword, newPassword, newKdfIterations); expect(changeSuccess).toBe(true); console.log(" Password change successful."); await db.close(); db = null; await delay(200); // 4. Attempt to open with OLD password (should fail or not decrypt DEK correctly) console.log("--- Attempting to open with OLD password (expect failure) ---"); let oldPassOpenThrew = false; try { db = new Indinis(testDataDir, getOptions(initialPassword, initialKdfIterations)); // Old creds await delay(100); // Accessing data should fail if KEK validation fails due to wrong KEK await db.transaction(async tx => { await tx.get(key1); }); // This might throw from C++ or return null } catch (e: any) { console.log(` Caught expected error opening with old password: ${e.message}`); oldPassOpenThrew = true; } // More robust check: initializeEncryptionState should throw from C++ if password invalid expect(oldPassOpenThrew || (db === null || (await db.transaction(async tx=>tx.get(key1))) === null )).toBe(true); if (db) { await db.close(); db = null; } await delay(200); // 5. Open with NEW password and NEW KDF iterations - verify data console.log(`--- Opening with NEW password and NEW KDF iterations: ${newKdfIterations} ---`); db = new Indinis(testDataDir, getOptions(newPassword, newKdfIterations)); await delay(100); await db.transaction(async tx => { const r = await tx.get(key1); expect(r).not.toBeNull(); expect(JSON.parse(r as string).data).toEqual(doc1.data); }); console.log(" Data verified with new credentials."); // 6. Write NEW data with new password setup const key2 = `${colPath}/doc2_new_pass`; const doc2: PwChangeTestDoc = { id: "doc2", data: "Data under new password" }; await db.transaction(async tx => { await tx.set(key2, s(doc2)); }); console.log(" New data written under new password setup."); await db.close(); db = null; await delay(200); // 7. Open with NEW password but OMIT KDF iterations (should use STORED newKdfIterations) console.log("--- Opening with NEW password and OMITTED KDF iterations (should use stored) ---"); db = new Indinis(testDataDir, getOptions(newPassword, undefined)); // Undefined iterations await delay(100); await db.transaction(async tx => { const r1 = await tx.get(key1); expect(r1).not.toBeNull(); expect(JSON.parse(r1 as string).data).toEqual(doc1.data); const r2 = await tx.get(key2); expect(r2).not.toBeNull(); expect(JSON.parse(r2 as string).data).toEqual(doc2.data); }); console.log(" Data verified with new password and implicitly used stored KDF iterations."); await db.close(); db = null; }); });