first commit
This commit is contained in:
commit
ff482ebe23
251 files changed
+19126
No files matched your search
@@ -0,0 +1,103 @@
|
||||
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:[email protected]', 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());
|
||||
});
|
||||
Reference in new issue
Block a user