import { Buffer } from 'node:buffer'; import { createHash, createPublicKey, verify } from 'node:crypto'; import { readFileSync } from 'node:fs'; export const updateSource = JSON.parse( readFileSync(new URL('./update-source.json', import.meta.url), 'utf8'), ); export function compareVersions(left, right) { const pattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; if (!pattern.test(left) || !pattern.test(right)) { throw new Error( 'Update versions must be stable MAJOR.MINOR.PATCH versions.', ); } const a = left.split('.').map(BigInt); const b = right.split('.').map(BigInt); for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) { return a[i] > b[i] ? 1 : -1; } } return 0; } export function verifyUpdateSignature( bytes, signature, publicKey = updateSource.publicKey, ) { try { const key = Buffer.from( Buffer.from(publicKey, 'base64') .toString('utf8') .trim() .split('\n')[1], 'base64', ); const lines = Buffer.from(signature.trim(), 'base64') .toString('utf8') .trim() .split('\n'); const signed = Buffer.from(lines[1], 'base64'); if ( key.length !== 42 || signed.length !== 74 || !key.subarray(2, 10).equals(signed.subarray(2, 10)) || !lines[2].startsWith('trusted comment: ') ) { throw new Error('Invalid signature structure'); } const algorithm = signed.subarray(0, 2).toString(); if (!['Ed', 'ED'].includes(algorithm)) { throw new Error('Unsupported signature'); } const cryptoKey = createPublicKey({ key: Buffer.concat([ Buffer.from('302a300506032b6570032100', 'hex'), key.subarray(10), ]), format: 'der', type: 'spki', }); const contents = algorithm === 'ED' ? createHash('blake2b512').update(bytes).digest() : bytes; if ( !verify(null, contents, cryptoKey, signed.subarray(10)) || !verify( null, Buffer.concat([ signed.subarray(10), Buffer.from(lines[2].slice('trusted comment: '.length)), ]), cryptoKey, Buffer.from(lines[3], 'base64'), ) ) { throw new Error('Signature mismatch'); } } catch { throw new Error( 'Updater archive signature does not match the permanent public key.', ); } } export function updateManifest(metadata, publicPrefix, signature, notes) { compareVersions(metadata.version, '0.0.0'); const platform = { 'aarch64-apple-darwin': 'darwin-aarch64', 'x86_64-apple-darwin': 'darwin-x86_64', }[metadata.target]; if (!platform || !signature.trim()) { throw new Error('Invalid updater platform or missing signature.'); } return { version: metadata.version, notes: notes?.trim() || `KanbanGit ${metadata.version}`, platforms: { [platform]: { url: `${publicPrefix}/${metadata.updaterFilename}`, signature: signature.trim(), }, }, }; }