computer_auth.mjs 4.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. // A private synthetic website and AES-encrypted application token. No accounts.
  2. import {createServer} from 'node:http'
  3. import {spawn} from 'node:child_process'
  4. import {mkdir,readFile,writeFile} from 'node:fs/promises'
  5. import {join} from 'node:path'
  6. import {randomBytes,createCipheriv,createDecipheriv} from 'node:crypto'
  7. const login=process.argv[1]==='login',home=process.env.HOME
  8. const app=join(home,'.config/yaoyao-auth-fixture'),keyring=join(home,'.local/share/keyrings/yaoyao-auth-fixture')
  9. if(login){
  10. await mkdir(app,{recursive:true});await mkdir(join(home,'.local/share/keyrings'),{recursive:true})
  11. const key=randomBytes(32),iv=randomBytes(12),cipher=createCipheriv('aes-256-gcm',key,iv)
  12. const token=Buffer.concat([cipher.update('fixture-app-authorization'),cipher.final()])
  13. await writeFile(keyring,key,{mode:0o600})
  14. await writeFile(join(app,'token'),JSON.stringify({iv:iv.toString('hex'),tag:cipher.getAuthTag().toString('hex'),token:token.toString('hex')}))
  15. }
  16. const saved=JSON.parse(await readFile(join(app,'token'),'utf8')),decipher=createDecipheriv('aes-256-gcm',await readFile(keyring),Buffer.from(saved.iv,'hex'))
  17. decipher.setAuthTag(Buffer.from(saved.tag,'hex'))
  18. if(Buffer.concat([decipher.update(Buffer.from(saved.token,'hex')),decipher.final()]).toString()!=='fixture-app-authorization')throw Error('app authorization lost')
  19. const server=createServer((req,res)=>{
  20. if(req.url==='/session'){res.end(JSON.stringify((req.headers.cookie??'').includes('session=fixture-cookie')));return}
  21. if(login)res.setHeader('Set-Cookie','session=fixture-cookie; HttpOnly; SameSite=Lax; Max-Age=86400; Path=/')
  22. res.setHeader('Content-Type','text/html');res.end('<!doctype html><body>authorization fixture</body>')
  23. })
  24. await new Promise(resolve=>server.listen(18765,'127.0.0.1',resolve))
  25. const browser=spawn('google-chrome',['--headless','--no-sandbox','--disable-dev-shm-usage','--disable-gpu','--no-first-run','--no-default-browser-check','--disable-background-networking','--password-store=basic','--remote-debugging-pipe',`--user-data-dir=${join(home,'.config/yaoyao-auth-browser')}`],{stdio:['ignore','ignore','pipe','pipe','pipe']})
  26. let buffer='',id=0,errors='';const waiting=new Map()
  27. browser.stderr.on('data',chunk=>{errors=(errors+chunk).slice(-1500)})
  28. browser.stdio[4].on('data',chunk=>{
  29. buffer+=chunk.toString();let end
  30. while((end=buffer.indexOf('\0'))>=0){
  31. const message=JSON.parse(buffer.slice(0,end));buffer=buffer.slice(end+1)
  32. if(message.id){const handler=waiting.get(message.id);waiting.delete(message.id);if(message.error)handler?.reject(Error(JSON.stringify(message.error)));else handler?.resolve(message.result)}
  33. }
  34. })
  35. const closed=new Promise(resolve=>browser.once('close',resolve))
  36. const command=(method,params={},sessionId)=>new Promise((resolve,reject)=>{
  37. const next=++id;waiting.set(next,{resolve,reject});browser.stdio[3].write(JSON.stringify({id:next,method,params,...(sessionId?{sessionId}:{})})+'\0')
  38. })
  39. const timeout=setTimeout(()=>{process.stderr.write('browser fixture timed out: '+errors);browser.kill();process.exit(1)},40000)
  40. try{
  41. const target=await command('Target.createTarget',{url:'about:blank'})
  42. const {sessionId}=await command('Target.attachToTarget',{targetId:target.targetId,flatten:true})
  43. await command('Page.enable',{},sessionId)
  44. await command('Page.navigate',{url:'http://127.0.0.1:18765/'},sessionId)
  45. // CDP awaits the actual IndexedDB transaction, not a virtual-time delay.
  46. const result=await command('Runtime.evaluate',{awaitPromise:true,returnByValue:true,expression:`(async()=>{
  47. if(document.readyState!=='complete')await new Promise(resolve=>addEventListener('load',resolve,{once:true}));
  48. const login=${JSON.stringify(login)};
  49. if(login)localStorage.setItem('authorization','fixture-local');
  50. const db=await new Promise((resolve,reject)=>{const request=indexedDB.open('fixture',1);request.onupgradeneeded=()=>request.result.createObjectStore('tokens');request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error)});
  51. if(login)await new Promise((resolve,reject)=>{const tx=db.transaction('tokens','readwrite');tx.objectStore('tokens').put('fixture-idb','auth');tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)});
  52. const token=await new Promise(resolve=>{const read=db.transaction('tokens').objectStore('tokens').get('auth');read.onsuccess=()=>resolve(read.result)});
  53. const cookie=await (await fetch('/session')).json();db.close();
  54. return cookie&&localStorage.getItem('authorization')==='fixture-local'&&token==='fixture-idb';
  55. })()`},sessionId)
  56. if(result.exceptionDetails||result.result?.value!==true)throw Error('browser authorization lost: '+JSON.stringify(result))
  57. await command('Browser.close');await closed
  58. console.log(JSON.stringify({browser:true,encryptedApplicationToken:true}))
  59. }finally{clearTimeout(timeout);browser.kill();server.close()}