一、项目背景详细介绍

SM4 是中国国家商用密码算法之一,由中国密码学会提出并经国家密码管理局批准,用于保护敏感通信与数据存储安全。作为一种对称分组密码,SM4 具有:

  • 128 位分组长度

  • 128 位密钥长度

  • 32 轮迭代变换

SM4 广泛应用于无线局域网(WAPI)、金融支付、工业控制和物联网等领域。掌握 SM4 算法的 Java 实现,有助于在国内合规环境下进行加密通信、数据加密以及与硬件加速模块对接。


二、项目需求详细介绍

本项目要求以纯 Java 实现 SM4 分组密码算法,包括以下功能:

  1. 密钥扩展

    • 提供方法 int[] keySchedule(byte[] key),根据 128 位密钥生成 32 轮轮密钥(每轮 32 位)。

  2. 分组加解密

    • 提供方法 byte[] encryptBlock(byte[] in, int[] roundKeys),对单个 16 字节分组加密;

    • 提供方法 byte[] decryptBlock(byte[] in, int[] roundKeys),对单个分组解密(使用逆序轮密钥)。

  3. 分组模式链

    • 支持 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)

  4. 填充方式

    • 支持 PKCS#5/#7 填充和去填充。

  5. 命令行工具

    • 提供 Main 类,解析参数:

      • --mode encrypt|decrypt

      • --cipher ECB|CBC

      • --key <hexKey>

      • --iv <hexIV>(CBC 模式)

      • 输入/输出文件路径

  6. 单元测试覆盖

    • 使用 JUnit 5 验证密钥扩展、单块加解密与完整模式结果与标准参考向量一致;

  7. 性能与扩展

    • 纯 Java 实现;可后续集成 JNI 硬件加速;


三、相关技术详细介绍

  1. Java 基础

    • 字节与无符号整型转换;

    • 数组与位运算(移位、异或、字节轮换)。

  2. SM4 算法细节

    • 非线性 S 盒与线性变换 L;

    • 轮函数 F:F(X0,X1,X2,X3, rk) = X0 ⊕ T(X1⊕X2⊕X3⊕rk)

    • 轮常量 CK[0..31] 与系统参数 FK[0..3]。

  3. 分组模式与填充

    • ECB(电子密码本模式):各分组独立;

    • CBC(密码分组链接模式):前一分组密文异或后加密;

    • PKCS#7 填充:填充字节值等于填充长度。

  4. 文件 I/O 与命令行解析

    • java.nio.file.Files 读取与写入文件;

    • Apache Commons CLI 解析参数。


四、实现思路详细介绍

  1. 工具类 Sm4Util

    • 私有构造;静态常量 S 盒、FK、CK;

    • 方法 keySchedule 生成轮密钥数组;

  2. 轮函数与线性变换

    • 子方法 tau(int word):将 32 位字节拆分,经 S 盒替代后重组;

    • 子方法 L(int b):线性变换 b ⊕ (b ≪ 2) ⊕ (b ≪ 10) ⊕ (b ≪ 18) ⊕ (b ≪ 24)

    • 综合 T(int x) = L(tau(x))

  3. 单分组加密

    • 将输入 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] 的字节序列;

  4. 解密

    • 与加密相同,只是轮密钥逆序使用;

  5. 模式与填充

    • 对输入数据按块分割,ECB 模式直接加解密;

    • CBC 模式对每个分组先与上一密文/IV 异或;

    • 填充:加密前对末尾不足 16 字节的数据添加 PKCS#7 填充,加密后解密时去填充;

  6. 命令行 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 提高性能。


九、扩展方向与性能优化

  1. 硬件加速
    通过 JNI 调用 CPU 指令集或 HSM 模块,实现高速加解密。

  2. 并行加密
    在多核环境对独立分组进行并行处理,提升吞吐量。

  3. 更多填充与模式
    支持 NoPadding、CTS、CBC-MAC 等多种填充与认证模式。

  4. 流式 API
    实现 InputStream/OutputStream 包装,加解密可边读边写。

  5. 安全审核
    集成常见密码学库(如 BouncyCastle)对实现进行对比与审计。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐