SM4 国密算法企业级实战:加密解密、数据脱敏与合规存储完整方案

作者:忆笙智云官方 | 发布时间:2026-06-08 11:00 | 更新时间:2026-06-08 11:00

SM4国密算法在企业应用中的实践

引言

随着《密码法》和《数据安全法》的实施,国内金融、政务、医疗等行业对数据加密提出了明确的合规要求——必须使用国家密码管理局认定的国密算法。SM4作为我国自主研发的分组密码算法,是对称加密的国密标准,在国际上与AES具有同等安全强度。

本文将介绍SM4算法的基本原理,与AES的对比分析,以及在密码加密、数据脱敏等企业场景中的实践方案。

一、SM4算法概述

1.1 算法基本信息

属性 SM4 AES
发布时间 2006年 2001年
发布机构 国家密码管理局 NIST(美国)
算法类型 分组密码 分组密码
分组长度 128位 128位
密钥长度 128位 128/192/256位
加密轮数 32轮 10/12/14轮
安全强度 128位 128/192/256位

1.2 SM4加密流程

flowchart LR
    A[128位明文] --> B[初始变换]
    B --> C[32轮迭代变换]
    C --> D[反序变换]
    D --> E[128位密文]

    subgraph 每轮变换
        F[输入4个32位字] --> G[轮函数F]
        G --> H[S盒非线性变换]
        H --> I[线性变换L]
        I --> J[轮密钥异或]
        J --> K[输出4个32位字]
    end

    C --> F

    style E fill:#f9d5d5
    style A fill:#d5f9d5

SM4的核心是32轮非线性迭代变换,每轮使用不同的轮密钥。轮密钥由加密密钥通过密钥扩展算法生成,共32个轮密钥。

1.3 工作模式

SM4支持多种工作模式,不同模式适用于不同场景:

模式 特点 适用场景
ECB 每块独立加密,相同明文→相同密文 不推荐(不安全)
CBC 链式加密,需要IV 通用加密
CTR 计数器模式,可并行加密 高性能场景
GCM 认证加密,提供完整性校验 推荐模式

二、Java实现SM4加密

2.1 使用BouncyCastle库

BouncyCastle是Java生态中最成熟的安全库,完整支持SM4算法:

<!-- Maven依赖 -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

2.2 SM4工具类

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;

/**
 * SM4加密工具类
 * 支持CBC和GCM两种工作模式
 */
public class Sm4Util {

    static {
        // 注册BouncyCastle安全提供者
        Security.addProvider(new BouncyCastleProvider());
    }

    /** 算法名称 */
    private static final String ALGORITHM = "SM4";

    /** CBC模式 */
    private static final String CBC_MODE = "SM4/CBC/PKCS7Padding";

    /** GCM模式(推荐) */
    private static final String GCM_MODE = "SM4/GCM/NoPadding";

    /** IV长度(16字节 = 128位) */
    private static final int IV_LENGTH = 16;

    /** GCM认证标签长度(128位) */
    private static final int GCM_TAG_LENGTH = 128;

    /**
     * 生成SM4密钥
     * @return Base64编码的密钥
     */
    public static String generateKey() {
        SecureRandom random = new SecureRandom();
        byte[] key = new byte[16];  // SM4密钥固定16字节
        random.nextBytes(key);
        return Base64.getEncoder().encodeToString(key);
    }

    /**
     * 生成随机IV
     * @return IV字节数组
     */
    private static byte[] generateIv() {
        SecureRandom random = new SecureRandom();
        byte[] iv = new byte[IV_LENGTH];
        random.nextBytes(iv);
        return iv;
    }

    /**
     * SM4-CBC加密
     * @param plaintext 明文
     * @param keyBase64 Base64编码的密钥
     * @return Base64编码的密文(前16字节为IV)
     */
    public static String encryptCbc(String plaintext, String keyBase64) {
        try {
            byte[] key = Base64.getDecoder().decode(keyBase64);
            byte[] iv = generateIv();
            byte[] data = plaintext.getBytes(StandardCharsets.UTF_8);

            Cipher cipher = Cipher.getInstance(CBC_MODE, BouncyCastleProvider.PROVIDER_NAME);
            cipher.init(Cipher.ENCRYPT_MODE,
                    new SecretKeySpec(key, ALGORITHM),
                    new IvParameterSpec(iv));

            byte[] encrypted = cipher.doFinal(data);

            // 将IV拼接在密文前面(解密时需要)
            byte[] result = new byte[iv.length + encrypted.length];
            System.arraycopy(iv, 0, result, 0, iv.length);
            System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);

            return Base64.getEncoder().encodeToString(result);
        } catch (Exception e) {
            throw new RuntimeException("SM4加密失败", e);
        }
    }

    /**
     * SM4-CBC解密
     * @param ciphertextBase64 Base64编码的密文(前16字节为IV)
     * @param keyBase64 Base64编码的密钥
     * @return 明文
     */
    public static String decryptCbc(String ciphertextBase64, String keyBase64) {
        try {
            byte[] key = Base64.getDecoder().decode(keyBase64);
            byte[] result = Base64.getDecoder().decode(ciphertextBase64);

            // 提取IV(前16字节)
            byte[] iv = new byte[IV_LENGTH];
            System.arraycopy(result, 0, iv, 0, IV_LENGTH);

            // 提取密文
            byte[] encrypted = new byte[result.length - IV_LENGTH];
            System.arraycopy(result, IV_LENGTH, encrypted, 0, encrypted.length);

            Cipher cipher = Cipher.getInstance(CBC_MODE, BouncyCastleProvider.PROVIDER_NAME);
            cipher.init(Cipher.DECRYPT_MODE,
                    new SecretKeySpec(key, ALGORITHM),
                    new IvParameterSpec(iv));

            byte[] decrypted = cipher.doFinal(encrypted);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("SM4解密失败", e);
        }
    }
}

2.3 使用示例

public class Sm4Example {
    public static void main(String[] args) {
        // 1. 生成密钥(通常在系统初始化时生成一次,存储在安全配置中)
        String key = Sm4Util.generateKey();
        System.out.println("密钥: " + key);

        // 2. 加密
        String plaintext = "这是需要加密的敏感数据";
        String ciphertext = Sm4Util.encryptCbc(plaintext, key);
        System.out.println("密文: " + ciphertext);

        // 3. 解密
        String decrypted = Sm4Util.decryptCbc(ciphertext, key);
        System.out.println("明文: " + decrypted);

        // 验证
        assert plaintext.equals(decrypted);
    }
}

三、密码加密场景

3.1 密码存储的正确姿势

重要原则:密码永远不应使用对称加密存储,而应使用不可逆的哈希算法。SM4用于密码加密的场景是可逆加密(如需要还原明文的场景),而非密码存储。

密码存储推荐使用SM3(国密哈希算法)或bcrypt:

/**
 * 密码加密服务
 * 使用SM3+盐值进行密码哈希
 */
@Component
public class PasswordEncoder {

    /** 盐值长度 */
    private static final int SALT_LENGTH = 32;

    /** 迭代次数 */
    private static final int ITERATIONS = 1024;

    /**
     * 加密密码
     * @param rawPassword 原始密码
     * @return 加密后的密码(格式:盐值$迭代次数$哈希值)
     */
    public String encode(String rawPassword) {
        // 生成随机盐值
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_LENGTH];
        random.nextBytes(salt);
        String saltHex = Hex.toHexString(salt);

        // SM3哈希 + 迭代
        byte[] hash = rawPassword.getBytes(StandardCharsets.UTF_8);
        for (int i = 0; i < ITERATIONS; i++) {
            hash = sm3Hash(saltHex.getBytes(StandardCharsets.UTF_8), hash);
        }

        return saltHex + "$" + ITERATIONS + "$" + Hex.toHexString(hash);
    }

    /**
     * 验证密码
     * @param rawPassword 原始密码
     * @param encodedPassword 加密后的密码
     * @return 是否匹配
     */
    public boolean matches(String rawPassword, String encodedPassword) {
        String[] parts = encodedPassword.split("\$");
        String salt = parts[0];
        int iterations = Integer.parseInt(parts[1]);

        byte[] hash = rawPassword.getBytes(StandardCharsets.UTF_8);
        for (int i = 0; i < iterations; i++) {
            hash = sm3Hash(salt.getBytes(StandardCharsets.UTF_8), hash);
        }

        return Hex.toHexString(hash).equals(parts[2]);
    }

    /**
     * SM3哈希计算
     */
    private byte[] sm3Hash(byte[]... datas) {
        SM3Digest digest = new SM3Digest();
        for (byte[] data : datas) {
            digest.update(data, 0, data.length);
        }
        byte[] result = new byte[digest.getDigestSize()];
        digest.doFinal(result, 0);
        return result;
    }
}

四、数据脱敏场景

4.1 可逆脱敏

某些场景需要在展示时脱敏,但在业务处理时需要还原原始数据。SM4加密是理想的方案:

/**
 * 数据脱敏服务
 * 支持可逆脱敏(SM4加密)和不可逆脱敏(掩码)
 */
@Component
public class DataMaskingService {

    @Autowired
    private Sm4Config sm4Config;

    /**
     * 手机号脱敏(可逆)
     * 存储:SM4加密后的密文
     * 展示:138****1234
     */
    public String maskPhone(String phone) {
        // 存储时加密
        return Sm4Util.encryptCbc(phone, sm4Config.getKey());
    }

    /**
     * 手机号展示脱敏(不可逆掩码)
     * 13812341234 → 138****1234
     */
    public String displayPhone(String phone) {
        if (phone == null || phone.length() < 7) {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }

    /**
     * 身份证号展示脱敏
     * 110101199001011234 → 110101****1234
     */
    public String displayIdCard(String idCard) {
        if (idCard == null || idCard.length() < 8) {
            return idCard;
        }
        return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
    }

    /**
     * 邮箱展示脱敏
     * example@domain.com → e****e@domain.com
     */
    public String displayEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String name = parts[0];
        if (name.length() <= 2) {
            return name.charAt(0) + "****@" + parts[1];
        }
        return name.charAt(0) + "****" + name.charAt(name.length() - 1) + "@" + parts[1];
    }

    /**
     * 解密还原
     */
    public String unmask(String maskedData) {
        return Sm4Util.decryptCbc(maskedData, sm4Config.getKey());
    }
}

4.2 MyBatis-Plus类型处理器

通过MyBatis-Plus的类型处理器,实现数据库字段的透明加解密:

/**
 * SM4加密类型处理器
 * 写入数据库时自动加密,读取时自动解密
 */
@MappedTypes(String.class)
public class Sm4EncryptTypeHandler extends BaseTypeHandler<String> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                     String parameter, JdbcType jdbcType) throws SQLException {
        // 写入时加密
        ps.setString(i, Sm4Util.encryptCbc(parameter, getSm4Key()));
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 读取时解密
        String value = rs.getString(columnName);
        return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return value != null ? Sm4Util.decryptCbc(value, getSm4Key()) : null;
    }

    private String getSm4Key() {
        // 从安全配置中获取密钥
        return SpringUtils.getBean(Sm4Config.class).getKey();
    }
}

五、密钥管理

5.1 密钥管理架构

flowchart TB
    A[应用启动] --> B{密钥来源}
    B -->|开发环境| C[配置文件]
    B -->|生产环境| D[密钥管理服务KMS]

    C --> E[加载密钥到内存]
    D --> F[从KMS获取密钥]
    F --> G[密钥缓存到Redis<br/>TTL=1小时]
    G --> E

    E --> H[应用使用密钥]

    subgraph 密钥轮换
        I[定期轮换密钥] --> J[生成新密钥]
        J --> K[双密钥并行期<br/>新旧密钥均可解密]
        K --> L[数据重新加密]
        L --> M[切换到新密钥]
    end

    style D fill:#d5f9d5
    style H fill:#e8f4f8

5.2 密钥安全要求

  1. 密钥不硬编码:密钥存储在环境变量或KMS中,不写入代码
  2. 传输加密:密钥在传输过程中使用TLS加密
  3. 定期轮换:建议每90天轮换一次密钥
  4. 访问控制:密钥访问需要最小权限原则
  5. 审计日志:记录所有密钥访问和操作

六、SM4 vs AES对比总结

维度 SM4 AES
安全强度 128位(等效AES-128) 128/192/256位
合规性 符合国密标准,国内合规 国际标准,海外合规
性能 略慢于AES(约5-10%) 硬件加速支持好
生态支持 BouncyCastle支持 JDK原生支持
国际认可 ISO/IEC 18033-3标准 全球通用标准

结论与建议

核心要点

  1. SM4是国密合规的必选项:面向国内金融、政务、医疗行业的系统必须支持SM4
  2. 密码存储用SM3/bcrypt,不用SM4:密码存储必须使用不可逆哈希
  3. 数据脱敏用SM4:需要还原的敏感数据使用SM4可逆加密
  4. 推荐GCM模式:提供加密+完整性校验,比CBC更安全

实践建议

  1. 密钥管理:生产环境密钥必须通过KMS管理,禁止硬编码
  2. 双算法支持:同时支持SM4和AES,满足国内外不同合规要求
  3. 性能测试:SM4性能略低于AES,高并发场景需做压测
  4. 密钥轮换:建立密钥轮换机制,支持双密钥并行过渡
  5. 审计合规:记录所有加解密操作,满足等保审计要求

相关资源