国密算法SM2与SM3在Java项目中的实践指南

国密算法SM2与SM3在Java项目中的实践指南

在信息安全领域,密码算法是保障数据安全的核心基石。随着国家对信息安全自主可控要求的不断提升,国密算法(即国家商用密码算法)的应用越来越广泛。其中,SM2椭圆曲线公钥密码算法和SM3密码杂凑算法作为国密体系中的核心算法,分别用于数据加密签名和数据完整性校验。本文将结合Java项目实战,详细讲解SM2和SM3的原理概要及具体使用方法,帮助开发者快速上手国密算法集成。

一、国密算法基础认知:SM2与SM3是什么?

在进行代码实现前,我们先简单梳理下SM2和SM3的核心定位与区别,避免在实际使用中混淆场景。

1.1 SM2:椭圆曲线公钥密码算法

SM2是由我国自主设计的椭圆曲线公钥密码算法,其安全性基于椭圆曲线离散对数问题(ECDLP),相较于国际通用的RSA算法,在相同安全强度下,SM2的密钥长度更短(推荐256位),计算效率更高,更适合移动设备、物联网等资源受限场景。

SM2的核心应用场景包括:数据加密/解密数字签名/验签。与RSA不同的是,SM2在签名过程中会加入用户的身份信息,进一步提升了签名的安全性和抗攻击能力。

1.2 SM3:密码杂凑算法

SM3是一种密码散列函数,类似于国际通用的SHA-256,其作用是将任意长度的输入数据(明文)转换为固定长度(256位)的输出数据(哈希值/摘要)。SM3具有抗碰撞性、抗原像性等特性,即无法通过哈希值反推明文,也难以找到两个不同明文对应相同的哈希值。

SM3的核心应用场景包括:数据完整性校验密码加密存储 (如用户密码加盐哈希后存储)、数字签名的辅助(对明文先做SM3哈希,再用SM2签名哈希值,提升效率)。

二、Java项目集成国密算法的前提:环境搭建

Java原生的JCE(Java Cryptography Extension)并不直接支持SM2和SM3算法,因此需要借助第三方开源库来实现。目前主流的国密算法Java实现库是BouncyCastle(一个开源的密码学库,支持多种国际和国家密码算法)。

2.1 引入依赖(Maven)

在Java项目的pom.xml文件中引入BouncyCastle的依赖,推荐使用最新稳定版本:

xml 复制代码
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15to18</artifactId>
    <version>1.78.1</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk15to18</artifactId>
    <version>1.78.1</version>
</dependency>

说明:bcprov-jdk15to18是基础密码算法库,包含SM2、SM3的核心实现;bcpkix-jdk15to18是扩展库,提供了更便捷的密钥管理和签名加密封装。

2.2 注册BouncyCastle安全提供者

Java中使用密码算法需要通过"安全提供者"(Security Provider)机制加载,因此需要在使用SM2和SM3前,将BouncyCastle注册为安全提供者。有两种注册方式:

方式一:代码动态注册(推荐)

在项目启动时或算法使用前执行以下代码,无需修改系统配置,灵活性高:

java 复制代码
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

// 注册BouncyCastle提供者
public class SMUtil {
    static {
        // 避免重复注册
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
}
方式二:系统级注册(不推荐)

修改JRE安装目录下的lib/security/java.security文件,添加一行:security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider(数字10需根据现有提供者顺序调整)。该方式影响所有使用该JRE的项目,灵活性差,不推荐生产环境使用。

三、SM3算法在Java中的实现:数据哈希

SM3的使用相对简单,核心是通过BouncyCastle提供的算法名称"SM3"获取消息摘要器,然后对输入数据进行哈希计算。以下是完整的工具类实现,包含普通哈希和加盐哈希两种场景(加盐哈希更适合密码存储,可防止彩虹表攻击)。

3.1 SM3工具类实现

java 复制代码
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

/**
 * SM3哈希算法工具类
 */
public class SM3Util {
    // 算法名称
    private static final String ALGORITHM_NAME = "SM3";
    // 编码格式
    private static final String CHARSET = "UTF-8";
    // 哈希结果长度(256位,转换为16进制后为64位)
    private static final int HASH_LENGTH = 64;

    static {
        // 注册BouncyCastle提供者
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    /**
     * 普通SM3哈希(无盐)
     * @param content 待哈希的明文
     * @return 16进制格式的哈希值
     * @throws NoSuchAlgorithmException 算法不存在异常
     */
    public static String sm3Hash(String content) throws NoSuchAlgorithmException {
        return sm3HashWithSalt(content, null);
    }

    /**
     * SM3加盐哈希(推荐密码存储使用)
     * @param content 待哈希的明文
     * @param salt 盐值(可为null,为null时等同于普通哈希)
     * @return 16进制格式的哈希值
     * @throws NoSuchAlgorithmException 算法不存在异常
     */
    public static String sm3HashWithSalt(String content, String salt) throws NoSuchAlgorithmException {
        // 若有盐值,将盐值拼接到明文前(也可拼接到后面,需统一规则)
        String contentWithSalt = content + (salt != null ? salt : "");
        // 获取SM3消息摘要器
        MessageDigest messageDigest = MessageDigest.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        // 计算哈希值(字节数组)
        byte[] hashBytes = messageDigest.digest(contentWithSalt.getBytes());
        // 转换为16进制字符串
        return bytesToHex(hashBytes);
    }

    /**
     * 字节数组转换为16进制字符串
     * @param bytes 字节数组
     * @return 16进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = Integer.toHexString(b & 0xFF);
            if (hex.length() == 1) {
                sb.append("0");
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    // 测试方法
    public static void main(String[] args) throws NoSuchAlgorithmException {
        String content = "测试SM3哈希";
        String salt = "random_salt_123"; // 实际使用中建议随机生成盐值,每个用户单独存储

        // 普通哈希
        String normalHash = sm3Hash(content);
        System.out.println("SM3普通哈希结果:" + normalHash);
        System.out.println("哈希结果长度:" + normalHash.length()); // 应输出64

        // 加盐哈希
        String saltHash = sm3HashWithSalt(content, salt);
        System.out.println("SM3加盐哈希结果:" + saltHash);
    }
}

3.2 关键说明

  • 盐值使用:密码存储时,务必使用加盐哈希,盐值建议随机生成(如16位随机字符串),并与哈希结果一同存储(每个用户单独一个盐值)。

  • 算法名称:BouncyCastle中SM3的算法名称为"SM3",获取MessageDigest时需指定提供者为BouncyCastleProvider.PROVIDER_NAME(即"BC")。

  • 哈希长度:SM3输出为256位哈希值,转换为16进制字符串后长度为64位,可通过此特性验证哈希结果是否正确。

四、SM2算法在Java中的实现:加密解密与签名验签

SM2的使用场景比SM3更复杂,核心包括"密钥对生成""加密解密""签名验签"三个环节。以下将基于BouncyCastle实现完整的工具类,并讲解各环节的关键注意事项。

4.1 SM2工具类实现(核心代码)

java 复制代码
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;

/**
 * SM2椭圆曲线算法工具类(加密解密、签名验签)
 */
public class SM2Util {
    // 算法名称
    private static final String ALGORITHM_NAME = "SM2";
    // 编码格式
    private static final String CHARSET = "UTF-8";
    // SM2推荐曲线参数(国密标准曲线:sm2p256v1)
    private static final X9ECParameters X9_EC_PARAMETERS = GMNamedCurves.getByName("sm2p256v1");
    private static final ECParameterSpec EC_PARAMETER_SPEC = new ECParameterSpec(
            X9_EC_PARAMETERS.getCurve(),
            X9_EC_PARAMETERS.getG(),
            X9_EC_PARAMETERS.getN(),
            X9_EC_PARAMETERS.getH()
    );
    private static final ECDomainParameters EC_DOMAIN_PARAMETERS = new ECDomainParameters(
            X9_EC_PARAMETERS.getCurve(),
            X9_EC_PARAMETERS.getG(),
            X9_EC_PARAMETERS.getN(),
            X9_EC_PARAMETERS.getH()
    );

    static {
        // 注册BouncyCastle提供者
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    /**
     * 生成SM2密钥对(公钥+私钥)
     * @return 密钥对(包含公钥和私钥的字节数组)
     * @throws Exception 异常
     */
    public static KeyPairVO generateKeyPair() throws Exception {
        // 获取SM2密钥对生成器
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        // 初始化密钥对生成器(指定曲线参数和随机数)
        keyPairGenerator.initialize(EC_PARAMETER_SPEC, new SecureRandom());
        // 生成密钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        // 提取公钥和私钥(字节数组格式)
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        // 封装为VO返回(16进制格式,便于存储和传输)
        return new KeyPairVO(
                Hex.toHexString(publicKeyBytes),
                Hex.toHexString(privateKeyBytes)
        );
    }

    /**
     * SM2加密(公钥加密)
     * @param publicKeyHex 16进制格式的公钥
     * @param content 待加密的明文
     * @return 16进制格式的密文
     * @throws Exception 异常
     */
    public static String encrypt(String publicKeyHex, String content) throws Exception {
        // 1. 解析公钥(从16进制字符串转换为PublicKey对象)
        byte[] publicKeyBytes = Hex.decode(publicKeyHex);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
        PublicKey publicKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
                .generatePublic(x509EncodedKeySpec);
        ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) ECKeyParameters.getInstance(publicKey);

        // 2. 初始化SM2加密引擎(使用C1C3C2格式,国密标准推荐)
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));

        // 3. 执行加密并返回结果(字节数组转换为16进制字符串)
        byte[] contentBytes = content.getBytes(CHARSET);
        byte[] encryptedBytes = sm2Engine.processBlock(contentBytes, 0, contentBytes.length);
        return Hex.toHexString(encryptedBytes);
    }

    /**
     * SM2解密(私钥解密)
     * @param privateKeyHex 16进制格式的私钥
     * @param encryptedContentHex 16进制格式的密文
     * @return 解密后的明文
     * @throws Exception 异常
     */
    public static String decrypt(String privateKeyHex, String encryptedContentHex) throws Exception {
        // 1. 解析私钥(从16进制字符串转换为PrivateKey对象)
        byte[] privateKeyBytes = Hex.decode(privateKeyHex);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        PrivateKey privateKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
                .generatePrivate(pkcs8EncodedKeySpec);
        ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters) ECKeyParameters.getInstance(privateKey);

        // 2. 初始化SM2解密引擎(需与加密格式一致,C1C3C2)
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        sm2Engine.init(false, ecPrivateKeyParameters);

        // 3. 执行解密并返回结果(字节数组转换为字符串)
        byte[] encryptedBytes = Hex.decode(encryptedContentHex);
        byte[] decryptedBytes = sm2Engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
        return new String(decryptedBytes, CHARSET);
    }

    /**
     * SM2签名(私钥签名)
     * @param privateKeyHex 16进制格式的私钥
     * @param content 待签名的明文
     * @return 16进制格式的签名结果
     * @throws Exception 异常
     */
    public static String sign(String privateKeyHex, String content) throws Exception {
        // 1. 解析私钥
        byte[] privateKeyBytes = Hex.decode(privateKeyHex);
        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
        PrivateKey privateKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
                .generatePrivate(pkcs8EncodedKeySpec);
        ECPrivateKeyParameters ecPrivateKeyParameters = (ECPrivateKeyParameters) ECKeyParameters.getInstance(privateKey);

        // 2. 初始化SM2签名引擎(使用SM3作为哈希算法,国密标准)
        org.bouncycastle.crypto.signers.SM2Signer sm2Signer = new org.bouncycastle.crypto.signers.SM2Signer();
        sm2Signer.init(true, new ParametersWithRandom(ecPrivateKeyParameters, new SecureRandom()));

        // 3. 执行签名并返回结果
        byte[] contentBytes = content.getBytes(CHARSET);
        sm2Signer.update(contentBytes, 0, contentBytes.length);
        byte[] signBytes = sm2Signer.generateSignature();
        return Hex.toHexString(signBytes);
    }

    /**
     * SM2验签(公钥验签)
     * @param publicKeyHex 16进制格式的公钥
     * @param content 待验签的明文
     * @param signHex 16进制格式的签名结果
     * @return 验签结果(true:验签通过,false:验签失败)
     * @throws Exception 异常
     */
    public static boolean verify(String publicKeyHex, String content, String signHex) throws Exception {
        // 1. 解析公钥
        byte[] publicKeyBytes = Hex.decode(publicKeyHex);
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
        PublicKey publicKey = Security.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME)
                .generatePublic(x509EncodedKeySpec);
        ECPublicKeyParameters ecPublicKeyParameters = (ECPublicKeyParameters) ECKeyParameters.getInstance(publicKey);

        // 2. 初始化SM2验签引擎
        org.bouncycastle.crypto.signers.SM2Signer sm2Signer = new org.bouncycastle.crypto.signers.SM2Signer();
        sm2Signer.init(false, ecPublicKeyParameters);

        // 3. 执行验签并返回结果
        byte[] contentBytes = content.getBytes(CHARSET);
        sm2Signer.update(contentBytes, 0, contentBytes.length);
        return sm2Signer.verifySignature(Hex.decode(signHex));
    }

    /**
     * 密钥对VO(用于封装公钥和私钥)
     */
    public static class KeyPairVO {
        private String publicKey; // 16进制公钥
        private String privateKey; // 16进制私钥

        public KeyPairVO(String publicKey, String privateKey) {
            this.publicKey = publicKey;
            this.privateKey = privateKey;
        }

        // getter和setter
        public String getPublicKey() { return publicKey; }
        public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
        public String getPrivateKey() { return privateKey; }
        public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
    }

    // 测试方法
    public static void main(String[] args) throws Exception {
        // 1. 生成密钥对
        KeyPairVO keyPairVO = generateKeyPair();
        System.out.println("SM2公钥:" + keyPairVO.getPublicKey());
        System.out.println("SM2私钥:" + keyPairVO.getPrivateKey());

        // 2. 加密解密测试
        String content = "测试SM2加密解密";
        String encrypted = encrypt(keyPairVO.getPublicKey(), content);
        System.out.println("SM2加密后密文:" + encrypted);
        String decrypted = decrypt(keyPairVO.getPrivateKey(), encrypted);
        System.out.println("SM2解密后明文:" + decrypted);

        // 3. 签名验签测试
        String sign = sign(keyPairVO.getPrivateKey(), content);
        System.out.println("SM2签名结果:" + sign);
        boolean verifyResult = verify(keyPairVO.getPublicKey(), content, sign);
        System.out.println("SM2验签结果:" + verifyResult); // 应输出true
    }
}

4.2 核心环节关键说明

(1)密钥对生成
  • 曲线参数:SM2必须使用国密标准推荐的曲线"sm2p256v1",不可使用其他椭圆曲线(如secp256r1),否则会导致兼容性问题。

  • 密钥存储:生成的公钥和私钥建议转换为16进制字符串或Base64格式存储(工具类中使用16进制),避免直接存储字节数组。私钥需严格保密,建议加密后存储(如使用KMS密钥管理服务)。

(2)加密解密
  • 加密模式:SM2有C1C2C3和C1C3C2两种加密结果格式,国密标准推荐使用C1C3C2格式,工具类中已指定,需确保加密和解密使用相同格式。

  • 公钥加密:加密使用公钥,任何人都可获取公钥对数据加密,但只有对应的私钥持有者才能解密,适用于敏感数据传输(如用户密码传输)。

(3)签名验签
  • 哈希算法:SM2签名默认使用SM3作为哈希算法(工具类中已集成),无需额外指定,符合国密标准要求。

  • 私钥签名:签名使用私钥,验签使用公钥,可用于身份认证和数据防篡改(如接口请求参数签名)。即使明文被篡改,验签也会失败。

五、生产环境使用注意事项

在将SM2和SM3集成到生产环境时,除了代码实现外,还需关注以下安全和兼容性问题:

5.1 安全防护

  • 私钥安全:SM2私钥是核心保密信息,禁止硬编码在代码中或明文存储在配置文件中,建议使用密钥管理服务(KMS)、加密机或硬件安全模块(HSM)存储和管理。

  • 随机数安全:密钥生成、加密、签名过程中使用的随机数必须是密码学安全的随机数(工具类中使用SecureRandom),不可使用普通随机数生成器(如java.util.Random)。

  • 盐值管理:SM3加盐哈希的盐值需随机生成,每个用户单独存储,不可多个用户共用一个盐值。

5.2 兼容性问题

  • 库版本一致性:项目中使用的BouncyCastle版本需统一,避免不同模块使用不同版本导致冲突。建议在pom.xml中锁定版本。

  • 跨语言兼容性:若Java项目需与其他语言(如C++、Go、Python)的系统进行SM2/SM3交互,需确保双方使用相同的曲线参数、加密格式(如C1C3C2)、哈希规则和编码格式(16进制/Base64)。

5.3 性能优化

  • 密钥缓存:SM2密钥对生成成本较高,若需频繁使用同一密钥对(如服务端密钥),建议将公钥和私钥缓存到内存中,避免重复生成或解析。

  • 批量处理:若需对大量数据进行SM3哈希或SM2加密,建议分批次处理,避免单次处理过大数据导致内存溢出。

六、总结

SM2和SM3作为国密体系的核心算法,在数据安全保障中发挥着重要作用。本文通过BouncyCastle库实现了SM3的哈希计算和SM2的加密解密、签名验签功能,并提供了完整的工具类和测试方法。开发者在实际集成时,需重点关注密钥安全、算法参数一致性和跨系统兼容性问题,确保国密算法的正确、安全使用。随着国密合规要求的不断提升,掌握国密算法的集成技巧将成为开发者的必备能力之一。

相关推荐
wjs202419 小时前
迭代器模式
开发语言
bulucc19 小时前
使用Flask框架实现 webhook 和 api,并对比区别
开发语言·python
鹅鹅呀19 小时前
态势感知?什么是态势感知?安全与防护
安全
醉风塘19 小时前
Python基础语法完全指南:从零入门到掌握核心概念
开发语言·python
whm277719 小时前
Visual Basic 键盘事件
开发语言·visual studio
Cloud Traveler19 小时前
镜界药典:Rokid AR眼镜赋能的智能用药安全守护系统
安全·ar
2201_7578308719 小时前
Maven
java·maven
m0_7400437319 小时前
SpringMVC/Spring Boot 控制器返回视图路径(相对 / 绝对路径)核心总结
java·spring boot·后端·spring