那些年我们踩过的坑——SDF_Encrypt函数对数据填充处理方式"

admin 2026-10-20 21:26:43 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

那些年我们踩过的坑——SDF\_Encrypt 函数对数据填充处理方式

原创

利刃信安
利刃信安

利刃信安

2026年6月4日 12:12
北京

在小说阅读器读本章

去阅读

那些年我们踩过的坑——SDF\_Encrypt 函数对数据填充处理方式

摘要:翻开 GM/T 0018-2023 国密 SDF 接口标准,SDF_Encrypt 的说明里有一句不起眼的话:\"此函数不对数据进行填充处理。\" 十个字,轻描淡写。但如果你漏看了这句话——或者看了却没意识到它的后果——你的 SM4-CBC 加密代码就会把非对齐的明文传给 SDF,违反\"明文必须是分组整数倍\"的前置条件:轻则返回错误码被你的代码忽略,重则尾字节数据在你眼皮底下消失。本文从接口规范原文出发,拆解 SDF_Encrypt 的设计逻辑,讲清楚如何正确地调用它来加密任意长度的明文。


一、一句话,两条信息

GM/T 0018-2023 对 SDF_Encrypt 的描述只有一句话和一张参数表:

功能:使用指定密钥句柄和 IV 对数据进行单包对称加密(不填充)。

关键参数:

参数 类型 方向 说明
pucData BYTE* [in] 明文数据缓冲区指针
uiDataLength ULONG [in] 明文数据长度(字节)
pucEncData BYTE* [out] 密文数据缓冲区指针
puiEncDataLength ULONG* [out] 密文数据长度(字节)

这句话传达了两条信息:

  1. 1. SDF_Encrypt 不填充。
    1. 因为不填充,输出长度 puiEncDataLength 等于输入长度 uiDataLength。

很多人只读到第二条——\"哦,输出等于输入,那我 outlen = data_len 就行了\"——然后写下:

int outlen = data_len;
SDF_Encrypt(hSession, hKey, SGD_SM4_CBC,
            IV, data, (ULONG)data_len,
            enc, (ULONG *)&outlen);

编译通过。测试数据恰好都是 32 字节(两个分组)。上线。一切正常。

直到有一天,用户输入了一条 35 字节的地址。


二、\"不填充\"到底是什么意思

\"此函数不对数据进行填充处理\"的字面含义很清楚,但它的设计含义经常被忽视:

SDF\_Encrypt 是一个纯分组运算引擎,不是加密工具箱。

它干的活只有一件:把分组密码算法(SM4)套上工作模式(CBC/ECB/CTR),在硬件密码卡上执行。至于你的明文是不是分组的整数倍、需不需要 PKCS#7 补齐——那是你的事。

对比一下常见的密码库:

库 SM4-CBC 行为
OpenSSL EVP 默认 PKCS#7 填充 ,自动处理
Java Cipher 默认 PKCS#5 填充 (等同于 PKCS#7)
Python cryptography 默认 PKCS#7 填充
SDF\_Encrypt(GM/T 0018) 不填充。明文必须是分组整数倍。

大多数开发者习惯了前三个库的\"自动填充\"行为,带着这个心智模型来写 SDF 代码,坑就埋下了。


三、那非整数倍明文怎么加密

答案是:调用者自己填,填完再调 SDF\_Encrypt。

以 data_len = 35(2 个完整分组 + 3 个零头字节)为例,正确流程:

1. PKCS#7 填充(调用者负责)
   ├── 35 % 16 = 3,需补齐 16 - 3 = 13 字节
   ├── 每字节填充值 = 13
   └── padded_len = 35 + 13 = 48 ✓(16 的整数倍)

2. SDF_Encrypt(硬件执行)
   ├── uiDataLength = 48
   ├── 返回 0(成功)
   └── *puiEncDataLength = 48

3. 解密后去填充(对端负责)
   └── 读最后一字节 0x0D → 去掉末尾 13 字节 → 还原 35 字节

伪代码实现:

// 第一步:调用者自己 PKCS#7 填充
ULONG pad = 16 - (data_len % 16);
// PKCS#7 规则:即使明文已是分组的整数倍,也必须补一个完整填充块
// 例如 data_len=32 → pad=16, padded_len=48;data_len=35 → pad=13, padded_len=48
ULONG padded_len = data_len + pad;

BYTE *padded_data = (BYTE *)malloc(padded_len);
memcpy(padded_data, data, data_len);
memset(padded_data + data_len, (BYTE)pad, pad);  // 填充字节值 = 填充数量

// 第二步:调用 SDF_Encrypt(此时长度已是分组的整数倍)
ULONG enc_len = padded_len;
LONG ret = SDF_Encrypt(hSession, hKey, SGD_SM4_CBC,
                       IV, padded_data, padded_len,
                       enc_data, &enc_len);

free(padded_data);

if (ret != 0) {
    // 处理错误
}
// enc_len == padded_len == 48

四、SDF 为什么要这样设计

这个设计不是缺陷,而是分层解耦:

┌──────────────────────────────────┐
│  业务层:决定填充方案             │
│  PKCS#7 / Zero-pad / ISO 10126  │
├──────────────────────────────────┤
│  SDF 层:纯分组运算              │
│  SM4-ECB / SM4-CBC / SM4-CTR    │
├──────────────────────────────────┤
│  硬件层:密码卡/密码机           │
│  密钥存储、抗侧信道、物理熵源    │
└──────────────────────────────────┘

填充属于数据格式层面的决策,不应该由密码硬件来做。密码卡不知道你的业务协议是用 PKCS#7 还是 ANSI X9.23 还是自定义的零填充;它只知道\"给我 16 字节一组的明文,我还你 16 字节一组的密文\"。

把填充留在调用者手里,反而给了最大的灵活性——代价是调用者必须意识到这个责任。


五、自查:你的 SDF\_Encrypt 调用对了吗

问自己三个问题:

  1. 1. 加密前:传入的 uiDataLength 是 16 的整数倍吗?如果不是,你做了填充吗?
  2. 2. 加密后:你检查了返回值是否等于 0 吗?如果 SDF 因为长度不对齐而返回了错误码,你的代码能捕获吗?
  3. 3. 解密后:你按对应的填充方案去掉了填充字节吗?puiEncDataLength 输出值是否被正确使用?

如果任何一个答案是\"不确定\",你大概率正在踩 第十一题 里的同一个坑。


结语

GM/T 0018 那句\"此函数不对数据进行填充处理\"只有十个字,但它是一条接口契约,不是一条免责声明。它划清了 SDF 的职责边界:加密归我,填充归你。你在写代码的时候,要把 SDF_Encrypt 当成一道裸的分组运算——给它对齐的明文,它给你对齐的密文,其他一切你来兜底。


《AI与系统原生双轮驱动下的移动全栈安全洞察》研究启动" 网络安全文章

《AI与系统原生双轮驱动下的移动全栈安全洞察》研究启动"

安全牛启动《AI与系统原生双轮驱动下的移动全栈安全洞察》研究,聚焦移动安全在系统原生重构与AI技术叠加下面临的新型威胁。研究涵盖底层架构、供应链、应用权限及数据流转四大领域,分析传统安全承压现状,并探讨防御体系向AI安全、微内核适配、AIAgent行为管控等方向的演进路径,旨在构建实战化的移动全栈安全防线。
上游为何不愿对外经营数据?控制降级、衍生数据与不确定性下的经营决策" 网络安全文章

上游为何不愿对外经营数据?控制降级、衍生数据与不确定性下的经营决策"

本文分析上游企业不愿对外经营数据的三重障碍:控制权降级风险、衍生数据归属不确定性及合规成本压力。核心发现表明数据持有者面临使用权与所有权分离的治理难题,需通过契约设计明确数据使用边界。建议建立分层授权机制并参考欧盟《数据法》等政策框架平衡数据流通与权益保护。
西城初一下学期的一道“新定义”" 网络安全文章

西城初一下学期的一道“新定义”"

该文档分析西城区初一下学期数学新定义题型,重点讲解点与点特征值的概念及其在图形特征值计算中的应用。题目涉及线段平移运动与正方形位置关系的动态分析,要求求解特征值d的取值范围。文档建议考生根据题目分值权衡解题时间,对复杂题型可采取选择性放弃策略。
评论:0   参与:  0