Files
koring-launcher/electron/core/device-id.ts
T

120 lines
3.9 KiB
TypeScript
Raw Normal View History

// 设备唯一标识(组合指纹 + 回退 MachineGuid
//
// 用途:为“设备识别码”提供尽量唯一且稳定的标识(授权/激活/统计)。
// 来源优先级(均为硬件级,重装系统不变):
// 1. board — 主板 UUIDWin32_ComputerSystemProduct.UUID
// 2. disk — 硬盘序列号(Win32_DiskDrive 首个物理盘)
// 3. bios — BIOS 序列号(Win32_BIOS;存在大量 OEM 默认值,故置后)
// 4. machine— 注册表 MachineGuid(系统安装级,作为最终回退)
// 取第一个有效值 → SHA-256 → 前 32 位十六进制 → UUID 样式分段展示。
import { execSync } from 'child_process';
import { createHash } from 'crypto';
export type DeviceIdSource = 'board' | 'disk' | 'bios' | 'machine' | 'none';
export interface DeviceIdentity {
deviceId: string | null;
source: DeviceIdSource;
}
/** OEM/虚拟机常见的占位序列号,视为无效 */
const INVALID_VALUES = [
'to be filled',
'o.e.m',
'default string',
'system serial number',
'not specified',
'not available',
'none',
'n/a',
'unknown',
'unspecified',
'innotek',
'bochs',
'vmware',
];
function isValid(value?: string | null): value is string {
if (!value) return false;
const v = value.trim();
if (!v) return false;
if (/^0+$/.test(v)) return false; // 全 0
if (/^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(v)) return false; // 00000000-... 全空 UUID
const low = v.toLowerCase();
return !INVALID_VALUES.some((bad) => low.includes(bad));
}
/** 读取四类候选标识(Windows 一条 PowerShell 完成,逐项容错) */
function probeSources(): { board: string; disk: string; bios: string; machine: string } {
const empty = { board: '', disk: '', bios: '', machine: '' };
if (process.platform !== 'win32') return empty;
const ps = [
'-NoProfile -Command',
"$ErrorActionPreference='SilentlyContinue';",
'$b=(Get-CimInstance Win32_ComputerSystemProduct).UUID;',
'$d=(Get-CimInstance Win32_DiskDrive|Select-Object -First 1).SerialNumber;',
'$i=(Get-CimInstance Win32_BIOS).SerialNumber;',
"$g=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid;",
'[pscustomobject]@{board=[string]$b;disk=[string]$d;bios=[string]$i;machine=[string]$g}|ConvertTo-Json -Compress',
].join(' ');
try {
const output = execSync(`powershell ${ps}`, { encoding: 'utf-8', timeout: 8000, windowsHide: true });
const parsed = JSON.parse(output.trim() || '{}');
return {
board: String(parsed.board ?? ''),
disk: String(parsed.disk ?? ''),
bios: String(parsed.bios ?? ''),
machine: String(parsed.machine ?? ''),
};
} catch {
return empty;
}
}
/** 32 位十六进制 → UUID 样式(8-4-4-4-12,小写) */
function toUuidLike(hex32: string): string {
const h = hex32.toLowerCase();
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
}
function compute(seed: string): string {
const hex = createHash('sha256').update(`koring-device:${seed}`, 'utf-8').digest('hex');
return toUuidLike(hex.slice(0, 32));
}
let cached: DeviceIdentity | null = null;
/** 获取设备唯一标识(进程内缓存,只探测一次) */
export function getDeviceId(): DeviceIdentity {
if (cached) return cached;
const { board, disk, bios, machine } = probeSources();
let seed: string | undefined;
let source: DeviceIdSource = 'none';
if (isValid(board)) {
seed = board;
source = 'board';
} else if (isValid(disk)) {
seed = disk;
source = 'disk';
} else if (isValid(bios)) {
seed = bios;
source = 'bios';
} else if (isValid(machine)) {
seed = machine;
source = 'machine';
}
cached = seed
? { deviceId: compute(seed), source }
: { deviceId: null, source: 'none' };
return cached;
}
/** 调试用:清空缓存(下次调用重新探测) */
export function resetDeviceIdCache(): void {
cached = null;
}