mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: 新增更新包本地完整性校验功能
- 新增更新包SHA512哈希和文件大小校验逻辑,仅在校验通过后才允许安装更新,防止损坏或被篡改的安装包被安装 - 更新更新页面UI,根据更新包是否已通过校验显示差异化提示文本 - 临时隐藏设置菜单中的Koring账户设置选项 - 修复发布说明生成脚本,改为按标签创建时间排序,并针对不同构建模式调整基准标签选取逻辑
This commit is contained in:
File diff suppressed because one or more lines are too long
+77
-1
@@ -1,5 +1,7 @@
|
|||||||
import { autoUpdater, CancellationToken, type ProgressInfo } from 'electron-updater';
|
import { autoUpdater, CancellationToken, type ProgressInfo } from 'electron-updater';
|
||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { getConfig, updateConfig, flushConfig } from './config';
|
import { getConfig, updateConfig, flushConfig } from './config';
|
||||||
@@ -33,6 +35,8 @@ export interface UpdateStatusPayload {
|
|||||||
source?: string;
|
source?: string;
|
||||||
/** 当前更新通道(woker / runner) */
|
/** 当前更新通道(woker / runner) */
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
/** 安装包是否已通过本地核验(sha512 / 大小) */
|
||||||
|
verified?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +156,12 @@ class UpdateService {
|
|||||||
private suppressErrors = false;
|
private suppressErrors = false;
|
||||||
/** 当前下载的取消令牌(暂停/取消时 cancel) */
|
/** 当前下载的取消令牌(暂停/取消时 cancel) */
|
||||||
private downloadToken: CancellationToken | null = null;
|
private downloadToken: CancellationToken | null = null;
|
||||||
|
/** 目标安装包是否已通过本地核验(sha512 + 大小),核验通过前不允许安装 */
|
||||||
|
private verified = false;
|
||||||
|
/** update-available 时记录的期望 sha512(来自 latest.yml) */
|
||||||
|
private expectedSha512 = '';
|
||||||
|
/** 期望文件大小(字节) */
|
||||||
|
private expectedSize = 0;
|
||||||
/** 当前更新通道(woker 慢走 / runner 跑步;从配置读取,可运行时切换) */
|
/** 当前更新通道(woker 慢走 / runner 跑步;从配置读取,可运行时切换) */
|
||||||
private channelKey: UpdateChannelKey = 'woker';
|
private channelKey: UpdateChannelKey = 'woker';
|
||||||
|
|
||||||
@@ -190,11 +200,19 @@ class UpdateService {
|
|||||||
this.state = 'available';
|
this.state = 'available';
|
||||||
this.version = info.version;
|
this.version = info.version;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
|
this.verified = false;
|
||||||
|
// 记录期望安装包校验值(来自 latest.yml 的 files[0])
|
||||||
|
const anyInfo = info as unknown as { files?: Array<{ sha512?: string; size?: number }> };
|
||||||
|
const first = Array.isArray(anyInfo?.files) ? anyInfo.files[0] : null;
|
||||||
|
this.expectedSha512 = String(first?.sha512 ?? '');
|
||||||
|
this.expectedSize = Number(first?.size ?? 0);
|
||||||
|
console.log(`[updater] 可用更新 ${info.version},期望 sha512=${this.expectedSha512.slice(0, 12)}… size=${this.expectedSize}`);
|
||||||
this.emit();
|
this.emit();
|
||||||
});
|
});
|
||||||
autoUpdater.on('update-not-available', () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
this.state = 'not-available';
|
this.state = 'not-available';
|
||||||
this.version = undefined;
|
this.version = undefined;
|
||||||
|
this.verified = false;
|
||||||
this.emit();
|
this.emit();
|
||||||
});
|
});
|
||||||
autoUpdater.on('download-progress', (p) => {
|
autoUpdater.on('download-progress', (p) => {
|
||||||
@@ -202,8 +220,22 @@ class UpdateService {
|
|||||||
this.progress = p;
|
this.progress = p;
|
||||||
this.emit();
|
this.emit();
|
||||||
});
|
});
|
||||||
autoUpdater.on('update-downloaded', (info) => {
|
// 下载完成 → 本地核验安装包(sha512 + 大小)通过后才置为"已下载可安装";
|
||||||
|
// 核验失败 → 进入 error,拒绝安装(防下载损坏 / 篡改)
|
||||||
|
autoUpdater.on('update-downloaded', async (info) => {
|
||||||
|
const errMsg = await this.verifyDownloadedPackage();
|
||||||
|
if (errMsg) {
|
||||||
|
console.error(`[updater] 安装包核验失败: ${errMsg}`);
|
||||||
|
this.state = 'error';
|
||||||
|
this.error = errMsg;
|
||||||
|
this.version = undefined;
|
||||||
|
this.verified = false;
|
||||||
|
this.emit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[updater] 安装包核验通过(sha512 + 大小)');
|
||||||
this.state = 'downloaded';
|
this.state = 'downloaded';
|
||||||
|
this.verified = true;
|
||||||
this.version = info.version;
|
this.version = info.version;
|
||||||
this.emit();
|
this.emit();
|
||||||
});
|
});
|
||||||
@@ -238,10 +270,46 @@ class UpdateService {
|
|||||||
bytesPerSecond: this.progress?.bytesPerSecond,
|
bytesPerSecond: this.progress?.bytesPerSecond,
|
||||||
source: this.source,
|
source: this.source,
|
||||||
channel: this.channelKey,
|
channel: this.channelKey,
|
||||||
|
verified: this.verified,
|
||||||
error: this.error,
|
error: this.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地核验已下载的安装包(在安装前执行):
|
||||||
|
* 1) 文件存在性;
|
||||||
|
* 2) 大小与 latest.yml 记录一致(有期望值时);
|
||||||
|
* 3) sha512 与 latest.yml 记录一致(逐块流式计算,防篡改/下载损坏)。
|
||||||
|
* 返回 null = 通过;返回字符串 = 失败原因(调用方进入 error,拒绝安装)。
|
||||||
|
*/
|
||||||
|
private async verifyDownloadedPackage(): Promise<string | null> {
|
||||||
|
const helper = (autoUpdater as unknown as { downloadedUpdateHelper?: { file?: string } }).downloadedUpdateHelper;
|
||||||
|
const filePath = helper?.file;
|
||||||
|
if (!filePath) return '未找到已下载的安装包';
|
||||||
|
try {
|
||||||
|
const stat = await fs.promises.stat(filePath);
|
||||||
|
if (this.expectedSize > 0 && stat.size !== this.expectedSize) {
|
||||||
|
return `安装包大小不符(期望 ${this.expectedSize} 字节,实际 ${stat.size} 字节)`;
|
||||||
|
}
|
||||||
|
if (this.expectedSha512) {
|
||||||
|
const hash = crypto.createHash('sha512');
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => resolve());
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
const actual = hash.digest('hex').toLowerCase();
|
||||||
|
if (actual !== this.expectedSha512.toLowerCase()) {
|
||||||
|
return '安装包校验和不符(sha512 不匹配),文件可能已损坏或被篡改';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return `安装包核验失败:${String((e as Error)?.message ?? e)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 将当前状态与下载进度写入配置(下载/安装进度落盘) */
|
/** 将当前状态与下载进度写入配置(下载/安装进度落盘) */
|
||||||
private persist(payload: UpdateStatusPayload): void {
|
private persist(payload: UpdateStatusPayload): void {
|
||||||
try {
|
try {
|
||||||
@@ -615,6 +683,14 @@ class UpdateService {
|
|||||||
*/
|
*/
|
||||||
quitAndInstall(): void {
|
quitAndInstall(): void {
|
||||||
if (!this.ready || this.state !== 'downloaded') return;
|
if (!this.ready || this.state !== 'downloaded') return;
|
||||||
|
// 安装前必须已通过本地核验(sha512 + 大小),防损坏/篡改包被安装
|
||||||
|
if (!this.verified) {
|
||||||
|
console.warn('[updater] 安装被拒:安装包未通过核验');
|
||||||
|
this.state = 'error';
|
||||||
|
this.error = '安装包未通过核验,已拒绝安装,请重新检查并下载更新';
|
||||||
|
this.emit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.state = 'installing';
|
this.state = 'installing';
|
||||||
this.emit();
|
this.emit();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -27,11 +27,21 @@ $rec = [char]0x1e # 记录分隔符(每个 commit 一条)
|
|||||||
$sep = [char]0x1f # 字段分隔符(hash / subject / body)
|
$sep = [char]0x1f # 字段分隔符(hash / subject / body)
|
||||||
$format = "%x1f%H%x1f%s%x1f%b%x1e"
|
$format = "%x1f%H%x1f%s%x1f%b%x1e"
|
||||||
|
|
||||||
# 自上个 release tag(v*)以来的提交;无 tag 则取全部提交
|
# 提交范围(base tag):按 tag 创建时间从新到旧取"上一个 release"。
|
||||||
$lastTag = git tag --sort=-version:refname 2>$null | Where-Object { $_ -match '^v' } | Select-Object -First 1
|
# run(正式版)→ 上一个【正式版】tag:把上一个正式版之后所有 beta 的提交也写进正式版更新内容
|
||||||
if ($lastTag) {
|
# beta → 上一个 release tag(任意通道)
|
||||||
Write-Host "Commits since tag: $lastTag"
|
# 注意用 creatordate 而非 git 版本序:git 对 -beta.N / -N 混排不可靠(会把 beta.16 排到 -17 之前)。
|
||||||
$raw = git log --format=$format "$lastTag..HEAD"
|
$tagsNewestFirst = git for-each-ref --sort=-creatordate --format '%(refname:short)' refs/tags 2>$null |
|
||||||
|
Where-Object { $_ -match '^v' }
|
||||||
|
$baseTag = if ($Mode -eq 'run') {
|
||||||
|
($tagsNewestFirst | Where-Object { $_ -notmatch '-beta\.' } | Select-Object -First 1)
|
||||||
|
} else {
|
||||||
|
($tagsNewestFirst | Select-Object -First 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($baseTag) {
|
||||||
|
Write-Host "Commits since tag: $baseTag (mode=$Mode)"
|
||||||
|
$raw = git log --format=$format "$baseTag..HEAD"
|
||||||
} else {
|
} else {
|
||||||
Write-Host "No release tags found, listing all commits"
|
Write-Host "No release tags found, listing all commits"
|
||||||
$raw = git log --format=$format "HEAD"
|
$raw = git log --format=$format "HEAD"
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface UpdateStatusPayload {
|
|||||||
source?: string;
|
source?: string;
|
||||||
/** 当前更新通道(woker / runner) */
|
/** 当前更新通道(woker / runner) */
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
/** 安装包是否已通过本地核验(sha512 / 大小;核验通过前不允许安装) */
|
||||||
|
verified?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
UserCircle,
|
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
Palette,
|
Palette,
|
||||||
Download,
|
Download,
|
||||||
@@ -37,7 +36,8 @@ function ShortcutTile({ icon, label, desc, navKey, onClick }: ShortcutItem & { o
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shortcuts: ShortcutItem[] = [
|
const shortcuts: ShortcutItem[] = [
|
||||||
{ icon: <UserCircle className="w-4 h-4" />, label: "Koring 账户", desc: "同步数据、皮肤与个人配置", navKey: "account" },
|
// Koring 账户(暂时隐藏)
|
||||||
|
// { icon: <UserCircle className="w-4 h-4" />, label: "Koring 账户", desc: "同步数据、皮肤与个人配置", navKey: "account" },
|
||||||
{ icon: <Gamepad2 className="w-4 h-4" />, label: "游戏账户与档案", desc: "管理游戏内账户和档案配置", navKey: "game-account" },
|
{ icon: <Gamepad2 className="w-4 h-4" />, label: "游戏账户与档案", desc: "管理游戏内账户和档案配置", navKey: "game-account" },
|
||||||
{ icon: <Palette className="w-4 h-4" />, label: "主题与背景", desc: "深色模式、背景图片与视差", navKey: "theme-bg" },
|
{ icon: <Palette className="w-4 h-4" />, label: "主题与背景", desc: "深色模式、背景图片与视差", navKey: "theme-bg" },
|
||||||
{ icon: <Download className="w-4 h-4" />, label: "下载设置", desc: "下载线程数与存储路径", navKey: "download" },
|
{ icon: <Download className="w-4 h-4" />, label: "下载设置", desc: "下载线程数与存储路径", navKey: "download" },
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useCallback, type ReactNode } from "react";
|
import { useState, useCallback, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Home,
|
Home,
|
||||||
UserCircle,
|
|
||||||
Info,
|
Info,
|
||||||
Copyright,
|
Copyright,
|
||||||
Gamepad2,
|
Gamepad2,
|
||||||
@@ -23,7 +22,7 @@ import {
|
|||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
|
|
||||||
import { HomeSetting } from "./general/home";
|
import { HomeSetting } from "./general/home";
|
||||||
import { AccountSetting } from "./general/account";
|
// import { AccountSetting } from "./general/account"; // Koring 账户(暂时隐藏)
|
||||||
import { AboutSetting } from "./general/about";
|
import { AboutSetting } from "./general/about";
|
||||||
import { CopyrightSetting } from "./general/copyright";
|
import { CopyrightSetting } from "./general/copyright";
|
||||||
import { GameAccountSetting } from "./game/game-account";
|
import { GameAccountSetting } from "./game/game-account";
|
||||||
@@ -65,7 +64,8 @@ function buildMenuData(
|
|||||||
title: "通用",
|
title: "通用",
|
||||||
items: [
|
items: [
|
||||||
{ key: "home", label: "主页", icon: <Home className={iconCls} />, component: <HomeSetting onNavigate={switchPage} /> },
|
{ key: "home", label: "主页", icon: <Home className={iconCls} />, component: <HomeSetting onNavigate={switchPage} /> },
|
||||||
{ key: "account", label: "Koring 账户", icon: <UserCircle className={iconCls} />, component: <AccountSetting /> },
|
// Koring 账户(暂时隐藏)
|
||||||
|
// { key: "account", label: "Koring 账户", icon: <UserCircle className={iconCls} />, component: <AccountSetting /> },
|
||||||
{ key: "about", label: "关于", icon: <Info className={iconCls} />, component: <AboutSetting /> },
|
{ key: "about", label: "关于", icon: <Info className={iconCls} />, component: <AboutSetting /> },
|
||||||
{ key: "copyright", label: "版权", icon: <Copyright className={iconCls} />, component: <CopyrightSetting /> },
|
{ key: "copyright", label: "版权", icon: <Copyright className={iconCls} />, component: <CopyrightSetting /> },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ export function UpdatePage() {
|
|||||||
: st === "paused"
|
: st === "paused"
|
||||||
? `下载已暂停(${pct.toFixed(0)}%)`
|
? `下载已暂停(${pct.toFixed(0)}%)`
|
||||||
: st === "downloaded"
|
: st === "downloaded"
|
||||||
? "更新已下载完成"
|
? status?.verified
|
||||||
|
? "更新已下载完成(安装包已核验,点击安装)"
|
||||||
|
: "更新已下载完成"
|
||||||
: st === "installing"
|
: st === "installing"
|
||||||
? "正在安装更新,应用即将重启..."
|
? "正在安装更新,应用即将重启..."
|
||||||
: st === "error"
|
: st === "error"
|
||||||
|
|||||||
Reference in New Issue
Block a user