32a5479086
Use OS machine identifiers (Windows MachineGuid, macOS IOPlatformUUID, Linux machine-id) hashed as deviceId so all accounts on one PC share one license slot. Keep legacy userData/device.id for migration and retire it on re-activation. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
6.2 KiB
TypeScript
194 lines
6.2 KiB
TypeScript
import { execFileSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
type ExecFile = (
|
|
file: string,
|
|
args: readonly string[],
|
|
options: { encoding: 'utf8'; windowsHide?: boolean; timeout?: number },
|
|
) => string;
|
|
|
|
export type MachineFingerprintDeps = {
|
|
platform?: NodeJS.Platform;
|
|
env?: NodeJS.ProcessEnv;
|
|
execFileSync?: ExecFile;
|
|
readFileSync?: (p: string, encoding: 'utf8') => string;
|
|
existsSync?: (p: string) => boolean;
|
|
mkdirSync?: (p: string, opts: { recursive: boolean }) => void;
|
|
writeFileSync?: (p: string, data: string, opts?: { mode?: number }) => void;
|
|
};
|
|
|
|
const HASH_PREFIX = 'TTRPGPlayer.machine.v1\0';
|
|
|
|
/** Стабильный opaque id из сырого машинного идентификатора ОС. */
|
|
export function hashMachineRawId(platform: string, rawId: string): string {
|
|
return createHash('sha256')
|
|
.update(HASH_PREFIX, 'utf8')
|
|
.update(platform, 'utf8')
|
|
.update('\0', 'utf8')
|
|
.update(rawId.trim().toLowerCase(), 'utf8')
|
|
.digest('hex');
|
|
}
|
|
|
|
export function parseWindowsMachineGuid(regOutput: string): string | null {
|
|
const m = /MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{8,})/.exec(regOutput);
|
|
const id = m?.[1]?.trim();
|
|
return id && id.length >= 8 ? id : null;
|
|
}
|
|
|
|
export function parseMacIOPlatformUUID(ioregOutput: string): string | null {
|
|
const m = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(ioregOutput);
|
|
const id = m?.[1]?.trim();
|
|
return id && id.length >= 8 ? id : null;
|
|
}
|
|
|
|
export function parseWmicUuid(wmicOutput: string): string | null {
|
|
const lines = wmicOutput
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter(Boolean);
|
|
for (const line of lines) {
|
|
if (/^uuid$/i.test(line)) continue;
|
|
if (/^[0-9a-fA-F-]{8,}$/.test(line) && !/^0+-?0+-?0+-?0+-?0+$/.test(line)) {
|
|
return line;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function tryExec(exec: ExecFile, file: string, args: readonly string[]): string | null {
|
|
try {
|
|
return exec(file, args, { encoding: 'utf8', windowsHide: true, timeout: 8_000 });
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readWindowsRawId(exec: ExecFile): string | null {
|
|
const regOut = tryExec(exec, 'reg', [
|
|
'query',
|
|
'HKLM\\SOFTWARE\\Microsoft\\Cryptography',
|
|
'/v',
|
|
'MachineGuid',
|
|
]);
|
|
if (regOut) {
|
|
const guid = parseWindowsMachineGuid(regOut);
|
|
if (guid) return guid;
|
|
}
|
|
const wmicOut = tryExec(exec, 'wmic', ['csproduct', 'get', 'uuid']);
|
|
if (wmicOut) {
|
|
const uuid = parseWmicUuid(wmicOut);
|
|
if (uuid) return uuid;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readDarwinRawId(exec: ExecFile): string | null {
|
|
const out = tryExec(exec, 'ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
|
|
if (!out) return null;
|
|
return parseMacIOPlatformUUID(out);
|
|
}
|
|
|
|
function readLinuxRawId(readFile: (p: string, encoding: 'utf8') => string): string | null {
|
|
for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
try {
|
|
const id = readFile(p, 'utf8').trim();
|
|
if (id.length >= 8 && !/^0+$/.test(id)) return id;
|
|
} catch {
|
|
/* try next */
|
|
}
|
|
}
|
|
try {
|
|
const id = readFile('/sys/class/dmi/id/product_uuid', 'utf8').trim();
|
|
if (id.length >= 8 && !/^0+-?0+-?0+-?0+-?0+$/i.test(id)) return id;
|
|
} catch {
|
|
/* optional, often root-only */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Каталог на уровне машины (не профиль пользователя), для редкого fallback UUID. */
|
|
export function machineWideIdPath(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string {
|
|
if (platform === 'win32') {
|
|
const base = env.PROGRAMDATA?.trim() || path.join(env.SystemDrive || 'C:', 'ProgramData');
|
|
return path.join(base, 'TTRPGPlayer', 'machine.id');
|
|
}
|
|
if (platform === 'darwin') {
|
|
return path.join('/Library/Application Support', 'TTRPGPlayer', 'machine.id');
|
|
}
|
|
return path.join('/var/lib', 'ttrpg-player', 'machine.id');
|
|
}
|
|
|
|
function readOrCreateMachineWideFallback(
|
|
platform: NodeJS.Platform,
|
|
env: NodeJS.ProcessEnv,
|
|
deps: Required<
|
|
Pick<MachineFingerprintDeps, 'existsSync' | 'readFileSync' | 'mkdirSync' | 'writeFileSync'>
|
|
>,
|
|
): string | null {
|
|
const p = machineWideIdPath(platform, env);
|
|
try {
|
|
if (deps.existsSync(p)) {
|
|
const existing = deps.readFileSync(p, 'utf8').trim();
|
|
if (existing.length >= 8) return existing;
|
|
}
|
|
} catch {
|
|
/* create below */
|
|
}
|
|
const id = createHash('sha256')
|
|
.update(`TTRPGPlayer.machine.fallback\0${platform}\0${os.hostname()}\0${Date.now()}\0${Math.random()}`, 'utf8')
|
|
.digest('hex');
|
|
try {
|
|
deps.mkdirSync(path.dirname(p), { recursive: true });
|
|
deps.writeFileSync(p, `${id}\n`, { mode: 0o644 });
|
|
return id;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Стабильный идентификатор физической машины (одинаковый для всех пользователей ОС на одном ПК).
|
|
* Источники: Windows MachineGuid, macOS IOPlatformUUID, Linux /etc/machine-id.
|
|
*/
|
|
export function resolveMachineFingerprint(deps: MachineFingerprintDeps = {}): string {
|
|
const platform = deps.platform ?? process.platform;
|
|
const env = deps.env ?? process.env;
|
|
const exec = deps.execFileSync ?? (execFileSync as ExecFile);
|
|
const readFile = deps.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
|
|
const existsSync = deps.existsSync ?? ((p) => fs.existsSync(p));
|
|
const mkdirSync = deps.mkdirSync ?? ((p, opts) => {
|
|
fs.mkdirSync(p, opts);
|
|
});
|
|
const writeFileSync = deps.writeFileSync ?? ((p, data, opts) => {
|
|
fs.writeFileSync(p, data, opts);
|
|
});
|
|
|
|
const override = env.DND_LICENSE_DEVICE_ID?.trim();
|
|
if (override && override.length >= 8) return override;
|
|
|
|
let raw: string | null = null;
|
|
if (platform === 'win32') raw = readWindowsRawId(exec);
|
|
else if (platform === 'darwin') raw = readDarwinRawId(exec);
|
|
else if (platform === 'linux') raw = readLinuxRawId(readFile);
|
|
|
|
if (!raw) {
|
|
raw = readOrCreateMachineWideFallback(platform, env, {
|
|
existsSync,
|
|
readFileSync: readFile,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
});
|
|
}
|
|
|
|
if (!raw) {
|
|
throw new Error(
|
|
'LICENSE_MACHINE_ID_UNAVAILABLE: не удалось получить идентификатор физической машины',
|
|
);
|
|
}
|
|
|
|
return hashMachineRawId(platform, raw);
|
|
}
|