34 lines
3.3 KiB
JavaScript
34 lines
3.3 KiB
JavaScript
// Build the inventory AFTER final binaries exist; signing is performed by an
|
|
// external trusted release signer. No private key is generated or stored here.
|
|
import {lstat,readdir,open} from 'node:fs/promises';
|
|
import {createHash} from 'node:crypto';
|
|
import path from 'node:path';
|
|
const OMIT=new Set(['release-envelope.json','deployment-envelope.json']);
|
|
function fail(code){throw Object.assign(new Error(code),{code});}
|
|
async function ancestors(root){for(let current=path.resolve(root);;current=path.dirname(current)){const stat=await lstat(current);if(!stat.isDirectory()||stat.isSymbolicLink())fail('windows-release-path-unsafe');if(path.dirname(current)===current)break;}}
|
|
async function hash(file){const input=await open(file,'r');try{const before=await input.stat(),sha=createHash('sha256');let bytes=0;for await(const chunk of input.createReadStream({autoClose:false})){bytes+=chunk.length;sha.update(chunk);}const after=await input.stat();if(!before.isFile()||before.size!==bytes||after.size!==bytes||before.mtimeMs!==after.mtimeMs||before.ctimeMs!==after.ctimeMs||before.ino!==after.ino||before.dev!==after.dev)fail('windows-release-source-changed');return {bytes,sha256:sha.digest('hex')};}finally{await input.close();}}
|
|
export async function buildReleaseInventory(root,{browserPath='SafetyNet.exe',nativeHostPath='safetynet-service-host.exe'}={}){
|
|
await ancestors(root);const files=[],seen=new Set();let count=0;
|
|
async function walk(directory,prefix=''){
|
|
for(const entry of await readdir(directory,{withFileTypes:true})){
|
|
const relative=prefix+entry.name;
|
|
if(++count>15000||!relative.split('/').every(part=>/^[A-Za-z0-9_.-]+$/.test(part)&&part!=='.'&&part!=='..'&&!/[. ]$/.test(part)&&!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))||relative.length>240||entry.isSymbolicLink())fail('windows-release-path-unsafe');
|
|
const target=path.join(directory,entry.name),stat=await lstat(target);if(stat.isSymbolicLink())fail('windows-release-path-unsafe');
|
|
if(stat.isDirectory())await walk(target,relative+'/');else if(stat.isFile()){
|
|
if(OMIT.has(relative))continue;
|
|
if(seen.has(relative.toLowerCase())||files.length>=10000)fail('windows-release-inventory-invalid');seen.add(relative.toLowerCase());files.push({path:relative,...await hash(target)});
|
|
}else fail('windows-release-path-unsafe');
|
|
}
|
|
}
|
|
await walk(root);files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
for(const required of [browserPath,nativeHostPath,'resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py'])if(!files.some(file=>file.path===required))fail('windows-release-component-missing');
|
|
if(!files.some(file=>file.path.startsWith('resources/semantic/')))fail('windows-release-component-missing');
|
|
return files;
|
|
}
|
|
export async function verifyReleaseInventory(root,verifiedRelease,options){
|
|
// Cryptographic authenticity is established separately by windows-trust.cjs.
|
|
// This API only compares an already trusted immutable inventory to disk.
|
|
const actual=await buildReleaseInventory(root,options),expected=verifiedRelease?.manifest?.files;
|
|
if(!Array.isArray(expected)||actual.length!==expected.length||actual.some((file,index)=>file.path!==expected[index]?.path||file.bytes!==expected[index]?.bytes||file.sha256!==expected[index]?.sha256))fail('windows-release-inventory-mismatch');return true;
|
|
}
|