国密套件的另一种选择:腾讯Kona国密套件

2,467 阅读8分钟

前言

市面上关于国密的文章其实已经很多了,我在好多年前也写过使用 bouncycastle 库的博客,但我用的那个bc库对国密的支持不能说没有吧,只能说坑很多。具体版本是1.57,现在你去百度搜索“bc库国密踩坑”这几个关键字,不出意外的话出来的第一个结果就是我曾经写的踩坑指南。阅读量还不少,可见踩坑的人也不少。后来我想升级,但发现新版的写法变化很大,都变化这么大了,我干嘛不找找别的国密库呢。

正文开始

在一次看国内开源项目的时候看到阿里的铜锁、腾讯国密Kona套件(腾讯有个jdk也加kona),不过铜锁好像没有java版,然后我就选择了kona作为新的国密组件。

那kona和BC库的使用又有什么不一样呢,它遵循标准的JCA框架实现了国密密码学算法,这个还是比较吸引人的,如果从RSA改为SM2可以说只需要很小的改动就可以了,当然你的RSA也需要时标准的写法。官方文档有些细节的地方没写出来,可以参考我的笔记。

首先是依赖

需要最新版去看github仓库的最新release。

<dependency>
    <groupId>com.tencent.kona</groupId>
    <artifactId>kona-crypto</artifactId>
    <version>1.0.12</version>
</dependency>

kona套件提供了国密证书的解析与验证和国密传输层的支持,这里我们只需要SM2 SM3 SM4,所以只需要引入 kona-crypto 就可以了。

然后可能还需要一个16进制转换相关的包,我用的是 org.apache.commons 包提供的 HEX 工具类

使用

辅助类

这里我创建了一个辅助类来封装公钥私钥对。

public class KeyPairOfString {
    /**公钥*/
    private String publicKey;
    /**私钥*/
    private String privateKey;

    /**
     * 无参构造方法
     */
    public KeyPairOfString(){
        super();
    }

    /**
     * 构造方法
     * @param publicKey
     * @param privateKey
     */
    public KeyPairOfString(String publicKey, String privateKey){
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }
}

加载

在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider,所以我是所有调用国密方法中都加上了下面的代码

// 内部已经做了判断,重复调用不影响
Security.addProvider(new KonaCryptoProvider());

创建密钥对

/**
 * 生成SM2密钥对并放入自定义封装中
 */
public static KeyPairOfString generateSm2KeyPair() {
    // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
    Security.addProvider(new KonaCryptoProvider());
    KeyPairGenerator keyPairGenerator = null;
    try {
        // 标准JCA写法
        keyPairGenerator = KeyPairGenerator.getInstance("SM2");
    } catch (NoSuchAlgorithmException e) {
        // 转换为运行时异常
        throw new RuntimeException(e);
    }
    // 拿到密钥对象
    KeyPair keyPair = keyPairGenerator.generateKeyPair();
    ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
    ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
    // 将公钥和私钥转换成16进制字符串,方便传输存储
    // 公钥为长度为65字节,格式为04||x||y,其中04表示非压缩格式,x和y分别为该公钥点在椭圆曲线上的仿射横坐标和纵坐标的值
    String publickeyHex = Hex.encodeHexString(publicKey.getEncoded());
    // 私钥长度为32字节
    String privateHex = Hex.encodeHexString(privateKey.getEncoded());
    return new KeyPairOfString(publickeyHex, privateHex);
}

SM2公钥加密

国密官方文档中密文排序格式为C1||C3||C2,最早的版本有过C1||C2||C3的标准,所以导致某些库的老版本支持C1||C2||C3的模式。

注意:一般js库或者BC库,都没有按照ASN.1 DER 格式来处理。但是国密标准中密文传输推荐采用ASN.1 DER 格式,所以Kona默认生成和接受的都是该格式的数据,和这些工具对接时要注意转换,Kona提供了一个转换工具 SM2Ciphertext

  • C1||C3||C2 with ASN.1 DER 格式转成原始的C1C3C2格式
// C1||C3||C2 with ASN.1 DER 格式转成原始的C1C3C2格式
byte[] rawC1C3C2 = SM2Ciphertext.builder()
                 .format(SM2Ciphertext.Format.DER_C1C3C2) // 指定输入数据的格式
                 .encodedCiphertext(temp) // 按输入格式进行编码
                 .build()  
                 .rawC1C3C2(); // 转换格式

  • 将原始的C1C3C2格式转换成C1||C3||C2 with ASN.1 DER 格式
// 将原始的C1C3C2格式转换成C1||C3||C2 with ASN.1 DER 格式
// js加密的数据一般都不带04前缀,没有的情况补上
if (!ciphertext.startsWith("04")){
    ciphertext = "04" + ciphertext;
}
byte[] derC1C3C2 = SM2Ciphertext.builder()
                 .format(SM2Ciphertext.Format.RAW_C1C3C2)
                 .encodedCiphertext(ciphertext)
                 .build()
                 .derC1C3C2();
  • 使用公钥加密
/**
 * sm2公钥加密
 * <p>出于性能考虑,与其它的非对称加密算法(如RSA和EC)相同,SM2加密算法一般只用于加密少量的关键性数据.</p>
 * <p>生成的格式为标准的C1||C3||C2 with ASN.1 DER 格式,如需其他格式请自行转换</p>
 * @param publicKeyStr sm2公钥(16进制字符串)
 * @param data 待加密数据
 * @return 加密后的数据(16进制字符串,C1||C3||C2 with ASN.1 DER 格式)
 */
public static String encrypt(String publicKeyStr, String data){
    // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
    Security.addProvider(new KonaCryptoProvider());
    try {
        KeyFactory keyFactory = KeyFactory.getInstance("SM2");
        SM2PublicKeySpec publicKeySpec = new SM2PublicKeySpec(Hex.decodeHex(publicKeyStr));
        ECPublicKey publicKey = (ECPublicKey)keyFactory.generatePublic(publicKeySpec);
        Cipher cipher = Cipher.getInstance("SM2");
        // 使用公钥对Cipher进行初始化,指定其使用加密模式。
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        // 待加密数据转成byte并加密
        byte[] message = data.getBytes(CHARSETS_DEFULT);
        byte[] ciphertext = cipher.doFinal(message);
        return Hex.encodeHexString(ciphertext);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

SM2私钥解密

/**
 * sm2私钥解密,密文格式为C1||C3||C2 with ASN.1 DER 格式,如果是其他格式请先使用SM2Ciphertext工具类进行转换
 * @param privateKeyStr sm2私钥(16进制字符串)
 * @param data 待解密数据(16进制字符串,C1||C3||C2 with ASN.1 DER 格式)
 */
public static String decrypt(String privateKeyStr, String data) {
    try {
        // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
        Security.addProvider(new KonaCryptoProvider());
        // 获取密钥工厂,获取私钥对象
        KeyFactory keyFactory = KeyFactory.getInstance("SM2");
        SM2PrivateKeySpec privateKeySpec = new SM2PrivateKeySpec(Hex.decodeHex(privateKeyStr));
        PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
        // 初始化cipher
        Cipher cipher = Cipher.getInstance("SM2");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        // 已加密数据解成byte并解密
        byte[] dataByte = Hex.decodeHex(data);
        byte[] cleartext = cipher.doFinal(dataByte);
        return new String(cleartext, CHARSETS_DEFULT);
    }catch (Exception e){
        throw new RuntimeException(e);
    }
}

SM3

使用SM3算法与使用JDK自带的其它哈希算法(如SHA-256)的方式是完全相同的,仅需要调用JDK API就可以生成消息摘要(哈希值)。

可以一次性输入全部消息数据,然后生成消息摘要。也可以分多次传递消息数据的片断,最后再生成消息摘要。

/**
 * 使用sm3生成信息摘要
 * @param content 需要生成摘要的消息内容
 */
public static String digest(String... content) {
    try {
        // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
        Security.addProvider(new KonaCryptoProvider());
        MessageDigest md = MessageDigest.getInstance("SM3");
        // 循环调用输入消息内容
        for (String s : content){
            md.update(s.getBytes(CHARSETS_DEFULT));
        }
        // 最后再生成消息摘要
        byte[] digest = md.digest();
        return Hex.encodeHexString(digest);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

SM4

使用SM4算法与使用JDK自带的其它分组加密算法(如AES)的方式是完全相同的,仅需要调用JDK API就可以进行SM4加密和解密操作。KonaCrypto支持了SM4的四种分组操作模式,包括CBC,CTR,ECB和GCM,同时还支持了PKCS#7填充规范。

准备密钥,其长度为16字节。这里我只写一下 ECB 模式的写法

/**
 * SM4/ECB/PKCS7Padding对称加密算法
 * @param key 密钥,每两位组长一个16进制数,需要16组
 */
public static String encryptByEcb(String content, String key){
    try {
        // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
        Security.addProvider(new KonaCryptoProvider());
        // 这里密钥的解码方式有两种按理应该是Hex.decodeHex(key),但是这种模key.length()要等于32才行也就是两个字符标识一个16进制数, 想用16个字符就用 key.getbytes()。这里可能我也没有理解清楚,欢迎有懂得兄弟指教。 
        byte[] keys;
        if (key.length() == 16){
            keys = key.getBytes(StandardCharsets.UTF_8);
        }else{
            keys = Hex.decodeHex(key);
        }
        SecretKey secretKey = new SecretKeySpec(keys, "SM4");
        // 创建Cipher实例
        Cipher cipher = Cipher.getInstance(SM4_ECB_PKCS7_TRANSFORMATION);
        // 初始化cipher,指定使用加密模式
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        // 传入消息内容并加密
        byte[] contentByte = content.getBytes(CHARSETS_DEFULT);
        byte[] ciphertext = cipher.doFinal(contentByte);
        return Hex.encodeHexString(ciphertext);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
/**
 * SM4/ECB/PKCS7Padding对称解密
 * @param key 密钥,每两位组长一个16进制数,需要16组
 */
public static String decryptByEcb(String cipherContent, String key){
    try {
        // 在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider
        Security.addProvider(new KonaCryptoProvider());
        // 兼容密钥的length为16和32的
        byte[] keys;
        if (key.length() == 16){
            keys = key.getBytes(StandardCharsets.UTF_8);
        }else{
            keys = Hex.decodeHex(key);
        }
        SecretKey secretKey = new SecretKeySpec(keys, "SM4");
        // 创建Cipher实例
        Cipher cipher = Cipher.getInstance(SM4_ECB_PKCS7_TRANSFORMATION);
        // 初始化cipher,指定使用解密模式
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        // 解密
        byte[] cipherByte = Hex.decodeHex(cipherContent);
        byte[] cleartext = cipher.doFinal(cipherByte);
        return new String(cleartext, CHARSETS_DEFULT);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

结束语

以上只是简单的写了国密SM2、SM3、SM4的最常见的用法,如果你只是找一个能用的国密工具类,复制粘贴就可以了,但你想要多了解一点,那么还是建议你看看其他资料。

参考资料

Kona国密套件直接搜腾讯kona的话出来的可能kona jdk

用腾讯KonaCrypto国密SM2套件的时候,无法解密JS加密的SM2 · Issue #363

国家密码管理局关于发布《SM2椭圆曲线公钥密码算法》公告(国密局公告第21号)