Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_changepass.mjs

9.2 KiB, 1 run

created by r2519314175:265, 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_changepass.mjs — changing the passphrase now GENERATES the new one.
2//
3// The create screen already generates the passphrase (verify_genpass.mjs). The
4// account a person already has, though, still carries the old user-chosen — and
5// so probably short and reused — passphrase. This asserts that Change passphrase
6// now offers a generated one by default, gates it behind the same "written it
7// down" acknowledgement, keeps a "choose my own" escape hatch with the old floor
8// and confirm, and that whichever passphrase is set actually becomes the one that
9// unlocks the account (and the old one stops working).
10//
11// node dev/verify_changepass.mjs
12//
13// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway and no model: this is
14// the login only.
15
16import { open, PASS } from './harness.mjs';
17
18let failures = 0;
19const check = (cond, msg, detail) => {
20 console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : ''));
21 if (!cond) failures++;
22};
23const sleep = ms => new Promise(r => setTimeout(r, ms));
24
25// Open the account section of the admin home, where "Change passphrase…" lives.
26async function openChangePassphrase(page) {
27 await page.evaluate(() => document.getElementById('user-row').click());
28 await sleep(200);
29 await page.evaluate(() => {
30 const b = [...document.querySelectorAll('#admin-home .admin-item')]
31 .find(x => /Change passphrase/.test(x.textContent));
32 if (b) b.click();
33 });
34 // The first dialog asks for the CURRENT passphrase (a promptDialog).
35 await page.waitForSelector('.dlg-input', { timeout: 8000 });
36}
37
38// Answer the current-passphrase prompt, after which the generated-new dialog
39// (#cp-modal) opens.
40async function passCurrent(page, cur) {
41 await page.fill('.dlg-input', cur); // secret mask tracks input events.
42 await page.click('.dlg-ok');
43 await page.waitForSelector('#cp-modal', { timeout: 8000 });
44 await sleep(150);
45}
46
47const s = await open({ name: 'changepass-' + Date.now(), connect: false });
48const { page } = s;
49await page.waitForSelector('#user-row', { timeout: 15000 });
50
51// ── The new-passphrase dialog offers a generated phrase by default ──
52await openChangePassphrase(page);
53await passCurrent(page, PASS);
54
55let gen = await page.evaluate(() => {
56 const w = document.getElementById('cp-words');
57 return {
58 words: w ? w.textContent : '',
59 genShown: !!document.querySelector('#cp-modal .pass-gen'),
60 typedHid: getComputedStyle(document.querySelector('#cp-modal #cp-pass').parentNode).display === 'none'
61 || document.querySelector('#cp-modal #cp-pass').offsetParent === null,
62 disabled: document.querySelector('#cp-modal .dlg-ok').disabled,
63 fromList: window.DaimondWords.isFromList((w ? w.textContent : '').trim()),
64 note: (document.querySelector('#cp-modal .pass-gen-note') || {}).textContent || '',
65 };
66});
67check(gen.genShown, 'changing the passphrase offers a generated one');
68check(gen.words.trim().split(/\s+/).length === 8, 'it is eight words', gen.words.trim().split(/\s+/).length + ' words');
69check(gen.fromList, 'every word comes from the shipped wordlist');
70check(/\b103 bits\b/.test(gen.note), 'the note names ~103 bits', /(\d+) bits/.exec(gen.note)?.[0]);
71check(gen.typedHid, 'the typed confirm fields are hidden while generating');
72check(gen.disabled, 'the change button is disabled until it is acknowledged');
73
74// Generating another really changes the phrase and clears the acknowledgement.
75const before = gen.words;
76await page.click('#cp-modal .pass-gen-btn'); // "Generate another".
77await sleep(150);
78const after = await page.evaluate(() => ({
79 words: document.getElementById('cp-words').textContent,
80 checked: document.getElementById('cp-wrote').checked,
81 disabled: document.querySelector('#cp-modal .dlg-ok').disabled,
82}));
83check(after.words !== before && after.words.trim().split(/\s+/).length === 8, 'Generate another draws a fresh phrase');
84check(!after.checked && after.disabled, 'and re-arms the acknowledgement gate');
85
86// ── The escape hatch: choose your own, with the old floor and confirm ──
87await page.click('#cp-modal .id-choose');
88await sleep(120);
89const own = await page.evaluate(() => ({
90 genHid: !document.querySelector('#cp-modal .pass-gen') ||
91 getComputedStyle(document.querySelector('#cp-modal .pass-gen')).display === 'none',
92 newShown: document.querySelector('#cp-modal #cp-pass').offsetParent !== null,
93}));
94check(own.genHid, 'choosing your own hides the generated phrase');
95check(own.newShown, 'and shows a typed new-passphrase field');
96
97// A passphrase equal to the current one is refused.
98await page.fill('#cp-modal #cp-pass', PASS);
99await page.fill('#cp-modal #cp-pass2', PASS);
100await page.click('#cp-modal .dlg-ok');
101await sleep(150);
102let err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent);
103check(/current passphrase/.test(err), 'the current passphrase is refused as the new one', err);
104
105// Too short is refused.
106await page.fill('#cp-modal #cp-pass', 'short');
107await page.fill('#cp-modal #cp-pass2', 'short');
108await page.click('#cp-modal .dlg-ok');
109await sleep(150);
110err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent);
111check(/at least 8/.test(err), 'a short chosen passphrase is refused', err);
112
113// A mismatched confirmation is refused.
114await page.fill('#cp-modal #cp-pass', 'a fine new passphrase');
115await page.fill('#cp-modal #cp-pass2', 'a different one entirely');
116await page.click('#cp-modal .dlg-ok');
117await sleep(150);
118err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent);
119check(/do not match/.test(err), 'a mismatched confirmation is refused', err);
120
121// A valid typed passphrase goes through.
122const TYPED = 'a fine new passphrase';
123await page.fill('#cp-modal #cp-pass', TYPED);
124await page.fill('#cp-modal #cp-pass2', TYPED);
125await page.click('#cp-modal .dlg-ok');
126const typedTook = await page.waitForSelector('#cp-modal', { state: 'detached', timeout: 8000 })
127 .then(() => true).catch(() => false);
128check(typedTook, 'a valid typed passphrase is accepted');
129// Dismiss the "Passphrase changed" notice.
130await page.waitForSelector('.dlg-ok', { timeout: 8000 });
131await page.click('.dlg-ok');
132await sleep(200);
133
134// ── The typed change took: the OLD passphrase no longer unlocks ──
135await page.reload({ waitUntil: 'domcontentloaded' });
136await page.waitForSelector('#id-primary', { timeout: 15000 });
137await page.waitForTimeout(300);
138await page.fill('#id-pass', PASS);
139await page.evaluate(() => document.getElementById('id-primary').click());
140await page.waitForTimeout(2500);
141let stillLocked = await page.evaluate(() => document.getElementById('identity-modal').style.display !== 'none');
142check(stillLocked, 'the original passphrase no longer unlocks after the change');
143// The new typed one does.
144await page.fill('#id-pass', TYPED);
145await page.evaluate(() => document.getElementById('id-primary').click());
146let openedTyped = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 15000 })
147 .then(() => true).catch(() => false);
148check(openedTyped, 'the new typed passphrase unlocks the account');
149
150// ── Now change again, taking the GENERATED default, and prove it unlocks ──
151await page.waitForSelector('#user-row', { timeout: 10000 });
152await openChangePassphrase(page);
153await passCurrent(page, TYPED);
154const GENERATED = await page.evaluate(() =>
155 window.DaimondWords.normalise(document.getElementById('cp-words').textContent));
156check(GENERATED.split(' ').length === 8, 'the generated new passphrase is eight words');
157await page.check('#cp-modal #cp-wrote', { force: true });
158const nowEnabled = await page.evaluate(() => !document.querySelector('#cp-modal .dlg-ok').disabled);
159check(nowEnabled, 'acknowledging it enables the change button');
160await page.click('#cp-modal .dlg-ok');
161const genTook = await page.waitForSelector('#cp-modal', { state: 'detached', timeout: 8000 })
162 .then(() => true).catch(() => false);
163check(genTook, 'the generated passphrase change goes through');
164await page.waitForSelector('.dlg-ok', { timeout: 8000 });
165await page.click('.dlg-ok');
166await sleep(200);
167
168await page.reload({ waitUntil: 'domcontentloaded' });
169await page.waitForSelector('#id-primary', { timeout: 15000 });
170await page.waitForTimeout(300);
171// The previous (typed) passphrase is now stale.
172await page.fill('#id-pass', TYPED);
173await page.evaluate(() => document.getElementById('id-primary').click());
174await page.waitForTimeout(2500);
175stillLocked = await page.evaluate(() => document.getElementById('identity-modal').style.display !== 'none');
176check(stillLocked, 'the previous passphrase no longer unlocks after generating a new one');
177// The generated one does — proving the generated phrase on screen is the real key.
178await page.fill('#id-pass', GENERATED);
179await page.evaluate(() => document.getElementById('id-primary').click());
180const openedGen = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 15000 })
181 .then(() => true).catch(() => false);
182check(openedGen, 'the generated passphrase unlocks the account');
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);