文章总结: 本文解析GM/T0018-2023标准中SDF_Encrypt函数不进行数据填充的设计原则,指出直接传递非对齐明文会导致数据丢失或错误。文章对比常见密码库的自动填充行为,详细说明调用者需自行实现PKCS#7填充的正确流程,并提供代码示例和自查要点,强调接口分层设计的合理性。 综合评分: 85 文章分类: 技术标准,安全开发,应用安全,代码审计,安全工具
那些年我们踩过的坑——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.
SDF_Encrypt不填充。 - 2. 因为不填充,输出长度
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. 加密前:传入的
uiDataLength是 16 的整数倍吗?如果不是,你做了填充吗? - 2. 加密后:你检查了返回值是否等于 0 吗?如果 SDF 因为长度不对齐而返回了错误码,你的代码能捕获吗?
- 3. 解密后:你按对应的填充方案去掉了填充字节吗?
puiEncDataLength输出值是否被正确使用?
如果任何一个答案是”不确定”,你大概率正在踩 第十一题 里的同一个坑。
结语
GM/T 0018 那句”此函数不对数据进行填充处理”只有十个字,但它是一条接口契约,不是一条免责声明。它划清了 SDF 的职责边界:加密归我,填充归你。你在写代码的时候,要把 SDF_Encrypt 当成一道裸的分组运算——给它对齐的明文,它给你对齐的密文,其他一切你来兜底。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:利刃信安 利刃信安 利刃信安《那些年我们踩过的坑——SDF_Encrypt 函数对数据填充处理方式》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论