蓝牙智能锁实战:安全认证、开锁指令、日志记录、离线模式
智能锁这个项目,是我个人觉得最能体现 BLE 开发综合能力的一个场景。你想想看,一把锁要同时搞定安全、实时响应、日志追溯,还得在没网的时候正常工作——这几乎把蓝牙开发的所有痛点都踩了一遍。
今天我们就来拆解这个实战案例。我会把我在智能锁项目里踩过的坑、总结的经验,都揉进这四个核心模块里。
一、安全认证:别让锁变成“开门器”
智能锁最怕什么?怕被中间人攻击,怕重放攻击。说白了,就是怕别人捡到你的蓝牙包,然后照着发一遍就把门打开了。
我在第一个智能锁原型里就吃过这个亏。当时图省事,直接明文传输开锁指令。结果测试同事用抓包工具一抓,复制粘贴就开了锁……嗯,那个版本连夜重写了。
1.1 动态密钥 + 时间戳
正确的做法是:每次连接都生成一个动态密钥。手机和锁之间通过 ECDH 密钥交换协议协商出一个会话密钥。开锁指令必须带上当前时间戳,锁端校验时间差在 5 秒内才放行。
// 手机端:生成开锁指令
fun generateUnlockCommand(sessionKey: ByteArray, timestamp: Long): ByteArray {
val payload = "UNLOCK:$timestamp".toByteArray()
val mac = HmacSHA256(sessionKey, payload).take(4) // 取前4字节做签名
return payload + mac
}
// 锁端:验证指令
fun verifyCommand(data: ByteArray, sessionKey: ByteArray): Boolean {
val timestamp = data.sliceArray(0..12).toString(Charsets.UTF_8).split(":")[1].toLong()
if (abs(System.currentTimeMillis() - timestamp) > 5000) return false
val mac = HmacSHA256(sessionKey, data.sliceArray(0..12)).take(4)
return mac.contentEquals(data.sliceArray(13..16))
}
1.2 绑定流程:谁才是主人?
第一次绑定必须走带外认证。我一般用两种方式:
- 扫码绑定: 锁体上印二维码,里面包含锁的蓝牙 MAC 和初始密钥。手机扫码后通过密钥加密通信。
- 按键确认: 手机发起绑定请求,锁体上物理按键按下确认。这种方式适合没有屏幕的廉价锁。
我曾经见过一个产品,绑定流程只校验了蓝牙名称……结果隔壁老王用手机一搜,也能绑定。这哪是智能锁,分明是“共享锁”。
二、开锁指令:快、准、稳
开锁这个动作,用户体验要求极高。你想想,人站在门口,掏出手机,等个三五秒才开门——这体验还不如掏钥匙呢。
2.1 指令结构设计
我习惯把指令设计成固定长度,16字节。这样锁端解析起来不用动态分配内存,对 MCU 非常友好。
| 字节偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 1 | 0xA5 | 帧头 |
| 1 | 1 | 0x01 | 指令类型(开锁) |
| 2-7 | 6 | 时间戳 | 精确到秒 |
| 8-11 | 4 | 签名 | HMAC 前4字节 |
| 12-15 | 4 | 0x5A5A5A5A | 帧尾 |
2.2 响应速度优化
开锁指令从手机发出到锁动作,我要求控制在 200ms 以内。怎么做到?
- MTU 协商: 连接后立即协商到最大 MTU(一般是 512 字节),减少分包。
- 写操作使用 Write Without Response: 开锁指令不需要锁端确认收到,只要锁执行了就行。
- 锁端中断处理: 收到指令后直接在 BLE 中断回调里解析,不要进 RTOS 任务调度。
我记得有一次,锁端用了 FreeRTOS 的消息队列来传递指令,结果从收到指令到电机转动花了 800ms。后来改成直接在回调里处理,直接降到 120ms。嗯,实时性就是这么抠出来的。
三、日志记录:出了事得有据可查
智能锁的日志,说白了就是“谁、什么时候、干了什么”。这个功能平时没人看,但一旦出了纠纷,它就是铁证。
3.1 日志存储策略
锁端的存储空间有限,我一般用循环覆盖的方式:
- 分配 4KB 的 Flash 空间给日志
- 每条日志固定 32 字节
- 最多存 128 条,满了就覆盖最旧的
// 日志结构
data class LockLog(
val timestamp: Long, // 8字节
val action: Byte, // 1字节:0x01开锁 0x02关锁 0x03异常
val userId: Short, // 2字节:用户ID
val reserved: ByteArray // 21字节:预留
)
3.2 日志同步到手机
手机连接锁后,自动拉取增量日志。我用的方法是:
- 手机记录上次同步的时间戳
- 锁端只返回时间戳大于该值的日志
- 手机收到后存入本地 SQLite 数据库
这样做的好处是,即使锁端日志被覆盖了,手机端还有完整的历史记录。
四、离线模式:没网也得干活
智能锁最尴尬的场景是什么?就是用户手机没信号,或者家里 WiFi 断了。这时候如果锁不能工作,那就真成“智障锁”了。
4.1 离线密钥缓存
我的方案是:手机在最后一次联网时,从服务器预取接下来 7 天的离线密钥。每个密钥对应一天,用日期做索引。
// 离线密钥结构
data class OfflineKey(
val date: String, // "2024-01-15"
val key: ByteArray, // 16字节密钥
val unlockCount: Int // 当日剩余开锁次数
)
开锁时,手机用当天的密钥生成指令。锁端也存有同样的密钥列表,验证通过就开门。
4.2 离线模式下的日志
离线开锁的记录会暂存在手机本地。等网络恢复后,手机自动把日志上传到服务器。锁端本身不关心网络状态,它只负责验证指令和执行动作。
我曾经遇到一个坑:手机离线开锁后,日志里记录的时间是手机本地时间。结果用户手机时间设置错了,日志里显示凌晨 3 点开门,用户非说不是他开的……后来我改成用锁端时间戳来记录日志,手机只负责上传,不负责生成时间。
五、整体架构图
下面这张图,是我画给团队新人的。它把整个智能锁的通信链路和数据流向都串起来了。
这张图里,我把整个系统分成了三层:手机 App、蓝牙智能锁、云端服务器。离线模式和安全认证作为两个独立模块,贯穿在手机和锁的通信过程中。
说实话,智能锁这个项目让我学到最多的不是蓝牙技术本身,而是怎么在资源受限的 MCU 上做安全、做存储、做容错。你想想看,一把锁可能要在零下 20 度的北方冬天工作,也可能在 40 度的南方夏天暴晒——硬件扛得住,软件也得扛得住。
嗯,今天就聊到这里。代码示例里的 HMAC 和 ECDH 实现,我建议你直接用 Android 的 javax.crypto 包,别自己造轮子。锁端的话,找个成熟的 MCU 加密库,比如 mbed TLS 的裁剪版,够用就行。