breached-passwords.mjs
66 lines 2.2 KB
Raw
sha256:c2dbf04d56308f3bbf2d06e6d2eb022b8948b1e827195fe525a44e5e18d5f9c0 feat(auth): Phase B Connect cloud agent (RFC 8628) + Hermes… Human minor ⚠ breaking 11 days ago
1 /**
2 * Phase 8 P1b-b — offline breached-password SHA-1 lookup (HIBP-style, no network).
3 * Loads hub/data/breached-passwords-sha1.txt once at import.
4 */
5
6 import fs from 'fs';
7 import path from 'path';
8 import crypto from 'crypto';
9 import { fileURLToPath } from 'url';
10
11 // Netlify gateway bundle: import.meta.url may be missing at load time — never throw here.
12 function hubLibDirname() {
13 try {
14 const u = typeof import.meta !== 'undefined' ? import.meta.url : '';
15 if (u) return path.dirname(fileURLToPath(u));
16 } catch (_) {}
17 return path.join(process.cwd(), 'hub', 'lib');
18 }
19
20 const __dirname = hubLibDirname();
21 const HASH_FILE = path.join(__dirname, 'breached-passwords-sha1.txt');
22
23 /** @type {Set<string>|null} */
24 let cachedSet = null;
25
26 /**
27 * @returns {Set<string>}
28 */
29 export function loadBreachedPasswordSha1Set() {
30 if (cachedSet) return cachedSet;
31 cachedSet = new Set();
32 if (!fs.existsSync(HASH_FILE)) return cachedSet;
33 const raw = fs.readFileSync(HASH_FILE, 'utf8');
34 for (const line of raw.split('\n')) {
35 const h = line.trim().toUpperCase();
36 if (/^[0-9A-F]{40}$/.test(h)) cachedSet.add(h);
37 }
38 return cachedSet;
39 }
40
41 /**
42 * @param {string} passphrase
43 * @returns {boolean}
44 */
45 export function isBreachedPassphrase(passphrase) {
46 if (typeof passphrase !== 'string' || !passphrase) return false;
47 const sha1 = crypto.createHash('sha1').update(passphrase.toUpperCase()).digest('hex').toUpperCase();
48 return loadBreachedPasswordSha1Set().has(sha1);
49 }
50
51 /**
52 * Passphrase strength policy (§6.2): ≥12 chars, mixed case, digit, symbol, not breached.
53 * @param {string} passphrase
54 * @returns {{ ok: boolean, code?: string }}
55 */
56 export function validatePassphraseStrength(passphrase) {
57 if (typeof passphrase !== 'string' || passphrase.length < 12) {
58 return { ok: false, code: 'WEAK_PASSPHRASE' };
59 }
60 if (!/[a-z]/.test(passphrase)) return { ok: false, code: 'WEAK_PASSPHRASE' };
61 if (!/[A-Z]/.test(passphrase)) return { ok: false, code: 'WEAK_PASSPHRASE' };
62 if (!/[0-9]/.test(passphrase)) return { ok: false, code: 'WEAK_PASSPHRASE' };
63 if (!/[^a-zA-Z0-9]/.test(passphrase)) return { ok: false, code: 'WEAK_PASSPHRASE' };
64 if (isBreachedPassphrase(passphrase)) return { ok: false, code: 'WEAK_PASSPHRASE' };
65 return { ok: true };
66 }
File History 1 commit
sha256:93bcf8f9bd56d8c5b9339f4ec73b9ebd66571398d56262d38eedc2cfa9db9882 fix(test): align Band B landing assertion with desktop MCP … Human 11 days ago