那个不填充的函数

admin 2026-09-03 04:51:07 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文通过作者亲身经历,揭示国密标准GM/T0018中SDF_Encrypt函数不做数据填充的特性。该函数是纯分组运算引擎,要求明文长度必须为16字节整数倍,填充责任在调用者。作者详细演示了PKCS#7填充实现方法,强调填充值等于补位个数,即使数据长度恰好为16的整数倍也需补一整块16字节,解密后需按约定去除填充。文章提醒开发者勿默认其行为与OpenSSL等库一致,具有很强实践指导价值。 综合评分: 88 文章分类: 实战经验,安全开发,数据安全


那个不填充的函数

原创

利刃信安 利刃信安

利刃信安

2026年8月31日 17:30 北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

那些年我们踩过的坑 · 其一

那个不填充的函数

GM/T 0018 里那句话只有十个字。我花了整整一年,才让它变得有意义。


一 · 那句话

GM/T 0018-2023 里,SDF_Encrypt 的功能说明只有一句话,短得像没写完。

“此函数不对数据进行填充处理。” 十个字。我没多想,把它当一条备注抄进代码。后来的几个月,我以为我读懂了它;再后来的某个下午,用户在电话那头,报出一条 35 字节的地址。从那一刻起,我才开始读这句话里剩下的大部分东西。

我那时做的是 SM4-CBC。分组 16 字节,两个分组恰好 32 字节,而我的测试数据,长度正好都在 32。我写:outlen = data_len。编译通过。测试通过。上线。一切正常。我甚至把这段代码写得相当干净:

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

我把“输出等于输入”当成了这条接口的全部。大多数人也是。可那句话其实传达了两条信息:第一,它不填充;第二,正因为不填充,输出才等于输入。我只读懂了第二条,把第一条留在了下一页。

二 · “不填充”

字面很好懂:明文必须是分组的整数倍,也就是 16 的倍数。但它的设计含义经常被忽略——纯分组运算引擎,不是加密工具箱。它只做一件事:把 SM4 套上 CBC、ECB、CTR,在密码卡上执行。你的明文要不要补齐、用什么方案补,它一概不知,也一概不管。

这恰恰是它和常见密码库的分野。OpenSSL 默认 PKCS#7 填充,Java 默认 PKCS#5(与 PKCS#7 等价),Python 的 cryptography 默认也是 PKCS#7。我们用惯自动填充,于是在脑子里养成一个关于“加密”的心智模型:丢一段任意长度的明文进去,拿一段密文出来,别的都不用我管。

SDF 不这样。它像一扇有洁癖的门:你给我 16 字节一组,我还你 16 字节一组。脏东西,留在门外面。

我那 35 字节的地址,就是门外的脏东西。35 除 16,余 3。差 13 个字节凑满一整块。谜底的形状几乎是自己掉出来的。

三 · 我自己填

答案不在接口里,在接口外面:填充归我,加密归它。35 + 13 = 48,十六的三倍,正好一次还清。值得被写成一行规矩的是,填充值不是固定不变的——它等于补位的个数。补 13 个,每一字节就写 13;哪怕明文已经是 16 的整数倍,按 PKCS#7 也得补一整块 16,不能补零个。

我在加密之前,先替它把这道算术做完:

补充值 = 补位个数;即使整除(如 32)也补一整块 16

0–1516–3132–3435–47 填充13

35 + 13 = 48,恰为 16 的整数倍

| | | — | | // 第一步:调用者自己先填充(PKCS#7) ULONG pad = 16 – (data_len % 16);   // 35 → 13 ULONG plen = data_len + pad;        // 35 → 48 BYTE *p = (BYTE *)malloc(plen); memcpy(p, data, data_len); memset(p + data_len, (BYTE)pad, pad); // 每字节 = 补的个数 // 第二步:此时长度已是 16 的整数倍 ULONG enc_len = plen; LONG ret = SDF_Encrypt(hSession, hKey, SGD_SM4_CBC,             IV, p, plen, enc_data, &enc_len); free(p); // ret == 0,enc_len == 48 |

四 · 收尾

密文回来了,事情只算做完一半。另外一半在对端:读密文最后一个字节,记下它等于 n,从头往前去掉末尾的 n 字节,还原的才是那 35 字节的地址。填充是约定,加密的双方都要遵守同一条。

所以,每逢我又要碰这接口,我会先问自己三个问题。

加密之前,我传进去的 uiDataLength 是 16 的整数倍吗? 加密之后,我核对过返回值是不是 0 吗? 解密之后,我有没有按当初的方案,去掉那层填充?

只要有一个答不上来,我大概率正在踩同一个坑。

五 · 那道门

GM/T 0018 那句话只有十个字,但它不是免责声明,是一道契约,一条边界。加密归我,填充归你。它在文档里划下这条线,剩下的,谁也没替你兜。后来我每次调用 SDF_Encrypt,都先把 16 除净,才敢把它递给那扇门。

规范从来没有亏待我。亏待我的是我自己的假设——我默认它和 OpenSSL 一样体贴,默认我永远不会收到第 33 字节。直到那通电话。那通电话之后,我在每一个分组边界上,都比从前多停了一拍。

我没有再犯过。


免责声明:

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

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

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

本文转载自:利刃信安 利刃信安 利刃信安《那个不填充的函数》

那个不填充的函数 网络安全文章

那个不填充的函数

文章总结: 本文通过作者亲身经历,揭示国密标准GM/T0018中SDF_Encrypt函数不做数据填充的特性。该函数是纯分组运算引擎,要求明文长度必须为16字节
评论:0   参与:  0