credentials.mjs 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. import {createHash} from 'node:crypto'
  2. import {execFile} from 'node:child_process'
  3. import {readFileSync} from 'node:fs'
  4. import {join} from 'node:path'
  5. import {homedir} from 'node:os'
  6. const prefix=Buffer.from('YAOYAO-RUNNER-KEYCHAIN-1\n')
  7. const windowsPrefix=Buffer.from('YAOYAO-WINDOWS-DPAPI-1\n')
  8. export class DesktopCredentials {
  9. constructor({home,helper,legacyDecrypt,platform=process.platform,safeStorage}){Object.assign(this,{home,helper,legacyDecrypt,platform,safeStorage})}
  10. get account(){
  11. let identity=this.home
  12. try {
  13. const migration=JSON.parse(readFileSync(join(this.home,'.data-home-migration.json'),'utf8'))
  14. const old=join(homedir(),'.hermes-yaoyao')
  15. if(this.home===join(homedir(),'.yaoyao')&&migration.source===old&&migration.target===this.home)identity=old
  16. }catch{/* Fresh directories retain their own Keychain identity. */}
  17. return createHash('sha256').update(identity).digest('hex')
  18. }
  19. async invoke(operation,input){
  20. if(input.length>1048576)throw new Error('执行节点配置超过加密大小上限')
  21. return new Promise((resolve,reject)=>{
  22. const child=execFile(this.helper,[operation,this.account],{encoding:'buffer',timeout:30000,killSignal:'SIGKILL',maxBuffer:1048576},(error,stdout,stderr)=>{
  23. if(error){
  24. const detail=stderr&&stderr.length?stderr.toString('utf8').trim():''
  25. reject(new Error(detail||'无法访问当前数据目录的系统加密密钥,请完成钥匙串授权后重试'))
  26. }
  27. else resolve(stdout)
  28. })
  29. child.stdin.on('error',()=>{});child.stdin.end(input)
  30. })
  31. }
  32. async encrypt(value){
  33. if(this.platform==='win32'){
  34. if(Buffer.byteLength(value)>1048576)throw new Error('电脑配置超过加密大小上限')
  35. if(!await this.safeStorage?.isAsyncEncryptionAvailable())throw new Error('系统加密暂不可用,请稍后重试')
  36. return Buffer.concat([windowsPrefix,await this.safeStorage.encryptStringAsync(value)])
  37. }
  38. return Buffer.concat([prefix,await this.invoke('encrypt',Buffer.from(value))])
  39. }
  40. async decrypt(value){
  41. if(this.platform==='win32'){
  42. if(!value.subarray(0,windowsPrefix.length).equals(windowsPrefix))throw new Error('此电脑配置不属于 Windows,请重新登录授权')
  43. try{return (await this.safeStorage.decryptStringAsync(value.subarray(windowsPrefix.length))).result}
  44. catch{throw new Error('无法解锁电脑配置,请使用原 Windows 账号或重新登录授权')}
  45. }
  46. if(value.subarray(0,prefix.length).equals(prefix))return (await this.invoke('decrypt',value.subarray(prefix.length))).toString('utf8')
  47. // Existing safeStorage files remain readable after the user grants normal OS access.
  48. let timer
  49. try{return await Promise.race([this.legacyDecrypt(value),new Promise((_,reject)=>{timer=setTimeout(()=>reject(new Error('旧配置等待钥匙串授权,请完成系统授权后重试')),30000)})])}finally{clearTimeout(timer)}
  50. }
  51. }