import {createHmac} from 'node:crypto'; // No production accounts, network providers or header overrides are used. const [debug, base] = process.argv.slice(2); const version=await (await fetch(debug+'/json/version')).json(); const ws=new WebSocket(version.webSocketDebuggerUrl); await new Promise(resolve=>ws.addEventListener('open',resolve,{once:true})); let next=0;const pending=new Map(); ws.addEventListener('message',event=>{const m=JSON.parse(event.data);if(pending.has(m.id)){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(Error(m.error.message)):p.resolve(m.result);}}); const call=(method,params={},sessionId)=>new Promise((resolve,reject)=>{const id=++next;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params,...(sessionId?{sessionId}:{})}));}); const {targetId}=await call('Target.createTarget',{url:'about:blank'}); const {sessionId}=await call('Target.attachToTarget',{targetId,flatten:true}); const cmd=(method,params)=>call(method,params,sessionId); await cmd('Page.enable');await cmd('Network.enable'); const evaluate=async expression=>{const r=await cmd('Runtime.evaluate',{expression,returnByValue:true});if(r.exceptionDetails)throw Error(r.exceptionDetails.exception?.description?.split('\n')[0] || 'Browser evaluation failed');return r.result.value;}; async function waitFor(expression){for(let n=0;n<300;n++){if(await evaluate(expression))return;await new Promise(r=>setTimeout(r,100));}throw Error('Browser condition not met: '+expression);} async function navigate(path){await cmd('Page.navigate',{url:base+path});await waitFor('location.origin === '+JSON.stringify(base)+' && document.readyState !== "loading"');} async function identity(who){await cmd('Network.clearBrowserCookies');if(who)await cmd('Network.setCookie',{name:'ue_session',value:who,url:base,path:'/',httpOnly:true,sameSite:'Lax'});} let checks=0;async function check(expression,name){if(!await evaluate(expression))throw Error(name);console.log('PASS '+name);checks++;} const captures=[];const responses=new Set(); ws.addEventListener('message', async event=>{const message=JSON.parse(event.data);if(message.method==='Network.responseReceived' && message.params.response.url.split('?')[0].endsWith('/token/init'))responses.add(message.params.requestId);if(message.method==='Network.loadingFinished' && responses.has(message.params.requestId)){try{const body=await cmd('Network.getResponseBody',{requestId:message.params.requestId});captures.push(JSON.parse(body.body));}catch{}}}); try { await navigate('/'); await waitFor('!!document.querySelector("#username") && document.querySelector("#username").offsetParent!==null'); await evaluate(`document.querySelector('#username').value='alice@fixture';document.querySelector('#username').dispatchEvent(new Event('input',{bubbles:true}));const password=Array.from(document.querySelectorAll('input[type=password]')).find(e=>e.offsetParent!==null);password.value='fixture-password';password.dispatchEvent(new Event('input',{bubbles:true}));Array.from(document.querySelectorAll('button')).find(b=>b.offsetParent!==null && b.textContent.trim()==='Log In').click()`); await waitFor('!!Array.from(document.links).find(a=>a.textContent.trim()==="Enroll Token")'); await check(`document.body.innerText.includes('alice @fixture (user)')`,'provider password login identifies the correct account'); async function enroll(){ const count=captures.length; await evaluate(`location.hash='!/token/enroll///'`); await waitFor('!!document.querySelector("#tokentype") && document.querySelector("#tokentype").offsetParent!==null'); await waitFor('document.querySelector("#tokentype").value === "string:totp"'); await evaluate(`document.querySelector('#tokentype').dispatchEvent(new Event('change',{bubbles:true}));`); await evaluate(`Array.from(document.querySelectorAll('button')).find(b=>b.offsetParent!==null && b.textContent.trim()==='Enroll Token').click()`); for(let i=0;i<300 && captures.length===count;i++)await new Promise(r=>setTimeout(r,100)); const response=captures[count];if(response?.detail?.rollout_state!=='verify')throw Error('Enrollment did not require possession confirmation'); await waitFor('!!document.querySelector("#verifyResponse") && document.querySelector("#verifyResponse").offsetParent!==null'); return response.detail; } const first=await enroll(); await check(`!!document.querySelector('#verifyResponse')`,'provider pending setup requests possession proof'); await evaluate(`location.hash='!/token/list'`); await waitFor('!!Array.from(document.links).find(a=>a.textContent.trim()==='+JSON.stringify(first.serial)+')'); await evaluate(`Array.from(document.links).find(a=>a.textContent.trim()===${JSON.stringify(first.serial)}).click()`); await waitFor(`!!document.querySelector('[ng-click="deleteTokenAsk()"]')`); await waitFor(`!document.querySelector('#deleteButton').disabled && Array.from(document.querySelectorAll('td')).some(e=>e.textContent.trim()==='verify')`); await evaluate(`document.querySelector('[ng-click="deleteTokenAsk()"]').click()`); await waitFor('location.hash.includes("/token/list") && !Array.from(document.links).find(a=>a.textContent.trim()==='+JSON.stringify(first.serial)+')'); await check(`!document.body.innerText.includes(${JSON.stringify(first.serial)})`,'provider unfinished setup can be cancelled'); const second=await enroll(); const uri=new URL(second.googleurl.value);const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';let bits=0,value=0;const bytes=[]; for(const character of uri.searchParams.get('secret').replace(/=/g,'')){value=(value<<5)|alphabet.indexOf(character);bits+=5;if(bits>=8){bits-=8;bytes.push((value>>>bits)&255);}} const counter=Buffer.alloc(8);counter.writeBigUInt64BE(BigInt(Math.floor(Date.now()/30000))); const digest=createHmac('sha1',Buffer.from(bytes)).update(counter).digest();const offset=digest[digest.length-1]&15;const otp=String((digest.readUInt32BE(offset)&0x7fffffff)%1000000).padStart(6,'0'); await evaluate(`const field=document.querySelector('#verifyResponse');field.value=${JSON.stringify(otp)};field.dispatchEvent(new Event('input',{bubbles:true}));Array.from(document.querySelectorAll('button')).find(b=>b.offsetParent!==null && b.textContent.trim()==='Verify Token').click()`); await waitFor('document.body.innerText.includes("Token successfully verified")'); await check(`document.body.innerText.includes('Token successfully verified')`,'provider browser possession confirmation activates TOTP'); // A password-only provider session must not enroll an attacker-controlled replacement. await evaluate(`location.hash='!/token/list'`); await waitFor('!!Array.from(document.links).find(a=>a.textContent.trim()==='+JSON.stringify(second.serial)+')'); await evaluate(`location.hash='!/token/enroll///'`); await waitFor('!!document.querySelector("#tokentype") && document.querySelector("#tokentype").offsetParent!==null'); await waitFor('document.querySelector("#tokentype").value === "string:totp"'); await evaluate(`document.querySelector('#tokentype').dispatchEvent(new Event('change',{bubbles:true}));`); await evaluate(`Array.from(document.querySelectorAll('button')).find(b=>b.offsetParent!==null && b.textContent.trim()==='Enroll Token').click()`); await waitFor('document.body.innerText.includes("active authenticator already exists")'); await check(`document.body.innerText.includes('account recovery process')`,'provider active-factor replacement has a recovery route'); console.log(JSON.stringify({checks,result:'passed',scope:'installed provider browser, disposable database and fixture identity only'})); } finally {await call('Target.closeTarget',{targetId});ws.close();}