release: v2.1.12 reliability and security hardening
CI / verify (push) Waiting to run

Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
This commit is contained in:
Sucukdeluxe
2026-08-11 22:36:21 +02:00
parent 2c124c848c
commit 2ba0106ef0
47 changed files with 2069 additions and 1192 deletions
+60 -29
View File
@@ -47,15 +47,13 @@ function resolveReleaseVersion(release) {
return '';
}
function pickSetupAsset(assets) {
function pickSetupAsset(assets, remoteVersion = '') {
if (!Array.isArray(assets)) return null;
// Prefer asset with "setup" in the name (case-insensitive)
const setup = assets.find(a =>
/setup/i.test(a.name) && /\.exe$/i.test(a.name)
);
if (setup) return setup;
// Fallback: any .exe
return assets.find(a => /\.exe$/i.test(a.name)) || null;
const candidates = assets.filter(asset => {
const name = String(asset?.name || '');
return /setup/i.test(name) && /\.exe$/i.test(name) && (!remoteVersion || name.includes(remoteVersion));
});
return candidates.length === 1 ? candidates[0] : null;
}
function findLatestYml(assets) {
@@ -137,7 +135,7 @@ async function checkForUpdate() {
return cachedCheck;
}
const setupAsset = pickSetupAsset(release.assets);
const setupAsset = pickSetupAsset(release.assets, remoteVersion);
const latestYml = findLatestYml(release.assets);
if (!setupAsset) {
@@ -161,16 +159,37 @@ async function checkForUpdate() {
return cachedCheck;
}
async function parseLatestYml(url, fetchImpl = fetch) {
if (!url) return null;
async function parseLatestYml(url, expected = {}, fetchImpl = fetch) {
if (!url) throw new Error('Prüfsummen-Metadaten fehlen');
try {
const res = await fetchImpl(url, { redirect: 'follow' });
if (!res.ok) throw new Error(`Prüfsummen-Metadaten konnten nicht geladen werden: HTTP ${res.status}`);
const text = await res.text();
// Extract sha512 from latest.yml
const match = text.match(/sha512:\s*([A-Za-z0-9+/=]+)/);
return match ? match[1] : null;
} catch {
return null;
const version = text.match(/^version:\s*([^\r\n]+)$/m)?.[1]?.trim() || '';
const assetPath = text.match(/^path:\s*([^\r\n]+)$/m)?.[1]?.trim() || '';
const sha512 = text.match(/^sha512:\s*([A-Za-z0-9+/=]+)$/m)?.[1] || '';
const sizeText = text.match(/^\s*size:\s*(\d+)\s*$/m)?.[1] || '';
const size = Number(sizeText);
if (!version || !assetPath || !sha512 || !Number.isSafeInteger(size) || size <= 0) {
throw new Error('Prüfsummen-Metadaten sind unvollständig');
}
const decodedSha = Buffer.from(sha512, 'base64');
if (decodedSha.length !== 64 || decodedSha.toString('base64') !== sha512) {
throw new Error('Prüfsummen-Metadaten enthalten keine gültige SHA-512-Prüfsumme');
}
if (expected.version && version !== expected.version) {
throw new Error('Prüfsummen-Metadaten gehören zu einer anderen Version');
}
if (expected.assetName && path.basename(assetPath) !== path.basename(expected.assetName)) {
throw new Error('Prüfsummen-Metadaten gehören nicht zum ausgewählten Installer');
}
if (expected.assetSize && size !== Number(expected.assetSize)) {
throw new Error('Prüfsummen-Metadaten enthalten eine abweichende Dateigröße');
}
return { version, path: assetPath, size, sha512 };
} catch (error) {
if (error && /Prüfsummen-Metadaten/.test(error.message)) throw error;
throw new Error(`Prüfsummen-Metadaten konnten nicht geladen werden: ${error.message}`);
}
}
@@ -185,6 +204,7 @@ async function prepareUpdate(onProgress, options = {}) {
activeAbort = new AbortController();
const signal = activeAbort.signal;
const fetchImpl = options.fetchImpl || fetch;
let stagedInstallerPath = '';
try {
// Stage: starting
@@ -201,6 +221,15 @@ async function prepareUpdate(onProgress, options = {}) {
if (!check.assetUrl || !check.assetName) {
throw new Error('Update-Asset unvollständig (URL oder Name fehlt)');
}
if (!check.latestYmlUrl) {
throw new Error('Prüfsummen-Metadaten fehlen');
}
const manifest = await parseLatestYml(check.latestYmlUrl, {
version: check.remoteVersion,
assetName: check.assetName,
assetSize: check.assetSize
}, fetchImpl);
const expectedSha = manifest.sha512;
// Stage: downloading
const tmpDir = options.tempDir || app.getPath('temp');
@@ -255,6 +284,9 @@ async function prepareUpdate(onProgress, options = {}) {
}
const fileBuffer = Buffer.concat(chunks);
if (fileBuffer.length !== manifest.size) {
throw new Error('Heruntergeladene Datei hat eine abweichende Größe');
}
// Stage: verifying
if (onProgress) onProgress({ stage: 'verifying', percent: 0 });
@@ -263,21 +295,19 @@ async function prepareUpdate(onProgress, options = {}) {
throw new Error('Heruntergeladene Datei ist keine gültige EXE');
}
// Optional SHA-512 verification from latest.yml
const expectedSha = await parseLatestYml(check.latestYmlUrl, fetchImpl);
if (expectedSha) {
const actualSha = crypto.createHash('sha512').update(fileBuffer).digest('base64');
if (actualSha !== expectedSha) {
// Try hex comparison
const actualHex = crypto.createHash('sha512').update(fileBuffer).digest('hex');
if (actualHex !== expectedSha.toLowerCase()) {
throw new Error('SHA-512 Prüfung fehlgeschlagen');
}
const actualSha = crypto.createHash('sha512').update(fileBuffer).digest('base64');
if (actualSha !== expectedSha) {
const actualHex = crypto.createHash('sha512').update(fileBuffer).digest('hex');
if (actualHex !== expectedSha.toLowerCase()) {
throw new Error('SHA-512 Prüfung fehlgeschlagen');
}
}
// Write to disk
fs.writeFileSync(installerPath, fileBuffer);
stagedInstallerPath = path.join(tmpDir, `${path.basename(check.assetName)}.${crypto.randomUUID()}.download`);
fs.writeFileSync(stagedInstallerPath, fileBuffer);
fs.rmSync(installerPath, { force: true });
fs.renameSync(stagedInstallerPath, installerPath);
stagedInstallerPath = '';
const prepared = {
installerPath,
@@ -289,6 +319,7 @@ async function prepareUpdate(onProgress, options = {}) {
return prepared;
} catch (err) {
if (stagedInstallerPath) fs.rmSync(stagedInstallerPath, { force: true });
if (onProgress) onProgress({ stage: 'error', error: err.message });
throw err;
} finally {
@@ -322,4 +353,4 @@ function abortUpdate() {
}
}
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion };
module.exports = { checkForUpdate, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, abortUpdate, isNewer, resolveReleaseVersion, pickSetupAsset, parseLatestYml };