44 lines
3.4 KiB
JavaScript
44 lines
3.4 KiB
JavaScript
'use strict';
|
|
const {randomUUID}=require('node:crypto');
|
|
function createClipboardGuard({getContext,stage,validate,commit,discard,readClipboard,writeClipboard,inspect,audit,timeoutMs=10000}){
|
|
let pending=null;
|
|
const current=c=>{const n=getContext(c.webContents);return n.ready&&n.tabId===c.tabId&&n.activation===c.activation&&n.policy===c.policy&&n.url===c.url&&n.navigationId===c.navigationId&&n.webContents===c.webContents&&n.webContents?.focusedFrame===n.webContents?.mainFrame&&Date.parse(n.policy?.expiresAt)>Date.now();};
|
|
async function run(operation,webContents){
|
|
const c=getContext(webContents),token=randomUUID();let text='',timer,expired=false;
|
|
const denied=async ruleId=>{await audit('clipboard.block',ruleId);return{completed:false,ruleId};};
|
|
const work=async()=>{try{
|
|
if(!['copy','cut','paste'].includes(operation)||!current(c))return denied('clipboard-context-denied');
|
|
const limit=Math.min(c.policy.security?.clipboard?.maxTextBytes||0,c.policy.dlp.maxBytes);
|
|
if(!limit)return denied('clipboard-policy-unavailable');
|
|
const snapshot=await stage({operation,token,url:c.url,limit,webContents:c.webContents});
|
|
if(!current(c)||expired)return denied('clipboard-context-changed');
|
|
if(operation==='paste')text=await readClipboard(limit);else text=snapshot.text;
|
|
if(!current(c)||expired)return denied('clipboard-context-changed');
|
|
if(typeof text!=='string'||Buffer.byteLength(text)>limit||text.includes('\0'))return denied('clipboard-size-limit');
|
|
const verdict=await inspect(Buffer.from(text),c.policy,operation);
|
|
if(!current(c)||expired)return denied('clipboard-context-changed');
|
|
if(verdict?.decision!=='allow')return denied(verdict?.ruleId||'clipboard-inspection-failed');
|
|
// Persist authorization before side effects; never audit raw clipboard text.
|
|
await audit(`clipboard.${operation}`);
|
|
if(!current(c)||expired)return denied('clipboard-context-changed');
|
|
const args={operation,token,url:c.url,text,limit,webContents:c.webContents};
|
|
if(operation==='cut'){
|
|
const validated=await validate(args);
|
|
if(validated?.completed!==true||!current(c)||expired)return denied('clipboard-target-changed');
|
|
await writeClipboard(text);
|
|
if(!current(c)||expired)return denied('clipboard-context-changed');
|
|
}
|
|
const result=await commit(args);
|
|
if(result?.completed!==true||!current(c)||expired)return denied('clipboard-target-changed');
|
|
if(operation==='copy'){await writeClipboard(text);if(!current(c)||expired)return denied('clipboard-context-changed');}
|
|
return {completed:true};
|
|
}finally{try{await discard?.({token,webContents:c.webContents});}catch{}}};
|
|
try{return await Promise.race([work(),new Promise(resolve=>{timer=setTimeout(()=>{expired=true;Promise.resolve(discard?.({token,webContents:c.webContents})).catch(()=>{});denied('clipboard-timeout').then(resolve,()=>resolve({completed:false,ruleId:'clipboard-timeout'}));},timeoutMs);})]);}
|
|
catch{return denied('clipboard-inspection-failed');}
|
|
finally{clearTimeout(timer);text='';}
|
|
}
|
|
function perform(operation,webContents){if(pending)return Promise.resolve({completed:false,ruleId:'clipboard-busy'});const task=run(operation,webContents);pending=task;task.finally(()=>{if(pending===task)pending=null;}).catch(()=>{});return task;}
|
|
return {perform,isPending:()=>!!pending};
|
|
}
|
|
module.exports={createClipboardGuard};
|