4. SM4 对称加密算法原理与 Java 实现
SM4 国密算法,说白了,就是咱们国家自己的 AES。加密强度够用,性能也不错。我最早接触它是在一个金融项目里,那时候国产化刚起步,踩了不少坑,今天咱们就把核心的 S 盒、密钥扩展和轮函数掰开揉碎讲清楚。
4.1 S 盒:算法的灵魂
SM4 的 S 盒是一个 8 位输入、8 位输出的非线性置换表。你想想看,整个算法的安全性,一大半都靠它撑着。
我第一次看到这个 S 盒的时候,总觉得它就是 256 个随机数。后来仔细研究才发现,它是基于有限域 GF(2^8) 上的仿射变换和逆映射构造的,有严格的数学背景。
// S盒表,长度256,对应0x00~0xFF的每个输入
public static final int[] S_BOX = new int[] {
0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7,
0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05,
// ... 剩下240个字节(实际256个)
0xB3, 0xE5, 0x40, 0x79, 0x6B
};
这里有个坑:S 盒的输入是 8 位(0-255),但 Java 的 byte 是带符号的。如果你直接拿 byte 值当数组索引,负索引会报 ArrayIndexOutOfBoundsException。
(byteValue & 0xFF) 转成无符号整数。我曾经在项目里吃过这个亏,上线后诡异的数组越界,查了两天才找到原因。
4.2 密钥扩展:从 128 位到 32 轮
SM4 的主密钥是 128 位,但加密需要 32 个轮密钥,每个轮密钥 32 位。怎么变出来的?密钥扩展算法。
咱们先看固定参数。系统参数 FK(四个 32 位常数),用来初始化密钥状态:
// FK 常数(十六进制)
int[] FK = {0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC};
还有固定参数 CK(32 个 32 位常数),每一轮选一个,用来打乱密钥中间态。CK 的计算公式是 ck = (i * 7) % 256,国内算法标准里已经算好了。
// 密钥扩展核心逻辑
private static int[] keyExpansion(byte[] key) {
int[] MK = bytesToInts(key); // 转为4个32位字
int[] K = new int[4 + 32];
int[] rk = new int[32];
// 初始化:MK 异或 FK
K[0] = MK[0] ^ FK[0];
K[1] = MK[1] ^ FK[1];
K[2] = MK[2] ^ FK[2];
K[3] = MK[3] ^ FK[3];
// 32 轮迭代生成轮密钥
for (int i = 0; i < 32; i++) {
K[i + 4] = K[i] ^ T_prime(K[i + 1] ^ K[i + 2] ^ K[i + 3] ^ CK[i]);
rk[i] = K[i + 4];
}
return rk;
}
这里的关键是 T_prime 变换。它先对输入的 32 位字做 4 次 S 盒置换,再做一个线性变换 L'。
T_prime 与加密的 T 有什么不同?
- 密钥扩展的 L' 是:
B ^ (B <<< 13) ^ (B <<< 23) - 加密轮函数的 L 是:
B ^ (B <<< 2) ^ (B <<< 10) ^ (B <<< 18) ^ (B <<< 24) - 线性变换不同,安全性目标也不一样。
4.3 加密轮函数
加密流程分了 32 轮,每一轮就干三件事:异或、S 盒替换、线性变换。
举个例子,第 i 轮,我有四个 32 位字:(Xi, Xi+1, Xi+2, Xi+3),再加上轮密钥 rki。计算过程如下:
// 第 i 轮的加密函数
private int roundFunction(int x0, int x1, int x2, int x3, int rk) {
int temp = x1 ^ x2 ^ x3 ^ rk; // 第一步:异或
int sBoxResult = sBoxTransform(temp); // 第二步:S盒变换
int linearResult = linearL(sBoxResult); // 第三步:线性变换L
return x0 ^ linearResult; // 输出
}
// S盒变换:将32位字拆成4个字节,分别查表
private int sBoxTransform(int input) {
int[] result = new int[4];
for (int i = 0; i < 4; i++) {
int byteVal = (input >> (8 * (3 - i))) & 0xFF;
result[i] = S_BOX[byteVal];
}
return (result[0] << 24) | (result[1] << 16) |
(result[2] << 8) | result[3];
}
// 线性变换 L
private int linearL(int input) {
return input ^ rotl(input, 2) ^ rotl(input, 10) ^
rotl(input, 18) ^ rotl(input, 24);
}
32 轮跑完以后,把最后四个字反序输出就是密文。嗯,这里要注意,SM4 用的是反序输出,不是直接输出。
4.4 解密:把轮密钥倒过来用
解密比加密更简单——你想想看,它就是加密的逆过程。
具体做法:加密时 rk0 到 rk31 依次用,解密时 rk31 到 rk0 逆序用。其他一模一样,甚至连轮函数都不用改。
// 加密:顺序使用轮密钥
int[] rk = keyExpansion(key);
byte[] cipher = encrypt(plainText, rk);
// 解密:逆序使用轮密钥
byte[] decrypted = encrypt(cipher, reverseArray(rk));
为什么会这样?因为 SM4 的加密和解密轮函数是自逆的。说白了,同样的硬件和代码,换一下密钥顺序就能同时做加解密,特别适合嵌入式场景。
4.5 知识结构总览
下面是本章的知识图谱,SM4 算法的核心脉络一目了然:
4.6 Java 完整实现要点
总结一下 Java 实现中的几个关键点:
| 模块 | 核心方法/数据结构 | 注意事项 |
|---|---|---|
| S 盒 | static int[] S_BOX = { ... } |
查表时 byte & 0xFF 防负索引 |
| 密钥扩展 | keyExpansion(byte[] key) |
FK 初始化 + 32 轮 T' 迭代 |
| 加密轮函数 | roundFunction(x0,x1,x2,x3, rk) |
异或 → S盒 → 线性 L → 输出异或 |
| 解密 | 加密函数 + 逆序轮密钥 | 函数体完全复用 |
| 循环移位 | rotl(int x, int bits) |
用 (x << bits) | (x >>> (32 - bits)) |
性能小贴士: 我在生产环境里常用的优化手段:
- S 盒查表尽量用局部变量,减少数组边界检查
- 32 轮循环可以手工展开,减少循环开销
- 密钥扩展一次后缓存,加解密复用
嗯,SM4 的算法原理和实现就这些了。S 盒、密钥扩展、轮函数三件事串起来,就能搭起一套完整的加解密引擎。你自己动手实现一遍,比看十遍文章都管用。