The PIN could only be six digits and was set from three bare inputs; the unlock prompt sat at the top of the page; and the password manager only filled when you found the key chip, never offered to save, and "Clear cookies on quit" signed you out of every site, including the ones whose login the vault already holds. - PINs are 6 to 8 digits. The PIN record stores its length so pads draw the right number of dots and submit on the last digit; a PIN of the wrong length is refused without a strike, so an older Aegis pad cannot burn the count against an 8-digit PIN. - Settings sets a PIN in steps: master password, choose the PIN on a pad (6/7/8), repeat it, done. The locked vault opens Theseus's own prompt, which is now centred, with the PIN pad or the master password field. - After a sign-in or sign-up form is sent and the page moves on, Theseus offers to save (or update) the login, with an optional "ask for my PIN or password before filling it". Focusing a login form offers the saved logins under it; on a locked vault it offers to unlock first. A failed login (the password field still showing) gets no offer. - "Keep sign-ins for sites in your vault" (on): the quit clear spares the cookies and site storage of sites with a saved login. Their hostnames are kept sealed with the OS keystore so the list is readable at quit while the vault is locked. Verified end to end on a scratch profile: signed in, restarted, still signed in; another site's cookie was cleared.
66 lines
2.8 KiB
JavaScript
66 lines
2.8 KiB
JavaScript
// 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");
|
|
});
|