加密通信
QXJ 采用 SM4-GCM 模式实现端到端数据加密,确保数据在传输和存储过程中的安全性。
保护范围
加密能力对应验收指标 FR_1_1,覆盖系统中两类典型链路:
| 链路 | 路径 | 保护内容 |
|---|---|---|
| 移动设备接入链路 | 鸿蒙平板(安全浏览器 APP)↔ DMZ 区观测需求申请服务 | 身份鉴别信息、观测需求申请及回复信息 |
| 主备站间链路 | 北京主站 MCS ↔ 乌兰察布灾备站 MCS | 任务时间表、调度令、公共配置参数 |
两条链路均以 SM2 完成身份认证与会话密钥协商、SM4-GCM 完成业务数据的加密与完整性保护: 前者是终端与服务端的 C/S 链路,后者可通过 PCIE 密码卡(GM/T 0018-2012 接口)调用硬件密码资源。
加密流程
SM4-GCM 模式
SM4-GCM 是 SM4 算法的 GCM(Galois/Counter Mode)模式,提供:
- 机密性:SM4 计数器加密保护数据内容
- 完整性 / 真实性:GCM 认证标签(Tag)覆盖密文与附加数据,防止篡改与伪造
- 语义安全:每帧随机 IV 保证同一明文每次产生不同密文,攻击者无法通过密文比对推断内容
注意
随机 IV 本身只提供语义安全,不等于抗重放:截获一帧合法密文的攻击者仍可能原样重发。 QXJ 的抗重放由专门的重放占位与帧序号机制保证,见下文重放防护。
重放防护
QXJ 在三个层面设防,均以 Redis 作为共享状态(多进程/多实例部署时同样有效):
1. 加密登录:密文指纹原子占位
加密登录入口对整个请求体计算 SHA256 指纹,以 SET NX(cache.add)原子占位, 消除「先查后写」窗口期的并发竞争:
auth:login_encrypted:used:{sha256(raw_body)}| 阶段 | 占位状态与 TTL | 设计意图 |
|---|---|---|
| 进入处理 | processing,90 秒 | 同一密文处理中不得重复进入业务逻辑 |
| 登录成功 | 转为冷却值,300 秒 | 成功帧在 5 分钟内重放一律拒绝(HTTP 403) |
| 业务失败 | 立即删除占位 | 允许客户端修正账密/验证码后用同一密文重试,不锁死正常用户 |
2. SM2 握手阶段:四包各自去重
握手四包(INIT/RESP/ACK/TOKEN)均携带时间戳与随机数,服务端按报文指纹做 processing 占位(随会话上下文,TTL 10 分钟);INIT 帧时间戳允许偏移 ±10 分钟, 过期帧直接丢弃。TOKEN 帧一次性消费,产出的设备会话密钥发布后旧 TOKEN 不可再用。
3. 帧序号 frame_seq
帧头偏移 6~7 字节为 16 位大端帧序号(frame_seq),通信双方各自维护、依次递增, 为业务帧提供序列连续性检测;密钥协商阶段序号不作约束(可置 0)。
设备身份与信封绑定
加密登录解密后,明文 JSON 中的 device_id 必须与帧头 sender(@8:44)完全一致, 否则返回 403。这一步防止「用 A 设备协商的合法信封投递 B 设备身份」的跨主体替换攻击: 信封密钥本身已证明发送方,明文侧不允许再切换设备。
此外,登录成功签发的 JWT 还通过内层 SM3-HMAC 与该设备的会话密钥绑定, 机制详见 访问控制 - 令牌与设备绑定。
enc_auth 字段与模式演进
帧头第 44 字节的「加密认证模式」(enc_auth)在设计文档(密钥协商协议 v8)中 本是一个两段式字段:
高 4 位 · 加密模式 低 4 位 · 认证校验模式
0x0 未加密 0x0 未签名
0x1 SM4-CBC 0x1 SM2 签名
0x2 SM4-ECB 0x2 SM4-CMAC
0x3 SM4-CTR2
3
4
5
- 该设计下机密性(分组模式)与完整性(签名/CMAC)要分别选择、分别计算;
- 工程落地时统一改为 SM4-GCM,标记值
0x40:GCM 在一次处理中同时输出 密文与 16B 认证标签,不再需要「加密模式 + 独立签名/CMAC」的组合,也避免了 CBC/ECB 自带的安全短板(ECB 无语义安全、CBC 需自行保证 IV 与填充); - 因此当前实现中握手帧 enc_auth=
0x00(明文,由显式 SA/SB 做密钥确认)、 加密完保帧 enc_auth=0x40(SM4-GCM,帧尾附 16B tag),不会再出现0x11/0x22等组合值。
帧格式
QXJ 的 SM4-GCM 信封 = 64B 标准帧头 + 密文 + 16B GCM tag, 整体作为 application/octet-stream 传输:
┌──────────────────────────────────────────────────────────┐
│ 64B 帧头 │
│ @0 version 1B / main 1B / sub 2B / total_len 2B │
│ @6 frame_seq 2B(大端帧序号) │
│ @8 sender 36B(ASCII UUID,上行 IDA / 下行 IDB) │
│ @44 enc_auth 1B(0x40 = SM4-GCM) │
│ @45 IV 16B(SM4-GCM 初始向量) │
│ @61 reserved 3B │
├──────────────────────────────────────────────────────────┤
│ Ciphertext(变长,SM4-GCM 密文) │
├──────────────────────────────────────────────────────────┤
│ GCM Tag(16B,完整性校验) │
└──────────────────────────────────────────────────────────┘2
3
4
5
6
7
8
9
10
11
12
13
登录上行帧要求 main=0x02 / sub=0x0001 / enc_auth=0x40; 登录下行帧 sub=0x8002(下行 = 最高位 1 | 功能码),sender 填服务器 ID_B。
密钥分层
QXJ 的密码材料分三层,长短期分离、用途单一:
| 层次 | 材料 | 产生 / 存储 | 生命周期 |
|---|---|---|---|
| 长期身份层 | 设备 / 服务器 SM2 密钥对 | 设备 SM2 私钥存系统加密目录;服务器密钥对由管理员管理,公钥通过二维码(active_qrcode)分发 | 长期有效,支持轮换 |
| 会话层 | SM2 握手 + KDF 派生的会话密钥(16B / 32B 两种长度) | 设备侧存关键资产(Asset);服务端只存 Redis | 握手上下文 10 分钟;发布后会话密钥 TTL 7 天 |
| 业务层 | SM4-GCM 的每帧随机 IV | 帧头携带,随帧生成 | 一次性,不重用 |
握手会话密钥不落业务表;设备侧重新握手、服务端 TTL 过期或密钥轮换都会使旧密钥失效。
实现示例
浏览器侧(ArkTS)
网页业务统一走 JSBridge 的 sm4GcmEncrypt/Decrypt(返回结构化 JSON, 密钥不出应用侧);原生侧由 KeyNegotiationUtil / QxjFrameUtil 完成封包。 JS 封装见 qxj-frontend-sdk。
Python 业务后端
高层接口自动按 dev_id 从 Redis 取会话密钥,不需要手工传 IV/key:
from qxj_backend_sdk import sm4_gcm_decrypt, sm4_gcm_encrypt
# 解密:cipher 为帧内密文(hex 或 bytes),dev_id 从 token/帧头得到
plaintext, code = sm4_gcm_decrypt(cipher, redis_client=rc,
as_hex=False, dev_id=device_id)
if code != 0:
... # 对照 CryptoErrorCode:-3 无密钥 / -5 tag 失败 / -4 信封错 ...
# 回加密
ciphertext, code = sm4_gcm_encrypt(dev_id, plain, redis_client=rc, is_hex=False)2
3
4
5
6
7
8
9
10
最佳实践
IV 与密钥管理
- 每帧使用随机 16B IV,由应用侧生成并放入帧头;
- 会话密钥(K_client)在浏览器侧存关键资产(Asset),不持久、卸载即清;
- Redis 中的设备会话密钥 TTL 7 天;密钥协商失败自动清理残留。