argus/test/password-vault.test.mjs
Local Dev b4a550ef41 Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
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
2026-08-14 23:17:18 +02:00

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");
}
});