Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11215 -60

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
'use strict';
const {BrokerAuditEventSchema}=require('../../packages/core/broker-audit.cjs');
// Parent-only adapter. persistQueue must synchronously encrypt, write, fsync and
// commit the complete queue, return undefined on success and throw on failure.
// An OTLP send, boolean result or asynchronous enqueue is insufficient.
// Bind current() to the authenticated child and the accepted policy generation.
function createBrokerAuditPersistence({current,policyVersion,getQueue,persistQueue,onFailure}={}){
if(![current,getQueue,persistQueue,onFailure].every(fn=>typeof fn==='function')||!Number.isSafeInteger(policyVersion)||policyVersion<1)throw Error('broker-audit-parent-invalid');
let failed=false;
const fail=()=>{const first=!failed;failed=true;if(first)try{onFailure();}catch{}throw Error('broker-audit-parent-unavailable');};
const active=()=>{try{return !failed&&current()===true;}catch{return false;}};
return Object.freeze({
persist(input){
if(!active())return fail();
let event,queue;
try{
let nodes=0;
const plain=(value,depth=0)=>{
if(++nodes>128||depth>6)return false;
if(!value||typeof value!=='object')return true;
if(![Object.prototype,null].includes(Object.getPrototypeOf(value)))return false;
return Object.values(Object.getOwnPropertyDescriptors(value)).every(item=>'value'in item&&plain(item.value,depth+1));
};
if(!input||typeof input!=='object'||!plain(input))return fail();
event=BrokerAuditEventSchema.parse(input);queue=getQueue();
if(event.policyVersion!==policyVersion||!Array.isArray(queue)||queue.length>=10000||queue.some(item=>item.eventId===event.eventId))return fail();
Object.freeze(event.request);if(event.file)Object.freeze(event.file);if(event.broker)Object.freeze(event.broker);Object.freeze(event);
queue.push(event);
const result=persistQueue();
// Fail closed for an accidentally asynchronous writer; suppress its rejected
// promise without issuing a receipt or retrying uncertain persistence.
if(result&&typeof result.then==='function'){Promise.resolve(result).catch(()=>{});return fail();}
if(result!==undefined)return fail();
if(!active())return fail();
return Object.freeze({persisted:true,eventId:event.eventId});
}catch{return fail();}
}
});
}
module.exports={createBrokerAuditPersistence};
+55
View File
@@ -0,0 +1,55 @@
'use strict';
const {X509Certificate}=require('node:crypto');
const {isIP}=require('node:net');
const fail=()=>{throw Error('broker-certificate-pin-rejected');};
function authorityFacts(value){
if(typeof value!=='string'||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))fail();
const match=/^(\[[^\]]+\]|[^:]+):([1-9][0-9]{0,4})$/.exec(value);if(!match||Number(match[2])>65535)fail();
let url;try{url=new URL('https://'+value+'/');}catch{fail();}
const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname;
if(!isIP(hostname)&&hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))fail();
if(`${isIP(hostname)===6?'['+hostname+']':hostname}:${Number(match[2])}`!==value)fail();
return Object.freeze({hostname,authority:value});
}
/** For one managed Electron session only. Electron's verifier has NO port and
* caches results: CONNECT/SNI/inner-Host gates must still bind the port. Revoke
* must also close existing connections/stop the old broker before replacement.
* Management sessions retain the platform verifier and receive no such pins.
*/
function createBrokerCertificatePins({generation,policyVersion,current,authorizedAuthority,maxPins=128}={}){
if(typeof generation!=='string'||!generation||!Number.isSafeInteger(policyVersion)||policyVersion<1||typeof current!=='function'||typeof authorizedAuthority!=='function'||!Number.isInteger(maxPins)||maxPins<1||maxPins>1024)fail();
let revoked=false;const pins=new Map();
const active=()=>{try{return !revoked&&current()===true;}catch{return false;}};
const authorized=authority=>{try{return authorizedAuthority(authority)===true;}catch{return false;}};
return Object.freeze({
accept(input){
if(!active()||!input||input.generation!==generation||input.policyVersion!==policyVersion||typeof input.cert!=='string'||Buffer.byteLength(input.cert)>16384||!/^-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+\r?\n-----END CERTIFICATE-----\s*$/.test(input.cert))fail();
const facts=authorityFacts(input.authority);if(!authorized(facts.authority))fail();
let certificate;try{certificate=new X509Certificate(input.cert);}catch{fail();}
const now=Date.now(),until=Date.parse(certificate.validTo),from=Date.parse(certificate.validFrom);
const matches=isIP(facts.hostname)?certificate.checkIP(facts.hostname):certificate.checkHost(facts.hostname,{subject:'never',wildcards:false});
if(certificate.ca||!certificate.subjectAltName||certificate.subjectAltName.includes(',')||!matches||!certificate.verify(certificate.publicKey)||!Number.isFinite(until)||!Number.isFinite(from)||from>now||until<=now||until>now+3601000||input.fingerprint256!==certificate.fingerprint256)fail();
for(const [key,pin]of pins)if(pin.expiresAt<=now)pins.delete(key);
const key=facts.authority;
if(!pins.has(key)&&pins.size>=maxPins)fail();
if(!active()||!authorized(key))fail();
pins.set(key,Object.freeze({...facts,fingerprint:certificate.fingerprint256,expiresAt:until}));return true;
},
verify(request,callback){
let result=-3;
try{
// Electron exposes Certificate.data/fingerprint, not Node's fingerprint256.
// Derive our fingerprint from the actual presented PEM, never a claim.
const data=request?.certificate?.data,host=request?.hostname;
if(typeof data!=='string'||Buffer.byteLength(data)>16384)throw Error('certificate-data-unavailable');
const fingerprint=new X509Certificate(data).fingerprint256;
const match=[...pins.values()].find(pin=>pin.hostname===host&&pin.fingerprint===fingerprint);
if(match)result=active()&&match.expiresAt>Date.now()&&authorized(match.authority)?0:-2;
}catch{result=-3;}
callback(result);
},
revoke(){revoked=true;},
status(){return Object.freeze({active:active(),pins:pins.size});}
});
}
module.exports={createBrokerCertificatePins,authorityFacts};
@@ -0,0 +1,34 @@
'use strict';
const path=require('node:path');
const {spawn}=require('node:child_process');
const {EventEmitter}=require('node:events');
const {createBrokerSupervisor}=require('./broker-supervisor.cjs');
const {createFramedChannel}=require('../network-broker/framed-channel.cjs');
// Privileged browser construction only. Installation verification covers the
// fixed native client as well as the signed broker payload. No renderer-supplied
// command, path, pipe name, environment or identity enters this transport.
function createServiceBrokerSupervisor({installationRoot,verifyInstallation,verifyPackage,persistAudit,acceptCertificate,onQuarantine,launch=spawn,deadlineMs,shutdownMs,traceOperation}={}){
if(!path.isAbsolute(installationRoot||'')||typeof verifyInstallation!=='function'||typeof launch!=='function'||typeof verifyPackage!=='function')throw Error('broker-supervisor-invalid');
const root=path.resolve(installationRoot),resourceRoot=path.join(root,'resources','broker');
function connect(){
const env={};for(const name of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[name])env[name]=process.env[name];
// --client-stdio authenticates the retained actual browser parent and the
// connected service pipe handle in native code, before forwarding frames.
const child=launch(path.join(root,'safetynet-service-host.exe'),['--client-stdio'],{cwd:root,env,windowsHide:true,shell:false,stdio:['pipe','pipe','ignore']});
if(!child||!child.stdin||!child.stdout||typeof child.kill!=='function')throw Error('broker-supervisor-unavailable');
const transport=new EventEmitter();
const channel=createFramedChannel({readable:child.stdout,writable:child.stdin,inboundFirstLimit:65536,outboundFirstLimit:2097152});
Object.defineProperties(transport,{exitCode:{get:()=>child.exitCode},signalCode:{get:()=>child.signalCode}});
channel.on('message',message=>transport.emit('message',message));
channel.once('disconnect',()=>transport.emit('disconnect'));
child.once('error',()=>{channel.disconnect();transport.emit('error',Error('broker-supervisor-unavailable'));});
child.once('exit',(code,signal)=>{channel.disconnect();transport.emit('exit',code,signal);});
transport.send=(message,callback)=>channel.send(message,callback);
// Only the retained child handle is terminated. The service owns the broker
// Job; losing this connection causes its generation to close independently.
transport.kill=signal=>{channel.disconnect();return child.kill(signal);};
return transport;
}
return createBrokerSupervisor({resourceRoot,verifyPackage:async location=>await verifyInstallation(root)===true&&await verifyPackage(location)===true,persistAudit,acceptCertificate,onQuarantine,spawn:connect,deadlineMs,shutdownMs,traceOperation});
}
module.exports={createServiceBrokerSupervisor};
+111
View File
@@ -0,0 +1,111 @@
'use strict';
const {fork}=require('node:child_process');
const path=require('node:path');
const {randomUUID,X509Certificate}=require('node:crypto');
const UUID=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
const ENTRY='apps/network-broker/production-entry.cjs';
const STARTUP_CODES=new Set(['broker-bootstrap-invalid','broker-route-unsupported','broker-startup-inspector','broker-startup-certificate','broker-startup-transport','broker-startup-managed','broker-startup-failed']);
// Parent-only: resourceRoot and verifyPackage come from the trusted embedded
// installer contract. Never accept executable/module paths from renderer IPC.
function createBrokerSupervisor({resourceRoot,verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn=fork,deadlineMs=15000,shutdownMs=5000,traceOperation=(_name,_attributes,run)=>run()}={}){
if(!path.isAbsolute(resourceRoot||'')||![verifyPackage,persistAudit,acceptCertificate,onQuarantine,spawn].every(f=>typeof f==='function')||!Number.isInteger(deadlineMs)||deadlineMs<1||deadlineMs>60000)throw Error('broker-supervisor-invalid');
if(!Number.isInteger(shutdownMs)||shutdownMs<1||shutdownMs>30000)throw Error('broker-supervisor-invalid');
if(typeof traceOperation!=='function')throw Error('broker-supervisor-invalid');
// Instrumentation may neither skip a protected operation nor replace its
// result, retry it, or invoke it twice. Only fixed stage names are exported.
async function stage(name,operation){
let original,called=false;
const once=()=>{if(called)throw Error('broker-diagnostics-invalid');called=true;try{original=Promise.resolve(operation());}catch(error){original=Promise.reject(error);}original.catch(()=>{});return original;};
try{await traceOperation(name,{component:'network'},once);}catch(error){if(called)throw error;return once();}
if(!called)once();return original;
}
let child,current,failed=false,closed=false,starting=false,timer,readyResolve,readyReject,lastSequence=0,stopping,pendingCertificates=0,startupCode;
const stopChild=()=>{
clearTimeout(timer);if(stopping)return stopping;
const previous=child;if(!previous)return Promise.resolve();
previous.removeAllListeners('message');
stopping=stage('browser.broker.shutdown',()=>new Promise((resolve,reject)=>{
let done=false;
const finish=()=>{if(done)return;done=true;clearTimeout(limit);child=undefined;resolve();};
const limit=setTimeout(()=>{if(done)return;done=true;failed=true;try{onQuarantine('broker-shutdown-unconfirmed');}catch{}reject(Error('broker-shutdown-unconfirmed'));},shutdownMs);
previous.once('exit',finish);
// ChildProcess retains ownership state; no numeric PID/taskkill operation
// is performed after exit, avoiding a reused-PID tree kill. Native job-based
// descendant containment remains an installer/launcher prerequisite.
if(previous.exitCode!==null&&previous.exitCode!==undefined||previous.signalCode)finish();
else try{previous.kill('SIGKILL');}catch{}
}));
// Fault callers do not await shutdown, but close() receives its real outcome.
stopping.catch(()=>{});return stopping;
};
const fault=()=>{if(failed||closed)return;failed=true;stopChild();try{onQuarantine('broker-supervisor-unavailable');}catch{}readyReject?.(Error('broker-supervisor-unavailable'));};
const send=value=>{if(!child||failed||closed)throw Error('broker-supervisor-unavailable');child.send(value,error=>{if(error)fault();});};
const context=()=>({version:1,generation:current.generation,policyVersion:current.policyVersion});
const exact=(m,keys)=>Object.keys(m).sort().join(',')===keys.sort().join(',');
async function message(m,owner,generation){
if(owner!==child||generation!==current.generation)return;
try{
const bytes=Buffer.byteLength(JSON.stringify(m));
if(bytes>65536||!m||typeof m!=='object'||Array.isArray(m)||m.version!==1||m.generation!==current.generation||m.policyVersion!==current.policyVersion)throw Error();
const common=['type','version','generation','policyVersion'];
if(m.type==='startup-fault'){
if(current.ready||!exact(m,[...common,'code'])||!STARTUP_CODES.has(m.code))throw Error();
// Bounded local pre-enrollment state only; no raw errors or unauthorised
// telemetry exporter. This never grants readiness or permits a retry.
startupCode=m.code;fault();return;
}
if(m.type==='ready'){
if(m.sequence!==0||m.authMode!=='basic'||current.ready||!exact(m,[...common,'port','credentials','authMode','sequence'])||!Number.isInteger(m.port)||m.port!==current.port||!m.credentials||!exact(m.credentials,['username','password'])||!/^safetynet-[a-f0-9]{32}$/.test(m.credentials.username)||!/^[A-Za-z0-9_-]{43}$/.test(m.credentials.password))throw Error();
current.ready=true;clearTimeout(timer);readyResolve(Object.freeze({generation:current.generation,policyVersion:current.policyVersion,port:m.port,credentials:Object.freeze({...m.credentials})}));return;
}
if(!UUID.test(m.requestId)||!Number.isSafeInteger(m.sequence)||m.sequence!==lastSequence+1)throw Error();lastSequence=m.sequence;
if(m.type==='audit'&&exact(m,[...common,'requestId','sequence','event'])){
const receipt=persistAudit(m.event);
if(!receipt||receipt.persisted!==true||receipt.eventId!==m.event.eventId||receipt.then)throw Error();
if(owner!==child||generation!==current.generation)throw Error();
send({type:'audit-receipt',...context(),requestId:m.requestId,sequence:m.sequence,receipt});return;
}
if(m.type==='certificate'&&exact(m,[...common,'requestId','sequence','authority','cert'])){
if(pendingCertificates>=16)throw Error();
if(typeof m.authority!=='string'||m.authority.length>300||typeof m.cert!=='string'||m.cert.length>16384||!/^-----BEGIN CERTIFICATE-----/.test(m.cert))throw Error();
const certificate=new X509Certificate(m.cert);if(certificate.ca)throw Error();
pendingCertificates++;
const certificateDeadline=setTimeout(fault,shutdownMs);
let accepted;
// Admission remains occupied until the ORIGINAL callback settles even if
// the deadline quarantines. A timed-out operation cannot free capacity.
try{accepted=await acceptCertificate(Object.freeze({...context(),authority:m.authority,cert:m.cert,fingerprint256:certificate.fingerprint256}));}
finally{clearTimeout(certificateDeadline);pendingCertificates--;}
if(accepted!==true||owner!==child||generation!==current.generation)throw Error();
send({type:'certificate-receipt',...context(),requestId:m.requestId,sequence:m.sequence,accepted:true});return;
}
throw Error();
}catch{fault();}
}
return Object.freeze({
async start({policyVersion,port,config}={}){
if(failed||closed||child||starting||!Number.isSafeInteger(policyVersion)||policyVersion<1||!Number.isInteger(port)||port<1||port>65535)throw Error('broker-supervisor-invalid');
starting=true;
try{
if(await stage('browser.broker.verify',()=>verifyPackage(resourceRoot))!==true)throw Error();
if(failed||closed)throw Error();
current={generation:randomUUID(),policyVersion,port,ready:false};lastSequence=0;
const envelope={type:'start',...context(),config};if(Buffer.byteLength(JSON.stringify(envelope))>2097152)throw Error();
// Allowlisted Windows runtime essentials only; inherited Node injection,
// proxy credentials and developer environment never reach the child.
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(process.env[key])env[key]=process.env[key];
const pending=new Promise((resolve,reject)=>{readyResolve=resolve;readyReject=reject;});
const owner=spawn(path.join(resourceRoot,ENTRY),[],{execPath:path.join(resourceRoot,'node.exe'),cwd:resourceRoot,env,execArgv:[],windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],serialization:'json'});
child=owner;const generation=current.generation;
owner.on('message',m=>{void message(m,owner,generation);});
for(const event of ['error','exit','disconnect'])owner.once(event,()=>{if(owner===child)fault();});
timer=setTimeout(fault,deadlineMs);send(envelope);return await stage('browser.broker.start',()=>pending);
}catch{fault();throw Error('broker-supervisor-unavailable');}
},
async close(){closed=true;readyReject?.(Error('broker-supervisor-closed'));await stopChild();},
// Replacement is deliberately stop-and-new-supervisor: no automatic retry
// and no stale ready state or credentials survive a policy generation.
status(){return Object.freeze({ready:!!current?.ready&&!failed&&!closed,failed,closed,generation:current?.generation,startupCode});}
});
}
module.exports={createBrokerSupervisor};
+2
View File
@@ -10,6 +10,8 @@ const {trace,ROOT_CONTEXT,SpanStatusCode,TraceFlags}=require('@opentelemetry/api
const names=new Set(['browser.window','browser.network','browser.ipc','browser.agent','browser.management','browser.navigation','browser.dns','browser.request','browser.request.headers','browser.inspect.local','browser.inspect.enterprise','browser.download','browser.clipboard','browser.visual','browser.password.fill','browser.ai','browser.policy','browser.startup','browser.parser','browser.semantic','browser.yara','browser.icap','browser.tab','browser.security','browser.evidence']);
const operations=new Set(['audit.deliver','evidence.observer.install','evidence.observer.drain','evidence.observer.dispose','policy.verify','policy.bookmarks','policy.agent-reconcile','policy.page-configure','policy.audit','evidence.capture','evidence.authorize','evidence.challenge','evidence.encrypt','evidence.send','evidence.ack','agent.request','agent.challenge','agent.decrypt','agent.transport','agent.authorize','agent.audit','agent.dispatch','agent.response','agent.project','agent.encrypt','library:organize-plan','library:organize-apply','application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','state','authorize-setting','settings-update','window-control','chrome-layout','share-url','bookmarks-mutate','enroll','sso','navigate','back','forward','reload','ai','inspect-file','agent-connection','ai-panel','vault-panel','vault-status','vault-create','vault-unlock','vault-lock','vault-list','vault-save','vault-delete','vault-fill','vault-generate','tab-create','tab-activate','tab-close','tab-group','sessions','read','inspect','scroll','click','type','select','check','screenshot','copy','cut','paste','scan','download','sync','create','activate','close','group','policy','request','dns','parse','embed','yara','icap']);
for(const operation of ['visual.probe','visual.capture','visual.inspect.local','visual.inspect.enterprise','visual.audit','visual.validate','visual.project','visual.release'])operations.add(operation);
for(const operation of ['broker.request','broker.framing','broker.buffer','broker.inspect','broker.authorize','broker.forward'])operations.add(operation);
for(const name of ['browser.broker.verify','browser.broker.start','browser.broker.shutdown'])names.add(name);
const components=new Set(['browser','ipc','agent','management','navigation','network','dns','dlp','inspector','parser','semantic','yara','icap','download','clipboard','visual','vault','ai','policy','tabs']);
for(const operation of ['text.local','text.enterprise'])operations.add(operation);
for(const operation of ['history:list','history:go','window:state','window:control'])operations.add(operation);
+34 -14
View File
@@ -1,20 +1,40 @@
const {app}=require('electron');
const {spawn}=require('node:child_process');
const path=require('node:path');
function capabilityCrypto(operation,input){
const root=path.join(__dirname,'../..');
const python=app.isPackaged?path.join(process.resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
const script=app.isPackaged?path.join(process.resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
if(!['protect','unprotect'].includes(operation)||Buffer.byteLength(input)>32768)return Promise.reject(Error('Capability rejected'));
function createCapabilityCrypto({packaged=false,resourcesPath,root=path.join(__dirname,'../..'),launch=spawn,environment=process.env}={}){
const python=packaged?path.join(resourcesPath,'python/python.exe'):path.join(root,'.runtime/windows/python/python.exe');
const script=packaged?path.join(resourcesPath,'inspector/dpapi.py'):path.join(root,'apps/inspector/dpapi.py');
const active=new Set();
return function convert(operation,input){
const inputLimit=operation==='protect'?32768:65536;
if(!['protect','unprotect'].includes(operation)||!(typeof input==='string'||Buffer.isBuffer(input))||!Buffer.byteLength(input)||Buffer.byteLength(input)>inputLimit)return Promise.reject(Error('Capability rejected'));
if(active.size>=8)return Promise.reject(Error('Windows capability protection failed'));
return new Promise((resolve,reject)=>{
const environment={...process.env};for(const key of ['ADMIN_TOKEN','OIDC_CLIENT_SECRET','AI_API_KEY','INSPECTOR_TOKEN','SIEM_HTTP_TOKEN','SAFETYNET_PROXY_PASSWORD'])delete environment[key];
const child=spawn(python,[script,operation],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:environment});let output='',settled=false;
const timer=setTimeout(()=>{child.kill();fail();},5000);
function fail(){if(!settled){settled=true;clearTimeout(timer);reject(Error('Windows capability protection failed'));}}
child.on('error',fail);child.stdin.on('error',fail);child.stderr.on('data',()=>{});
child.stdout.on('data',data=>{output+=data;if(output.length>65536){child.kill();fail();}});
child.on('close',code=>{if(code!==0||!output)return fail();if(!settled){settled=true;clearTimeout(timer);resolve(output);}});
child.stdin.end(input);
// Python startup hooks and inherited secrets must not enter a capability
// helper. Isolated mode also disables user-site and Python environment paths.
const env={};for(const key of ['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'])if(environment[key])env[key]=environment[key];
let child,timer,settled=false,terminal=false,bytes=0;
const output=Buffer.alloc(operation==='protect'?65536:32768),slot={};active.add(slot);
function fail(){
if(settled)return;settled=true;clearTimeout(timer);output.fill(0);
// A rejected operation does not free the slot until actual child close.
if(child&&!terminal){try{child.kill();}catch{}child.stdin?.destroy?.();child.stdout?.destroy?.();child.stderr?.destroy?.();}
reject(Error('Windows capability protection failed'));
}
try{
child=launch(python,['-I','-B',script,operation],{windowsHide:true,shell:false,stdio:['pipe','pipe','pipe'],env});
child.once('close',code=>{
terminal=true;active.delete(slot);
if(settled)return;if(code!==0||!bytes){fail();return;}
settled=true;clearTimeout(timer);const result=output.subarray(0,bytes).toString('utf8');output.fill(0);resolve(result);
});
child.on('error',fail);child.stdin.on('error',fail);child.stdout.on('error',fail);child.stderr.on('error',fail);child.stderr.on('data',()=>{});
child.stdout.on('data',data=>{if(settled)return;if(!Buffer.isBuffer(data)||bytes+data.length>output.length){fail();return;}data.copy(output,bytes);bytes+=data.length;});
timer=setTimeout(fail,5000);child.stdin.end(input);
}catch{if(!child)active.delete(slot);fail();}
});
};
}
module.exports={capabilityCrypto};
let sharedConverter;
function capabilityCrypto(operation,input){sharedConverter??=createCapabilityCrypto({packaged:app.isPackaged,resourcesPath:process.resourcesPath});return sharedConverter(operation,input);}
module.exports={capabilityCrypto,createCapabilityCrypto};
+3 -2
View File
@@ -102,6 +102,7 @@ let message='Enroll this browser to receive company policy.',syncTimer,localInsp
const recentRequests=new Map();
const agentFile=()=>path.join(app.getPath('userData'),'agent-connection.json');
const agentGrants=require('./agent-grants.cjs');
function protectAgentCapability(value){return diagnosticSpan('browser.credential',{operation:'credential.issue',component:'agent',bytes:Buffer.byteLength(value)},()=>require('./dpapi.cjs').capabilityCrypto('protect',value));}
const agentActorStorage=new (require('node:async_hooks').AsyncLocalStorage)();
let agentServiceMode;
function effectiveAgentPolicy(){if(!policy)return policy;const {grants}=agentGrants.reconcileGenerationFloor(policy.agents.grants||[],device?.agentGenerationFloor||{});return {...policy,agents:{...policy.agents,grants}};}
@@ -121,7 +122,7 @@ async function pairIndividualAgent(principalId){
const grant=approvedAgentGrants().find(g=>g.id===principalId),initial=policy,service=agentService;
if(!grant||!service||agentServiceMode!=='individual')throw Error('Agent grant unavailable');
const connection=service.issuePrincipal(grant.id,grant.generation,grant.expiresAt);
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
return {address:service.address,credentialPath,principalId:grant.id,generation:grant.generation};
}
@@ -457,7 +458,7 @@ async function ensureAgentAPI(){
if(!checkURL(url))return {allowed:false};
return {allowed:true,role:'agent',scopes:[scope]};
},audit:auditAgent,
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:protectAgentCapability,isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
});emit();
}finally{agentStarting=false;}
}