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 | |
| 16 | import { open, PASS } from './harness.mjs'; |
| 17 | |
| 18 | let failures = 0; |
| 19 | const check = (cond, msg, detail) => { |
| 20 | console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : '')); |
| 21 | if (!cond) failures++; |
| 22 | }; |
| 23 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 24 | |
| 25 | // Open the account section of the admin home, where "Change passphrase…" lives. |
| 26 | async 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. |
| 40 | async 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 | |
| 47 | const s = await open({ name: 'changepass-' + Date.now(), connect: false }); |
| 48 | const { page } = s; |
| 49 | await page.waitForSelector('#user-row', { timeout: 15000 }); |
| 50 | |
| 51 | // ── The new-passphrase dialog offers a generated phrase by default ── |
| 52 | await openChangePassphrase(page); |
| 53 | await passCurrent(page, PASS); |
| 54 | |
| 55 | let 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 | }); |
| 67 | check(gen.genShown, 'changing the passphrase offers a generated one'); |
| 68 | check(gen.words.trim().split(/\s+/).length === 8, 'it is eight words', gen.words.trim().split(/\s+/).length + ' words'); |
| 69 | check(gen.fromList, 'every word comes from the shipped wordlist'); |
| 70 | check(/\b103 bits\b/.test(gen.note), 'the note names ~103 bits', /(\d+) bits/.exec(gen.note)?.[0]); |
| 71 | check(gen.typedHid, 'the typed confirm fields are hidden while generating'); |
| 72 | check(gen.disabled, 'the change button is disabled until it is acknowledged'); |
| 73 | |
| 74 | // Generating another really changes the phrase and clears the acknowledgement. |
| 75 | const before = gen.words; |
| 76 | await page.click('#cp-modal .pass-gen-btn'); // "Generate another". |
| 77 | await sleep(150); |
| 78 | const 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 | })); |
| 83 | check(after.words !== before && after.words.trim().split(/\s+/).length === 8, 'Generate another draws a fresh phrase'); |
| 84 | check(!after.checked && after.disabled, 'and re-arms the acknowledgement gate'); |
| 85 | |
| 86 | // ── The escape hatch: choose your own, with the old floor and confirm ── |
| 87 | await page.click('#cp-modal .id-choose'); |
| 88 | await sleep(120); |
| 89 | const 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 | })); |
| 94 | check(own.genHid, 'choosing your own hides the generated phrase'); |
| 95 | check(own.newShown, 'and shows a typed new-passphrase field'); |
| 96 | |
| 97 | // A passphrase equal to the current one is refused. |
| 98 | await page.fill('#cp-modal #cp-pass', PASS); |
| 99 | await page.fill('#cp-modal #cp-pass2', PASS); |
| 100 | await page.click('#cp-modal .dlg-ok'); |
| 101 | await sleep(150); |
| 102 | let err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent); |
| 103 | check(/current passphrase/.test(err), 'the current passphrase is refused as the new one', err); |
| 104 | |
| 105 | // Too short is refused. |
| 106 | await page.fill('#cp-modal #cp-pass', 'short'); |
| 107 | await page.fill('#cp-modal #cp-pass2', 'short'); |
| 108 | await page.click('#cp-modal .dlg-ok'); |
| 109 | await sleep(150); |
| 110 | err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent); |
| 111 | check(/at least 8/.test(err), 'a short chosen passphrase is refused', err); |
| 112 | |
| 113 | // A mismatched confirmation is refused. |
| 114 | await page.fill('#cp-modal #cp-pass', 'a fine new passphrase'); |
| 115 | await page.fill('#cp-modal #cp-pass2', 'a different one entirely'); |
| 116 | await page.click('#cp-modal .dlg-ok'); |
| 117 | await sleep(150); |
| 118 | err = await page.evaluate(() => document.querySelector('#cp-modal .dlg-err').textContent); |
| 119 | check(/do not match/.test(err), 'a mismatched confirmation is refused', err); |
| 120 | |
| 121 | // A valid typed passphrase goes through. |
| 122 | const TYPED = 'a fine new passphrase'; |
| 123 | await page.fill('#cp-modal #cp-pass', TYPED); |
| 124 | await page.fill('#cp-modal #cp-pass2', TYPED); |
| 125 | await page.click('#cp-modal .dlg-ok'); |
| 126 | const typedTook = await page.waitForSelector('#cp-modal', { state: 'detached', timeout: 8000 }) |
| 127 | .then(() => true).catch(() => false); |
| 128 | check(typedTook, 'a valid typed passphrase is accepted'); |
| 129 | // Dismiss the "Passphrase changed" notice. |
| 130 | await page.waitForSelector('.dlg-ok', { timeout: 8000 }); |
| 131 | await page.click('.dlg-ok'); |
| 132 | await sleep(200); |
| 133 | |
| 134 | // ── The typed change took: the OLD passphrase no longer unlocks ── |
| 135 | await page.reload({ waitUntil: 'domcontentloaded' }); |
| 136 | await page.waitForSelector('#id-primary', { timeout: 15000 }); |
| 137 | await page.waitForTimeout(300); |
| 138 | await page.fill('#id-pass', PASS); |
| 139 | await page.evaluate(() => document.getElementById('id-primary').click()); |
| 140 | await page.waitForTimeout(2500); |
| 141 | let stillLocked = await page.evaluate(() => document.getElementById('identity-modal').style.display !== 'none'); |
| 142 | check(stillLocked, 'the original passphrase no longer unlocks after the change'); |
| 143 | // The new typed one does. |
| 144 | await page.fill('#id-pass', TYPED); |
| 145 | await page.evaluate(() => document.getElementById('id-primary').click()); |
| 146 | let openedTyped = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 15000 }) |
| 147 | .then(() => true).catch(() => false); |
| 148 | check(openedTyped, 'the new typed passphrase unlocks the account'); |
| 149 | |
| 150 | // ── Now change again, taking the GENERATED default, and prove it unlocks ── |
| 151 | await page.waitForSelector('#user-row', { timeout: 10000 }); |
| 152 | await openChangePassphrase(page); |
| 153 | await passCurrent(page, TYPED); |
| 154 | const GENERATED = await page.evaluate(() => |
| 155 | window.DaimondWords.normalise(document.getElementById('cp-words').textContent)); |
| 156 | check(GENERATED.split(' ').length === 8, 'the generated new passphrase is eight words'); |
| 157 | await page.check('#cp-modal #cp-wrote', { force: true }); |
| 158 | const nowEnabled = await page.evaluate(() => !document.querySelector('#cp-modal .dlg-ok').disabled); |
| 159 | check(nowEnabled, 'acknowledging it enables the change button'); |
| 160 | await page.click('#cp-modal .dlg-ok'); |
| 161 | const genTook = await page.waitForSelector('#cp-modal', { state: 'detached', timeout: 8000 }) |
| 162 | .then(() => true).catch(() => false); |
| 163 | check(genTook, 'the generated passphrase change goes through'); |
| 164 | await page.waitForSelector('.dlg-ok', { timeout: 8000 }); |
| 165 | await page.click('.dlg-ok'); |
| 166 | await sleep(200); |
| 167 | |
| 168 | await page.reload({ waitUntil: 'domcontentloaded' }); |
| 169 | await page.waitForSelector('#id-primary', { timeout: 15000 }); |
| 170 | await page.waitForTimeout(300); |
| 171 | // The previous (typed) passphrase is now stale. |
| 172 | await page.fill('#id-pass', TYPED); |
| 173 | await page.evaluate(() => document.getElementById('id-primary').click()); |
| 174 | await page.waitForTimeout(2500); |
| 175 | stillLocked = await page.evaluate(() => document.getElementById('identity-modal').style.display !== 'none'); |
| 176 | check(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. |
| 178 | await page.fill('#id-pass', GENERATED); |
| 179 | await page.evaluate(() => document.getElementById('id-primary').click()); |
| 180 | const openedGen = await page.waitForSelector('#identity-modal', { state: 'hidden', timeout: 15000 }) |
| 181 | .then(() => true).catch(() => false); |
| 182 | check(openedGen, 'the generated passphrase unlocks the account'); |
| 183 | |
| 184 | const hardErrs = s.errs.filter(e => !/502|Bad Gateway|Failed to load resource/.test(e)); |
| 185 | check(hardErrs.length === 0, 'no console errors', hardErrs.join(' | ') || 'none'); |
| 186 | |
| 187 | console.log(`\n${failures ? failures + ' FAILED' : 'all passed'}`); |
| 188 | await s.close(); |
| 189 | process.exit(failures ? 1 : 0); |