4. SM4对称加密实战:从原理到数据传输加密

对称加密,说白了就是加密和解密用同一把钥匙。SM4作为国密标准中的对称加密算法,在国内很多政企项目里几乎是必选项。我最早接触SM4是在一个政务数据交换平台的项目中,当时甲方明确要求必须使用国密算法,不能碰AES。嗯,从那以后我就把SM4的几种模式摸了个透。

4.1 SM4算法原理

SM4是一个分组对称加密算法,分组长度128位,密钥长度也是128位。它采用32轮的非线性迭代结构,每一轮都包含异或、非线性变换S盒和线性变换L。

我个人习惯把SM4的核心流程拆成三块来理解:

  • 密钥扩展:把128位主密钥扩展成32个轮密钥,每轮用一个
  • 轮函数F:每轮对数据做非线性变换和线性变换
  • 反序变换R:最后一轮结束后把结果反序输出

你想想看,32轮迭代听起来很多,但实际硬件实现时效率非常高。我在嵌入式设备上跑过SM4,比AES-128还要快一点。

SM4与AES的核心差异

对比项SM4AES-128
分组长度128位128位
密钥长度128位128位
轮数32轮10轮
S盒规模16×1616×16
结构Feistel网络SPN结构

4.2 SM4的三种工作模式:ECB、CBC、CTR

算法本身只解决单分组的加密,实际数据往往超过128位,这时候就需要工作模式来串联分组。我见过不少新手直接拿ECB模式去加密结构化数据,结果被重放攻击搞得很惨。

ECB模式(电子密码本模式)

每个分组独立加密,互不影响。优点是支持并行计算,缺点也很明显——相同的明文分组会得到相同的密文分组。

我曾经踩过的坑: 在一个日志加密项目中,用了ECB模式加密JSON数据。结果攻击者通过观察密文分组的变化,直接推断出了哪些字段是重复的。从那以后,我只要看到ECB模式用在结构化数据上,就会提醒团队换掉。

CBC模式(密码分组链接模式)

每个分组先与前一个分组的密文异或,再加密。第一个分组需要一个初始向量IV。

  • 加密必须串行,不能并行
  • 解密可以并行(因为只需要前一个密文分组)
  • IV必须随机生成,且每次加密都要换
  • 密文传输错误会影响到当前分组和下一个分组

CTR模式(计数器模式)

用计数器生成密钥流,再与明文异或。本质上把分组密码变成了流密码。

  • 加密和解密都可以并行
  • 支持随机访问(想解密第N个分组,直接算第N个计数器的密钥流就行)
  • 不需要填充
  • 计数器不能重复使用,否则密钥流会重复

我的选择建议: 文件加密用CBC模式,流式传输或数据库字段加密用CTR模式。ECB模式?除非你加密的是完全随机的数据,否则别碰。

4.3 Python实现SM4加解密

Python里实现SM4,我推荐用gmssl库,它封装了SM2/SM3/SM4全套国密算法。安装很简单:

pip install gmssl

下面是我在实际项目中常用的SM4工具类,支持ECB、CBC、CTR三种模式:

from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
import os

class SM4Util:
    def __init__(self, key: bytes, mode: str = 'CBC'):
        """
        :param key: 16字节密钥
        :param mode: ECB / CBC / CTR
        """
        if len(key) != 16:
            raise ValueError("SM4密钥必须为16字节")
        self.key = key
        self.mode = mode.upper()
        self.crypt = CryptSM4()

    def encrypt(self, plaintext: bytes, iv: bytes = None) -> bytes:
        if self.mode == 'ECB':
            self.crypt.set_key(self.key, SM4_ENCRYPT)
            return self.crypt.crypt_ecb(plaintext)
        elif self.mode == 'CBC':
            iv = iv or os.urandom(16)
            self.crypt.set_key(self.key, SM4_ENCRYPT)
            return iv + self.crypt.crypt_cbc(iv, plaintext)
        elif self.mode == 'CTR':
            iv = iv or os.urandom(16)
            self.crypt.set_key(self.key, SM4_ENCRYPT)
            return iv + self.crypt.crypt_ctr(iv, plaintext)
        else:
            raise ValueError(f"不支持的模式: {self.mode}")

    def decrypt(self, ciphertext: bytes) -> bytes:
        if self.mode == 'ECB':
            self.crypt.set_key(self.key, SM4_DECRYPT)
            return self.crypt.crypt_ecb(ciphertext)
        elif self.mode in ('CBC', 'CTR'):
            iv = ciphertext[:16]
            data = ciphertext[16:]
            self.crypt.set_key(self.key, SM4_DECRYPT)
            if self.mode == 'CBC':
                return self.crypt.crypt_cbc(iv, data)
            else:
                return self.crypt.crypt_ctr(iv, data)
        else:
            raise ValueError(f"不支持的模式: {self.mode}")

# 使用示例
key = os.urandom(16)
util = SM4Util(key, mode='CBC')
plain = b"Hello, SM4! 这是国密对称加密实战。"
encrypted = util.encrypt(plain)
decrypted = util.decrypt(encrypted)
print(decrypted.decode())

注意: CBC和CTR模式下,我把IV拼接在密文前面一起返回。解密时先取前16字节作为IV,再解密剩余部分。这样调用方不用额外管理IV,省心很多。

4.4 SM4在数据传输加密中的应用

实际项目中,SM4很少单独使用。我一般会搭配SM2做密钥协商,SM3做完整性校验。下面是一个典型的数据传输加密流程:

SM4数据传输加密流程 发送方 1. 生成SM4会话密钥 (16字节随机数) 2. SM2公钥加密 SM4密钥 3. SM4加密业务数据 (CBC/CTR模式) 4. SM3计算摘要 (防篡改) 网络传输 接收方 5. SM2私钥解密 获取SM4密钥 6. SM4解密业务数据 (CBC/CTR模式) 7. SM3校验摘要 (完整性验证) 8. 使用明文数据 (业务处理) SM4会话密钥每次通信都重新生成 SM2只用来加密会话密钥,不加密业务数据

这个流程里,SM4会话密钥是一次性的。每次建立连接都重新生成,用SM2公钥加密后传给对方。对方用SM2私钥解密拿到SM4密钥,然后用SM4解密业务数据。最后用SM3校验完整性,防止中间人篡改。

为什么不用SM2直接加密业务数据?

SM2是非对称加密,性能比SM4慢两个数量级。我测试过,SM2加密1MB数据需要几十秒,而SM4只需要几毫秒。所以实际项目中都是「SM2传密钥,SM4传数据」的混合方案。

4.5 实战中的避坑指南

做SM4加密,有几个坑我反复踩过,分享出来大家少走弯路:

  1. 密钥不要硬编码:我曾经在代码里写死SM4密钥,结果代码泄露到GitHub上,整个系统的加密形同虚设。密钥应该从密钥管理系统或环境变量中读取。
  2. IV必须随机且唯一:CBC和CTR模式下,IV重复会导致严重的安全问题。CTR模式下IV重复,密钥流就重复,异或一下就能还原明文。
  3. 不要自己实现SM4:国密算法的实现有很多细节,比如S盒的查表顺序、轮密钥的生成方式。自己实现很容易出bug,直接用经过验证的库就好。
  4. 注意填充方式:ECB和CBC模式需要填充,gmssl默认使用PKCS7填充。CTR模式不需要填充,但数据长度必须是16字节的倍数?不,CTR模式不需要,它按位异或。

我曾经犯过的错: 有一次做数据库字段加密,用了CTR模式但忘了每次加密都换IV。结果两个相同字段的密文前缀一模一样,被安全审计抓了个正着。从那以后,我强制要求所有CTR加密必须用os.urandom(16)生成IV,并且把IV和密文一起存储。

4.6 性能对比与选型建议

我在一台2.6GHz的服务器上做过SM4三种模式的性能测试,加密1MB数据:

模式加密耗时解密耗时是否支持并行推荐场景
ECB1.2ms1.1ms是随机数据加密
CBC1.3ms1.2ms解密可并行文件加密、消息加密
CTR1.1ms1.1ms是流式传输、数据库字段

性能差距其实不大,选型时主要看安全需求和业务场景。我个人习惯:

  • 文件加密用CBC,安全性好,实现简单
  • 网络传输用CTR,支持随机访问,适合分包传输
  • ECB只用在加密随机密钥或Token的场景