Several concurrent workstreams committed together as a checkpoint: - Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic, AndroidManifest + build.ps1 - Theseus password manager — settings.html/chrome.html/settings-preload.js UI + main.js wiring + package.json resource; Argus password-vault.js, record-picker.js (+ tests) and resolver-web.d.ts - Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored) - Argus public-gateway.mjs updates - Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant), site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup, Decentralized Storage map, coordination notes - .gitignore — exclude /.keys/ and Hephaestus/.env
121 lines
5.5 KiB
JavaScript
121 lines
5.5 KiB
JavaScript
// Wire-format + determinism tests for the password vault.
|
|
// Runs offline — no chain, no IPC, no filesystem beyond a tmp path.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync, existsSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import {
|
|
bip39ToSeed, seedToPurposeRoot, derivePassword,
|
|
createVault, unlockVault, saveVault, vaultExists,
|
|
newEntry, resolvePassword, listMetadata,
|
|
bytesToHex,
|
|
} from "../src/lib/password-vault.js";
|
|
|
|
const tmp = mkdtempSync(join(tmpdir(), "sm-pv-"));
|
|
const vaultPath = join(tmp, "passwords.vault");
|
|
|
|
// A fixed test seed (32 random bytes hex) — real vaults use HKDF from a
|
|
// BIP39 seed; this is just deterministic input for the derivePassword tests.
|
|
const TEST_ROOT = "9a3f6d21b4e0c8127f4d5a6789bcdef012345678901234567890abcdef123456";
|
|
|
|
test("derivePassword — same inputs → same output", async () => {
|
|
const a = await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 1 });
|
|
const b = await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 1 });
|
|
assert.equal(a, b);
|
|
});
|
|
|
|
test("derivePassword — different domain → different password", async () => {
|
|
const a = await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 1 });
|
|
const b = await derivePassword(TEST_ROOT, { domain: "other.com", username: "alice", version: 1 });
|
|
assert.notEqual(a, b);
|
|
});
|
|
|
|
test("derivePassword — different version → different password (rotation)", async () => {
|
|
const a = await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 1 });
|
|
const b = await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 2 });
|
|
assert.notEqual(a, b);
|
|
});
|
|
|
|
test("derivePassword — different root → different password", async () => {
|
|
const OTHER = "1111111111111111111111111111111111111111111111111111111111111111";
|
|
const a = await derivePassword(TEST_ROOT, { domain: "example.com" });
|
|
const b = await derivePassword(OTHER, { domain: "example.com" });
|
|
assert.notEqual(a, b);
|
|
});
|
|
|
|
test("derivePassword — length + charset rules honored", async () => {
|
|
const p = await derivePassword(TEST_ROOT, { domain: "e.com", rules: { length: 24, upper: true, lower: true, digits: true, symbols: true } });
|
|
assert.equal(p.length, 24);
|
|
assert.match(p, /[a-z]/);
|
|
assert.match(p, /[A-Z]/);
|
|
assert.match(p, /[0-9]/);
|
|
assert.match(p, /[^A-Za-z0-9]/);
|
|
});
|
|
|
|
test("derivePassword — digits-only rules produce digits-only password", async () => {
|
|
const p = await derivePassword(TEST_ROOT, { domain: "e.com", rules: { length: 12, digits: true, upper: false, lower: false, symbols: false } });
|
|
assert.equal(p.length, 12);
|
|
assert.match(p, /^[0-9]+$/);
|
|
});
|
|
|
|
test("bip39ToSeed → matches BIP39 test vector", async () => {
|
|
// BIP39 official vector — mnemonic + empty passphrase.
|
|
// https://github.com/trezor/python-mnemonic/blob/master/vectors.json
|
|
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
const seed = await bip39ToSeed(mnemonic, "");
|
|
const expected = "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4";
|
|
assert.equal(bytesToHex(seed), expected);
|
|
});
|
|
|
|
test("seedToPurposeRoot — deterministic, different purpose → different root", async () => {
|
|
const seed = new Uint8Array(64).fill(0x42);
|
|
const r1 = await seedToPurposeRoot(seed, "passwords/0");
|
|
const r2 = await seedToPurposeRoot(seed, "passwords/0");
|
|
const rMsg = await seedToPurposeRoot(seed, "messenger/0");
|
|
assert.deepEqual(r1, r2);
|
|
assert.notDeepEqual(r1, rMsg);
|
|
});
|
|
|
|
test("createVault + unlockVault round-trip with the right master password", async () => {
|
|
const path = join(tmp, "roundtrip.vault");
|
|
const rootHex = TEST_ROOT;
|
|
await createVault(path, "correct-horse-battery-staple", rootHex);
|
|
assert.ok(vaultExists(path));
|
|
const st = await unlockVault(path, "correct-horse-battery-staple");
|
|
assert.equal(st.purposeRoot, rootHex);
|
|
assert.deepEqual(st.entries, []);
|
|
});
|
|
|
|
test("unlockVault — wrong master password → throws", async () => {
|
|
const path = join(tmp, "wrongpw.vault");
|
|
await createVault(path, "right", TEST_ROOT);
|
|
await assert.rejects(unlockVault(path, "wrong"), /bad master password/);
|
|
});
|
|
|
|
test("newEntry + saveVault + unlockVault preserves entries; resolvePassword works", async () => {
|
|
const path = join(tmp, "entries.vault");
|
|
const st = await createVault(path, "pw", TEST_ROOT);
|
|
const e1 = newEntry({ domain: "example.com", username: "alice" });
|
|
const e2 = newEntry({ domain: "bank.com", username: "bob", literal: "hunter2!!" });
|
|
st.entries.push(e1, e2);
|
|
await saveVault(path, st);
|
|
const st2 = await unlockVault(path, "pw");
|
|
assert.equal(st2.entries.length, 2);
|
|
assert.equal(await resolvePassword(st2, e1.id), await derivePassword(TEST_ROOT, { domain: "example.com", username: "alice", version: 1 }));
|
|
assert.equal(await resolvePassword(st2, e2.id), "hunter2!!");
|
|
});
|
|
|
|
test("listMetadata — never leaks passwords", () => {
|
|
const st = { entries: [
|
|
{ id: "1", domain: "a.com", username: "a", literal: "secret1", addedAt: "" },
|
|
{ id: "2", domain: "b.com", username: "b", generated: { version: 1, rules: {} }, addedAt: "" },
|
|
]};
|
|
const meta = listMetadata(st);
|
|
for (const m of meta) {
|
|
assert.equal(m.literal, undefined);
|
|
assert.equal(m.generated, undefined);
|
|
assert.ok(m.kind === "literal" || m.kind === "generated");
|
|
}
|
|
});
|