算法选择:AES vs. SM4,哪些坑让我重写了一半代码
很多项目在启动前都会问一个问题:用国际标准还是国密标准?说实话,一开始我也觉得这不就是个对称加密算法嘛,换一个实现不就行了。但实际做下来才发现,这个决定能让你后面改到怀疑人生。
简单说说这两个算法的背景。AES是美国标准,全球通用。SM4是中国国密标准,2012年发布。两者都是128位分组密码,区别就在轮函数、密钥编排和S盒设计上。但注意——这些差异在纯软件层还好,一旦涉及芯片、网络协议、密文格式,坑就出来了。
我个人的习惯是:项目启动前,先看业务系统里有没有已经固化了的加密协议(比如TLS、IPsec),再看数据跨不跨境。如果只是内部系统,我用SM4;如果是跨平台交互,我倾向AES。千万别中途换算法,代价太大。
为什么会差点重写代码?
我之前参与过一个金融级别的字段级加密中间件。技术选型的时候,老板说“优先用国密”,好,我们选了SM4。但问题出在:这个中间件需要跟一个老旧的C++网关对接——那个网关只支持AES-256-GCM。
你想想看,SM4的密钥长度只有128位,而AES-256需要256位密钥。这两个算法的密钥编排方式、加解密模式、IV处理细节都是不同的。我一开始天真地以为做个密钥映射转换就行——结果测试阶段发现,两边对同一段数据的解密结果完全不对。
为什么会这样?因为SM4的ECB模式下,同一组明文加密结果固定;但AES在不同密钥长度下,密钥扩展算法不同,S盒不同,轮数也不同。简单说,这两个密码算法不是“换参数”就能兼容的。
| 对比维度 | AES-256 | SM4 |
|---|---|---|
| 密钥长度 | 128/192/256 bits | 128 bits(固定) |
| 分组大小 | 128 bits | 128 bits |
| 轮数 | 14轮(256位时) | 32轮(固定) |
| S盒大小 | 8x8 | 8x8(不同代数构造) |
| 密钥编排 | Rijndael密钥扩展 | 非线性反馈移位寄存器 |
| 国密支持度 | 无原生支持 | 国家推荐标准 |
看到上面这个表了吧?表面上都是对称加密,底层实现差异很大。我曾在项目中做过一个适配层,想把SM4加密的结果解密成AES能识别的格式——结果发现SM4的S盒替换和轮密钥生成逻辑跟AES完全不是一个套路,底层无法直接映射。最后我只能写一个“双重加密”机制,两边各自加解密,中间无密码转换。那一次,整整重写了一半代码。
第二个坑:状态语 (State Word) 处理
我刚接触SM4的时候,以为它跟AES一样内部用4x4的字节矩阵。结果看了SM4的算法规范才发现,它用的是4个32位字组成的内部状态。这个区别会导致一个问题:加解密中间结果的端序处理。
在AES里,每个字节在状态矩阵里的位置是确定的。但在SM4里,轮函数F(x0, x1, x2, x3) 里,x0~x3是32位字,内部没有明确的字节序概念。如果你把SM4加密的结果直接认为是“大端字节序”,而系统里默认是小端(常见于x86架构),那么同一段明文在两种架构下加密出的密文就会不一样。
我曾经踩过这个坑:在一台ARM开发板上测试SM4加密结果,然后把密文发送到x86服务器解密——解密出来全是乱码。排查了两天才发现,SM4的规范里没有明确说字节序,但参考实现里默认用了大端风格。而AES的很多实现(尤其是GCM模式)是跟平台端序相关的。如果你的系统需要在不同架构间传递密文,一定在协议里写死端序转换规则。
算法库的API设计差异
很多人说“AES和SM4不就是换个算法名嘛?” 但实际用了就知道,两个算法的API设计差异很大。比如,OpenSSL里AES-GCM的初始化需要额外提供的IV(初始化向量)、AAD(附加认证数据)、tag长度。而SM4-GCM在国密标准里,IV长度、AAD处理方式、标签长度跟AES不太一样。
// AES-GCM 初始化示例(libcrypto)
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, key, iv);
// SM4-GCM 初始化示例(GmSSL / 国密库)
EVP_CIPHER_CTX *ctx_sm4 = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx_sm4, EVP_sm4_gcm(), NULL, key, iv);
// 注意:SM4的tag长度默认是16字节,AES有时默认12或16
看代码,函数名几乎一样。但你如果直接把AES的密钥和IV填进去,结果就不对。因为AES-256密钥是32字节,SM4密钥是16字节;AES-GCM的IV推荐长度是12字节,SM4-GCM的IV也是12字节,但有些库的默认实现里IV处理方式不同(比如是否需要高位补零)。
我的建议:如果你在同一个项目中同时用AES和SM4(比如兼容旧系统),一定要为每个算法建立独立的加解密上下文工厂函数。不要用同一套参数去初始化两个算法,否则出了bug很难定位。最好是封装一个 CipherManager 类,内部根据算法名分发到不同的底层实现。
性能与密钥管理的隐藏陷阱
从性能上看,SM4在纯软件上比AES要慢一些(因为轮数多),在有国密指令集的硬件上差距不明显。但在字段级加密场景,加密的对象是小字段(比如身份证号、银行卡号),底层性能差异可以忽略。不过密钥管理就不一样了——AES-256的密钥长度比SM4多一倍,对于用硬件安全模块(HSM)的场景,密钥的存储空间和轮换策略会有不同。
举个例子:你在硬件安全模块上建密钥时,SM4只能存128位的密钥对象,而AES可以存256位的。如果你的HSM不支持国密密钥类型,那就只能走软件库。程序里如果对密钥类型做了硬编码,换算法时就得重新设计密钥存储层。
避坑指南:我建议你这样规划
- 第一步:先明确密文数据最终流向。 只在本系统内部流转 → SM4;要出系统或跨平台 → AES-256,除非对方明确支持国密。
- 第二步:固定端序和IV长度。 不管用哪种算法,在项目文档里用一行字写死:“SM4/AES加密结果按大端序传输,IV固定12字节,AAD按协议头提取”。
- 第三步:不要信任“算法无关的加密接口”。 很多框架提供抽象加密层(比如Java的Cipher.getInstance),但你传入不同算法名,底层行为可能不一致——甚至抛异常。我建议你在代码里显式判断算法类型,分别调用不同的工厂方法。
- 第四步:密钥统一用字节数组,不要用字符串。 很多人在配置文件里把密钥写成base64字符串。但AES-256的密钥解码后是32字节,SM4的是16字节。一旦加解密方使用不同编码,结果出问题你根本不可能在日志里一眼看出来。
最后提醒一句:状态语和端序的坑,我曾经在项目上线前一夜被运维叫起来查问题,就是因为AES的实现按小端假定了密文结构,而SM4的库在某个旧版本里用了大端输出的密文。这种兼容性问题,比加密算法本身的数学实现更折磨人。所以,选算法的时候,不只是选密码,更要选好你对接的所有技术栈的“语言”。