在本文中,我们将探讨两个用于 SM2 加密的实用工具:Sm2Utils 和Sm2KeyPairUtil。这两个工具可以帮助您生成 SM2 加密密钥对、使用 SM2 算法进行加密和解密。

1. SM2 简介

SM2 国密SM2算法是中国国家密码管理局(CNCA)发布的一种非对称加密算法。它采用椭圆曲线密码体系(Elliptic Curve Cryptography,ECC)进行密钥交换、数字签名和公钥加密等操作。SM2算法和RSA算法都是公钥密码算法,SM2算法是一种更先进安全的算法,在我们国家商用密码体系中被用来替换RSA算法。

2. 添加BouncyCastle依赖

使用BouncyCastle库进行加解密操作,如果您使用Maven,则需要将以下依赖项添加到pom.xml文件中:

 <dependency>
     <groupId>org.bouncycastle</groupId>
     <artifactId>bcpkix-jdk15on</artifactId>
     <version>1.57</version>
 </dependency>
3. SM2 密钥对生成

首先,我们来看一下 Sm2KeyPairUtil 类,它负责生成 SM2 密钥对。


import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;
import java.security.*;
import java.security.spec.ECGenParameterSpec;

/**
 * author ks
 *
 * @version v0.9.0
 * @Package : com.xxxx.encrypt.utils.sm2
 * @Description : A utility class for generating and handling SM2 key pairs.
 * @Create on : 2024/6/20 16:03
 **/
public class Sm2KeyPairUtil {

    static {
        // Add BouncyCastle provider to the Security framework
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Generates an SM2 key pair.
     *
     * @return KeyPair containing SM2 public and private keys
     */
    public static KeyPair generateSM2KeyPair() {
        try {
            // Specify the SM2 algorithm parameter set
            ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
            // Initialize the KeyPairGenerator with the SM2 algorithm and BouncyCastle provider
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC");
            keyPairGenerator.initialize(sm2Spec, new SecureRandom());
            // Generate and return the key pair
            return keyPairGenerator.generateKeyPair();
        } catch (Exception e) {
            // Print the stack trace if an exception occurs
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Converts the SM2 public key to a hexadecimal string.
     *
     * @param keyPair The SM2 KeyPair containing the public key
     * @return Hexadecimal string representation of the SM2 public key
     */
    public static String getSM2PublicKeyHex(KeyPair keyPair) {
        if (keyPair == null || keyPair.getPublic() == null) {
            return null;
        }

        // Get the public key from the key pair and extract its ECPoint
        ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
        ECPoint q = publicKey.getQ();
        // Encode the ECPoint to a byte array and convert it to a hexadecimal string
        byte[] publicKeyBytes = q.getEncoded(false);
        return Hex.toHexString(publicKeyBytes);
    }

    /**
     * Converts the SM2 private key to a hexadecimal string.
     *
     * @param keyPair The SM2 KeyPair containing the private key
     * @return Hexadecimal string representation of the SM2 private key
     */
    public static String getSM2PrivateKeyHex(KeyPair keyPair) {
        if (keyPair == null || keyPair.getPrivate() == null) {
            return null;
        }

        // Get the private key from the key pair and convert its D value to a hexadecimal string
        ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
        return privateKey.getD().toString(16);
    }

    public static void main(String[] args) {
        // Generate an SM2 key pair
        KeyPair keyPair = generateSM2KeyPair();
        // Get the hexadecimal string representations of the public and private keys
        String sm2PublicKeyHex = getSM2PublicKeyHex(keyPair);
        String sm2PrivateKeyHex = getSM2PrivateKeyHex(keyPair);

        // Print the keys
        System.out.println("SM2 Public Key (Hex): " + sm2PublicKeyHex);
        System.out.println("SM2 Private Key (Hex): " + sm2PrivateKeyHex);
    }
}
说明
  • generateSM2KeyPair: 使用 BouncyCastle 提供程序生成 SM2 密钥对
  • getSM2PublicKeyHex: 将公钥转换为十六进制字符串
  • getSM2PrivateKeyHex: 将私钥转换为十六进制字符串
4. SM2 加密工具类

接下来,我们将探讨 Sm2Utils 类,它提供了使用 SM2 密钥进行数据加密和解密的方法。

package com.taryartar.encrypt.utils.sm2;

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.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;

import org.bouncycastle.util.encoders.Hex;

import java.security.Security;
import java.security.spec.InvalidKeySpecException;
/**
 * author ks
 *
 * @version v0.9.0
 * @Package : com.xxxx.encrypt.utils.sm2
 * @Description : Utility class for SM2 encryption and decryption.
 * @Create on : 2024/6/21 00:23
 **/
public class Sm2Util {

    private static final X9ECParameters X9_EC_PARAMETERS = GMNamedCurves.getByName("sm2p256v1");
    private static final ECParameterSpec EC_DOMAIN_PARAMETERS = new ECParameterSpec(X9_EC_PARAMETERS.getCurve(), X9_EC_PARAMETERS.getG(), X9_EC_PARAMETERS.getN());

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Encrypts data using an SM2 public key.
     *
     * @param publicKeyHex SM2 public key in hexadecimal format
     * @param plainText    Data to be encrypted
     * @return Encrypted data
     */
    public static String encrypt(String publicKeyHex, String plainText) throws InvalidCipherTextException, InvalidKeySpecException {
        ECPublicKey publicKey = convertHexToPublicKey(publicKeyHex);
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(publicKey.getQ(), new ECDomainParameters(publicKey.getParameters().getCurve(), publicKey.getParameters().getG(), publicKey.getParameters().getN()));

        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));

        byte[] plainBytes = plainText.getBytes();
        byte[] cipherBytes = sm2Engine.processBlock(plainBytes, 0, plainBytes.length);
        return Hex.toHexString(cipherBytes);
    }

    /**
     * Decrypts data using an SM2 private key.
     *
     * @param privateKeyHex SM2 private key in hexadecimal format
     * @param cipherText    Data to be decrypted
     * @return Decrypted data
     */
    public static String decrypt(String privateKeyHex, String cipherText) throws InvalidKeySpecException {
        BCECPrivateKey privateKey = convertHexToPrivateKey(privateKeyHex);
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKey.getD(), new ECDomainParameters(privateKey.getParameters().getCurve(), privateKey.getParameters().getG(), privateKey.getParameters().getN()));
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        sm2Engine.init(false, privateKeyParameters);
        try {
            byte[] cipherBytes = Hex.decode(cipherText);
            byte[] plainBytes = sm2Engine.processBlock(cipherBytes, 0, cipherBytes.length);
            return new String(plainBytes);
        } catch (Exception e) {
            System.err.println("Error occurred during SM2 decryption: " + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Converts a hexadecimal string to a BCECPublicKey.
     *
     * @param publicKeyHex Public key in hexadecimal format
     * @return BCECPublicKey instance
     */
    private static BCECPublicKey convertHexToPublicKey(String publicKeyHex) throws InvalidKeySpecException {
        if (publicKeyHex.length() > 128) {
            publicKeyHex = publicKeyHex.substring(publicKeyHex.length() - 128);
        }
        String xCoord = publicKeyHex.substring(0, 64);
        String yCoord = publicKeyHex.substring(64);
        BigInteger x = new BigInteger(xCoord, 16);
        BigInteger y = new BigInteger(yCoord, 16);
        ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(X9_EC_PARAMETERS.getCurve().createPoint(x, y), EC_DOMAIN_PARAMETERS);

        return new BCECPublicKey("EC", publicKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    /**
     * Converts a hexadecimal string to a BCECPrivateKey.
     *
     * @param privateKeyHex Private key in hexadecimal format
     * @return BCECPrivateKey instance
     */
    private static BCECPrivateKey convertHexToPrivateKey(String privateKeyHex) throws InvalidKeySpecException {
        BigInteger d = new BigInteger(privateKeyHex, 16);
        ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(d, EC_DOMAIN_PARAMETERS);

        return new BCECPrivateKey("EC", privateKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    /**
     * Sign data with a private key using SM2 and SM3 algorithms.
     *
     * @param data       The data to be signed.
     * @param privateKey The private key used for signing.
     * @return The signature bytes.
     * @throws Exception If an error occurs during the signing process.
     */
    public static byte[] signWithPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);
        signature.initSign(privateKey);
        signature.update(data);
        return signature.sign();
    }

    /**
     * Verify the signature of the data using a public key with SM2 and SM3 algorithms.
     *
     * @param data      The data whose signature needs to be verified.
     * @param publicKey The public key used for verification.
     * @param signature The signature bytes to be verified.
     * @return True if the signature is valid, false otherwise.
     * @throws Exception If an error occurs during the verification process.
     */
    public static boolean verifyWithPublicKey(byte[] data, PublicKey publicKey, byte[] signature) throws Exception {
        Signature signature1 = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);
        signature1.initVerify(publicKey);
        signature1.update(data);
        return signature1.verify(signature);
    }

    public static void main(String[] args) throws Exception {
        String publicKey = "04e7c3a338ed9ac8fb0d807bc8dbba3886daf6f7d09143452d3e624d54ad5c3c249408a7ad67caabf4221550f07d6563a1deef1fde2fb88dfe9ae7a2e78e27a693";
        String privateKey = "f6cfd33d9376aacf40edfae52a7e5b4b72a94cafc68089bb26cfe7e2b7817cc2";

        String plainText = "Hello, World!";
        String encryptedText = encrypt(publicKey, plainText);
        System.out.println("Encrypted Data: " + encryptedText);

        String decryptedText = decrypt(privateKey, "32e212000dab17ef2c4fdebeab603cebb36c5d6e11b08771078a9b0190a9d81f41ac8d61b976e0abcb0c9f1fa570b478f6c0faf51989d52fc9b3b64796a74efd60fdce3379e18e087fedf3647ea638d7c1c0e3608060be63d163b4f0d1c5b91fce8ba5aa26c0230af592b101c21e7c83b225f19fbf7397c71235869282");
        System.out.println("Decrypted Data: " + decryptedText);

        // Sign the data
        byte[] signature = Sm2Utils.signWithPrivateKey(plainText.getBytes(StandardCharsets.UTF_8), (PrivateKey) convertHexToPrivateKey(privateKey));
        System.out.println("signature Bytes (Hex): " + Hex.toHexString(signature));

        // Verify the signature
        boolean isValid = Sm2Utils.verifyWithPublicKey(plainText.getBytes(StandardCharsets.UTF_8), (PublicKey) convertHexToPublicKey(publicKey), signature);

        // Print the signature for debugging
        System.out.println("Signature: " + Base64.getEncoder().encodeToString(signature));
        System.out.println("Signature verify result: " + isValid);

        // Modify the data slightly
        byte[] modifiedData = "Hello, this is a tampered message.".getBytes();

        // Verify the signature with the modified data
        boolean isInvalid = Sm2Utils.verifyWithPublicKey(modifiedData, (PublicKey) convertHexToPublicKey(publicKey), signature);

        System.out.println("Signature verify result: " + isInvalid);

    }
}
说明
  • encrypt: 使用十六进制格式的 SM2 公钥加密数据
  • decrypt: 使用十六进制格式的 SM2 私钥解密数据
  • convertHexToPublicKey: 将十六进制字符串转换为 BCECPublicKey
  • convertHexToPrivateKey: 将十六进制字符串转换为 BCECPrivateKey
  • signWithPrivateKey: 使用带有SM2和SM3算法的私钥进行数据进行签名
  • verifyWithPublicKey: 使用带有SM2和SM3算法的公钥验证数据的签名
5. 结论

通过使用 Sm2KeyPairUtil,你可以轻松生成 SM2 密钥对,并且通过 Sm2Util,您可以使用 SM2 密钥进行数据加密和解密。这些实用工具简化了使用 SM2 算法的过程。

6. 问题集
  1. 前端采用sm-crypto进行加解密,需要仔细检查补码“04”的问题。
    例如:

后端加密字符串:
0432e212000dab17ef2c4fdebeab603cebb36c5d6e11b08771078a9b0190a9d81f41ac8d61b976e0abcb0c9f1fa570b478f6c0faf51989d52fc9b3b64796a74efd60fdce3379e18e087fedf3647ea638d7c1c0e3608060be63d163b4f0d1c5b91fce8ba5aa26c0230af592b101c21e7c83b225f19fbf7397c71235869282

前端解密字符串:
32e212000dab17ef2c4fdebeab603cebb36c5d6e11b08771078a9b0190a9d81f41ac8d61b976e0abcb0c9f1fa570b478f6c0faf51989d52fc9b3b64796a74efd60fdce3379e18e087fedf3647ea638d7c1c0e3608060be63d163b4f0d1c5b91fce8ba5aa26c0230af592b101c21e7c83b225f19fbf7397c71235869282

备注:解密时需要把16进制前端的04取消掉,按照如下字符串进行解密:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐