CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m0s
CI/CD / test_and_check_app (pull_request) Successful in 7m2s
CI/CD / Publish immutable image (pull_request) Skipped
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m35s
106 lines
3.4 KiB
JavaScript
106 lines
3.4 KiB
JavaScript
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(),
|
|
},
|
|
},
|
|
};
|
|
}
|