文件
OmniCard/backend/test/modules/nfc/anti-tamper.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

334 行
11 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import {
NfcAntiTamperService,
NfcAuditLogListResult,
NfcAuditLogRecordResult,
} from '../../../src/modules/nfc/service/anti-tamper';
describe('Point 15: 实体 NFC 卡片动态防伪验签算法 (anti-tamper)', () => {
let service: NfcAntiTamperService;
const mockUid = '04A1B2C3D4E5F6';
const mockCardId = 1001;
const mockSecret = 'k9#mZ$8vP!xL2@qR5*wY8^uN1(bC4)eT';
beforeEach(() => {
service = new NfcAntiTamperService();
});
describe('1. generateDeviceToken (生成动态防伪验签令牌)', () => {
it('应成功生成包含时间戳与 HMAC-SHA256 签名的动态 Token', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
expect(typeof token).toBe('string');
expect(token).toContain('.');
const [tsStr, signature] = token.split('.');
const ts = Number(tsStr);
expect(Number.isSafeInteger(ts)).toBe(true);
expect(Math.abs(Date.now() - ts)).toBeLessThan(2000);
expect(signature).toHaveLength(64); // SHA-256 16进制长度为 64
expect(/^[0-9a-f]{64}$/i.test(signature)).toBe(true);
});
it('支持显式指定 timestamp 生成 Token', () => {
const customTs = 1710000000000;
const token = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret,
customTs
);
const [tsStr] = token.split('.');
expect(Number(tsStr)).toBe(customTs);
});
it('入参缺失时应抛出明确的异常保护', () => {
expect(() =>
service.generateDeviceToken('', mockCardId, mockSecret)
).toThrow('缺少必要参数');
expect(() =>
service.generateDeviceToken(mockUid, 0, mockSecret)
).toThrow('缺少必要参数');
expect(() =>
service.generateDeviceToken(mockUid, -1, mockSecret)
).toThrow('缺少必要参数');
expect(() =>
service.generateDeviceToken(mockUid, mockCardId, '')
).toThrow('缺少必要参数');
});
});
describe('2. verifyDeviceToken - 合法令牌验证', () => {
it('合法未过期的令牌应验证通过', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(result.valid).toBe(true);
expect(result.reason).toBeUndefined();
});
});
describe('3. verifyDeviceToken - 过期超时拦截', () => {
it('超过默认 5 分钟 (300,000ms) 的令牌应被拦截', () => {
const expiredTs = Date.now() - 5 * 60 * 1000 - 1000; // 5分1秒前
const token = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret,
expiredTs
);
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/过期|expired/i);
});
it('支持自定义 maxAgeMs 过期时间阈值', () => {
const pastTs = Date.now() - 10 * 1000; // 10秒前
const token = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret,
pastTs
);
// 阈值 5 秒 -> 应当判定为过期
const resExpired = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret,
5000
);
expect(resExpired.valid).toBe(false);
expect(resExpired.reason).toMatch(/过期|expired/i);
// 阈值 20 秒 -> 应当判定为合法
const resValid = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret,
20000
);
expect(resValid.valid).toBe(true);
});
});
describe('4. verifyDeviceToken - 恶意参数篡改拦截', () => {
it('篡改 UID 时应拦截', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
const tamperedUid = '04A1B2C3D4E5F9';
const result = service.verifyDeviceToken(
tamperedUid,
mockCardId,
token,
mockSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
});
it('篡改 cardId 时应拦截', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
const tamperedCardId = 9999;
const result = service.verifyDeviceToken(
mockUid,
tamperedCardId,
token,
mockSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
});
it('篡改签名内容时应拦截', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
const [ts, sig] = token.split('.');
const tamperedSig = sig.substring(0, sig.length - 1) + (sig.endsWith('0') ? '1' : '0');
const tamperedToken = `${ts}.${tamperedSig}`;
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
tamperedToken,
mockSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
});
it('使用错误的密钥验证时应拦截', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
const wrongSecret = 'different_secret_key_1234567890';
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
wrongSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/签名无效|篡改|invalid signature/i);
});
});
describe('5. verifyDeviceToken - 空参与畸形输入保护', () => {
it('传入空参数时不应崩溃,应安全返回无效原因', () => {
const validToken = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret
);
expect(
service.verifyDeviceToken('', mockCardId, validToken, mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, 0, validToken, mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, -10, validToken, mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, mockCardId, '', mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, mockCardId, validToken, '').valid
).toBe(false);
});
it('传入畸形或格式错误的 Token 应安全拦截', () => {
expect(
service.verifyDeviceToken(mockUid, mockCardId, 'malformedToken', mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, mockCardId, 'abc.def', mockSecret).valid
).toBe(false);
expect(
service.verifyDeviceToken(mockUid, mockCardId, `${Date.now()}.short`, mockSecret).valid
).toBe(false);
});
});
describe('6. verifyDeviceToken - 时钟容差保护', () => {
it('时间稍微超前的令牌(在容差范围内)应被允许', () => {
const futureTs = Date.now() + 30 * 1000; // 30秒未来时间(网络或机器微小偏差)
const token = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret,
futureTs
);
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(result.valid).toBe(true);
});
it('异常超前(超过容差阈值)的未来令牌应被拒绝', () => {
const futureTs = Date.now() + 10 * 60 * 1000; // 10分钟后
const token = service.generateDeviceToken(
mockUid,
mockCardId,
mockSecret,
futureTs
);
const result = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(result.valid).toBe(false);
expect(result.reason).toMatch(/未来|时钟偏差|future/i);
});
});
describe('7. verifyDeviceToken - 防重放保护 (Replay Attack)', () => {
it('同一合法令牌不可重复验签使用', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
// 首次验签通过
const firstResult = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(firstResult.valid).toBe(true);
// 再次使用同一令牌验签,触发防重放拦截
const secondResult = service.verifyDeviceToken(
mockUid,
mockCardId,
token,
mockSecret
);
expect(secondResult.valid).toBe(false);
expect(secondResult.reason).toMatch(/重放|已使用|replay/i);
});
});
describe('8. 无持久化仓库时审计查询/记录不得伪装落库', () => {
it('无 auditRepository 时 getAuditLogs 返回空列表,并标明 not_persisted / persisted:false', () => {
// 触发验签失败,内存队列仍可入队,但对外查询不得当作已入库
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
service.verifyDeviceToken(mockUid, 9999, token, mockSecret);
const result: NfcAuditLogListResult = service.getAuditLogs();
expect(result.logs).toEqual([]);
expect(result.persisted).toBe(false);
expect(result.not_persisted === true || result.persisted === false).toBe(
true
);
});
it('无 auditRepository 时 recordAudit 不得返回成功落库', () => {
const result: NfcAuditLogRecordResult = service.recordAudit(
mockUid,
mockCardId,
'签名无效,检测到恶意参数篡改 (Invalid signature / Tampered)',
'dummy.token'
);
expect(result.persisted).toBe(false);
expect(result.not_persisted === true || result.persisted === false).toBe(
true
);
// 不得出现已落库语义
expect((result as { success?: boolean }).success).not.toBe(true);
expect((result as { saved?: boolean }).saved).not.toBe(true);
const query = service.getAuditLogs();
expect(query.logs).toEqual([]);
expect(query.persisted).toBe(false);
});
it('无库时内存防重放仍生效,但查询结果仍为空且未落库', () => {
const token = service.generateDeviceToken(mockUid, mockCardId, mockSecret);
expect(
service.verifyDeviceToken(mockUid, mockCardId, token, mockSecret).valid
).toBe(true);
expect(
service.verifyDeviceToken(mockUid, mockCardId, token, mockSecret).valid
).toBe(false);
const result = service.getAuditLogs();
expect(result.logs).toEqual([]);
expect(result.persisted).toBe(false);
});
});
});