Bootstrap

Java后台接口做加密、数字签名、验签

在Java项目中,提供给外部应用厂商服务接口是比较常见的。如何保证数据传输时的安全性、保密性呢?本文介绍了使用数字签名来确保接口数据传输的安全性与保密性。

签名和加密是两个不同的安全措施,它们在数字通信中扮演着不同的角色:

  1. 签名:数字签名用于验证消息的完整性和来源。它确保消息在传输过程中未被篡改,并且可以确认消息确实是由声称的发送者发送的。数字签名通常使用非对称加密算法,如RSA,其中发送者使用私钥对消息进行签名,接收者使用发送者的公钥来验证签名。

  2. 加密:加密用于保护消息内容的机密性,确保只有拥有正确密钥的接收者才能阅读消息内容。加密可以是对称的(如AES),也可以是非对称的(如RSA),取决于具体的应用场景。

  3. 本文采用国密算法SM2非对称加密,需要注意的是公钥是公开的!你用私钥加密后,发出的数据,如果有保密性要求,这么做是无法保密的,所有知道公钥的人,都可以解密你的数据。必须使用公钥加密,发出的数据,只有私钥的人能解开。

在某些情况下,签名和加密可以结合使用,以提供更强的安全性:

  • 签名后加密:发送者首先对消息进行签名,然后对签名后的消息进行加密。这样做的好处是,接收者可以验证消息的来源和完整性,并且只有拥有正确密钥的人才能解密消息,确保内容的机密性。

  • 加密后签名:发送者首先对消息进行加密,然后对加密后的消息进行签名。这样做的好处是,接收者可以验证消息的完整性和来源,但无法在不解密的情况下验证消息内容。

选择哪种方式取决于具体的需求和安全要求。在某些情况下,可能只需要签名或加密中的一个,而在其他情况下,结合使用两者可以提供更全面的安全保障。

先加密后签名流程图

先签名后加密流程图

1. 数字签名确保完整性和真实性:数字签名技术利用非对称加密算法,通过哈希函数对原文进行处理生成哈希值,然后使用发送者的私钥对这个哈希值进行加密,生成数字签名。这个数字签名与原文一起传送给接收者。接收者使用发送者的公钥解密被加密的哈希值,然后对收到的原文执行相同的哈希运算得到新的哈希值,与解密得到的数字签名哈希值比对。如果两者一致,则说明信息在传输过程中没有被篡改,且确实来自声称的发送者。

2. 加密确保数据的保密性:为了确保数据的保密性,发送者在发送数据时,可以使用接收者的公钥对原文本身进行加密。这样,即使原文在传输过程中被截获,也只有拥有对应私钥的接收者才能解密数据,从而保证数据的机密性。

3. 结合使用数字签名和加密:在实际应用中,数字签名和加密技术常常结合使用。发送者首先对数据进行哈希运算生成摘要,然后用发送者的私钥对摘要进行加密生成数字签名,同时使用接收者的公钥对原文进行加密。接收者收到数据后,首先使用自己的私钥解密数据,然后使用发送者的公钥验证数字签名,确保数据的完整性和发送者的身份。这种方式既保证了数据的保密性,也确保了数据的完整性和发送者身份的验证。

通过这种方式,数字签名解决了数据在传输过程中的完整性和真实性问题,而加密技术则解决了数据的保密性问题。两者结合使用,既保证了数据的安全,也满足了保密性的要求。

JAVA实现(先签名后加密)

1. pom引入

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.79</version>
</dependency>

2.创建SM2Utils工具类

import com.alibaba.fastjson2.JSON;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.CipherParameters;
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.crypto.signers.SM2Signer;
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.math.ec.ECPoint;
import org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: ZHL
 * @CreateTime: 2024-12-19  09:07
 * @Description:
 **/
public class SM2Utils {

    /**
     * 生成 SM2 公私钥对
     *
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidAlgorithmParameterException
     */
    public static KeyPair geneSM2KeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        //使用sm2p256v1,这是SM2标准曲线之一
        final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
        // 获取一个椭圆曲线类型的密钥对生成器,EC为椭圆曲线算法
        final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
        // 产生随机数
        SecureRandom secureRandom = new SecureRandom();
        // 使用SM2参数初始化生成器
        kpg.initialize(sm2Spec, secureRandom);
        // 生成密钥对
        KeyPair keyPair = kpg.generateKeyPair();
        return keyPair;
    }

    /**
     * 生产hex秘钥对
     */
    public static void geneSM2HexKeyPair(){
        try {
            KeyPair keyPair = geneSM2KeyPair();
            PrivateKey privateKey = keyPair.getPrivate();
            PublicKey publicKey = keyPair.getPublic();
            System.out.println("========  EC X Y 秘钥对    ========");
            System.out.println(privateKey);
            System.out.println(publicKey);
            System.out.println("========  hex 秘钥对       ========");
            System.out.println("hex 私钥: " + getPriKeyHexString(privateKey));
            System.out.println("hex 公钥: " + getPubKeyHexString(publicKey));
            System.out.println("========  base64 秘钥对    ========");
            System.out.println("base64 私钥: " + new String(Base64.getEncoder().encode(privateKey.getEncoded())));
            System.out.println("base64 公钥: " + new String(Base64.getEncoder().encode(publicKey.getEncoded())));
            System.out.println("========  生成完成 ========");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取私钥(16进制字符串,头部不带00长度共64)
     *
     * @param privateKey 私钥PrivateKey型
     * @return
     */
    public static String getPriKeyHexString(PrivateKey privateKey) {
//        //方式一
//        BCECPrivateKey key=(BCECPrivateKey)privateKey;
//        //将私钥的数值部分编码为字节数组,并转换为16进制字符串
//        String priKeyHexString = Hex.toHexString(key.getD().toByteArray());
//        //如果生成的16进制字符串以"00"开头且长度为66个字符,则去掉前两个字符(00)
//        if(null!= priKeyHexString && priKeyHexString.length()==66 && "00".equals(priKeyHexString.substring(0,2))){
//            return priKeyHexString.substring(2);
//        }
        //方式二
        BCECPrivateKey key = (BCECPrivateKey) privateKey;
        //获取私钥数值部分
        BigInteger intPrivateKey = key.getD();
        //转换为16进制字符串
        String priKeyHexString = intPrivateKey.toString(16);
        return priKeyHexString;
    }
    /**
     * 获取私钥 base64字符串
     *
     * @param privateKey 私钥PrivateKey型
     * @return
     */
    public static String getPriKeyBase64String(PrivateKey privateKey) {
        return new String(Base64.getEncoder().encode(privateKey.getEncoded()));
    }

    /**
     * 获取公钥(16进制字符串,头部带04长度共130)
     *
     * @param publicKey 公钥PublicKey型
     * @return
     */
    public static String getPubKeyHexString(PublicKey publicKey) {
        BCECPublicKey key = (BCECPublicKey) publicKey;
        //获取公钥的Q值(椭圆曲线上的点),并将其编码为字节数组
        //参数false表示不使用压缩格式
        byte[] encodedKey = key.getQ().getEncoded(false);
        //将字节数组转换为16进制字符串并返回
        return Hex.toHexString(encodedKey);
    }
    /**
     * 获取公钥 base64字符串
     *
     * @param publicKey 公钥PublicKey型
     * @return
     */
    public static String getPubKeyBase64String(PublicKey publicKey) {
        return new String(Base64.getEncoder().encode(publicKey.getEncoded()));
    }

    /**
     * SM2加密算法
     *
     * @param publicKey 公钥
     * @param data      明文数据
     * @return
     */
    public static String encrypt(String data, PublicKey publicKey) {
        return encrypt(data.getBytes(StandardCharsets.UTF_8), publicKey);
    }

    public static String encrypt(byte[] data, PublicKey publicKey) {
        BCECPublicKey key = (BCECPublicKey) publicKey;
        return encrypt(data, Hex.toHexString(key.getQ().getEncoded(false)));
    }

    public static String encrypt(String data, String pubKeyHexString) {
        return encrypt(data.getBytes(StandardCharsets.UTF_8), pubKeyHexString);
    }

    /**
     * SM2加密算法
     *
     * @param pubKeyHexString 公钥(16进制字符串)
     * @param data            明文数据
     * @return hex字符串
     */
    public static String encrypt(byte[] data, String pubKeyHexString) {
        //获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //提取公钥点
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(pubKeyHexString));
        //公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);

        //初始化SM2加密引擎
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        //设置为加密模式,并传入公钥参数和随机数生成器
        sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));

        byte[] arrayOfBytes = null;
        try {
            //使用SM2引擎对数据进行加密
            arrayOfBytes = sm2Engine.processBlock(data, 0, data.length);
        } catch (Exception e) {
            System.out.println("SM2加密时出现异常:" + e.getMessage());
        }

        //将加密后的字节数组转化为16进制字符串返回
        return Hex.toHexString(arrayOfBytes);

    }

    /**
     * SM2解密算法
     * @param cipherData    hex格式密文
     * @param privateKey    密钥PrivateKey型
     * @return              明文
     */
    public static String decrypt(String cipherData, PrivateKey privateKey) {
        return decrypt(Hex.decode(cipherData), privateKey);
    }

    public static String decrypt(byte[] cipherData, PrivateKey privateKey) {
        BCECPrivateKey key = (BCECPrivateKey) privateKey;
        return decrypt(cipherData, Hex.toHexString(key.getD().toByteArray()));
    }

    public static String decrypt(String cipherData, String priKeyHexString) {
        // 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上
        if (!cipherData.startsWith("04")) {
            cipherData = "04" + cipherData;
        }
        return decrypt(Hex.decode(cipherData), priKeyHexString);
    }

    /**
     * SM2解密算法
     *
     * @param cipherData      密文数据
     * @param priKeyHexString 私钥(16进制字符串)
     * @return
     */
    public static String decrypt(byte[] cipherData, String priKeyHexString) {
        //从BC库中获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //使用获取到的曲线参数,构造椭圆曲线域参数
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //将16进制字符串形式的私钥转换为BigInteger
        BigInteger privateKeyD = new BigInteger(priKeyHexString, 16);
        //使用该私钥和域参数创建私钥参数对象
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
        //初始化SM2引擎
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        //设置SM2引擎为解密模式,并传入私钥参数
        sm2Engine.init(false, privateKeyParameters);

        String result = "";
        try {
            //使用SM2引擎对数据进行解密,并将解密后的字节数组转换为字符串返回
            byte[] arrayOfBytes = sm2Engine.processBlock(cipherData, 0, cipherData.length);
            return new String(arrayOfBytes);
        } catch (Exception e) {
            System.out.println("SM2解密时出现异常:" + e.getMessage());
        }
        return result;
    }

    /**
     * @param data
     * @param priKeyHexString hex私钥,长度64
     * @return hex格式签名值
     * @throws Exception
     */
    public static String sign(String data, String priKeyHexString) throws Exception {
        //将输入的数据转换为UTF-8编码的字节数组
        return sign(data.getBytes(StandardCharsets.UTF_8), priKeyHexString);
    }

    /**
     * 签名
     * @param data              原始数据,字节数组
     * @param priKeyHexString   hex私钥,64长度
     * @return                  Hex字符串
     * @throws Exception
     */
    public static String sign(byte[] data, String priKeyHexString) throws Exception {
        String signValue = null;
        //初始化SM2签名器对象
        SM2Signer signer = new SM2Signer();
        //获取SM2曲线参数,使用sm2p256v1曲线
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //构造域参数,包括椭圆曲线,基点G和阶N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //将16进制格式的私钥字符串转换为BigInteger,并结合随机数生成器创建加密参数
        CipherParameters param = new ParametersWithRandom(new ECPrivateKeyParameters(new BigInteger(priKeyHexString, 16), domainParameters));

        //初始化签名器,设置为签名模式,传入加密参数
        signer.init(true, param);

        //更新签名器的数据,即待签名的数据
        signer.update(data, 0, data.length);

        //生成签名,并将签名结果转换为16进制字符串
        signValue = Hex.toHexString(signer.generateSignature());

        //返回签名的16进制字符串
        return signValue;
    }

    /**
     * 验签
     * @param data                  原始数据
     * @param signValue             原始签名值(hex型)
     * @param publicKeyHexString    hex130长度公钥
     * @return                      ture or false
     * @throws Exception
     */
    public static boolean verify(String data, String signValue, String publicKeyHexString) throws Exception {
        return verify(data.getBytes(StandardCharsets.UTF_8), Hex.decode(signValue), publicKeyHexString);
    }

    /**
     * 验签
     * @param data                  原始数据字节数组
     * @param sign                  字节数组()
     * @param publicKeyHexString    hex130长度公钥
     * @return                      true or false
     * @throws Exception
     */
    public static boolean verify(byte[] data, byte[] sign, String publicKeyHexString) throws Exception {
        //初始化签名器
        SM2Signer signer = new SM2Signer();
        //获取SM2曲线参数,使用sm2p256v1曲线
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //构造域参数,包括椭圆曲线,基点G和阶N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //如果公钥长度为128位,则在前面加上"04",使其成为未压缩格式的公钥
        if (publicKeyHexString.length() == 128) {
            publicKeyHexString = "04" + publicKeyHexString;
        }
        //将16进制格式的公钥解码为椭圆曲线上的点
        ECPoint ecPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKeyHexString));
        //使用解码后的公钥和域参数创建加密参数
        CipherParameters param = new ECPublicKeyParameters(ecPoint, domainParameters);
        //初始化签名器,设置为验证模式
        signer.init(false, param);
        //更新签名器数据
        signer.update(data, 0, data.length);
        //验证签名,并返回结果
        return signer.verifySignature(sign);
    }

    /**
     * 私钥生成公钥
     * @param priKeyHexString 私钥Hex格式,必须64位
     * @return 公钥Hex格式,04开头,130位
     * @throws Exception 例如:
     *                   04181db7fe400641115c0dec08e23d8ddb94c5999f2fb6efd03030780142e077a63eb4d47947ef5baee7f40fec2c29181d2a714d9c6cba87b582f252a4e3e9a9f8
     *                   11d0a44d47449d48d614f753ded6b06af76033b9c3a2af2b8b2239374ccbce3a
     */
    public static String getPubKeyByPriKey(String priKeyHexString) throws Exception {
        if (priKeyHexString == null || priKeyHexString.length() != 64) {
            System.err.println("priKey 必须是Hex 64位格式,例如:11d0a44d47449d48d614f753ded6b06af76033b9c3a2af2b8b2239374ccbce3a");
            return "";
        }
        String pubKeyHexString = null;
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
        //构造domain参数
        BigInteger privateKeyD = new BigInteger(priKeyHexString, 16);

        ECParameterSpec ecParameterSpec = new ECParameterSpec(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(privateKeyD, ecParameterSpec);
        PrivateKey privateKey = null;
        privateKey = KeyFactory.getInstance("EC", new BouncyCastleProvider()).generatePrivate(ecPrivateKeySpec);

        // 临时解决办法
        String pointString = privateKey.toString();
//        System.out.println(pointString);
        String pointString_X = pointString.substring(pointString.indexOf("X: ") + "X: ".length(), pointString.indexOf("Y: ")).trim();
        String pointString_Y = pointString.substring(pointString.indexOf("Y: ") + "Y: ".length()).trim();
//        System.out.println(pointString_X);
//        System.out.println(pointString_Y);

        pubKeyHexString = "04" + pointString_X + pointString_Y;
        return pubKeyHexString;

    }

    public static void main(String[] args) throws Exception {
        System.out.println("======  sm2测试  ======");

        System.out.println("begin 开始生成密钥对>>>");
        KeyPair keyPair = geneSM2KeyPair();

        PublicKey publicKey = keyPair.getPublic();
        String pubKeyHexString = getPubKeyHexString(publicKey);
        System.out.println("公钥\t" + pubKeyHexString);

        PrivateKey privateKey = keyPair.getPrivate();
        String priKeyHexString = getPriKeyHexString(privateKey);
        System.out.println("私钥\t" + priKeyHexString);
        System.out.println("end   结束生成密钥对>>>");


        System.out.println("begin  服务器A开始>>>");
        String dataA = "hello_world!!";
        System.out.println("A的原始数据\t" + dataA);

        String priKeyHexStringA = "53796db25a1979bf218b2372945eaf63822ff01efa9b9c664aea8ca63f6fb118";//A的私钥
        //A使用A的私钥对dataA进行数字签名
        String signA = sign(dataA, priKeyHexStringA);
        System.out.println("A的数字签名\t" + signA);

        Map<String, String> map = new HashMap<>();
        map.put("data",dataA);
        map.put("sign",signA);

        String pubKeyHexStringB = "0423100fa792033c4eba474f92570b0393dab862b136227a5d6b43c7c13f0c92afd794a717b460c73513c5f019f612e9efbb9a4115794e73b76aad1368d2e26e37";//B的公钥
        //A使用B的公钥对A的原始数据和A的数字签名进行加密
        String cipherData = encrypt(JSON.toJSONString(map), pubKeyHexStringB);
        System.out.println("A的密文\t" + cipherData);
        System.out.println("end  服务器A结束>>>");


        System.out.println("begin  服务器B开始>>>");

        String priKeyHexStringB = "bba3b603d30fe9bd290818dcb80a8cc6b5d3e1d481284bb4c61592f3bc3b639f";//B的私钥
        //B使用B的私钥对A的密文进行解密
        String json = decrypt(cipherData, priKeyHexStringB);
        System.out.println("B解密A的密文,得到原始data和数字签名\t" + json);
        Map<String,String> map1 = JSON.parseObject(json, Map.class);

        String pubKeyHexStringA = "040de9df6124ed4c56c02f2c28a9ab4df0cec034fd1cd223ba7c275af82675eedbad186491b702d517b0d7682640ffb5f19de5f49bd334ea132463b90f9d748474";//A的公钥

        //B使用A的公钥对A的数字签名进行验签
        boolean verifyResult = verify(map1.get("data"), map1.get("sign"), pubKeyHexStringA);
        System.out.println("B对A的验签结果\t" + verifyResult);

        System.out.println("B获得A的原始数据\t"+map1.get("data"));
        System.out.println("end  服务器B结束>>>");
    }
}
;