Files
clawd/lib/fingerprint.js
2026-03-14 22:21:13 +08:00

128 lines
4.8 KiB
JavaScript
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.
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { execSync } = require('child_process');
/**
* 生成硬件唯一指纹作为 box_id。
*
* 策略:
* 优先 将 machine-id + CPU serial + 磁盘 serial 拼接后取 SHA-256 前 32 字符,
* 三者至少能拿到一个即可用此方案,防止 ghost clone 场景下 machine-id 相同的问题。
*
* 若均拿不到则依次退化:
* DMI UUID → 持久化随机 UUID
*
* 注意MAC 地址故意排除,网卡更换/虚拟化/Docker 都会导致其变化。
*/
const PERSIST_FILE = '/etc/clawd/.box_id';
// ── 1. /etc/machine-id ───────────────────────────────────────────────────────
function getMachineId() {
try {
const id = fs.readFileSync('/etc/machine-id', 'utf8').trim();
if (id && /^[0-9a-f]{32}$/i.test(id)) return id;
} catch (_) {}
return null;
}
// ── 2. CPU SerialARM / Raspberry Pi───────────────────────────────────────
function getCpuSerial() {
try {
const info = fs.readFileSync('/proc/cpuinfo', 'utf8');
const match = info.match(/^Serial\s*:\s*([0-9a-fA-F]{8,})$/m);
if (match) {
const serial = match[1].replace(/^0+/, ''); // 去掉前导零
if (serial.length >= 8) return serial.padStart(16, '0');
}
} catch (_) {}
return null;
}
// ── 3. 主存储设备序列号 ────────────────────────────────────────────────────────
function getDiskSerial() {
// 按优先级依次尝试常见块设备
const candidates = [
'/sys/block/sda/device/serial',
'/sys/block/sdb/device/serial',
'/sys/block/nvme0n1/device/serial',
'/sys/block/mmcblk0/device/serial', // SD 卡RPi
'/sys/block/vda/device/serial', // 虚拟磁盘
];
for (const p of candidates) {
try {
const serial = fs.readFileSync(p, 'utf8').trim().replace(/\s+/g, '');
if (serial && serial.length >= 4 && serial !== '0000000000000000') {
return crypto.createHash('sha256').update('disk:' + serial).digest('hex').slice(0, 32);
}
} catch (_) {}
}
// 兜底:尝试 lsblk需要 util-linux大多数发行版自带
try {
const out = execSync(
"lsblk --nodeps -o SERIAL --noheadings 2>/dev/null | head -1",
{ timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }
).toString().trim();
if (out && out.length >= 4) {
return crypto.createHash('sha256').update('disk:' + out).digest('hex').slice(0, 32);
}
} catch (_) {}
return null;
}
// ── 4. DMI 产品 UUIDx86 主板)──────────────────────────────────────────────
function getDmiUuid() {
try {
const uuid = fs.readFileSync('/sys/class/dmi/id/product_uuid', 'utf8').trim();
// 排除全零/全F等无效值
if (uuid && uuid !== '00000000-0000-0000-0000-000000000000'
&& uuid !== 'ffffffff-ffff-ffff-ffff-ffffffffffff') {
return uuid.replace(/-/g, '').toLowerCase();
}
} catch (_) {}
return null;
}
// ── 5. 持久化随机 UUID 兜底 ───────────────────────────────────────────────────
function getPersistentUUID() {
// 先尝试读已有的
try {
const id = fs.readFileSync(PERSIST_FILE, 'utf8').trim();
if (id && id.length >= 16) return id;
} catch (_) {}
// 生成新的并写入
const id = crypto.randomUUID().replace(/-/g, '');
try {
fs.mkdirSync(path.dirname(PERSIST_FILE), { recursive: true });
fs.writeFileSync(PERSIST_FILE, id, 'utf8');
} catch (e) {
// 写不进去也没关系,本次用内存值(重启后会变,但这是最后兜底)
console.warn('[fingerprint] 无法持久化 box_id:', e.message);
}
return id;
}
// ── 主函数 ────────────────────────────────────────────────────────────────────
function getBoxId() {
const machineId = getMachineId();
const cpuSerial = getCpuSerial();
const diskSerial = getDiskSerial();
// 只要能拿到其中任意一项,就把三者拼接后取哈希,避免 ghost clone 场景
if (machineId || cpuSerial || diskSerial) {
const raw = [machineId || '', cpuSerial || '', diskSerial || ''].join(':');
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 32);
}
return getDmiUuid() || getPersistentUUID();
}
module.exports = { getBoxId };