加密与签名
本文档说明蓝牙通信协议中的密钥体系、加密机制和签名计算方法。
密钥体系
协议使用3个SM4密钥(各16字节)建立安全通信:
| 密钥 | 英文 | 获取命令 | 用途 |
|---|---|---|---|
| 公钥 | PublicKey | 0x3090 | 计算AuthCode(场景1) |
| 私钥 | PrivateKey | 0x3091 | SM4私钥模式加密通信 |
| 签名密钥 | SignKey | 0x3091 | 计算MD5签名验证 |
注意
- 所有密钥均为SM4密钥,不是RSA密钥
- 0x3091命令返回PrivateKey和SignKey两个密钥
- 密钥获取的详细说明见 设备管理
密钥获取流程
加密类型
协议支持多种加密方式,通过数据包的包标识字段低4位指定:
| 加密类型 | 说明 | 使用场景 |
|---|---|---|
| 0 | 明文,不加密 | 获取公钥(0x3090) |
| 1 | AES128加密 | (保留) |
| 2 | SM4公钥模式 | 获取私钥和签名密钥(0x3091) |
| 3 | SM4私钥模式 | 其他所有命令 |
包标识字段
包标识字段为1字节:
- 高4位: 表示包版本
- 低4位: 指示数据加密类型
SM4加密模式详解
模式2: SM4公钥模式
- 加密密钥:
LockID[0:16](锁ID的前16字节,不是PublicKey) - 用途: 0x3091命令请求数据加密
- 算法: SM4-ECB
命名误导
"SM4公钥模式"的密钥是LockID,与PublicKey无关。
关于"公钥"命名的历史说明
协议中"公钥"(PublicKey)和"SM4公钥模式"的命名存在历史遗留问题,容易造成误解。
命名与实际用途的差异
| 名称 | 字面理解 | 实际用途 |
|---|---|---|
| PublicKey | SM4公钥模式的密钥 | 仅用于计算AuthCode(场景1) |
| SM4公钥模式 | 使用PublicKey加密 | 使用LockID[0:16]加密 |
为什么会这样
早期协议设计时,"公钥"和"SM4公钥模式"的命名可能基于某种关联设想,但最终实现中:
- PublicKey 仅参与AuthCode的MD5计算,不作为任何加密密钥
- SM4公钥模式 的加密密钥是LockID的前16字节,与PublicKey完全无关
为什么不修正命名
由于历史原因,这些名称已广泛使用于:
- 现有实现代码的变量名和函数名
- 数据库字段定义
- 已部署设备的固件
- 第三方集成文档
修改命名会导致大量兼容性问题,因此保留原有命名,仅在文档中说明实际含义。
正确理解方式
| 术语 | 应理解为 |
|---|---|
| PublicKey | 用于初始认证的密钥(参与AuthCode计算) |
| SM4公钥模式 | 使用LockID作为密钥的SM4加密模式 |
| SM4私钥模式 | 使用PrivateKey作为密钥的SM4加密模式 |
模式3: SM4私钥模式
- 加密密钥:
PrivateKey(获取到的私钥) - 用途: 所有其他命令的加密通信
- 算法: SM4-ECB
动态口令(Token)
Token 是门锁分配给用户的动态口令,用于计算 AuthCode 进行身份验证。
工作流程
- 首次请求:APP 发送指令时 Token 填当前时间戳
- 设备响应:门锁返回校验失败,响应中携带分配的 Token
- 二次请求:APP 使用该 Token 计算 AuthCode,再次发送请求
特性
- Token 与用户关联
- 每次指令执行后 Token 会刷新
- Token 用于计算 AuthCode,防止重放攻击
AuthCode签名计算
由于锁的计算能力和内存空间有限,参与MD5计算的源数据不能太长。协议定义了两种MD5签名计算方法。
关于第一个参数
AuthCode公式中的第一个参数(AuthUserID、KeyID等),实际值只需与指令数据包中LockID字段的值一致即可。门锁校验时仅比对MD5结果,不关心参数名称。
字节拼接规则
MD5计算的是字节序列:
- 字符串:直接转为字节(UTF-8编码)
- 整型:转为大端字节序(4字节)
- 密钥:二进制字节直接拼接
所有字段按顺序拼接后计算MD5。
场景1: 使用公钥计算的签名
- 用于获取私钥(0x3091)指令
- 用于添加用户(0x3001)指令
- 用于删除用户(0x3002)指令
- 用于恢复出厂设置(0x3004)指令
计算公式
AuthCode = MD5(AuthUserID + KeyID + Token + PublicKey)字段说明
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
| AuthUserID | 字符串 | 实际长度 | 需要和指令中相同 |
| KeyID | 字符串 | 实际长度 | 需要和指令中相同 |
| Token | 整型 | 4 | 见下方说明 |
| PublicKey | 二进制 | 16 | 公钥(由0x3090获取) |
Token字段说明
- 0x3091指令:Token 为时间戳(Timestamp),大端字节序
- 0x3001、0x3002指令:Token 为动态口令,大端字节序
- 0x3004指令:AuthCode = MD5(LockID + Token + PublicKey)
示例代码
c
// 拼接源数据: AuthUserID + KeyID + Token + PublicKey
uint8_t source[256];
int offset = 0;
memcpy(source + offset, authUserID, strlen(authUserID));
offset += strlen(authUserID);
memcpy(source + offset, keyID, strlen(keyID));
offset += strlen(keyID);
// Token转大端字节序
uint8_t tokenBytes[4];
tokenBytes[0] = (token >> 24) & 0xFF;
tokenBytes[1] = (token >> 16) & 0xFF;
tokenBytes[2] = (token >> 8) & 0xFF;
tokenBytes[3] = token & 0xFF;
memcpy(source + offset, tokenBytes, 4);
offset += 4;
memcpy(source + offset, publicKey, 16);
offset += 16;
// 计算MD5
uint8_t authCode[16];
md5_calculate(source, offset, authCode);场景2: 使用签名密钥计算的签名
用于场景1以外所有需要AuthCode的指令,验证APP与门锁之间操作命令的身份合法性。
计算公式
MD5签名 = MD5(KeyID + UserID + Token + SignKey)字段说明
| 字段 | 类型 | 长度 | 说明 |
|---|---|---|---|
| KeyID | 字符串 | 实际长度 | 需要和指令中相同 |
| UserID | 字符串 | 实际长度 | 需要和指令中相同 |
| Token | 整型 | 4 | 动态口令,大端字节序 |
| SignKey | 二进制 | 16 | 签名密钥(由0x3091获取) |
示例代码
c
// 拼接源数据: KeyID + UserID + Token + SignKey
uint8_t source[256];
int offset = 0;
memcpy(source + offset, keyID, strlen(keyID));
offset += strlen(keyID);
memcpy(source + offset, userID, strlen(userID));
offset += strlen(userID);
// Token转大端字节序
uint8_t tokenBytes[4];
tokenBytes[0] = (token >> 24) & 0xFF;
tokenBytes[1] = (token >> 16) & 0xFF;
tokenBytes[2] = (token >> 8) & 0xFF;
tokenBytes[3] = token & 0xFF;
memcpy(source + offset, tokenBytes, 4);
offset += 4;
memcpy(source + offset, signKey, 16);
offset += 16;
// 计算MD5
uint8_t md5Signature[16];
md5_calculate(source, offset, md5Signature);