feat(round-11): 弱网离线队列/跨国会议一键预约/团队多语言名片批量向导/NFC硬件出厂封印 (83套件984测试全绿)
这个提交包含在:
@@ -90,7 +90,8 @@ export class NfcAntiTamperService {
|
||||
cardId: number,
|
||||
token: string,
|
||||
secret: string,
|
||||
maxAgeMs = DEFAULT_MAX_AGE_MS
|
||||
maxAgeMs = DEFAULT_MAX_AGE_MS,
|
||||
maxFutureDriftMs = 30000
|
||||
): NfcVerifyResult {
|
||||
// 1. 空参及参数合法性校验
|
||||
if (
|
||||
@@ -140,8 +141,8 @@ export class NfcAntiTamperService {
|
||||
|
||||
// 3. 时钟容差与过期校验
|
||||
const now = Date.now();
|
||||
if (ts - now > maxAgeMs) {
|
||||
const reason = 'Token 时间戳异常(未来时钟偏差过大)';
|
||||
if (ts - now > maxFutureDriftMs) {
|
||||
const reason = 'Token 时间戳异常(未来时钟偏差过大 clock drift detected)';
|
||||
this.recordAuditLog(uid, cardId, reason, token);
|
||||
return { valid: false, reason };
|
||||
}
|
||||
@@ -238,3 +239,98 @@ export class NfcAntiTamperService {
|
||||
this.usedTokens.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC 卡片离线碰一碰数据包结构
|
||||
*/
|
||||
export interface OfflineCardPayload {
|
||||
uid: string;
|
||||
cardId: number;
|
||||
batchNo: string;
|
||||
seal: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 NFC 硬件物理出厂数字封印 (Offline Anti-Tamper Seal)
|
||||
* 基于 UID + 批次号 + 工厂母密钥生成
|
||||
*/
|
||||
export function generateCardOfflineSeal(
|
||||
uid: string,
|
||||
batchNo: string,
|
||||
factoryKey: string
|
||||
): string {
|
||||
if (!uid || !batchNo || !factoryKey) {
|
||||
throw new Error('缺少生成硬件数字封印的必要参数');
|
||||
}
|
||||
const payload = `SEAL:${uid.trim()}:${batchNo.trim()}`;
|
||||
return crypto
|
||||
.createHmac('sha256', factoryKey)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 恒定时间校验卡片出厂数字封印
|
||||
*/
|
||||
export function verifyCardOfflineSeal(
|
||||
uid: string,
|
||||
batchNo: string,
|
||||
seal: string,
|
||||
factoryKey: string
|
||||
): boolean {
|
||||
if (!uid || !batchNo || !seal || !factoryKey) {
|
||||
return false;
|
||||
}
|
||||
const expected = generateCardOfflineSeal(uid, batchNo, factoryKey);
|
||||
const bufA = Buffer.from(seal.toLowerCase(), 'hex');
|
||||
const bufB = Buffer.from(expected.toLowerCase(), 'hex');
|
||||
if (bufA.length !== bufB.length) return false;
|
||||
return crypto.timingSafeEqual(bufA, bufB);
|
||||
}
|
||||
|
||||
/**
|
||||
* 离线快速核验碰一碰数据包 (无网络时手机端或离线设备快速验真)
|
||||
*/
|
||||
export function verifyOfflineCardPayload(
|
||||
payload: OfflineCardPayload,
|
||||
secret: string,
|
||||
factoryKey: string
|
||||
): { valid: boolean; isAuthenticNfc: boolean; reason?: string } {
|
||||
// 1. 验证出厂硬件封印
|
||||
const isSealValid = verifyCardOfflineSeal(
|
||||
payload.uid,
|
||||
payload.batchNo,
|
||||
payload.seal,
|
||||
factoryKey
|
||||
);
|
||||
if (!isSealValid) {
|
||||
return {
|
||||
valid: false,
|
||||
isAuthenticNfc: false,
|
||||
reason: '硬件出厂封印校验失败,疑似伪造假卡',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 校验防伪 Token
|
||||
const service = new NfcAntiTamperService();
|
||||
const tokenRes = service.verifyDeviceToken(
|
||||
payload.uid,
|
||||
payload.cardId,
|
||||
payload.token,
|
||||
secret
|
||||
);
|
||||
|
||||
if (!tokenRes.valid) {
|
||||
return {
|
||||
valid: false,
|
||||
isAuthenticNfc: false,
|
||||
reason: tokenRes.reason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
isAuthenticNfc: true,
|
||||
};
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户