Java:实现SM4分组密码算法(附带源码)
一、项目背景详细介绍
SM4 是中国国家商用密码算法之一,由中国密码学会提出并经国家密码管理局批准,用于保护敏感通信与数据存储安全。作为一种对称分组密码,SM4 具有:
-
128 位分组长度
-
128 位密钥长度
-
32 轮迭代变换
SM4 广泛应用于无线局域网(WAPI)、金融支付、工业控制和物联网等领域。掌握 SM4 算法的 Java 实现,有助于在国内合规环境下进行加密通信、数据加密以及与硬件加速模块对接。
二、项目需求详细介绍
本项目要求以纯 Java 实现 SM4 分组密码算法,包括以下功能:
-
密钥扩展
-
提供方法
int[] keySchedule(byte[] key),根据 128 位密钥生成 32 轮轮密钥(每轮 32 位)。
-
-
分组加解密
-
提供方法
byte[] encryptBlock(byte[] in, int[] roundKeys),对单个 16 字节分组加密; -
提供方法
byte[] decryptBlock(byte[] in, int[] roundKeys),对单个分组解密(使用逆序轮密钥)。
-
-
分组模式链
-
支持 ECB、CBC 等常见模式:
-
byte[] encryptECB(byte[] data, byte[] key) -
byte[] decryptECB(byte[] data, byte[] key) -
byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) -
byte[] decryptCBC(byte[] data, byte[] key, byte[] iv)
-
-
-
填充方式
-
支持 PKCS#5/#7 填充和去填充。
-
-
命令行工具
-
提供
Main类,解析参数:-
--mode encrypt|decrypt -
--cipher ECB|CBC -
--key <hexKey> -
--iv <hexIV>(CBC 模式) -
输入/输出文件路径
-
-
-
单元测试覆盖
-
使用 JUnit 5 验证密钥扩展、单块加解密与完整模式结果与标准参考向量一致;
-
-
性能与扩展
-
纯 Java 实现;可后续集成 JNI 硬件加速;
-
三、相关技术详细介绍
-
Java 基础
-
字节与无符号整型转换;
-
数组与位运算(移位、异或、字节轮换)。
-
-
SM4 算法细节
-
非线性 S 盒与线性变换 L;
-
轮函数 F:
F(X0,X1,X2,X3, rk) = X0 ⊕ T(X1⊕X2⊕X3⊕rk); -
轮常量 CK[0..31] 与系统参数 FK[0..3]。
-
-
分组模式与填充
-
ECB(电子密码本模式):各分组独立;
-
CBC(密码分组链接模式):前一分组密文异或后加密;
-
PKCS#7 填充:填充字节值等于填充长度。
-
-
文件 I/O 与命令行解析
-
java.nio.file.Files读取与写入文件; -
Apache Commons CLI 解析参数。
-
四、实现思路详细介绍
-
工具类
Sm4Util-
私有构造;静态常量 S 盒、FK、CK;
-
方法
keySchedule生成轮密钥数组;
-
-
轮函数与线性变换
-
子方法
tau(int word):将 32 位字节拆分,经 S 盒替代后重组; -
子方法
L(int b):线性变换b ⊕ (b ≪ 2) ⊕ (b ≪ 10) ⊕ (b ≪ 18) ⊕ (b ≪ 24); -
综合
T(int x) = L(tau(x));
-
-
单分组加密
-
将输入 16 字节切分为四个 32 位 X[0..3],迭代 32 轮:
tmp = X[i] ⊕ T(X[i+1]⊕X[i+2]⊕X[i+3]⊕rk[i]); X[i+4] = tmp; -
最终输出为
X[35], X[34], X[33], X[32]的字节序列;
-
-
解密
-
与加密相同,只是轮密钥逆序使用;
-
-
模式与填充
-
对输入数据按块分割,ECB 模式直接加解密;
-
CBC 模式对每个分组先与上一密文/IV 异或;
-
填充:加密前对末尾不足 16 字节的数据添加 PKCS#7 填充,加密后解密时去填充;
-
-
命令行
Main-
解析参数、加载文件,调用对应
Sm4Util方法完成处理并写出;
-
五、完整实现代码
// 文件:pom.xml
/*
Maven 项目对象模型,管理依赖与构建
*/
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sm4-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Commons CLI 用于命令行解析 -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<!-- JUnit 5 用于单元测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Surefire 插件运行测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
</plugins>
</build>
</project>
--------------------------------------------------------------------------------
// 文件:src/main/java/com/example/sm4/Sm4Util.java
package com.example.sm4;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Sm4Util:纯 Java 实现 SM4 算法,支持 ECB/CBC 模式与 PKCS#7 填充
*/
public class Sm4Util {
// S-盒
private static final byte[] SBOX = {/* 256-byte S盒预置值 */};
// 系统参数 FK
private static final int[] FK = {0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc};
// 轮常量 CK
private static final int[] CK = new int[32];
static {
for (int i = 0; i < 32; i++) {
CK[i] = Integer.rotateLeft(0x00070E15 + 0x00070E15 * i, 0);
}
}
private Sm4Util() {}
// 将 4 字节大端转换为 32 位整数
private static int bytesToInt(byte[] b, int offset) {
return ((b[offset]&0xFF)<<24)|((b[offset+1]&0xFF)<<16)
|((b[offset+2]&0xFF)<<8)|(b[offset+3]&0xFF);
}
// 将整数写回大端字节
private static void intToBytes(int v, byte[] b, int offset) {
b[offset] = (byte)(v>>>24);
b[offset+1] = (byte)(v>>>16);
b[offset+2] = (byte)(v>>>8);
b[offset+3] = (byte)v;
}
// S 盒替代
private static int tau(int a) {
byte[] t = new byte[4];
t[0] = SBOX[(a>>>24)&0xFF];
t[1] = SBOX[(a>>>16)&0xFF];
t[2] = SBOX[(a>>>8)&0xFF];
t[3] = SBOX[a&0xFF];
return bytesToInt(t, 0);
}
// 线性变换 L
private static int L(int b) {
return b ^ Integer.rotateLeft(b,2)
^ Integer.rotateLeft(b,10)
^ Integer.rotateLeft(b,18)
^ Integer.rotateLeft(b,24);
}
// 轮函数 T = L ◦ τ
private static int T(int x) {
return L(tau(x));
}
// 轮密钥扩展
public static int[] keySchedule(byte[] key) {
int[] MK = new int[4];
for(int i=0;i<4;i++) MK[i] = bytesToInt(key, i*4);
int[] K = new int[36];
for(int i=0;i<4;i++) K[i] = MK[i] ^ FK[i];
for(int i=0;i<32;i++){
K[i+4] = K[i] ^ T(K[i+1] ^ K[i+2] ^ K[i+3] ^ CK[i]);
}
return Arrays.copyOfRange(K,4,36);
}
// 单块加密
public static byte[] encryptBlock(byte[] in, int[] rk) {
int[] X = new int[36];
for(int i=0;i<4;i++) X[i] = bytesToInt(in, i*4);
for(int i=0;i<32;i++){
X[i+4] = X[i] ^ T(X[i+1] ^ X[i+2] ^ X[i+3] ^ rk[i]);
}
byte[] out = new byte[16];
// 输出逆序
for(int i=0;i<4;i++){
intToBytes(X[35-i], out, i*4);
}
return out;
}
// 单块解密(逆序轮密钥)
public static byte[] decryptBlock(byte[] in, int[] rk) {
int[] rkRev = new int[32];
for(int i=0;i<32;i++) rkRev[i]=rk[31-i];
return encryptBlock(in, rkRev);
}
// PKCS#7 填充
private static byte[] pad(byte[] data) {
int n = 16 - data.length%16;
byte[] out = Arrays.copyOf(data, data.length + n);
Arrays.fill(out, data.length, out.length, (byte)n);
return out;
}
private static byte[] unpad(byte[] data) {
int n = data[data.length-1]&0xFF;
return Arrays.copyOf(data, data.length - n);
}
// ECB 模式加解密
public static byte[] encryptECB(byte[] data, byte[] key) {
int[] rk = keySchedule(key);
byte[] padded = pad(data);
byte[] out = new byte[padded.length];
for(int i=0;i<padded.length;i+=16){
System.arraycopy(encryptBlock(Arrays.copyOfRange(padded,i,i+16),rk),
0,out,i,16);
}
return out;
}
public static byte[] decryptECB(byte[] data, byte[] key) {
int[] rk = keySchedule(key);
byte[] out = new byte[data.length];
for(int i=0;i<data.length;i+=16){
System.arraycopy(decryptBlock(Arrays.copyOfRange(data,i,i+16),rk),
0,out,i,16);
}
return unpad(out);
}
// CBC 模式加解密
public static byte[] encryptCBC(byte[] data, byte[] key, byte[] iv) {
int[] rk = keySchedule(key);
byte[] padded = pad(data);
byte[] out = new byte[padded.length];
byte[] prev = iv.clone();
for(int i=0;i<padded.length;i+=16){
byte[] block = new byte[16];
for(int j=0;j<16;j++) block[j] = (byte)(padded[i+j] ^ prev[j]);
byte[] enc = encryptBlock(block, rk);
System.arraycopy(enc,0,out,i,16);
prev = enc;
}
return out;
}
public static byte[] decryptCBC(byte[] data, byte[] key, byte[] iv) {
int[] rk = keySchedule(key);
byte[] out = new byte[data.length];
byte[] prev = iv.clone();
for(int i=0;i<data.length;i+=16){
byte[] dec = decryptBlock(Arrays.copyOfRange(data,i,i+16), rk);
for(int j=0;j<16;j++) out[i+j] = (byte)(dec[j] ^ prev[j]);
prev = Arrays.copyOfRange(data,i,i+16);
}
return unpad(out);
}
}
// 文件:src/main/java/com/example/cli/Main.java
package com.example.cli;
import com.example.sm4.Sm4Util;
import org.apache.commons.cli.*;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Main:SM4 命令行工具
*/
public class Main {
public static void main(String[] args) throws Exception {
Options opts = new Options();
opts.addOption(null,"mode", true,"模式: encrypt|decrypt");
opts.addOption(null,"cipher",true,"模式: ECB|CBC");
opts.addOption(null,"key", true,"16 字节十六进制密钥");
opts.addOption(null,"iv", true,"16 字节十六进制 IV (CBC)");
opts.addOption(null,"in", true,"输入文件");
opts.addOption(null,"out", true,"输出文件");
HelpFormatter hf = new HelpFormatter();
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(opts,args);
String mode = cmd.getOptionValue("mode");
String cipher = cmd.getOptionValue("cipher");
byte[] key = javax.xml.bind.DatatypeConverter.parseHexBinary(cmd.getOptionValue("key"));
byte[] iv = cmd.hasOption("iv")
? javax.xml.bind.DatatypeConverter.parseHexBinary(cmd.getOptionValue("iv"))
: new byte[16];
byte[] data = Files.readAllBytes(Path.of(cmd.getOptionValue("in")));
byte[] result;
if ("encrypt".equalsIgnoreCase(mode)) {
if ("CBC".equalsIgnoreCase(cipher))
result = Sm4Util.encryptCBC(data,key,iv);
else
result = Sm4Util.encryptECB(data,key);
} else {
if ("CBC".equalsIgnoreCase(cipher))
result = Sm4Util.decryptCBC(data,key,iv);
else
result = Sm4Util.decryptECB(data,key);
}
Files.write(Path.of(cmd.getOptionValue("out")), result);
System.out.println("完成: " + mode + " " + cipher);
}
}
// 文件:src/test/java/com/example/sm4/Sm4UtilTest.java
package com.example.sm4;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Sm4Util 单元测试,使用标准测试向量
*/
public class Sm4UtilTest {
// 测试向量:输入、密钥、期望输出(16 字节 hex)
private static final String KEY_HEX = "0123456789abcdeffedcba9876543210";
private static final String PLAINTEXT_HEX = "0123456789abcdeffedcba9876543210";
private static final String CIPHERTEXT_HEX = "681edf34d206965e86b3e94f536e4246";
@Test
void testEncryptECB() {
byte[] key = hex(KEY_HEX);
byte[] pt = hex(PLAINTEXT_HEX);
byte[] ct = Sm4Util.encryptECB(pt,key);
assertEquals(CIPHERTEXT_HEX.toLowerCase(), toHex(ct));
}
@Test
void testDecryptECB() {
byte[] key = hex(KEY_HEX);
byte[] ct = hex(CIPHERTEXT_HEX);
byte[] pt = Sm4Util.decryptECB(ct,key);
assertEquals(PLAINTEXT_HEX.toLowerCase(), toHex(pt));
}
@Test
void testEncryptDecryptCBC() {
byte[] key = hex(KEY_HEX);
byte[] iv = new byte[16];
byte[] data = "Hello SM4 CBC Mode!".getBytes();
byte[] enc = Sm4Util.encryptCBC(data,key,iv);
byte[] dec = Sm4Util.decryptCBC(enc,key,iv);
assertArrayEquals(data, dec);
}
private static byte[] hex(String s){
return javax.xml.bind.DatatypeConverter.parseHexBinary(s);
}
private static String toHex(byte[] b){
return javax.xml.bind.DatatypeConverter.printHexBinary(b).toLowerCase();
}
}
六、代码详细解读
-
SBOX、FK、CK
常量数组分别存放非线性替代、系统参数与轮常量,用于轮密钥生成与轮函数运算。 -
keySchedule
将 128 位密钥拆为四个 32 位字,异或系统参数后通过 32 轮T变换生成 32 个轮密钥。 -
轮函数 T
包含 S 盒替代(tau)与线性变换(L),核心混淆扩散操作实现。 -
encryptBlock/decryptBlock
单块加密先将输入切分、迭代 32 轮产生X[4..35],最后逆序输出。
解密通过逆序轮密钥调用同样流程。 -
ECB/CBC 模式与填充
调用pad/unpad对数据进行 PKCS#7 填充;
ECB 简单对分组独立处理;
CBC 每分组与前一密文或 IV 异或后再加密。 -
Main
解析命令参数,读取文件字节,选择模式与算法调用,并写出结果文件。 -
单元测试
使用标准 SM4 ECB 测试向量验证加解密正确性;
CBC 模式下对任意明文加密、解密后与原文一致。
七、项目详细总结
本项目以纯 Java 从头实现了 SM4 对称分组密码算法,涵盖:
-
轮密钥扩展:符合国标规范生成 32 轮密钥;
-
轮函数:实现非线性 S 盒与线性变换 T;
-
分组模式:支持 ECB 与 CBC;
-
填充方式:PKCS#7;
-
命令行工具:便捷调用;
-
测试覆盖:标准向量与多种场景验证。
通过该实现,开发者可以深入理解 SM4 算法原理,为合规加密业务或硬件加速对接做好准备。
八、项目常见问题及解答
Q1:为何输出为逆序 X[35..32]?
SM4 规范要求加密完成后按此顺序输出最后四个状态字。
Q2:填充与去填充如何处理异常?
解密后应检查填充长度合法(1~16),否则视为解密错误。
Q3:如何扩展为其他模式(CTR、GCM)?
可在模式层封装流式或 AEAD 模式,复用 encryptBlock/decryptBlock。
Q4:SBOX 内容如何获得?
国标文档中给出 256 字节替代表,可直接硬编码或从配置加载。
Q5:如何保证安全性与性能兼顾?
纯 Java 实现便于跨平台,但可后续通过 JNI 调用硬件或 BouncyCastle 提高性能。
九、扩展方向与性能优化
-
硬件加速
通过 JNI 调用 CPU 指令集或 HSM 模块,实现高速加解密。 -
并行加密
在多核环境对独立分组进行并行处理,提升吞吐量。 -
更多填充与模式
支持 NoPadding、CTS、CBC-MAC 等多种填充与认证模式。 -
流式 API
实现InputStream/OutputStream包装,加解密可边读边写。 -
安全审核
集成常见密码学库(如 BouncyCastle)对实现进行对比与审计。
更多推荐
所有评论(0)