simple_encryption.c
源文件路径:
common/simple_encryption.c所属模块:common(通用工具库) 代码行数:73
1. 文件职责
本文件实现 EMS 自有的轻量对称流式加解密算法:用一个 32 位「运行密钥」的低 8 位(实际取 key >> 8 的低 8 位)作为密钥流字节与明文异或,并在每个字节后用线性同余式结合刚产生的密文字节递推密钥,从而形成对「上一明文字节」自同步的字节流密码。它处在网络协议栈的数据域封装/解封装环节:发送侧 midware/ethernt/ethernet.c 在拼好 7 字节帧头与 JSON 数据域后调用 Encrypt 混淆数据域并重算 Modbus CRC;接收侧 net/server.c 在 CRC 校验通过后调用 decrypt 还原数据域,再交给 JSON 解析。仅当服务器配置为 NET_PRIVATE_ENCRYPTED(自建/测试服务器)时启用;正式云端走 TLS 通道时不走本算法。函数由调用方任务同步执行,无状态、无全局变量,因此可重入,但加密与解密双方必须严格同步(任一字节错位会导致其后全部字节错乱)。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
SIMPLE_KEY_C1 | 宏(来自头文件) | 26529,密钥递推乘数。奇数,使 x → x*C1 模 2^32 为双射 |
SIMPLE_KEY_C2 | 宏(来自头文件) | 87282,密钥递推加数 |
SIMPLE_KEY_CRYPTKEY | 宏(来自头文件) | 42529(0xA621),密钥初值。其第 8..15 位为 0xA6,故首字节的密钥流字节恒为 0xA6 |
key | 局部 int | 32 位运行密钥状态,加/解密各自从 SIMPLE_KEY_CRYPTKEY 起算 |
PrevBlock | 局部 char(仅 decrypt) | 保存当前密文字节,用于在就地解密时先读后写并参与密钥递推 |
本文件不含结构体、枚举、联合体、位域,也不使用任何全局变量;无静态数据。
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
Encrypt | 全局 | 对 len 字节做流式加密:dst[i] = src[i] ^ (key>>8),随后 key = (key + (signed char)dst[i]) * C1 + C2(第 29 行) |
decrypt | 全局 | 对 len 字节做流式解密:先存 PrevBlock = src[i],再 dst[i] = src[i] ^ (key>>8),随后用 PrevBlock 递推密钥(第 56 行) |
共 2 个全局函数,无 static 函数、无函数指针。两者互为逆运算。
4. 核心逻辑解析
本文件无状态机、无定时器、无 uCOS-II 原语,是纯字节循环。
4.1 加解密算法与公式
设明文 P[i]、密文 C[i]、运行密钥 K[i](int,32 位)、初值 K[0] = SIMPLE_KEY_CRYPTKEY = 42529 = 0x0000A621。
加密(第 38-42 行):
for i = 0 .. len-1:
C[i] = (char)( P[i] ^ (K[i] >> 8) ) // 密钥流字节 = (K[i]>>8) 的低 8 位 = K[i] 的第 8..15 位
K[i+1] = (K[i] + (signed char)C[i]) * 26529 + 87282 // 32 位有符号 int 运算
解密(第 66-71 行):
for i = 0 .. len-1:
Prev = C[i]
P[i] = (char)( C[i] ^ (K[i] >> 8) )
K[i+1] = (K[i] + (signed char)Prev) * 26529 + 87282
两式中的 (signed char) 强制转型是关键:无论编译器把裸 char 当作有符号还是无符号(ARM Compiler 默认裸 char 为无符号),递推中使用的都是密文字节的有符号值 [-128, 127],保证收发两端递推一致。K[i] >> 8 只取低 8 位参与异或,因此算术右移与逻辑右移在 i < 32 时结果相同,不构成平台差异。
算法性质:
- 自同步/链式:密钥流不仅依赖初始常量,还依赖每个已处理的密文字节,因此改动任意一个密文字节会破坏其后全部字节的解密结果(错误扩散无界)。这是一条重要的运维含义:该通道没有纠错能力,一次字节丢失/插入就会导致整帧后续 JSON 全乱,只能靠上层 CRC + 重传恢复。
- 周期:
K的递推含数据相关项,不是纯 LCG,无法给出简单周期分析;但因 32 位状态且移位只取 8 位,密钥空间远小于 AES。 - 就地安全:
Encrypt先读src[i]再写dst[i];decrypt先把src[i]存入PrevBlock再写dst[i]。因此src == dst时结果正确(工程内 3 处调用点全部就地调用)。
4.2 输入校验与边界
两个函数在开头都只做一项检查(第 35-36 行、第 63-64 行):
if (len < 0) return; // 不解密也不报错,静默返回
len == 0时循环 0 次,无操作。len > 0时不做任何上限检查,完全信任调用方给出的缓冲足够大。- 返回类型为
void,调用方无法从返回值判断是否成功。len < 0的静默返回意味着上层若误传负数(例如l_u32PackLen - 9在某些路径下变成负值再被隐式转成int),会得到「未解密的数据被当作已解密 JSON 解析」的结果,且没有任何日志。
4.3 协议帧格式与加密位置
EMS 与服务器之间的帧由 net/ems.c:333-350 的 fill_common_data 拼装,布局为:
| 偏移 | 长度 | 内容 | 是否被本算法加密 |
|---|---|---|---|
| 0 | 1 | 协议头 EMS | 否 |
| 1 | 1 | cmdType(REQUEST_COMM / RESPONSE_COMM) | 否 |
| 2 | 1 | instr 命令字 | 否 |
| 3-4 | 2 | 0x00 0x00 保留 | 否 |
| 5-6 | 2 | 数据域长度(大端) | 否 |
| 7 | N | 数据域(JSON 文本) | 是 |
| 7+N | 2 | Modbus CRC16(大端,覆盖 [0, 7+N)) | 否(加密后重算) |
加密区间即 [7, 总长-2),长度 = 总长 - 9,与调用式 msg->u32Len - 9 吻合。发送侧流程(ethernet.c:576-585、:791-800):
if (get_server_data_encryption_flag() == NET_PRIVATE_ENCRYPTED)判定通道加密;if ((msg->u32Len > 9) && (g_t_app_info.u8_app_connect_flag == FALSE))额外要求长度合法且未处于 APP 直连模式;Encrypt(&msg->pu8Info[7], &msg->pu8Info[7], msg->u32Len - 9);GetCrcCode_Modbus(msg->pu8Info, msg->u32Len - 2)重算 CRC 并写回最后两字节。
接收侧流程(server.c:6404-6418):先 GetCrcCode_Modbus 校验整帧,再判断 NET_PRIVATE_ENCRYPTED 且 l_u32PackLen > 9 且非 APP 直连,然后 decrypt 就地还原数据域,最后 strstr / JSON 解析。
4.4 超时重试与任务配合
本文件不参与超时/重试:加密后的帧交给以太网/Wi-Fi 发送任务,CRC 由对端校验,重传由上层(net/ems.c 的应答等待与重发计数、midware/ethernt/ethernet.c 的发送循环)负责。本文件没有自己的定时器、信号量或消息邮箱,调用即返回,耗时与 len 成正比(字节循环,无查表,约每字节十几次整数运算)。
5. 对外接口
本文件导出(供谁调用)
- 函数:
Encrypt(第 29 行)、decrypt(第 56 行)。两者均无static,符号全工程可见。 - 调用点(
grep -rn "\bEncrypt(\|\bdecrypt("):midware/ethernt/ethernet.c:581——有线通道发送前加密midware/ethernt/ethernet.c:796——无线通道发送前加密net/server.c:6414——接收帧 CRC 通过后解密
本文件调用的外部函数/变量
- 无任何函数调用、无全局变量读写。仅通过
#include "simple_encryption.h"(第 13 行)获得三个宏常量与自身原型。不依赖标准库、不依赖 uCOS-II。
6. 依赖关系
- 依赖:
common/simple_encryption.h(唯一 include,第 13 行)→system/sys/chip.h→gd32f4xx.h。本文件虽不需要任何硬件类型,但通过头文件间接把 GD32 寄存器定义带入编译单元。 - 被依赖:
midware/ethernt/ethernet.c(第 25 行 include)、net/server.c(第 45 行 include)、data/resume_data/flash/flash_data_store.c(第 26 行 include,未实际调用)。工程 Keil 目标user/EMS_3_0.uvprojx中包含simple_encryption.c,参与编译。
7. 备注与风险点
- 有符号整型溢出(UB):第 41、70 行的
(key + (signed char)dst[i]) * 26529 + 87282在数十个字节内即会超出int32_t范围。C 标准中有符号溢出是未定义行为,编译器有权据此优化(如假设不会溢出而消去某些分支)。依赖 ARM 的补码回绕行为在实践中「能用」,但换编译器/开高优化等级有理论风险;建议显式改为uint32_t运算。 len < 0静默返回,无返回值:函数返回void,上层无法区分「解密成功」与「因 len 非法而未解密」。若调用点算出负长度(l_u32PackLen - 9在l_u32PackLen < 9时),未解密的密文会被当作明文 JSON 解析,且无任何日志可查。虽然server.c:6412已用l_u32PackLen > 9做了前置保护,但保护在调用方而非被调方,属脆弱设计。- 无输出缓冲长度参数:
dst的容量完全由调用方保证,函数内部无任何越界防护;一旦len计算错误即静默踩内存。 - 密钥强度极低且全机共用:三个宏硬编码(
26529/87282/42529),密钥流只有 8 位有效状态参与异或,且递推是公开线性关系。这只能算「混淆/OTP 式扰码」,不构成密码学强度;对固件逆向者可秒解。用于自建/测试服务器尚可,若被误用于正式通道则是安全隐患。 - 错误扩散无界:由于密钥递推依赖前一个密文字节,任何一个字节的传输错误会导致从该字节起后续全部解密失败(而非只错一个字节)。与「逐字节独立加密」的直觉相反,这一点在协议设计文档中通常不会写明,排障时容易误判为服务器下发数据错误。
- 命名与实现风格倒退:加密函数首字母大写、解密函数全小写(
Encrypt/decrypt),无const修饰输入,无u8/s32等工程统一类型(用裸char/int),与common目录其余文件的风格不一致;(signed char)的关键转型缺少任何注释说明其必要性,后人「优化掉」这个转型就会立刻破坏收发一致性。 - 未使用的注释模板:函数头注释(第 18-28 行、第 45-55 行)保留了
@Brief、@Input、@Output、@Return全空白的模板,@Name写的是函数签名副本,属无效注释噪声。 - 有效密钥空间远小于 32 位:参与异或的只有
K[i]的第 8..15 位共 8 位,其余 24 位状态只影响后续字节的递推;即使分析者无法反推K[0],已知明文也能逐字节恢复全部密钥流字节。属算法层面的先天弱点,记录备查。