'use strict'; const {spawn}=require('node:child_process'); const path=require('node:path'); const {TextDecoder}=require('node:util'); const {installationRoot}=require('./service-store-client.cjs'); const {createWorkAdmission}=require('./work-admission.cjs'); const refusal=()=>Error('broker-management-refused'); const exact=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).sort().join(',')===keys.sort().join(','); const keys=['deviceId','origin','policyVersion','policyKeySha256','policyDigest']; function validBinding(value){ if(!exact(value,keys)||! /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(value.deviceId)||!Number.isSafeInteger(value.policyVersion)||value.policyVersion<1||![value.policyKeySha256,value.policyDigest].every(v=>typeof v==='string'&&/^[a-f0-9]{64}$/.test(v)))throw refusal(); if(typeof value.origin!=='string'||value.origin.length>2048)throw refusal();const u=new URL(value.origin);if(u.protocol!=='https:'||u.origin!==value.origin||u.username||u.password||u.pathname!=='/'||u.search||u.hash)throw refusal();return Object.freeze({...value}); } // Read-only contract for the fixed native protected store. Native command must // authenticate enabled SCM SID + retained signed Node parent and validate its // OS-minted user SID, signed deployment and exact accepted policy digest. // This module never creates a store or treats an environment SID as authority. function createNativeManagementCredentialLoader({expectedBinding,current,launch=spawn,deadlineMs=10000}={}){ let expected;try{expected=validBinding(expectedBinding);if(typeof current!=='function'||typeof launch!=='function'||!Number.isSafeInteger(deadlineMs)||deadlineMs<1||deadlineMs>30000)throw refusal();}catch{throw refusal();} let closed=false,closing;const stops=new Set(),operations=new Set(),admission=createWorkAdmission({maxJobs:1}),active=()=>{try{return !closed&¤t()===true;}catch{return false;}}; const load=async(binding,{signal}={})=>{ try{ const request=validBinding(binding);if(!active()||signal?.aborted||keys.some(k=>request[k]!==expected[k]))throw refusal(); const operation=admission.run(()=>new Promise((resolve,reject)=>{ let child,timer,total=0,failed=false;const output=Buffer.alloc(8196); const stop=()=>{failed=true;try{child?.kill();}catch{}};stops.add(stop); try{ const sid=process.env.SAFETYNET_SERVICE_CLIENT_SID;if(typeof sid!=='string'||! /^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/.test(sid)||sid.split('-').slice(4).some(n=>Number(n)>4294967295))throw refusal(); child=launch(path.join(installationRoot,'safetynet-service-host.exe'),['--management-credential'],{cwd:installationRoot,shell:false,windowsHide:true,stdio:['pipe','pipe','ignore'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR,SAFETYNET_SERVICE_CLIENT_SID:sid}}); signal?.addEventListener('abort',stop,{once:true});timer=setTimeout(stop,deadlineMs);child.once('error',stop);child.stdin.on('error',stop);child.stdout.on('error',stop); child.stdout.on('data',part=>{if(failed)return;if(!Buffer.isBuffer(part)||total+part.length>output.length){stop();return;}part.copy(output,total);total+=part.length;if(total>=4&&(output.readUInt32LE(0)<2||output.readUInt32LE(0)>8192||total>output.readUInt32LE(0)+4))stop();}); child.once('close',(code,endedSignal)=>{ stops.delete(stop);clearTimeout(timer);signal?.removeEventListener('abort',stop);let token; try{ if(failed||code!==0||endedSignal||!active()||signal?.aborted||total<6||output.readUInt32LE(0)!==total-4)throw refusal(); const value=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(output.subarray(4,total))); if(!exact(value,[...keys,'token'])||keys.some(k=>value[k]!==expected[k])||typeof value.token!=='string'||value.token.length<16||value.token.length>4096||! /^[A-Za-z0-9._~-]+$/.test(value.token))throw refusal(); token=Buffer.from(value.token);resolve(Object.freeze({deviceId:expected.deviceId,origin:expected.origin,token})); }catch{token?.fill(0);reject(refusal());}finally{output.fill(0);} }); const payload=Buffer.from(JSON.stringify(request));if(payload.length>4096){payload.fill(0);throw refusal();}const frame=Buffer.alloc(payload.length+4);frame.writeUInt32LE(payload.length);payload.copy(frame,4);payload.fill(0);child.stdin.end(frame,()=>frame.fill(0)); }catch{stop();if(!child){stops.delete(stop);clearTimeout(timer);output.fill(0);reject(refusal());}} })); operations.add(operation);try{return await operation;}finally{operations.delete(operation);} }catch{throw refusal();} }; return Object.freeze({load,close(){if(closing)return closing;closed=true;admission.close();for(const stop of stops)stop();closing=Promise.allSettled([...operations]).then(()=>undefined);return closing;}}); } module.exports={createNativeManagementCredentialLoader};