浅谈国密算法

一、简介

国密算法是我国自主研发的密码算法体系,是为保护我国信息安全而开发的一套密码学算法,其标准由国家密码管理局及其密码管理标准委员会制定。

国密算法包括对称密码算法、非对称密码算法和杂凑算法,即SM1、SM2、SM3、SM4、SM7、SM9、祖冲之密码算法(ZUC)等。其中,SM1是一种分组密码算法,用于加密和解密数据,该算法未公开;SM2是一种非对称加密算法,基于离散对数实现,常用于数字签名和密钥交换;SM3是一种杂凑算法,用于数据完整性校验和消息认证码,该算法为不可逆的算法,未公开;SM4是一种分组密码算法,可用于替代DES/AES等国际密码算法,拥有和AES算法相同的密钥长度和分组长度,均为128比特;SM7是一种分组加密算法,该算法没有公开,SM7适用于非接IC卡应用包括身份识别类应用(门禁卡、工作证、参赛证),票务类应用(大型赛事门票、展会门票),支付与通卡类应用(积分消费卡、校园一卡通、企业一卡通、公交一卡通);SM9是基于标识的非对称密码算法,用椭圆曲线对实现的基于标识的数字签名算法、密钥交换协议、密钥封装机制和公钥加密与解密算法,包括数字签名生成算法和验证算法,并给出了数字签名与验证算法及其相应的流程;祖冲之密码算法是一种流加密算法,该算法可适用于3GPP LTE通信中的加密和解密。

国密算法因其高安全性、高效率和自主可控而被大力推广。高安全性是指国密算法采用了先进的密码学技术,能够抵御各种攻击手段;高效率是指国密算法在保证安全性的同时,具有较快的加密和解密速度,能够满足各种应用场景的需求;自主可控是指国密算法是我国自主研发的,不依赖于外国技术和标准,是保障信息安全的前提。

目前国密算法在我国的各行各业中得到了广泛应用,为我国的信息安全提供了坚强的保障。

总结:对称加密用SM4,非对称加密用SM2.

二、图解

三、SM4对称加密的应用

在与外部系统的对接中,对敏感数据进行加密处理。

1.依赖的引用

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.6.2</version>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.58</version>
</dependency>

2.创建SM4对象的几种方式

第一种:实例化SM4对象

package com.umetrip.airport.proxy.config;

import cn.hutool.crypto.symmetric.SM4;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.nio.charset.StandardCharsets;

/**
 * @ClassName SM4CryptoAlgorithmConfig
 * @Description TODO
 * @Author zcy
 * @Date 2023/11/23 11:29 AM
 */
@Configuration
public class SM4CryptoAlgorithmConfig {

    /**
     * Description 国密-对称加密
     */
    @Bean
    public SM4 sm4(@Value("密钥") String key) {
        System.out.println("key = " + key);
        return new SM4(key.getBytes(StandardCharsets.UTF_8));
    }
}

使用方式一:
       @Autowired
        private SM4 sm4;
        byte[] encrypt = sm4.encrypt("要加密信息");
        String s = Base64.getEncoder().encodeToString(encrypt);
        System.out.println("s = " + s);
        String result = sm4.decryptStr("要解密的信息");
使用方式二:
     SM4CryptoAlgorithmConfig sm4CryptoAlgorithmConfig = new SM4CryptoAlgorithmConfig();
        SM4 sm4 = sm4CryptoAlgorithmConfig.sm4("密钥");
        byte[] encrypt = sm4.encrypt("要加密信息");
        String s = Base64.getEncoder().encodeToString(encrypt);
        System.out.println("s = " + s);
        String result = sm4.decryptStr("要解密的信息");
第二种:不通过bean对象实例化
SM4 sm4 = new SM4("密钥".getBytes(StandardCharsets.UTF_8));
String encrypt = sm4.encryptBase64("要加密的信息");
String s = Base64.getEncoder().encodeToString(encrypt);
System.out.println("s = " + s);
String result = sm4.decryptStr("要解密的信息");

3.有坑的地方

必须排掉如下的依赖,否则会显示没有SM4这个算法

<exclusion>
    <artifactId>bcprov-jdk16</artifactId>
    <groupId>org.bouncycastle</groupId>
</exclusion>

四、SM2非对称加密的应用

用于同一系统中前后端交互中涉及到的敏感数据,如证件号、手机号等进行非对称加密

1.依赖的引用

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.68</version>
</dependency>

2.封装工具包

package com.umetrip.airport.api.util;

import org.apache.commons.net.util.Base64;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.InvalidCipherTextException;
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 org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.security.*;
import java.security.spec.ECGenParameterSpec;


/**
 * @ClassName SM2Utils
 * @Description TODO
 * @Author zcy
 * @Date 2023/11/24 5:06 PM
 */
public class SM2Utils {

    private static final String publicKey = "04716cc4d36c44ed10ef06700795af9508ad5d83df37398e515ae4f8cc10454e971c550ff0946152270ea42f7b748cbd8fdd41e8e958634d6978937de18a099bfa";
    private static final String privateKey = "64241e946ea9e6ce291560c45843e13734f90d6643cea63e003e386266315384";


    /**
     * 解密,并且获取解密后的字符串
     *
     * @return 秦无解
     */
    public static String getJieMiJieGuo(String cipherData) {
        byte[] cipherDataByte = Hex.decode(cipherData);
        BigInteger privateKeyD = new BigInteger(privateKey, 16);
        X9ECParameters sm2ECParameters1 = GMNamedCurves.getByName("sm2p256v1");
        ECDomainParameters domainParameters1 = new ECDomainParameters(sm2ECParameters1.getCurve(), sm2ECParameters1.getG(), sm2ECParameters1.getN());
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters1);

        //用私钥解密,SM2Engine.Mode.C1C3C2得加,不然就会报错invalid cipher text
        SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
        sm2Engine.init(false, privateKeyParameters);

        //processBlock得到Base64格式,记得解码
        byte[] arrayOfBytes = null;
        try {
            arrayOfBytes = Base64.decodeBase64(sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }

        //得到明文:SM2 Encryption Test
        return new String(arrayOfBytes);
    }


    /**
     * @Description 生成秘钥对
     * @Author msx
     * @return KeyPair
     */
    public static KeyPair createECKeyPair() {
        //使用标准名称创建EC参数生成的参数规范
        final ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");

        // 获取一个椭圆曲线类型的密钥对生成器
        final KeyPairGenerator kpg;
        try {
            kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
            // 使用SM2算法域参数集初始化密钥生成器(默认使用以最高优先级安装的提供者的 SecureRandom 的实现作为随机源)
            // kpg.initialize(sm2Spec);

            // 使用SM2的算法域参数集和指定的随机源初始化密钥生成器
            kpg.initialize(sm2Spec, new SecureRandom());

            // 通过密钥生成器生成密钥对
            return kpg.generateKeyPair();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * @Description 公钥加密
     * @Author msx
     * @param publicKeyHex SM2十六进制公钥
     * @param data         明文数据
     * @return String
     */
    public static String encrypt1(String publicKeyHex, String data) {
        return encrypt(getECPublicKeyByPublicKeyHex(publicKeyHex), data, 1);
    }

    /**
     * @Description 公钥加密
     * @Author msx
     * @param data         明文数据
     * @return String
     */
    public static String encrypt(String data) {
        return encrypt(getECPublicKeyByPublicKeyHex(publicKey), data, 1);
    }

    /**
     * @Description 公钥加密
     * @Author msx
     * @param publicKey SM2公钥
     * @param data      明文数据
     * @param modeType  加密模式
     * @return String
     */
    public static String encrypt(BCECPublicKey publicKey, String data, int modeType) {
        //加密模式
        SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2;
        if (modeType != 1) {
            mode = SM2Engine.Mode.C1C2C3;
        }
        //通过公钥对象获取公钥的基本域参数。
        ECParameterSpec ecParameterSpec = publicKey.getParameters();
        ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                ecParameterSpec.getG(), ecParameterSpec.getN());
        //通过公钥值和公钥基本参数创建公钥参数对象
        ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(publicKey.getQ(), ecDomainParameters);
        //根据加密模式实例化SM2公钥加密引擎
        SM2Engine sm2Engine = new SM2Engine(mode);
        //初始化加密引擎
        sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
        byte[] arrayOfBytes = null;
        try {
            //将明文字符串转换为指定编码的字节串
            byte[] in = data.getBytes("utf-8");
            //通过加密引擎对字节数串行加密
            arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
        } catch (Exception e) {
            System.out.println("SM2加密时出现异常:" + e.getMessage());
            e.printStackTrace();
        }
        //将加密后的字节串转换为十六进制字符串
        return Hex.toHexString(arrayOfBytes);
    }

    /**
     * @Description 私钥解密
     * @Author msx
     * @param privateKeyHex SM2十六进制私钥
     * @param cipherData    密文数据
     * @return String
     */
    public static String decrypt1(String privateKeyHex, String cipherData) {
        return decrypt(getBCECPrivateKeyByPrivateKeyHex(privateKeyHex), cipherData, 1);
    }

    /**
     * @Description 私钥解密
     * @Author msx
     * @param s
     * @param cipherData    密文数据
     * @return String
     */
    public static String decrypt(String s, String cipherData) {
        return decrypt(getBCECPrivateKeyByPrivateKeyHex(privateKey), cipherData, 1);
    }

    /**
     * @Description 私钥解密
     * @Author msx
     * @param privateKey SM私钥
     * @param cipherData 密文数据
     * @param modeType   解密模式
     * @return
     */
    public static String decrypt(BCECPrivateKey privateKey, String cipherData, int modeType) {
        //解密模式
        SM2Engine.Mode mode = SM2Engine.Mode.C1C3C2;
        if (modeType != 1) {
            mode = SM2Engine.Mode.C1C2C3;
        }
        //将十六进制字符串密文转换为字节数组(需要与加密一致,加密是:加密后的字节数组转换为了十六进制字符串)
        byte[] cipherDataByte = Hex.decode(cipherData);
        //通过私钥对象获取私钥的基本域参数。
        ECParameterSpec ecParameterSpec = privateKey.getParameters();
        ECDomainParameters ecDomainParameters = new ECDomainParameters(ecParameterSpec.getCurve(),
                ecParameterSpec.getG(), ecParameterSpec.getN());
        //通过私钥值和私钥钥基本参数创建私钥参数对象
        ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(privateKey.getD(),
                ecDomainParameters);
        //通过解密模式创建解密引擎并初始化
        SM2Engine sm2Engine = new SM2Engine(mode);
        sm2Engine.init(false, ecPrivateKeyParameters);
        String result = null;
        try {
            //通过解密引擎对密文字节串进行解密
            byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
            //将解密后的字节串转换为utf8字符编码的字符串(需要与明文加密时字符串转换成字节串所指定的字符编码保持一致)
            result = new String(arrayOfBytes, "utf-8");
        } catch (Exception e) {
            System.out.println("SM2解密时出现异常" + e.getMessage());
        }
        return result;
    }
    //椭圆曲线ECParameters ASN.1 结构
    private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
    //椭圆曲线公钥或私钥的基本域参数。
    private static ECParameterSpec ecDomainParameters = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());

    /**
     * @Description 公钥字符串转换为 BCECPublicKey 公钥对象
     * @Author msx
     * @param pubKeyHex 64字节十六进制公钥字符串(如果公钥字符串为65字节首个字节为0x04:表示该公钥为非压缩格式,操作时需要删除)
     * @return BCECPublicKey SM2公钥对象
     */
    public static BCECPublicKey getECPublicKeyByPublicKeyHex(String pubKeyHex) {
        //截取64字节有效的SM2公钥(如果公钥首个字节为0x04)
        if (pubKeyHex.length() > 128) {
            pubKeyHex = pubKeyHex.substring(pubKeyHex.length() - 128);
        }
        //将公钥拆分为x,y分量(各32字节)
        String stringX = pubKeyHex.substring(0, 64);
        String stringY = pubKeyHex.substring(stringX.length());
        //将公钥x、y分量转换为BigInteger类型
        BigInteger x = new BigInteger(stringX, 16);
        BigInteger y = new BigInteger(stringY, 16);
        //通过公钥x、y分量创建椭圆曲线公钥规范
        ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(x9ECParameters.getCurve().createPoint(x, y), ecDomainParameters);
        //通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于SM2加密及验签)
        return new BCECPublicKey("EC", ecPublicKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    /**
     * @Description 私钥字符串转换为 BCECPrivateKey 私钥对象
     * @Author msx
     * @param privateKeyHex 32字节十六进制私钥字符串
     * @return BCECPrivateKey SM2私钥对象
     */
    public static BCECPrivateKey getBCECPrivateKeyByPrivateKeyHex(String privateKeyHex) {
        //将十六进制私钥字符串转换为BigInteger对象
        BigInteger d = new BigInteger(privateKeyHex, 16);
        //通过私钥和私钥域参数集创建椭圆曲线私钥规范
        ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, ecDomainParameters);
        //通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名)
        return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
    }

    public static void main(String[] args) {
        String publicKeyHex = "04716cc4d36c44ed10ef06700795af9508ad5d83df37398e515ae4f8cc10454e971c550ff0946152270ea42f7b748cbd8fdd41e8e958634d6978937de18a099bfa";
        String privateKeyHex = "64241e946ea9e6ce291560c45843e13734f90d6643cea63e003e386266315384";
//        KeyPair keyPair = createECKeyPair();
//        PublicKey publicKey = keyPair.getPublic();
//        if (publicKey instanceof BCECPublicKey) {
//            //获取65字节非压缩缩的十六进制公钥串(0x04)
//            publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false));
//            System.out.println("---->SM2公钥:" + publicKeyHex);
//        }
//        PrivateKey privateKey = keyPair.getPrivate();
//        if (privateKey instanceof BCECPrivateKey) {
//            //获取32字节十六进制私钥串
//            privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16);
//            System.out.println("---->SM2私钥:" + privateKeyHex);
//        }

        /**
         * 公钥加密
         */
        String data = "=========待加密数据=========";

        //将十六进制公钥串转换为 BCECPublicKey 公钥对象
        String encryptData = encrypt1(publicKeyHex, "131022200099876545");
        System.out.println("---->加密结果:" + encryptData);

        /**
         * 私钥解密
         */
        //将十六进制私钥串转换为 BCECPrivateKey 私钥对象
        data = decrypt1(privateKeyHex, "04c6506bce3c24b4ab91fc83584e335b2d9ce209215bd1a56f878b05f4b409fcaf2dc4d7e3c3b530e795a221e3e5643a10ec58a840e196dc743f9a93fc6e86337ba4e2f75224dc07d8a23400ab3fd79b947a3f75073c671926d6cafe85d46aab0d0f7f0745acd4a6adb42a8634a09638f0d323");
        System.out.println("---->解密结果:" + data);
    }
}

3.有坑的地方

 

密文:04fce1321337fe91763f0eb10023aedb64f28a39781b0e5b7a41af43c14837df8e9086bc3d15b67d163b7664389af2ef6362e33051c22726f3d4ec9a8890ac38a369485a0cc3784ddde4dc28703d396c001382700535dc8b52a92a432802a864ed46b0686db1fd71c63ff27e119e74c41f9d3e

  1. SM2 之前缀标志位 0x04

如果你在项目中使用 BouncyCastle 库过程中,在解密时得到了 Invalid point encoding 0x5c 这样的异常,那么很可能是因为密文开头前未追加前缀标识标志 0x04

这也是在前文第 5 小节提到 ASN.1 编码时所讲的,为何密文结果有时比明文多 96 字节、有时多 97 字节,这多出来的 1 个字节实际就是这个标志位。该标志位为 SM2 国标中所规定的固定值。

与之类似的是,在前文第 4 小节提到的 ECC 公钥中,其开头也是固定的 0x04,该标志位表示未压缩公钥。与之对应的还有压缩公钥,开头固定为 0x020x03(取决于构成 ECC 公钥两个坐标点的奇偶性)。

因此,当你在使用某些编程语言的加密库,或是某些在线加解密工具,请先注意其所支持的公钥或密文,是否开头需要额外追加或删除 0x04

  1. SM2 之前缀标志位 0x00

此问题一般出现在 Java 项目中,因为 Java 中的 byte 这种数据结构是有符号的,其表示范围并非像其他大多数语言中那样的 0~255、而是 -128~127。所以在构造 ECC 私钥所需的 BigInteger 时,需在开头额外指定符号标志位 0x00

换而言之,Java 中使用的 ECC 私钥长度为 33 字节,而非其他大多数语言中的 32 字节。

因此,当你在使用某些编程语言的加密库,或是某些在线加解密工具,请先注意其所支持的私钥,是否开头需要额外追加或删除 0x00

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值