Skip to Content
本人正在找工作,有合适的岗位可以联系我,简历
博客NFC 无源锁 NTAG213 协议与国密加密

NFC 无源锁开发实录:NTAG213 协议读写与国密加密通讯

NFC 无源锁项目是我做过技术含量最高、最靠近底层的项目之一。无源锁本身没有电池,完全由手机 NFC 感应线圈供电,通讯窗口只有几百毫秒。如何在这极短的时间内完成供电→认证→加密指令下发→解锁确认的完整流程,是整个项目最大的工程挑战。


1. 无源锁的工作原理

与普通 NFC 刷卡不同,无源锁的交互链路更长:

手机 NFC 线圈 → 为锁内芯片供电(能量收集) → 建立 ISO 14443-3A 通讯链路 → 读取 NTAG213 芯片的 UID 及用户数据区 → 下发加密解锁指令 → 锁验证指令合法性后执行机械开锁 → 手机收到应答,UI 反馈

整个流程对时序要求极严苛:手机离开感应区则立即断电,通讯中断。


2. Android NFC 层的 Kotlin 实现

React Native 官方没有 NTAG213 的现成库,必须自己写 Kotlin Native Module 直接调用 Android 的 NfcAdapter

2.1 前台调度(Foreground Dispatch)

要让 App 在前台优先接管 NFC 事件(而不是被系统其他 App 抢走),需要注册前台调度:

class NFCModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private var nfcAdapter: NfcAdapter? = null private var pendingIntent: PendingIntent? = null override fun getName() = "NFCModule" @ReactMethod fun startScan(promise: Promise) { val activity = currentActivity ?: return promise.reject("NO_ACTIVITY", "") nfcAdapter = NfcAdapter.getDefaultAdapter(activity) if (nfcAdapter == null || !nfcAdapter!!.isEnabled) { return promise.reject("NFC_UNAVAILABLE", "NFC not available or disabled") } pendingIntent = PendingIntent.getActivity( activity, 0, Intent(activity, activity.javaClass).apply { addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) }, PendingIntent.FLAG_MUTABLE ) // 只过滤 NfcA 技术(NTAG213 基于 ISO 14443-3A) val techFilters = arrayOf(arrayOf(NfcA::class.java.name)) nfcAdapter!!.enableForegroundDispatch(activity, pendingIntent, null, techFilters) // 存储 promise,在 onNewIntent 中 resolve pendingScanPromise = promise } }

2.2 NTAG213 低级命令交互

NTAG213 的读写不能直接用高层 API,需要通过 NfcA.transceive() 发送 ISO 14443-3A 原始字节指令:

private fun communicateWithTag(tag: Tag): Map<String, Any> { val nfcA = NfcA.get(tag) nfcA.connect() nfcA.timeout = 2000 // 2秒超时,超时即断链 try { // Step 1: 读取 UID(7字节),作为设备唯一标识 val uid = tag.id.toHexString() // Step 2: 读取用户数据区(Page 4 开始,每 Page 4 字节) // READ 命令格式:[0x30, pageNumber] val userDataBytes = nfcA.transceive(byteArrayOf(0x30, 0x04)) // 返回 16 字节(4 页),取前 8 字节作为锁的序列号 val lockSerialNumber = userDataBytes.slice(0..7).toByteArray() // Step 3: 生成解锁指令(含时间戳防重放攻击) val command = buildUnlockCommand(uid, lockSerialNumber) // Step 4: 发送加密解锁指令(写入 Page 8,自定义命令区) // WRITE 命令格式:[0xA2, pageNumber, 4字节数据] val writeCmd = byteArrayOf(0xA2.toByte(), 0x08) + command.take(4).toByteArray() val writeResponse = nfcA.transceive(writeCmd) if (!writeResponse.contentEquals(byteArrayOf(0x0A.toByte()))) { throw IOException("Write ACK failed: ${writeResponse.toHexString()}") } // Step 5: 读取锁的应答(锁执行开锁后会更新 Page 9) val responseBytes = nfcA.transceive(byteArrayOf(0x30, 0x09)) val unlockStatus = responseBytes[0].toInt() and 0xFF return mapOf( "uid" to uid, "success" to (unlockStatus == 0xAA), "timestamp" to System.currentTimeMillis() ) } finally { nfcA.close() } }

3. 国密 SM4 加密:保障通讯链路安全

解锁指令明文传输是绝对不允许的,因为 NFC 信号在近场范围内可被嗅探。项目要求使用国密 SM4 分组加密算法对指令进行加密。

3.1 密钥派生

每把锁有唯一的设备密钥,由后端根据 UID 和主密钥派生:

// 前端通过接口获取设备会话密钥(已加密传输,后端负责派生) const sessionKey = await api.getLockSessionKey({ uid, timestamp: Date.now() })

后端实现(Node.js 伪代码):

// 基于 HMAC-SM3 从主密钥派生设备密钥 const deviceKey = hmacSM3(masterKey, uid + timestamp.toString())

3.2 React Native 侧的 SM4 加密

JS 侧使用 sm-crypto 库(支持 SM2/SM3/SM4):

import { sm4 } from 'sm-crypto' interface UnlockCommand { lockUid: string timestamp: number nonce: string // 防重放的随机数 userId: string } function encryptUnlockCommand(command: UnlockCommand, sessionKey: string): string { const plaintext = JSON.stringify(command) // SM4-CBC 模式加密,IV 使用时间戳派生确保每次不同 const iv = generateIV(command.timestamp) const encrypted = sm4.encrypt(plaintext, sessionKey, { mode: 'cbc', iv, padding: 'pkcs#7' }) return encrypted } function generateIV(timestamp: number): string { // 取时间戳的 MD5(前 16 字节)作为 IV return sm3(timestamp.toString()).slice(0, 32) }

3.3 防重放攻击机制

NFC 通讯的一个安全隐患是重放攻击:攻击者录制一次合法的解锁信号,之后重放。防御方案:

  1. 时间戳校验:指令中包含当前时间戳,锁的芯片检查时间戳是否在 30 秒内
  2. Nonce 机制:每次解锁生成一个随机数,服务端记录已用 nonce,拒绝重复
  3. 计数器递增:锁内维护一个解锁计数器,指令中包含期望的下一个计数值
async function buildSecureUnlockPayload(lockInfo: LockInfo): Promise<string> { // 从服务端获取当前锁的计数器值 const { counter, sessionKey } = await api.getLockState(lockInfo.uid) const command: UnlockCommand = { lockUid: lockInfo.uid, timestamp: Date.now(), nonce: crypto.randomUUID(), userId: currentUser.id, // counter + 1 表示这是一条新指令 expectedCounter: counter + 1, } return encryptUnlockCommand(command, sessionKey) }

4. 通讯超时与用户体验的平衡

NFC 感应需要用户将手机贴近锁,时间窗口极短(约 500ms 内完成全部交互)。为了给用户清晰的反馈:

function NFCUnlockButton({ lockId }: { lockId: string }) { const [status, setStatus] = useState<'idle' | 'scanning' | 'success' | 'error'>('idle') const handleUnlock = async () => { setStatus('scanning') // 震动反馈,提示用户已准备好 Vibration.vibrate(50) try { const payload = await buildSecureUnlockPayload(lockId) const result = await NFCModule.writeAndRead(payload, { timeout: 3000, // 3秒内必须完成,否则提示用户重新靠近 retryOnTimeout: true, maxRetries: 2, }) if (result.success) { setStatus('success') Vibration.vibrate([0, 100, 50, 100]) // 双震动 = 成功 // 记录解锁日志 await api.logUnlockEvent({ lockId, timestamp: result.timestamp }) } } catch (e) { setStatus('error') Vibration.vibrate(500) // 长震动 = 失败 } finally { setTimeout(() => setStatus('idle'), 2000) } } return ( <TouchableOpacity onPress={handleUnlock} style={styles.button}> <NFCStatusIcon status={status} /> <Text>{STATUS_TEXT[status]}</Text> </TouchableOpacity> ) }

5. 踩坑记录

坑 1:NTAG213 的内存布局必须精确
Page 0-2 是只读的 UID 和锁定字节,Page 3 是 CC 容量容器,用户数据从 Page 4 开始,Page 41-44 是配置区。曾经误写了 Page 3 导致整个芯片被锁死,只能找厂商换芯片,教训极深。

坑 2:前台调度在 Activity 切换时失效
锁屏、来电等情况会导致 Activity 进入 onPause,前台调度自动停止。必须在 onResume 中重新注册,onPause 中调用 disableForegroundDispatch

坑 3:部分国产机的 NFC 驱动延迟
小米/OPPO 部分机型的 NFC 驱动在第一次 transceive 后有 50-80ms 的额外延迟,导致复杂指令序列超时。解决方案是将多条小指令合并为更少的大指令,减少往返次数。


总结

这个项目让我对”IoT 安全”有了完全不同的理解:软件安全与硬件时序约束的结合,比纯软件系统要复杂得多。国密算法的引入不仅是合规要求,更是在极短通讯窗口内保障安全的工程必要性。


最后更新:2026年4月

标签:NFC、NTAG213、React Native、Kotlin、国密、SM4、IoT安全

最近更新:4/19/2026, 3:19:53 PM