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

admin 2026-06-09 04:33:43 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析GM/T0018-2023标准中SDF_Encrypt函数不进行数据填充的设计原则,指出直接传递非对齐明文会导致数据丢失或错误。文章对比常见密码库的自动填充行为,详细说明调用者需自行实现PKCS#7填充的正确流程,并提供代码示例和自查要点,强调接口分层设计的合理性。 综合评分: 85 文章分类: 技术标准,安全开发,应用安全,代码审计,安全工具


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

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


结语

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


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:利刃信安 利刃信安 利刃信安《那些年我们踩过的坑——SDF_Encrypt 函数对数据填充处理方式》

评论:0   参与:  0