4. SM4算法Python实现
SM4 是中国国家密码管理局发布的商用分组密码标准(GB/T 32907-2016),采用 128 位密钥、128 位分组、32 轮非线性迭代结构。本章从底层 S 盒出发,逐步构建密钥扩展、轮函数、加解密核心,并实现 ECB 与 CBC 两种工作模式,最后通过单元测试验证实现的正确性。
4.1 SM4 算法整体架构
SM4 的加密过程可概括为:初始明文分组 → 32 轮迭代 → 反序变换 → 密文输出。每轮使用一个 32 位轮密钥,轮函数包含合成置换 T,由非线性变换 τ(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。
| 模式 | 加密公式 | 解密公式 | 特点 |
|---|---|---|---|
| ECB | C_i = E(P_i) | P_i = D(C_i) | 并行、无错误传播、模式不安全 |
| CBC | C_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 国密模块)。