import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { createRequire } from 'node:module'; const { createVault, generatePassword } = createRequire(import.meta.url)('../apps/browser/vault.cjs'); const master = 'A long separate master password 2026!'; function fixture(t) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'safetynet-vault-test-')); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const platformKey = crypto.randomBytes(32); const options = { filePath: path.join(directory, 'vault.json'), protect(data) { const nonce = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', platformKey, nonce); const body = Buffer.concat([cipher.update(data), cipher.final()]); return Buffer.concat([nonce, cipher.getAuthTag(), body]); }, unprotect(data) { const decipher = crypto.createDecipheriv('aes-256-gcm', platformKey, data.subarray(0, 12)); decipher.setAuthTag(data.subarray(12, 28)); return Buffer.concat([decipher.update(data.subarray(28)), decipher.final()]); } }; return { options, engine: createVault(options) }; } test('vault encrypts full metadata and roundtrips through actual ML-KEM', t => { const { engine, options } = fixture(t); t.after(engine.lock); engine.create(master); const { id } = engine.upsert({ origin: 'https://company.example', username: 'private-user-name', password: 'top-secret-credential', label: 'Confidential label' }); const stored = fs.readFileSync(options.filePath, 'utf8'); for (const secret of ['company.example', 'private-user-name', 'top-secret-credential', 'Confidential label', master]) assert.equal(stored.includes(secret), false); assert.equal(Buffer.from(JSON.parse(stored).kemCiphertext, 'base64').length, 1568); assert.equal('password' in engine.list()[0], false); engine.lock(); assert.throws(() => engine.list()); assert.throws(() => engine.credential(id, 'https://company.example')); engine.unlock(master); assert.deepEqual(engine.credential(id, 'https://company.example'), { username: 'private-user-name', password: 'top-secret-credential' }); assert.throws(() => engine.credential(id, 'https://company.example:444')); assert.throws(() => engine.credential(id, 'https://evil.example')); assert.throws(() => engine.credential(id, 'http://company.example')); assert.throws(() => engine.credential(id, 'https://company.example/path')); engine.delete(id); engine.lock(); engine.unlock(master); assert.deepEqual(engine.list(), []); }); test('wrong master password and wrong platform factor fail closed', t => { const { engine, options } = fixture(t); t.after(engine.lock); engine.create(master); engine.lock(); assert.throws(() => engine.unlock('Wrong long master password!!!'), /Vault operation refused/); assert.equal(engine.status().locked, true); const other = createVault({ ...options, unprotect: () => { throw new Error('DPAPI failed'); } }); assert.throws(() => other.unlock(master), /Vault operation refused/); assert.equal(other.status().locked, true); engine.unlock(master); assert.equal(engine.status().locked, false); }); test('ciphertext, header and key envelope tampering is refused', t => { const { engine, options } = fixture(t); t.after(engine.lock); engine.create(master); engine.lock(); const pristine = fs.readFileSync(options.filePath, 'utf8'); for (const mutate of [record => { record.payload.ciphertext = record.payload.ciphertext.replace(/^./, char => char === 'A' ? 'B' : 'A'); }, record => { record.vaultId = crypto.randomUUID(); }, record => { record.salt = crypto.randomBytes(32).toString('base64'); }, record => { record.wrappedKey.tag = crypto.randomBytes(16).toString('base64'); }, record => { record.kemCiphertext = crypto.randomBytes(1568).toString('base64'); }]) { const record = JSON.parse(pristine); mutate(record); fs.writeFileSync(options.filePath, JSON.stringify(record)); assert.throws(() => engine.unlock(master), /Vault operation refused/); assert.equal(engine.status().locked, true); } }); test('malformed oversized file and untrusted KDF parameters never weaken security', t => { const { engine, options } = fixture(t); t.after(engine.lock); engine.create(master); engine.lock(); const record = JSON.parse(fs.readFileSync(options.filePath, 'utf8')); record.scrypt = { N: 2, r: 1, p: 1 }; fs.writeFileSync(options.filePath, JSON.stringify(record)); engine.unlock(master); engine.lock(); fs.writeFileSync(options.filePath, Buffer.alloc(8 * 1024 * 1024 + 1)); assert.throws(() => engine.unlock(master)); fs.writeFileSync(options.filePath, '{bad json'); assert.throws(() => engine.unlock(master)); }); test('master requirements, idle bounds and credential validation', t => { const { engine, options } = fixture(t); t.after(engine.lock); assert.throws(() => engine.create('short')); assert.equal(engine.status().exists, false); assert.throws(() => createVault({ ...options, idleSeconds: 59 })); assert.throws(() => createVault({ ...options, idleSeconds: 901 })); engine.create(master); assert.throws(() => engine.create(master)); assert.equal(engine.status().locked, true); engine.unlock(master); assert.throws(() => engine.upsert({ origin: 'https://user:pass@company.example', username: 'user', password: 'secret' })); assert.throws(() => engine.upsert({ origin: 'http://company.example', username: 'user', password: 'secret' })); assert.throws(() => engine.upsert({ origin: 'https://company.example', username: 'user', password: '' })); assert.deepEqual(engine.list(), []); assert.equal(generatePassword().length, 32); assert.throws(() => generatePassword(23)); assert.notEqual(generatePassword(), generatePassword()); }); test('idle expiry locks credentials and atomic write failure preserves the last committed vault', t => { t.mock.timers.enable({ apis: ['setTimeout'] }); const { engine, options } = fixture(t); t.after(engine.lock); engine.create(master); const { id } = engine.upsert({ origin: 'https://company.example', username: 'user', password: 'original' }); const original = fs.readFileSync(options.filePath); const rename = t.mock.method(fs, 'renameSync', () => { throw new Error('Disk write failed'); }); assert.throws(() => engine.upsert({ id, origin: 'https://company.example', username: 'user', password: 'uncommitted' })); assert.equal(engine.credential(id, 'https://company.example').password, 'original'); assert.deepEqual(fs.readFileSync(options.filePath), original); assert.equal(fs.readdirSync(path.dirname(options.filePath)).some(name => name.endsWith('.tmp')), false); rename.mock.restore(); t.mock.timers.tick(300_001); assert.equal(engine.status().locked, true); assert.throws(() => engine.credential(id, 'https://company.example')); engine.unlock(master); assert.equal(engine.credential(id, 'https://company.example').password, 'original'); }); test('background metadata polling cannot postpone the idle lock deadline', t => { t.mock.timers.enable({ apis: ['setTimeout'] }); const { engine } = fixture(t); t.after(engine.lock); engine.create(master); for (let elapsed = 0; elapsed < 300_000; elapsed += 5000) { assert.deepEqual(engine.list(), []); t.mock.timers.tick(5000); } assert.equal(engine.status().locked, true); assert.throws(() => engine.list()); }); test('internal access generation invalidates refs across reopen and same-id credential changes', t => { const { engine } = fixture(t); t.after(engine.lock); const initial = engine.accessGeneration(); engine.create(master); assert.ok(engine.accessGeneration() > initial); const { id } = engine.upsert({ origin: 'https://company.example', username: 'user', password: 'first-fixture-value' }); const listed = engine.accessGeneration(); engine.upsert({ id, origin: 'https://company.example', username: 'user', password: 'second-fixture-value' }); assert.ok(engine.accessGeneration() > listed); assert.equal(engine.list()[0].id, id); const changed = engine.accessGeneration(); engine.lock(); assert.ok(engine.accessGeneration() > changed); const locked = engine.accessGeneration(); engine.unlock(master); assert.ok(engine.accessGeneration() > locked); assert.equal(engine.list()[0].id, id); const reopened = engine.accessGeneration(); engine.delete(id); assert.ok(engine.accessGeneration() > reopened); assert.equal('accessGeneration' in engine.status(), false); }); test('internal access generation and metadata reads neither renew idle unlock nor invalidate refs', t => { t.mock.timers.enable({ apis: ['setTimeout'] }); const { engine } = fixture(t); t.after(engine.lock); engine.create(master); const generation = engine.accessGeneration(); for (let elapsed = 0; elapsed < 300_000; elapsed += 5000) { assert.equal(engine.accessGeneration(), generation); assert.equal(engine.status().locked, false); assert.deepEqual(engine.list(), []); t.mock.timers.tick(5000); } assert.equal(engine.status().locked, true); assert.ok(engine.accessGeneration() > generation); });