Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_genpass.mjs

10.2 KiB, 1 run

created by r2519314175:433, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_genpass.mjs — the generated passphrase, and the login form a password
2// manager can actually see.
3//
4// Two changes are asserted here, and they belong together. The passphrase is now
5// GENERATED (eight words from the EFF long list, ~103 bits) rather than chosen,
6// which removes the two ways a chosen one fails: guessable, and reused from a
7// site that has since been breached. That is what makes the second change safe —
8// the login is a REAL form with REAL `type="password"` fields and the right
9// autocomplete tokens, so the browser and the OS keychain offer to keep it
10// instead of the user retyping a long phrase on a phone all day.
11//
12// node dev/verify_genpass.mjs
13//
14// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway and no model: this is
15// the gate only.
16
17import { open } from './harness.mjs';
18
19let failures = 0;
20const check = (cond, msg, detail) => {
21 console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : ''));
22 if (!cond) failures++;
23};
24
25const s = await open({ name: 'genpass-' + Date.now(), connect: false, signIn: false });
26const { page } = s;
27await page.waitForSelector('#id-primary', { timeout: 15000 });
28await page.waitForTimeout(400);
29
30// ── The create screen offers a generated passphrase ──
31const gen = await page.evaluate(() => ({
32 words: document.getElementById('id-genwords').textContent,
33 field: document.getElementById('id-pass').value,
34 visible: getComputedStyle(document.getElementById('id-genbox')).display !== 'none',
35 confirm: getComputedStyle(document.getElementById('id-pass2-row')).display,
36 disabled: document.getElementById('id-primary').disabled,
37 fromList: window.DaimondWords.isFromList(document.getElementById('id-pass').value),
38 bits: Math.round(window.DaimondWords.bits(8)),
39 listLen: window.DaimondWords.count(),
40}));
41check(gen.visible, 'the create screen shows a generated passphrase');
42check(gen.words.trim().split(/\s+/).length === 8, 'it is eight words', gen.words.trim().split(/\s+/).length + ' words');
43check(gen.fromList, 'every word comes from the shipped wordlist');
44check(gen.listLen === 7776, 'the wordlist is the full EFF long list', gen.listLen + ' words');
45check(gen.bits === 103, 'which is ~103 bits, the figure Oxegen settles on', gen.bits + ' bits');
46check(gen.words === gen.field, 'the readout shows exactly what the field holds');
47check(gen.confirm === 'none', 'no confirm field: the words are on screen to be read');
48check(gen.disabled, 'the create button is disabled until it is acknowledged');
49
50// ── The markup is one a password manager recognises ──
51const form = await page.evaluate(() => {
52 const p = document.getElementById('id-pass');
53 const n = document.getElementById('id-name');
54 const k = document.getElementById('cfg-api-key');
55 return {
56 inForm: !!p.closest('form'),
57 passType: p.type,
58 passAuto: p.getAttribute('autocomplete'),
59 nameAuto: n.getAttribute('autocomplete'),
60 submits: (document.getElementById('id-primary') || {}).type,
61 opts: p.hasAttribute('data-1p-ignore') || p.hasAttribute('data-lpignore')
62 || p.getAttribute('autocomplete') === 'off',
63 keyType: k ? k.type : '(absent)',
64 keyIgnored: k ? (k.hasAttribute('data-1p-ignore') && k.getAttribute('autocomplete') === 'off') : false,
65 };
66});
67check(form.inForm, 'the passphrase field is inside a real <form>');
68check(form.passType === 'password', 'it is a real type=password input', form.passType);
69check(form.passAuto === 'new-password', 'creating tags it autocomplete=new-password', form.passAuto);
70check(form.nameAuto === 'username', 'the name field is tagged autocomplete=username', form.nameAuto);
71check(form.submits === 'submit', 'the button submits the form', form.submits);
72check(!form.opts, 'none of the manager opt-outs are left on the passphrase');
73// The provider API key is a different kind of secret and stays out of keychains.
74check(form.keyType === 'text' && form.keyIgnored,
75 'the provider API key is still masked and opted out', form.keyType);
76
77// ── Choosing your own is still allowed, and still has a floor ──
78await page.click('#id-choose');
79const ownUi = await page.evaluate(() => ({
80 genbox: getComputedStyle(document.getElementById('id-genbox')).display,
81 confirm: getComputedStyle(document.getElementById('id-pass2-row')).display,
82 field: document.getElementById('id-pass').value,
83}));
84check(ownUi.genbox === 'none', 'choosing your own hides the generated phrase');
85check(ownUi.confirm !== 'none', 'and brings back the confirm field, since it is typed blind');
86check(ownUi.field === '', 'the generated phrase is cleared rather than left to be edited');
87
88await page.fill('#id-name', 'Own Tester');
89await page.fill('#id-pass', 'short');
90await page.fill('#id-pass2', 'short');
91await page.evaluate(() => document.getElementById('id-primary').click());
92await page.waitForTimeout(400);
93let ownErr = await page.evaluate(() => document.getElementById('id-error').textContent);
94check(/at least 8/.test(ownErr), 'a short chosen passphrase is refused', ownErr);
95
96await page.fill('#id-pass', 'a long enough one');
97await page.fill('#id-pass2', 'a different one entirely');
98await page.evaluate(() => document.getElementById('id-primary').click());
99await page.waitForTimeout(400);
100ownErr = await page.evaluate(() => document.getElementById('id-error').textContent);
101check(/do not match/.test(ownErr), 'a mismatched confirmation is refused', ownErr);
102
103// Back to a fresh create screen, generated by default again.
104await page.reload({ waitUntil: 'domcontentloaded' });
105await page.waitForSelector('#id-primary', { timeout: 15000 });
106await page.waitForTimeout(400);
107const backToGen = await page.evaluate(() =>
108 getComputedStyle(document.getElementById('id-genbox')).display !== 'none');
109check(backToGen, 'a fresh visit is back to a generated passphrase by default');
110
111// ── Generating another really changes it ──
112const first = await page.evaluate(() => document.getElementById('id-pass').value);
113await page.click('#id-regen');
114await page.waitForTimeout(150);
115const second = await page.evaluate(() => document.getElementById('id-pass').value);
116check(second !== first && second.split(' ').length === 8, 'Generate another draws a fresh phrase');
117const readoutTracks = await page.evaluate(() =>
118 document.getElementById('id-genwords').textContent === document.getElementById('id-pass').value);
119check(readoutTracks, 'and the readout follows the field');
120
121// ── Create the account with it ──
122const phrase = second;
123await page.fill('#id-name', 'Gen Tester');
124await page.check('#id-wrote', { force: true });
125const enabled = await page.evaluate(() => !document.getElementById('id-primary').disabled);
126check(enabled, 'acknowledging it enables the create button');
127await page.evaluate(() => document.getElementById('id-primary').click());
128const made = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 20000 })
129 .then(() => true).catch(() => false);
130check(made, 'the account is created from the generated passphrase');
131
132// ── A reload locks it; the same phrase unlocks ──
133await page.reload({ waitUntil: 'domcontentloaded' });
134await page.waitForSelector('#id-primary', { timeout: 15000 });
135await page.waitForTimeout(400);
136const unlockUi = await page.evaluate(() => ({
137 mode: document.getElementById('identity-modal').dataset.mode,
138 passAuto: document.getElementById('id-pass').getAttribute('autocomplete'),
139 nameVal: document.getElementById('id-name').value,
140 nameRO: document.getElementById('id-name').readOnly,
141 genbox: getComputedStyle(document.getElementById('id-genbox')).display,
142 confirm: getComputedStyle(document.getElementById('id-pass2-row')).display,
143 choose: getComputedStyle(document.getElementById('id-choose')).display,
144}));
145check(unlockUi.mode === 'unlock', 'a reload comes back locked', unlockUi.mode);
146check(unlockUi.passAuto === 'current-password', 'unlocking tags it autocomplete=current-password', unlockUi.passAuto);
147check(unlockUi.nameVal === 'Gen Tester', 'the username field carries the account name', unlockUi.nameVal);
148check(unlockUi.nameRO, 'and is read-only there, naming the account rather than choosing one');
149check(unlockUi.genbox === 'none', 'no passphrase is generated on the unlock screen');
150// The confirm field is a new-password input; if it shows on unlock the browser
151// offers to GENERATE a passphrase next to a box asking to confirm one, mid-unlock.
152check(unlockUi.confirm === 'none', 'no confirm-passphrase box on the unlock screen', unlockUi.confirm);
153check(unlockUi.choose === 'none', 'no "choose my own" on the unlock screen', unlockUi.choose);
154
155await page.fill('#id-pass', phrase);
156await page.evaluate(() => document.getElementById('id-primary').click());
157const opened = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 20000 })
158 .then(() => true).catch(() => false);
159check(opened, 'the generated passphrase unlocks it again');
160
161// ── A trailing space, as a phone keyboard adds, still unlocks ──
162await page.reload({ waitUntil: 'domcontentloaded' });
163await page.waitForSelector('#id-primary', { timeout: 15000 });
164await page.waitForTimeout(400);
165await page.fill('#id-pass', phrase + ' ');
166await page.evaluate(() => document.getElementById('id-primary').click());
167const padded = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 25000 })
168 .then(() => true).catch(() => false);
169check(padded, 'a trailing space does not lock the user out');
170
171// ── A wrong passphrase is still refused ──
172await page.reload({ waitUntil: 'domcontentloaded' });
173await page.waitForSelector('#id-primary', { timeout: 15000 });
174await page.waitForTimeout(400);
175await page.fill('#id-pass', 'not the passphrase at all');
176await page.evaluate(() => document.getElementById('id-primary').click());
177await page.waitForTimeout(3000);
178const refused = await page.evaluate(() => ({
179 shown: document.getElementById('identity-modal').style.display !== 'none',
180 err: document.getElementById('id-error').textContent,
181}));
182check(refused.shown && /did not match/.test(refused.err), 'a wrong passphrase is refused', refused.err);
183
184const hardErrs = s.errs.filter(e => !/502|Bad Gateway|Failed to load resource/.test(e));
185check(hardErrs.length === 0, 'no console errors', hardErrs.join(' | ') || 'none');
186
187console.log(`\n${failures ? failures + ' FAILED' : 'all passed'}`);
188await s.close();
189process.exit(failures ? 1 : 0);