Files
Yuzu-GCA/packages/client-android/src/updater/index.ts
LukeMackin 74619a2713 fix(android): 安全修复 — SHA256原生层计算+TOCTOU+域名白名单
- PackageInstallerModule: DigestInputStream边写边算SHA256, 原生层校验消除TOCTOU
- updater: remove expo-crypto, add download domain whitelist, 错误消息去敏感路径
- package.json: 移除expo-crypto依赖(不再需要JS侧SHA256)
2026-07-19 01:51:40 +08:00

115 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { parseVersion, checkUpdate } from '@yuzu-gca/shared';
import Constants from 'expo-constants';
import * as FileSystem from 'expo-file-system';
import { NativeModules, Platform } from 'react-native';
// ============================================================
// 1. 版本号读取
// ============================================================
export function getLocalVersion(): string {
return Constants.expoConfig?.version ?? '0.1.0';
}
export function getLocalBuildTag(): string {
const v = getLocalVersion();
const versionCode = Constants.expoConfig?.android?.versionCode ?? 1;
return `v${v}-dav-android-${versionCode}`;
}
// ============================================================
// 2. JS Bundle OTAexpo-updates 自动处理)
// ============================================================
export async function checkJsBundleUpdate(): Promise<{
hasUpdate: boolean;
remoteVersion: string;
}> {
const localVersion = getLocalVersion();
const localTag = getLocalBuildTag();
const local = parseVersion(localTag);
if (!local) return { hasUpdate: false, remoteVersion: localVersion };
const result = await checkUpdate('android', local);
if (!result.shouldUpdate || !result.remoteVersion) {
return { hasUpdate: false, remoteVersion: localVersion };
}
return { hasUpdate: true, remoteVersion: result.remoteVersion.raw };
}
// ============================================================
// 3. APK 静默安装(大更新)
// ============================================================
const ALLOWED_DOWNLOAD_HOSTS = [
'git.childish-ghost.com',
'github.com',
];
const { PackageInstallerModule } = NativeModules;
function validateUrl(url: string): void {
try {
const host = new URL(url).hostname;
if (!ALLOWED_DOWNLOAD_HOSTS.some(h => host === h || host.endsWith('.' + h))) {
throw new Error(`不允许的下载域名: ${host}`);
}
} catch (e) {
if (e instanceof TypeError) throw new Error('下载URL格式无效');
throw e;
}
}
export async function downloadAndInstallApk(apkUrl: string, sha256: string): Promise<void> {
validateUrl(apkUrl);
const localPath = `${FileSystem.cacheDirectory ?? ''}gca-update.apk`;
const download = FileSystem.createDownloadResumable(
apkUrl,
localPath,
{},
(progress) => {
const pct = (progress.totalBytesWritten / progress.totalBytesExpectedToWrite) * 100;
console.log(`[OTA] 下载进度: ${pct.toFixed(0)}%`);
},
);
const result = await download.downloadAsync();
if (!result?.uri) throw new Error('APK 下载失败');
if (Platform.OS === 'android' && PackageInstallerModule) {
// SHA256 校验在原生层完成(避免 TOCTOU + 密码学误用)
await PackageInstallerModule.installApk(result.uri, sha256);
console.log('[OTA] SHA256校验通过APK已提交安装');
} else {
throw new Error('PackageInstaller 原生模块不可用');
}
}
// ============================================================
// 4. 启动时一键检查
// ============================================================
export async function onAppStartCheckUpdate(): Promise<{
jsUpdate: boolean;
apkUpdate: boolean;
}> {
const localTag = getLocalBuildTag();
const local = parseVersion(localTag);
if (!local) return { jsUpdate: false, apkUpdate: false };
const result = await checkUpdate('android', local);
if (!result.shouldUpdate || !result.build) {
return { jsUpdate: false, apkUpdate: false };
}
if (result.build.type === 'apk') {
await downloadAndInstallApk(result.build.url, result.build.sha256);
return { jsUpdate: false, apkUpdate: true };
}
return { jsUpdate: true, apkUpdate: false };
}