门锁蓝牙模组可以提供与 MCU 之间串口通讯加密的服务,以便满足国标 GB21556.2 对锁板链路加密的要求。
加密方案由模组与 MCU 协同完成:MCU 在产品信息应答中声明支持加密后,模组生成随机种子(Seed)并发起协商;MCU 生成并保存索引列表,并依据模组下发的种子派生出会话密钥,此后双方使用该密钥对业务帧进行加解密。
适用于使用涂鸦公版门锁技术方案、且 MCU 与模组之间串口链路需要加密的产品。MCU 若暂不支持加密,不上报加密能力即可,模组不会启用加密,现有产品行为不受影响。
串口加密服务涉及以下命令字:
| 命令字 | 命令说明 |
|---|---|
| 0x01 | 在产品信息应答的 Option Config 区声明 MCU 串口加密能力 |
| 0xD8 | 提供串口加密协商,包含三步:查询索引列表、通知随机种子、加密试探 |
串口帧格式沿用涂鸦蓝牙模组串口通讯协议(55 AA 帧头 + 版本号 + 命令字 + 长度 + 数据 + 校验和),本节仅说明与加密相关的差异。帧的第 3 字节(byte3)在两种帧中含义不同:
| 帧类型 | byte3 | 数据区 |
|---|---|---|
| 明文帧 | 命令字 CMD | 明文数据 |
| 加密业务帧 | 加密方式 EncType(0x01=AES,0x02=Simple) |
加密后的 [CMD + 长度(2B) + 数据] |
加密业务帧即将 “命令字 + 两字节长度 + 数据” 组成的内层明文整体加密后作为数据区,byte3 改填加密方式(AES 模式内层需补零至 16 字节倍数,Simple 模式无填充)。协商命令 0xD8 始终以明文收发。
byte3 取值重合(须区分):
数值 明文 CMD EncType 0x01产品信息查询/应答 AES 0x02请求工作模式 Simple 要点:
CMD=0x01 && len=0(55 AA 00 01 00 00)应始终识别为明文的0x01查询 MCU 信息指令并清handshake_done,出现场景:模组重启;其余在已握手且byte3 == EncType时解密。
模组发送 CMD 0x01 查询 MCU 产品信息(明文),MCU 在应答的 Option Config 区(第 13 字节开始的 TLD 列表)中追加一个 TLD [08 01 01] 来声明支持串口加密。
| MCU 应答 | 模组行为 |
|---|---|
无该 TLD 或 D=0x00 |
不启用加密,全程保持明文 |
08 01 01 |
模组将发起 0xD8 加密协商流程 |
一旦双方已成功启用串口加密(enable=1),除非 恢复出厂 清除加密状态,否则不允许加密降级为明文;MCU 即使去掉本 TLD,模组也不会恢复明文业务。
以明文帧 cmd = 0xD8 承载,分三步完成:查询索引列表 → 通知随机种子 → 加密试探。完成后,双方业务帧进入加密通讯。
协商时序如下:
Data 区结构(0xD8 帧通用)
| Scope | OpType | 数据 |
|---|---|---|
| 0x00 | 见下 | 操作相关数据 |
Scope:固定 0x00(全量加密范围)。OpType:
0x00:查询索引列表。0x01:通知随机种子。0x03:加密试探。仅当模组 Flash 中无有效 IndexList(index_valid=0)时发起,例如首次上电或模组被重置。
模组发送 Data
| Scope | OpType | EncType 列表 |
|---|---|---|
| 0x00 | 0x00 | 0x01 0x02 |
EncType 列表(2 字节):模组支持的加密方式,当前为 [0x01, 0x02](AES 与简单加密)。MCU 回复 Data
| Scope | OpType | EncType | IndexList |
|---|---|---|---|
| 0x00 | 0x00 | 0x01/0x02 | xx xx … xx(16 字节) |
EncType(1 字节):MCU 固件配置的加密类型(须在模组请求的 [0x01, 0x02] 列表中),作为后续全程使用的加密方式。IndexList(16 字节):MCU 生成(或返回已保存)的索引列表,每字节取值 0x00~0x1F,用于会话密钥派生。MCU 首次生成 IndexList 后应写入自身 Flash,恢复出厂前保持不变。模组重启不会重新查询索引(仅模组被重置、丢失索引时才重新发起 Op 0x00),MCU 与模组保存的同一 IndexList,确保双方密钥一致。
示例(末字节校验和仅作格式示意,以实际计算为准):
55 AA 00 D8 00 04 00 00 01 02 DE55 AA 00 D8 00 13 00 00 01 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 72模组每次协商密钥都会生成新的随机种子并下发给 MCU。
模组发送 Data
| Scope | OpType | EncType | Seed |
|---|---|---|---|
| 0x00 | 0x01 | 0x01/0x02 | xx xx xx xx |
EncType(1 字节):与查询索引时 MCU 返回一致。Seed(4 字节):本次会话的随机种子。MCU 回复 Data
| Scope | OpType | EncType |
|---|---|---|
| 0x00 | 0x01 | 0x01/0x02 |
EncType 确认(与收到的保持一致)。每次加密协商模组会更新 Seed,但 不会 重新查询 IndexList。MCU 收到 Op 0x01 后必须用新的 Seed 重算 session_key。
示例
55 AA 00 D8 00 07 00 01 01 65 AA 89 D2 4955 AA 00 D8 00 03 00 01 01 DC协商最后一步,模组发送 16 字节随机明文,MCU 加密后回送,模组校验一致性。
模组发送 Data
| Scope | OpType | PlainRandom |
|---|---|---|
| 0x00 | 0x03 | xx xx … xx(16 字节) |
PlainRandom(16 字节):模组生成的随机明文。MCU 回复 Data
| Scope | OpType | Cipher |
|---|---|---|
| 0x00 | 0x03 | xx xx … xx(16 字节) |
Cipher(16 字节):MCU 用当前 session_key 与协商的 EncType 对 PlainRandom 加密的结果。模组将本地 PlainRandom 加密后与 MCU 回送的 Cipher 比对,一致则协商成功,模组会下发加密的 0x02 指令,MCU 可据此置加密使能 enable=1 并存 Flash,MCU 此后也可以主动给模组加密发送业务指令。协商失败模组侧重试最多 3 次。
MCU 侧建议维护如下字段(可映射至 Flash 结构体):
| 字段 | FLASH存储 | 说明 |
|---|---|---|
enable |
是 |
|
enc_type |
是 |
|
index_list[16] |
是 | 密钥派生索引表 |
index_valid |
是 | 索引是否已生成 |
seed[4] |
否 | 当前生效种子 |
session_key[16] |
否 | 运行时派生,上电或收到新 Seed 后重算 |
handshake_done |
否 | 运行时标志,协商完成后置 1 |
写入 Flash 的时机
| 时机 | 写入内容 |
|---|---|
Op 0x00 应答后 |
index_list、index_valid、enc_type |
Op 0x01 收到新 Seed |
仅更新内存中的 seed 并重算 session_key |
Op 0x03 试探成功后 |
enable=1(第一次协商成功) |
MCU 冷启动(enable=1)
index_valid、index_list、enc_type、enable。handshake_done须等模组再次完成 0xD8 协商成功后再置位。handshake_done=1 后再加密收发。会话密钥由模组下发的 Seed 与 MCU 生成并保存的 IndexList 共同决定,流程如下:
Seed[4]
→ MD5(Seed, 4) 得到 16 字节二进制摘要
→ 转为 32 字节小写 hex ASCII(candidate_key)
→ 按 IndexList[16] 逐字节选取 candidate_key 中的字节
→ session_key[16](会话密钥)
IndexList 长度固定 16 字节,每个元素为 candidate_key 的下标,取值范围 0x00~0x1F。MCU 和模组各自按上述流程派生出的 session_key 应保持一致。MD5、AES 等标准算法用芯片平台硬件引擎或成熟软件库实现,完整的平台接口声明与派生伪代码见 附录 A。
协商 Op 0x00 阶段,模组发送支持的类型列表 [0x01, 0x02]。MCU 返回其固件配置的 EncType(须落在该列表中),后续全程使用。
AES-128-ECB(EncType = 0x01)
session_key[16]。0x00 至长度为 16 的倍数。Len 字段截取有效 Payload,忽略 ZeroPad 区。简单加密(EncType = 0x02)
逐字节运算,加解密使用同一操作:
data[i] = ~(data[i] ^ key[i % 16]) // 取值低 8 位
[CMD(1) + Len(2) + Payload(N)]。AES 模式需先 Zero Pad 到 16 字节倍数再按块加密;简单加密直接对内层逐字节运算。Len 字段,仅取前 Len 字节作为有效 Payload,忽略尾部 ZeroPad;简单加密用同一运算还原。协商试探包(Op 0x03)的 16 字节明文/密文也按此处的加解密逻辑处理。业务帧加解密封装的参考伪代码见 附录 B。
加密启用后,除白名单外的业务命令(如 DP 0x06/0x07)须加密收发。
| 命令字 | 说明 |
|---|---|
0x00 |
心跳 |
0x01 |
产品信息查询/应答(含加密能力 TLD) |
0xD8 |
加密协商 |
0xEA~0xEE |
MCU OTA |
0x04/0x05 |
重置指令(条件放行) |
条件放行(非固定白名单):当模组处于 未绑定 状态时,明文重置指令 0x04/0x05 会被模组接受(便于非绑定状态完成模组侧清加密态);已绑定 时重置仍须加密。
其余场景行为:
| 条件 | 行为 |
|---|---|
enable=0(从未成功启用加密) |
全明文,兼容未加密设备 |
enable=1 且协商未完成(非 ACTIVE) |
非白名单明文 丢弃;不发非白名单业务 |
enable=1 且协商完成(ACTIVE) |
非白名单按 EncType 密文收发 |
enable=1 落盘后,即使本轮 MCU 不报加密能力或协商失败,也 不会 退回全明文通讯;清除 enable 仅允许恢复出厂操作。0x02(请求工作模式) 后再开始发送业务指令;支持加密时用密文,不支持加密时用明文。0xE8 → 0x02(现有 0x02 处理中再发 CMD 0x03 同步工作状态)。模组重启识别:模组每次重启都会发出明文查询帧 55 AA 00 01 00 00(CMD: 0x01,len: 0)。MCU 收到该帧时应:
handshake_done = 0(暂停向模组发送非白名单密文)。enable、index_list、enc_type(禁止因重启而降级)。0x01(含加密能力 TLD)。0xE8/0x02 后,再发初始化配置。0x00,随后发明文查询产品信息 0x01。0x01 带 TLD:
enable=0:模组发明文 0xE8、0x02。enable=1:禁止降级,不补发明文启动指令。0xD8 加密协商指令。0x02(加密场景为密文解密后的内层 CMD 0x02)。0x02 后再按需主动发业务指令。| 集成点 | 说明 |
|---|---|
CMD 0x01 组包 |
在 Option Config 区追加 TLD 08 01 01 声明支持加密 |
| UART 接收预处理 | 白名单放行明文;0x01 && len=0 识别模组重启,清 handshake_done;已握手且 byte3 == EncType 时解密;否则丢弃 |
| UART 发送挂钩 | 白名单明文;enable=1 且 handshake_done=1 时对其余 CMD 加密 |
CMD 0xD8 处理 |
响应 Op 0x00/0x01/0x03 |
| 初始化配置 | 收到 0x02 后再发(与是否加密无关;加密场景为密文 0x02);收到0x02 才能确保模组能正常解密 MCU 发的加密指令 |
| 恢复出厂设置 | 加密握手仍有效时不得先清加密态再发明文重置;蓝牙已未绑定时可用明文 0x04/0x05 |
当串口加密已启用且协商完成(enable=1 && handshake_done=1)时,MCU 本地重置必须按下列顺序执行,否则模组收不到重置、无法清自身加密 Flash,会出现双边 enable/IndexList 不一致、后续无法再协商:
0x05,或产品使用的同等恢复出厂通知):在仍持有 session_key / handshake_done 时经加密发送路径发出;禁止 此时因已清 enable 而回退明文。enable、seed、index_list、enc_type、handshake_done 等)。0x05,模组会丢弃该帧并保留加密使能,双方状态撕裂。0x04 / 0x05 会被模组接受(见白名单条件放行)。仍建议优先走 “先加密重置、再清本端” 主路径。enable=0)或尚未协商完成时,仍可按明文恢复出厂流程处理。若模组侧 enable=0(从未谈成过加密),则不会。MCU 不上报加密 TLD(无 T=0x08)或上报 D=0x00,模组走明文启动(补发 0xE8/0x02),与未加密产品一致。
若模组侧 enable=1(曾与支持加密的 MCU 协商成功),而 MCU 变成不支持加密的固件:属 不允许的加密降级,模组不响应非白名单明文,需 恢复出厂 后再配。
MCU 需完成以下工作:
0x01 应答的 Option Config 区增加 TLD 08 01 01,声明支持加密。< 0x20),恢复出厂前保持不变。0xD8 处理分支,响应 Op 0x00(返回 IndexList)、Op 0x01(保存 Seed 并派生密钥)、Op 0x03(回送试探密文)。0x02 后再发初始化配置(加密启用后为密文)。0x05),再清除本端加密状态与 Flash;详见 MCU 恢复出厂顺序。模组侧在收到 D=0x01 后会自动发起 0xD8 协商;成功后密文补发 0xE8、0x02。
两种均在模组支持的类型列表 [0x01, 0x02] 中。AES-128-ECB 安全性更高,但需 MCU 具备 AES 实现且密文长度为 16 字节倍数;简单加密运算轻量、密文长度等于明文长度,适合资源紧张的 MCU。MCU 在固件中配置一种 EncType,在 Op 0x00 应答时返回(须被模组列表包含),后续全程使用该类型,不可中途切换。
需要。详见 模组重启识别。模组重启后会先发出明文查询帧 55 AA 00 01 00 00,MCU 置 handshake_done=0 并保留 enable 与 IndexList,随后:
0xD8 Op 0x01 下发新 Seed,MCU 在内存中重算 session_key。0xD8 Op 0x03 完成加密试探,成功后模组密文补发 0xE8/0x02。模组重启 不会 重新查询 IndexList(除非模组被重置、丢失索引)。MCU 须持久化保存 IndexList,保证密钥派生一致。
若 Flash 中 enable=1,MCU 上电后应恢复 index_list、enc_type。模组通过 0x00 握手指令识别到 MCU 发生重启后,会重新发起加密协商流程。 MCU 须等再次完成 D8 并将 handshake_done 置位;初始化配置仍须等密文 0x02。勿在协商完成前向已 enable=1 的模组发送非白名单明文。
MCU 在首次收到 Op 0x00 时生成 IndexList 并写入 Flash。此后只要模组侧索引有效(index_valid=1),就不会再查询。仅当模组被重置、Flash 索引丢失时,才会重新发起 Op 0x00,此时 MCU 应返回已保存的同一 IndexList(或重新生成并保证双方一致)。
固定白名单:0x00、0x01、0xD8、OTA 0xEA~0xEE。另:模组蓝牙 未绑定 时,明文重置 0x04/0x05 条件放行。其余如 0xE8、0x02、0x03 等在 enable=1 且协商成功后均为密文。enable=0 时全链路明文。
不能。 禁止加密降级。enable=1 后模组不因 MCU 去掉能力声明而恢复明文业务。若需回到明文产品形态,须走 恢复出厂 清除加密状态后再配。
出于安全考虑,一旦 enable=1,模组与 MCU 均拒绝非白名单明文业务帧。即使链路被干扰或 MCU 固件被替换为不支持加密的版本,也不能通过明文命令绕过加密。仅 恢复出厂 可清除加密状态。
必须先发加密重置指令(如 0x05),再清除本端 enable/IndexList 等状态与 Flash。若先清再发明文,在仍绑定场景下模组会丢弃该帧。
若模组蓝牙已未绑定,明文 0x04/0x05 可被模组接受。详见 MCU 恢复出厂顺序。
0x01/0x02 会不会和明文命令 0x01/0x02 混淆?数值会重合,须按收发规则区分,见 帧格式说明。要点:
55 AA 00 01 00 00(0x01 且 len=0)识别模组重启。EncType=0x02)与明文工作模式 0x02:0x02 不在白名单,协商成功后应为密文,一般不会双白名单冲突。下述伪代码用于辅助开发者理解密钥派生与业务帧加解密的完整流程。其中 MD5、AES 为标准算法,本文档不提供其实现 —— MCU 直接调用芯片平台的 硬件 AES 引擎,或移植成熟软件库(如 mbedTLS、tinyAES、micro-ecc 等),只要满足下述接口语义即可。
平台接口(开发者自行替换)
/* MD5:对 in 前 in_len 字节做摘要,输出 16 字节二进制 */
extern int platform_md5(const uint8_t *in, uint16_t in_len, uint8_t out[16]);
/* AES-128-ECB 单块(16 字节)加 / 解密 */
extern int platform_aes128_ecb_encrypt(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]);
extern int platform_aes128_ecb_decrypt(const uint8_t key[16], const uint8_t in[16], uint8_t out[16]);
派生流程(Seed + IndexList → session_key)
#define SEED_LEN 4
#define INDEX_LIST_LEN 16
#define CANDIDATE_KEY_LEN 32 /* MD5 转 hex 后长度 */
#define SESSION_KEY_LEN 16
/* 16 字节二进制 → 32 字节小写 hex ASCII */
static void bin_to_hex_lower(const uint8_t in[16], uint8_t hex[CANDIDATE_KEY_LEN])
{
static const char tab[] = "0123456789abcdef";
uint8_t i;
for (i = 0; i < 16; i++) {
hex[i * 2] = (uint8_t)tab[(in[i] >> 4) & 0x0F];
hex[i * 2 + 1] = (uint8_t)tab[in[i] & 0x0F];
}
}
/* Seed[4] + IndexList[16] → session_key[16]
* 流程:Seed → MD5 → 16B → 32B 小写hex(candidate_key)
* → 按 IndexList 每字节作下标取值 → session_key */
int derive_session_key(const uint8_t seed[SEED_LEN],
const uint8_t index_list[INDEX_LIST_LEN],
uint8_t session_key[SESSION_KEY_LEN])
{
uint8_t md5_out[16];
uint8_t candidate_key[CANDIDATE_KEY_LEN];
uint8_t i;
if (platform_md5(seed, SEED_LEN, md5_out) != 0) {
return -1; /* MD5 失败 */
}
bin_to_hex_lower(md5_out, candidate_key);
for (i = 0; i < SESSION_KEY_LEN; i++) {
if (index_list[i] >= CANDIDATE_KEY_LEN) {
return -1; /* 索引非法,应为 0x00~0x1F */
}
session_key[i] = candidate_key[index_list[i]];
}
return 0;
}
#define AES_BLOCK_LEN 16
#define ENC_AES 0x01
#define ENC_SIMPLE 0x02
#define UART_CRYPTO_MAX_INNER_LEN 64 /* 视最大业务帧而定,需 >= padded */
/* 内层 [CMD|Len|Payload] → 密文 */
static int crypt_inner(uint8_t enc_type, const uint8_t key[16],
const uint8_t *inner, uint16_t inner_len,
uint8_t *out, uint16_t *out_len, int encrypt)
{
uint16_t padded = ((inner_len + 15) / 16) * 16;
uint16_t off;
if (enc_type == ENC_AES) {
uint8_t buf[UART_CRYPTO_MAX_INNER_LEN];
if (padded > sizeof(buf)) {
return -1;
}
memcpy(buf, inner, inner_len);
memset(buf + inner_len, 0, padded - inner_len);
for (off = 0; off < padded; off += AES_BLOCK_LEN) {
if (encrypt) {
platform_aes128_ecb_encrypt(key, &buf[off], &out[off]);
} else {
platform_aes128_ecb_decrypt(key, &buf[off], &out[off]);
}
}
*out_len = padded;
} else {
for (off = 0; off < inner_len; off++) {
out[off] = (uint8_t)(~(inner[off] ^ key[off % 16]));
}
*out_len = inner_len;
}
return 0;
}
/* 发送:内层加密并组装外层帧 55 AA 00 EncType EncLen Cipher Sum */
int wrap_encrypt_send(uint8_t enc_type, const uint8_t session_key[16],
uint8_t cmd, const uint8_t *payload, uint16_t len,
uint8_t *frame_out, uint16_t *frame_len)
{
uint8_t inner[UART_CRYPTO_MAX_INNER_LEN];
uint8_t cipher[UART_CRYPTO_MAX_INNER_LEN];
uint16_t inner_len = (uint16_t)(3 + len);
uint16_t cipher_len = 0;
uint16_t pos = 0;
inner[0] = cmd;
inner[1] = (uint8_t)(len >> 8);
inner[2] = (uint8_t)(len);
memcpy(&inner[3], payload, len);
if (crypt_inner(enc_type, session_key, inner, inner_len,
cipher, &cipher_len, 1) != 0) {
return -1;
}
frame_out[pos++] = 0x55;
frame_out[pos++] = 0xAA;
frame_out[pos++] = 0x00;
frame_out[pos++] = enc_type;
frame_out[pos++] = (uint8_t)(cipher_len >> 8);
frame_out[pos++] = (uint8_t)(cipher_len);
memcpy(&frame_out[pos], cipher, cipher_len);
pos = (uint16_t)(pos + cipher_len);
frame_out[pos++] = checksum8(frame_out, pos); /* 按平台校验算法实现 */
*frame_len = pos;
return 0;
}
/* 接收:byte3==EncType 时解密,还原明文帧 55 AA 00 CMD Len Payload Sum */
int unwrap_decrypt_recv(uint8_t enc_type, const uint8_t session_key[16],
const uint8_t *frame_in, uint16_t frame_len,
uint8_t *frame_out, uint16_t *frame_out_len)
{
uint16_t enc_len;
uint8_t plain[UART_CRYPTO_MAX_INNER_LEN];
uint16_t plain_len = 0;
uint8_t inner_cmd;
uint16_t payload_len;
uint16_t pos = 0;
if (frame_in[3] != enc_type) {
return -1; /* 非密文帧,交白名单或丢弃逻辑处理 */
}
enc_len = (uint16_t)((frame_in[4] << 8) | frame_in[5]);
if (crypt_inner(enc_type, session_key, &frame_in[6], enc_len,
plain, &plain_len, 0) != 0) {
return -1;
}
inner_cmd = plain[0];
payload_len = (uint16_t)((plain[1] << 8) | plain[2]);
if ((uint16_t)(3 + payload_len) > plain_len) {
return -1;
}
frame_out[pos++] = 0x55;
frame_out[pos++] = 0xAA;
frame_out[pos++] = 0x00;
frame_out[pos++] = inner_cmd;
frame_out[pos++] = plain[1];
frame_out[pos++] = plain[2];
memcpy(&frame_out[pos], &plain[3], payload_len);
pos = (uint16_t)(pos + payload_len);
frame_out[pos++] = checksum8(frame_out, pos);
*frame_out_len = pos;
return 0;
}
该内容对您有帮助吗?
是意见反馈该内容对您有帮助吗?
是意见反馈