那些年我们踩过的坑——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不填充。 -
- 因为不填充,输出长度
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 当成一道裸的分组运算——给它对齐的明文,它给你对齐的密文,其他一切你来兜底。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论