4. SM4算法Python实现

SM4 是中国国家密码管理局发布的商用分组密码标准(GB/T 32907-2016),采用 128 位密钥、128 位分组、32 轮非线性迭代结构。本章从底层 S 盒出发,逐步构建密钥扩展、轮函数、加解密核心,并实现 ECB 与 CBC 两种工作模式,最后通过单元测试验证实现的正确性。

4.1 SM4 算法整体架构

SM4 的加密过程可概括为:初始明文分组 → 32 轮迭代 → 反序变换 → 密文输出。每轮使用一个 32 位轮密钥,轮函数包含合成置换 T,由非线性变换 τ(S 盒)和线性变换 L 组成。

SM4 加密流程架构 128 位明文分组 32 轮迭代 (X_i, X_{i+1}, X_{i+2}, X_{i+3}) 每轮:轮函数 F + 轮密钥 rk_i 反序变换 R 128 位密文分组 密钥扩展 128 位密钥 → 32 个轮密钥 轮函数 F F(X0,X1,X2,X3,rk) = X0 ⊕ T(X1⊕X2⊕X3⊕rk) T = τ ∘ L (合成置换) 核心组件 S 盒 (16×16 查找表) → 非线性变换 τ → 线性变换 L (32 位循环移位与异或) 密钥扩展使用与轮函数相同的 S 盒,但线性变换不同 (L')

4.2 S 盒实现

SM4 的 S 盒是一个 8×8 比特的非线性置换,输入 8 位,输出 8 位。官方标准以 16×16 表格形式给出,行索引为高 4 位,列索引为低 4 位。

组件输入输出实现方式
S 盒8 位 (0x00~0xFF)8 位 (0x00~0xFF)预计算 256 字节列表,直接查表
τ 变换32 位 (4 字节)32 位 (4 字节)4 个 S 盒并行查表
L 变换32 位32 位循环左移与异或组合
# SM4 S 盒 (16×16 表格展开为 256 字节列表)
SM4_SBOX = [
    0xD6,0x90,0xE9,0xFE,0xCC,0xE1,0x3D,0xB7,0x16,0xB6,0x14,0xC2,0x28,0xFB,0x2C,0x05,
    0x2B,0x67,0x9A,0x76,0x2A,0xBE,0x04,0xC3,0xAA,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
    0x9C,0x42,0x50,0xF4,0x91,0xEF,0x98,0x7A,0x33,0x54,0x0B,0x43,0xED,0xCF,0xAC,0x62,
    0xE4,0xB3,0x1C,0xA9,0xC9,0x08,0xE8,0x95,0x80,0xDF,0x94,0xFA,0x75,0x8F,0x3F,0xA6,
    0x47,0x07,0xA7,0xFC,0xF3,0x73,0x17,0xBA,0x83,0x59,0x3C,0x19,0xE6,0x85,0x4F,0xA8,
    0x68,0x6B,0x81,0xB2,0x71,0x64,0xDA,0x8B,0xF8,0xEB,0x0F,0x4B,0x70,0x56,0x9D,0x35,
    0x1E,0x24,0x0E,0x5E,0x63,0x58,0xD1,0xA2,0x25,0x22,0x7C,0x3B,0x01,0x21,0x78,0x87,
    0xD4,0x00,0x46,0x57,0x9F,0xD3,0x27,0x52,0x4C,0x36,0x02,0xE7,0xA0,0xC4,0xC8,0x9E,
    0xEA,0xBF,0x8A,0xD2,0x40,0xC7,0x38,0xB5,0xA3,0xF7,0xF2,0xCE,0xF9,0x61,0x15,0xA1,
    0xE0,0xAE,0x5D,0xA4,0x9B,0x34,0x1A,0x55,0xAD,0x93,0x32,0x30,0xF5,0x8C,0xB1,0xE3,
    0x1D,0xF6,0xE2,0x2E,0x82,0x66,0xCA,0x60,0xC0,0x29,0x23,0xAB,0x0D,0x53,0x4E,0x6F,
    0xD5,0xDB,0x37,0x45,0xDE,0xFD,0x8E,0x2F,0x03,0xFF,0x6A,0x72,0x6D,0x6C,0x5B,0x51,
    0x8D,0x1B,0xAF,0x92,0xBB,0xDD,0xBC,0x7F,0x11,0xD9,0x5C,0x41,0x1F,0x10,0x5A,0xD8,
    0x0A,0xC1,0x31,0x88,0xA5,0xCD,0x7B,0xBD,0x2D,0x74,0xD0,0x12,0xB8,0xE5,0xB4,0xB0,
    0x89,0x69,0x97,0x4A,0x0C,0x96,0x77,0x7E,0x65,0xB9,0xF1,0x09,0xC5,0x6E,0xC6,0x84,
    0x18,0xF0,0x7D,0xEC,0x3A,0xDC,0x4D,0x20,0x79,0xEE,0x5F,0x3E,0xD7,0xCB,0x39,0x48,
]

def sm4_sbox(input_byte: int) -> int:
    """S 盒查表:输入 8 位,输出 8 位"""
    return SM4_SBOX[input_byte & 0xFF]

def sm4_tau(input_32: int) -> int:
    """非线性变换 τ:将 32 位拆为 4 字节分别查 S 盒"""
    b0 = sm4_sbox((input_32 >> 24) & 0xFF)
    b1 = sm4_sbox((input_32 >> 16) & 0xFF)
    b2 = sm4_sbox((input_32 >> 8) & 0xFF)
    b3 = sm4_sbox(input_32 & 0xFF)
    return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3

4.3 线性变换 L 与合成置换 T

线性变换 L 作用于 τ 的输出,通过循环左移与异或实现扩散。加密轮函数与密钥扩展使用不同的 L 变换。

变换公式用途
L (加密轮)L(B) = B ⊕ (B <<< 2) ⊕ (B <<< 10) ⊕ (B <<< 18) ⊕ (B <<< 24)轮函数中的线性扩散
L' (密钥扩展)L'(B) = B ⊕ (B <<< 13) ⊕ (B <<< 23)密钥扩展中的线性扩散
def rotate_left(x: int, n: int, bits=32) -> int:
    """32 位循环左移 n 位"""
    return ((x << n) | (x >> (bits - n))) & 0xFFFFFFFF

def sm4_L(B: int) -> int:
    """加密轮函数中的线性变换 L"""
    return B ^ rotate_left(B, 2) ^ rotate_left(B, 10) ^ rotate_left(B, 18) ^ rotate_left(B, 24)

def sm4_L_prime(B: int) -> int:
    """密钥扩展中的线性变换 L'"""
    return B ^ rotate_left(B, 13) ^ rotate_left(B, 23)

def sm4_T(input_32: int) -> int:
    """合成置换 T = L(τ(input))"""
    return sm4_L(sm4_tau(input_32))

4.4 密钥扩展

SM4 使用 128 位密钥生成 32 个 32 位轮密钥。系统参数 FK 用于初始白化,固定参数 CK 用于每轮迭代。

# 系统参数 FK
FK = [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC]

# 固定参数 CK (共 32 个)
CK = [
    0x00070E15, 0x1C232A31, 0x383F464D, 0x545B6269,
    0x70777E85, 0x8C939AA1, 0xA8AFB6BD, 0xC4CBD2D9,
    0xE0E7EEF5, 0xFC030A11, 0x181F262D, 0x343B4249,
    0x50575E65, 0x6C737A81, 0x888F969D, 0xA4ABB2B9,
    0xC0C7CED5, 0xDCE3EAF1, 0xF8FF060D, 0x141B2229,
    0x30373E45, 0x4C535A61, 0x686F767D, 0x848B9299,
    0xA0A7AEB5, 0xBCC3CAD1, 0xD8DFE6ED, 0xF4FB0209,
    0x10171E25, 0x2C333A41, 0x484F565D, 0x646B7279,
]

def sm4_key_expansion(master_key: bytes) -> list:
    """SM4 密钥扩展:输入 16 字节主密钥,输出 32 个轮密钥"""
    K = [0] * 36
    # 将 16 字节密钥拆为 4 个 32 位字,与 FK 异或
    for i in range(4):
        K[i] = int.from_bytes(master_key[i*4:(i+1)*4], 'big') ^ FK[i]

    rk = [0] * 32
    for i in range(32):
        tmp = K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i]
        K[i+4] = K[i] ^ sm4_L_prime(sm4_tau(tmp))
        rk[i] = K[i+4]
    return rk

4.5 轮函数与加解密核心

SM4 的加解密结构相同,仅轮密钥使用顺序相反:加密使用 rk[0..31],解密使用 rk[31..0]。

def sm4_one_round(X: list, rk: int) -> int:
    """单轮轮函数 F(X0,X1,X2,X3,rk) = X0 ⊕ T(X1⊕X2⊕X3⊕rk)"""
    return X[0] ^ sm4_T(X[1] ^ X[2] ^ X[3] ^ rk)

def sm4_encrypt_block(plaintext: bytes, rk: list) -> bytes:
    """SM4 加密一个 16 字节分组"""
    X = [0] * 36
    for i in range(4):
        X[i] = int.from_bytes(plaintext[i*4:(i+1)*4], 'big')

    for i in range(32):
        X[i+4] = sm4_one_round(X[i:i+4], rk[i])

    # 反序变换:最后四轮输出 (X35, X34, X33, X32)
    ciphertext = b''
    for i in range(4):
        ciphertext += X[35 - i].to_bytes(4, 'big')
    return ciphertext

def sm4_decrypt_block(ciphertext: bytes, rk: list) -> bytes:
    """SM4 解密一个 16 字节分组:使用逆序轮密钥"""
    return sm4_encrypt_block(ciphertext, rk[::-1])

4.6 ECB 与 CBC 模式实现

ECB 模式直接对每个分组独立加密;CBC 模式引入前一个密文分组与当前明文异或,需要初始向量 IV。

模式加密公式解密公式特点
ECBC_i = E(P_i)P_i = D(C_i)并行、无错误传播、模式不安全
CBCC_i = E(P_i ⊕ C_{i-1})P_i = D(C_i) ⊕ C_{i-1}串行、错误传播、需 IV
def pkcs7_pad(data: bytes, block_size=16) -> bytes:
    """PKCS#7 填充"""
    pad_len = block_size - (len(data) % block_size)
    return data + bytes([pad_len] * pad_len)

def pkcs7_unpad(data: bytes) -> bytes:
    """去除 PKCS#7 填充"""
    pad_len = data[-1]
    if pad_len < 1 or pad_len > 16:
        raise ValueError("Invalid padding")
    return data[:-pad_len]

def sm4_ecb_encrypt(plaintext: bytes, rk: list) -> bytes:
    """SM4-ECB 加密"""
    padded = pkcs7_pad(plaintext)
    ciphertext = b''
    for i in range(0, len(padded), 16):
        ciphertext += sm4_encrypt_block(padded[i:i+16], rk)
    return ciphertext

def sm4_ecb_decrypt(ciphertext: bytes, rk: list) -> bytes:
    """SM4-ECB 解密"""
    plaintext = b''
    for i in range(0, len(ciphertext), 16):
        plaintext += sm4_decrypt_block(ciphertext[i:i+16], rk)
    return pkcs7_unpad(plaintext)

def sm4_cbc_encrypt(plaintext: bytes, rk: list, iv: bytes) -> bytes:
    """SM4-CBC 加密"""
    padded = pkcs7_pad(plaintext)
    ciphertext = b''
    prev = iv
    for i in range(0, len(padded), 16):
        block = bytes(a ^ b for a, b in zip(padded[i:i+16], prev))
        enc_block = sm4_encrypt_block(block, rk)
        ciphertext += enc_block
        prev = enc_block
    return ciphertext

def sm4_cbc_decrypt(ciphertext: bytes, rk: list, iv: bytes) -> bytes:
    """SM4-CBC 解密"""
    plaintext = b''
    prev = iv
    for i in range(0, len(ciphertext), 16):
        dec_block = sm4_decrypt_block(ciphertext[i:i+16], rk)
        block = bytes(a ^ b for a, b in zip(dec_block, prev))
        plaintext += block
        prev = ciphertext[i:i+16]
    return pkcs7_unpad(plaintext)

4.7 单元测试与验证

使用标准测试向量验证实现的正确性。SM4 官方标准提供了已知明文、密钥和密文的对应关系。

测试策略:

  • S 盒正确性:验证 sm4_sbox(0x00) == 0xD6,sm4_sbox(0xFF) == 0x48
  • 密钥扩展:使用标准密钥验证前 4 个轮密钥
  • 加解密一致性:随机明文加密再解密应恢复原文
  • ECB/CBC 模式:验证多分组加解密正确性
import unittest

class TestSM4(unittest.TestCase):
    def test_sbox(self):
        self.assertEqual(sm4_sbox(0x00), 0xD6)
        self.assertEqual(sm4_sbox(0xFF), 0x48)

    def test_key_expansion(self):
        key = bytes.fromhex('0123456789ABCDEFFEDCBA9876543210')
        rk = sm4_key_expansion(key)
        # 标准前 4 个轮密钥 (示例值)
        expected_first = 0xF12186F9
        self.assertEqual(rk[0], expected_first)

    def test_encrypt_decrypt_consistency(self):
        key = bytes.fromhex('0123456789ABCDEFFEDCBA9876543210')
        rk = sm4_key_expansion(key)
        plaintext = b'Hello, SM4!'
        ct = sm4_ecb_encrypt(plaintext, rk)
        pt = sm4_ecb_decrypt(ct, rk)
        self.assertEqual(plaintext, pt)

    def test_cbc_mode(self):
        key = bytes.fromhex('0123456789ABCDEFFEDCBA9876543210')
        iv = bytes.fromhex('00000000000000000000000000000000')
        rk = sm4_key_expansion(key)
        plaintext = b'CBC mode test with SM4'
        ct = sm4_cbc_encrypt(plaintext, rk, iv)
        pt = sm4_cbc_decrypt(ct, rk, iv)
        self.assertEqual(plaintext, pt)

if __name__ == '__main__':
    unittest.main()

实现要点:

  • S 盒查表时务必使用 & 0xFF 确保索引范围正确
  • 循环左移使用 ((x << n) | (x >> (32-n))) & 0xFFFFFFFF
  • 解密时直接复用加密函数,仅将轮密钥列表反转
  • CBC 模式 IV 长度必须为 16 字节,且每次加密应使用随机 IV

4.8 性能与安全注意事项

方面说明
性能优化可预计算 T 表(4 个 256×32 位查找表)替代 S 盒+L 变换
侧信道防护生产环境应使用常数时间实现,避免 S 盒查表时序泄露
ECB 模式不应在正式协议中使用,推荐 CBC 或 GCM 模式
密钥管理主密钥应安全存储,轮密钥可预计算缓存

安全警告:本实现为教学示例,未包含侧信道攻击防护。生产环境请使用经过国密局认证的密码库(如 GmSSL、OpenSSL 国密模块)。