add SHA-256 checksum verification to auto-updater
Build and Release / build-and-release (push) Has been cancelled
Build and Release / build-and-release (push) Has been cancelled
This commit is contained in:
+1
-1
@@ -192,7 +192,7 @@ app.whenReady().then(async () => {
|
|||||||
if (!cancelled && !updateWin.isDestroyed()) {
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
sendProgress(`Downloading update... ${pct}%`, pct);
|
sendProgress(`Downloading update... ${pct}%`, pct);
|
||||||
}
|
}
|
||||||
});
|
}, update.sha256);
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
sendProgress('Installing update...', 100);
|
sendProgress('Installing update...', 100);
|
||||||
|
|||||||
+41
-5
@@ -1,5 +1,6 @@
|
|||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
import { createReadStream, createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { electronLog } from './logger';
|
import { electronLog } from './logger';
|
||||||
@@ -8,6 +9,7 @@ export interface UpdateInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
sha256: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProgressCallback = (percent: number) => void;
|
export type ProgressCallback = (percent: number) => void;
|
||||||
@@ -104,7 +106,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets: Array<{ name: string; browser_download_url: string; size: number }> = release.assets || [];
|
const assets: Array<{ name: string; browser_download_url: string; size: number; digest: string }> = release.assets || [];
|
||||||
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
||||||
if (!setupAsset) {
|
if (!setupAsset) {
|
||||||
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
||||||
@@ -119,11 +121,20 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| Download:', setupAsset.browser_download_url, '| Size:', setupAsset.size);
|
// Extract SHA-256 digest from GitHub API (format: "sha256:<hex>")
|
||||||
|
const sha256 = (setupAsset.digest || '').replace(/^sha256:/i, '');
|
||||||
|
if (!sha256) {
|
||||||
|
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| SHA-256:', sha256.substring(0, 16) + '...');
|
||||||
resolve({
|
resolve({
|
||||||
version: remoteVersion,
|
version: remoteVersion,
|
||||||
downloadUrl: setupAsset.browser_download_url,
|
downloadUrl: setupAsset.browser_download_url,
|
||||||
fileSize: setupAsset.size,
|
fileSize: setupAsset.size,
|
||||||
|
sha256,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
||||||
@@ -149,7 +160,22 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback): Promise<void> {
|
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => {
|
||||||
|
const actual = hash.digest('hex');
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 expected:', expectedSha256);
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 actual: ', actual);
|
||||||
|
resolve(actual === expectedSha256);
|
||||||
|
});
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback, expectedSha256?: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tmpPath = destPath + '.tmp';
|
const tmpPath = destPath + '.tmp';
|
||||||
|
|
||||||
@@ -194,8 +220,18 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
|
|||||||
res.pipe(file);
|
res.pipe(file);
|
||||||
|
|
||||||
file.on('finish', () => {
|
file.on('finish', () => {
|
||||||
file.close(() => {
|
file.close(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (expectedSha256) {
|
||||||
|
const valid = await verifyChecksum(tmpPath, expectedSha256);
|
||||||
|
if (!valid) {
|
||||||
|
electronLog.error('[KCC-Update] Checksum mismatch — file may be corrupted or tampered');
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(new Error('SHA-256 checksum mismatch'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
electronLog.log('[KCC-Update] Checksum verified');
|
||||||
|
}
|
||||||
if (existsSync(destPath)) unlinkSync(destPath);
|
if (existsSync(destPath)) unlinkSync(destPath);
|
||||||
renameSync(tmpPath, destPath);
|
renameSync(tmpPath, destPath);
|
||||||
resolve();
|
resolve();
|
||||||
|
|||||||
Reference in New Issue
Block a user