NapCatQQ source snapshot
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
Release Tag / validate-version (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:01:47 +08:00
commit f954252a34
1111 files changed
+139867

No files matched your search

+220
View File
@@ -0,0 +1,220 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
teaEncryptBlock,
teaDecryptBlock,
containerEncrypt,
containerDecrypt,
MacMachineInfoUtils,
MachineInfoUtils,
Registry20Utils,
resolveBackupPath,
} from '@/napcat-webui-backend/src/utils/guid';
// 参考实现: qq_guid_tool/macos (逆向自 macOS wrapper.node)
// 所有期望值均由该 Python 实现生成, 用于确保 TS 复刻逐字节一致。
const KEY = Buffer.from('000102030405060708090a0b0c0d0e0f', 'hex');
function prefixRng (n: number): Buffer {
return Buffer.from(Array.from({ length: n }, (_, i) => i));
}
function teaEncryptBlockG (block: Buffer, key = KEY): Buffer {
return teaEncryptBlock(block, key);
}
function teaDecryptBlockG (block: Buffer, key = KEY): Buffer {
return teaDecryptBlock(block, key);
}
describe('macOS TEA block', () => {
it('encrypts blocks identical to the python reference', () => {
expect(teaEncryptBlockG(Buffer.from('0001020304050607', 'hex')).toString('hex'))
.toBe('6328a50896bde3c8');
expect(teaEncryptBlockG(Buffer.from('ffffffffffffffff', 'hex')).toString('hex'))
.toBe('c79e81a2904419de');
expect(teaEncryptBlockG(Buffer.from('0011223344556677', 'hex')).toString('hex'))
.toBe('205f515574f0821c');
});
it('decrypts the reference ciphertext', () => {
expect(teaDecryptBlockG(Buffer.from('6328a50896bde3c8', 'hex')).toString('hex'))
.toBe('0001020304050607');
});
it('round-trips blocks', () => {
const block = Buffer.from('a1b2c3d4e5f60718', 'hex');
expect(teaDecryptBlockG(teaEncryptBlockG(block)).equals(block)).toBe(true);
});
});
describe('macOS TEA container', () => {
it('matches the reference for "hello world" with a deterministic rng', () => {
const blob = containerEncrypt(Buffer.from('hello world'), KEY, prefixRng);
expect(blob.toString('hex'))
.toBe('5e56df427ca3a4f56dfc8cbbd3438a437050558841226bb4');
expect(containerDecrypt(blob, KEY)!.toString('hex')).toBe('68656c6c6f20776f726c64');
});
it('matches the reference for an empty payload', () => {
const blob = containerEncrypt(Buffer.alloc(0), KEY, prefixRng);
expect(blob.toString('hex')).toBe('b00e46ba3e788626272ec631c28ab195');
expect(containerDecrypt(blob, KEY)!.length).toBe(0);
});
it('matches the reference for a 200 byte payload', () => {
const payload = Buffer.from(Array.from({ length: 200 }, (_, i) => i));
const blob = containerEncrypt(payload, KEY, prefixRng);
const expected = 'b00e46ba3e788626bea151d61ae65f4ee433279c69c420d5e86f2e9d9844f335' +
'fc3387a05609b7b3b294a99f8d5eb630e205a6b2d2778c01c2c860fc9b5cbcdaf07aacdece0b3a3' +
'760245fae521f96422044562bc913ce1ac90456d810a981430ed54f7cec55207b85d6b5d991b1aa0f' +
'4b033f9f8d2d86a06577c621d197a15c4c416efa3860f47b25bf71d8cfb6a08a6b3b158e1df47f950' +
'911ab355be47101f37f989b0fe0c8afc432891f761aeadc29a78eabfc8a0bbef190028f85357cbf' +
'489bb0c4a7353967dddb0254fa85f82122d4a003637bdc35';
expect(blob.toString('hex')).toBe(expected);
expect(containerDecrypt(blob, KEY)!.equals(payload)).toBe(true);
});
it('rejects a wrong key', () => {
const blob = containerEncrypt(Buffer.from('hello world'), KEY, prefixRng);
expect(containerDecrypt(blob, Buffer.alloc(16))).toBeNull();
});
});
describe('macOS machineid-info', () => {
// 真实样本: 取自 macOS 14.8.9 (x86_64 VM, IOPlatformUUID 全零),
// 源文件 ~/Library/Containers/com.tencent.qq/Data/.../global/nt_data/msf/machineid-info
const REAL_CIPHER = '062c59f4bd7972c3baeab0dd857d9482d57ecafe96b7ec4d566e416871d7e829' +
'0d117016cd94f644ea968bc641bae40d';
const REAL_PLAIN = '00000008525400c91827000000000014514d303030323120202020202020202020202020';
const REAL_KEY_UUID = '00000000-0000-0000-0000-000000000000';
it('decrypts a real QQ-generated machineid-info identically to the python reference', () => {
const key = MacMachineInfoUtils.keyFromUuid(REAL_KEY_UUID);
const plain = containerDecrypt(Buffer.from(REAL_CIPHER, 'hex'), key);
expect(plain).not.toBeNull();
expect(plain!.toString('hex')).toBe(REAL_PLAIN);
const parsed = MacMachineInfoUtils.parseMachineIdInfo(plain!);
expect(parsed.machineId.toString('hex')).toBe('525400c918270000');
expect(parsed.sn).toBe('QM00021 ');
expect(MacMachineInfoUtils.computeGuid(parsed.machineId, parsed.sn).toString('hex'))
.toBe('110cac8ab81e1b7dca6e3f48b1127d92');
});
it('re-encrypts the real payload to a byte-identical container', () => {
// 期望容器由参考 python 实现以相同 rng 生成
const key = MacMachineInfoUtils.keyFromUuid(REAL_KEY_UUID);
const plain = MacMachineInfoUtils.buildMachineIdInfo(
Buffer.from('525400c918270000', 'hex'), 'QM00021 ');
expect(plain.toString('hex')).toBe(REAL_PLAIN);
const blob = containerEncrypt(plain, key, prefixRng);
expect(blob.toString('hex'))
.toBe('b73dfc91350349448b9ca9a255de7bffd75a548719ab4fa6311a439386fed1c29fbf7a40d0b77b1c7699d27d11545ca7');
});
it('builds and parses fields like the reference', () => {
const plain = MacMachineInfoUtils.buildMachineIdInfo(
Buffer.from('aabbccddeeff0011', 'hex'), 'C02TEST');
expect(plain.toString('hex')).toBe('00000008aabbccddeeff00110000000743303254455354');
const parsed = MacMachineInfoUtils.parseMachineIdInfo(plain);
expect(parsed.machineId.toString('hex')).toBe('aabbccddeeff0011');
expect(parsed.sn).toBe('C02TEST');
});
it('computes the reference guid', () => {
const guid = MacMachineInfoUtils.computeGuid(Buffer.from('aabbccddeeff0011', 'hex'), 'C02TEST');
expect(guid.toString('hex')).toBe('99902c9e4658f3495eac9932da8de575');
});
it('computes the MAC fallback guid', () => {
const guid = MacMachineInfoUtils.computeGuid(
Buffer.concat([Buffer.from('aabbccddeeff', 'hex'), Buffer.from([0, 0])]), 'C02TEST');
expect(guid.toString('hex')).toBe('90daceec0ac283834cd1dba777e9d54a');
});
it('derives the TEA key from IOPlatformUUID', () => {
expect(MacMachineInfoUtils.keyFromUuid('0F1E2D3C-4B5A-6978-8E9F-A0B1C2D3E4F5').toString('hex'))
.toBe('30463145324433432d344235412d3639');
});
it('round-trips a container encrypt/decrypt', () => {
const key = MacMachineInfoUtils.keyFromUuid('0F1E2D3C-4B5A-6978-8E9F-A0B1C2D3E4F5');
const plain = MacMachineInfoUtils.buildMachineIdInfo(Buffer.from('0011223344556677', 'hex'), 'SN123456');
const blob = containerEncrypt(plain, key);
const back = containerDecrypt(blob, key);
expect(back!.equals(plain)).toBe(true);
const parsed = MacMachineInfoUtils.parseMachineIdInfo(back!);
expect(parsed.machineId.toString('hex')).toBe('0011223344556677');
expect(parsed.sn).toBe('SN123456');
});
});
function withTempDir (fn: (dir: string) => void): void {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'napcat-guid-test-'));
try {
fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
describe('backup naming', () => {
it('does not overwrite when creating backups in quick succession', () => {
withTempDir((dir) => {
const target = path.join(dir, 'machineid-info');
fs.writeFileSync(target, Buffer.from('payload'));
const first = MacMachineInfoUtils.backup(target);
const second = MacMachineInfoUtils.backup(target);
const third = MachineInfoUtils.backup(target);
expect(new Set([first, second, third]).size).toBe(3);
for (const backup of [first, second, third]) {
expect(fs.existsSync(backup)).toBe(true);
}
expect(MacMachineInfoUtils.getBackups(target)).toHaveLength(3);
});
});
});
describe('restore path validation', () => {
it('accepts legacy and new backup names present in the whitelist', () => {
withTempDir((dir) => {
const target = path.join(dir, 'Registry20');
fs.writeFileSync(target, Buffer.from('x'));
fs.writeFileSync(`${target}.bak.20251009123456`, Buffer.from('legacy'));
const created = Registry20Utils.backup(target);
const list = Registry20Utils.getBackups(target);
expect(resolveBackupPath(target, path.basename(`${target}.bak.20251009123456`), list))
.toBe(`${target}.bak.20251009123456`);
expect(resolveBackupPath(target, path.basename(created), list)).toBe(created);
});
});
it('rejects directory traversal and unknown backup names', () => {
withTempDir((dir) => {
const target = path.join(dir, 'machine-info');
fs.writeFileSync(target, Buffer.from('x'));
const list = MachineInfoUtils.getBackups(target);
expect(() => resolveBackupPath(target, '../../../non-backup.txt', list)).toThrow();
expect(() => resolveBackupPath(target, '../machine-info.bak.20251009123456', list)).toThrow();
expect(() => resolveBackupPath(target, 'machine-info.bak.20251009123456', list)).toThrow();
expect(() => resolveBackupPath(target, '/etc/passwd', list)).toThrow();
expect(() => resolveBackupPath(target, 'other.bak.20251009123456', list)).toThrow();
});
});
it('refuses to restore a backup whose content is not a valid device file', () => {
withTempDir((dir) => {
const target = path.join(dir, 'machine-info');
fs.writeFileSync(target, Buffer.from('x'));
const evil = `${target}.bak.20251009123456`;
fs.writeFileSync(evil, Buffer.from('not-a-machine-info'));
expect(() => MachineInfoUtils.restore(target, path.basename(evil))).toThrow();
// 原始文件未被破坏
expect(fs.readFileSync(target).toString()).toBe('x');
});
});
});