// node --test TheseusNavigator/dev/vault-pin.test.cjs // lib/vault-pin.cjs with a fake OS keystore and no TPM, so a wrong PIN here // never costs a real TPM dictionary-attack strike. "use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { createVaultPin, MAX_FAILS, PIN_MIN, PIN_MAX } = require("../lib/vault-pin.cjs"); const fakeSafe = { isEncryptionAvailable: () => true, getSelectedStorageBackend: () => "gnome_libsecret", encryptString: (s) => Buffer.from("SEALED:" + s), decryptString: (b) => { const s = b.toString(); if (!s.startsWith("SEALED:")) throw new Error("bad seal"); return s.slice(7); }, }; const noTpm = { supported: () => false }; const make = () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vpin-")); const file = path.join(dir, "vault-pin.json"); return Object.assign(createVaultPin({ file, safeStorage: fakeSafe, tpm: noTpm }), { file }); }; test("lengths 6 to 8 are accepted, others refused", async () => { assert.equal(PIN_MIN, 6); assert.equal(PIN_MAX, 8); const p = make(); for (const bad of ["12345", "123456789", "12a456", ""]) await assert.rejects(p.set(bad, "pw"), /6 to 8 digits/); for (const good of ["123456", "1234567", "12345678"]) { await p.set(good, "master"); assert.equal(p.status().length, good.length); assert.equal(await p.open(good), "master"); } }); test("a wrong-length PIN costs no strike", async () => { const p = make(); await p.set("12345678", "master"); for (let i = 0; i < MAX_FAILS + 2; i++) { await assert.rejects(p.open("123456"), (e) => e.code === "wrong-length" && e.length === 8); } assert.equal(p.status().fails, 0); assert.equal(await p.open("12345678"), "master"); }); test("wrong PINs of the right length still lock after MAX_FAILS", async () => { const p = make(); await p.set("1234567", "master"); for (let i = 1; i < MAX_FAILS; i++) await assert.rejects(p.open("7654321"), (e) => e.code === "wrong-pin" && e.remaining === MAX_FAILS - i); await assert.rejects(p.open("7654321"), (e) => e.code === "locked"); await assert.rejects(p.open("1234567"), (e) => e.code === "locked"); }); test("records from 6-only builds (no len) read as 6 digits", async () => { const p = make(); await p.set("123456", "master"); // Strip len, as an older build would have written it. const rec = JSON.parse(fs.readFileSync(p.file, "utf8")); const blob = JSON.parse(fakeSafe.decryptString(Buffer.from(rec.data, "base64"))); delete blob.len; rec.data = fakeSafe.encryptString(JSON.stringify(blob)).toString("base64"); fs.writeFileSync(p.file, JSON.stringify(rec)); assert.equal(p.status().length, 6); assert.equal(await p.open("123456"), "master"); await assert.rejects(p.open("1234567"), (e) => e.code === "wrong-length"); });