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

121 lines
4.7 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优先级
*
* 1. /etc/machine-id — systemd 生成,现代 Linux 标配,重装系统才变
* 2. CPU Serial — /proc/cpuinfo 中的 Serial 字段ARM/RPi 常见)
* 3. 主存储设备序列号 — /sys/block/<dev>/device/serialsda/nvme/mmcblk
* 4. DMI 产品 UUID — /sys/class/dmi/id/product_uuidx86 主板)
* 5. 随机 UUID 持久化 — 生成后写入 /etc/clawd/.box_id确保重启不变
*
* 注意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() {
return getMachineId()
|| getCpuSerial()
|| getDiskSerial()
|| getDmiUuid()
|| getPersistentUUID();
}
module.exports = { getBoxId };