English | 中文
腾讯Kona Crypto包含三个Java Security Provider,包括KonaCrypto,KonaCrypto-Native和KonaCrypto-NativeOneShot。它们遵循相关的国家标准实现了如下的国密基础算法:
为了提供上述特性,这些Provider基于JDK标准的Java Cryptography Architecture (JCA)框架,实现了JDK定义的KeyPairGeneratorSpi,SignatureSpi,CipherSpi,MessageDigestSpi,MacSpi和KeyAgreementSpi等Service Provider Interface (SPI)。
目前提供了纯Java语言实现的KonaCrypto Provider,以及基于JNI与OpenSSL实现的KonaCrypto-Native和KonaCrypto-NativeOneShot Provider。后两者仅支持Linux x86_64/aarch64平台。本项目默认使用的OpenSSL版本为3.4.0,但可以支持3.0及之后的版本。
KonaCrypto-Native和KonaCrypto-NativeOneShot的主要区别:
KonaCrypto-Native基于PhantomReference自动地管理JNI本地内存,会复用OpenSSL本地上下文,这会提高计算性能。但过多的创建算法实例,如Cipher,会增大GC的开销,GC时间会显著增长。KonaCrypto-NativeOneShot不会自动地管理JNI本地,应用程序需要确保调用最终操作,如Cipher::doFinal,去释放内存。而且它不会复用OpenSSL本地上下文,计算性能可能会低一些。但该Provider几乎不会增加GC的开销,GC时长与纯Java实现的GC时长相当。可以使用系统属性com.tencent.kona.openssl.crypto.lib.path去指定使用其他的OpenSSL crypto库文件(libcrypto.so),该系统属性的值是一个本地绝对路径。
应用程序使用KonaCrypto,KonaCrypto-Native和KonaCrypto-NativeOneShot的方法基本相同,所以本文仅以KonaCrypto为例来描述用法。
由于KonaCrypto是基于JCA框架的,所以在使用风格上,与其它的JCA实现(如JDK自带的SunJCE和SunEC)是一样的。正常地,应用程序并不需要直接访问KonaCrypto中的算法实现类,而是通过相关的JDK API去调用指定算法的实现。了解JCA的设计原理与代码风格,对于应用KonaCrypto是非常有帮助的,请阅读官方的[参考指南]。
在使用KonaCrypto中的任何特性之前,必须要加载KonaCryptoProvider,
Security.addProvider(new KonaCryptoProvider());
上面的方法会将这个Provider加到整个Provider列表的最后一位,其优先级则为最低。如有必要,可以使用下面的方法将它们插入到Provider列表的指定位置,
Security.insertProviderAt(new KonaCryptoProvider(), position);
position的值越小,代表的优先级越高,最小可为1。然而,并不推荐提升该Provider的优先级,故推荐使用Security.addProvider。
生成SM2密钥对与生成JDK自带的其它算法(如EC)密钥对的方式是完全相同的,仅需要调用标准的JDK API就可以了。KonaCrypto提供了两个KeyPairGenerator实现去生成SM2密钥对:
ECKeyPairGenerator。它生成的密钥对中,私钥格式为PKCS#8,公钥格式为X.509。SM2KeyPairGenerator。它生成的密钥对中,私钥和公钥格式均为RAW。私钥长度为32字节。公钥为长度为65字节,格式为04||x||y,其中04表示非压缩格式,x和y分别为该公钥点在椭圆曲线上的仿射横坐标和纵坐标的值。创建使用ECKeyPairGenerator的KeyPairGenerator实例。
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC); keyPairGenerator.initialize(spec);
其中,spec可以为SM2ParameterSpec(使用SM2ParameterSpec.instance()创建它的实例)或者是ECGenParameterSpec(使用new ECGenParameterSpec("curveSM2")创建它的实例)。
若创建使用SM2KeyPairGenerator的KeyPairGenerator实例,则代码如下。
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("SM2);
生成密钥对。
KeyPair keyPair = keyPairGenerator.generateKeyPair(); ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
关于密钥对生成器API的更详细用法,请参考KeyPairGenerator的官方文档。
一般情况下,在签名和加密操作中,都是使用已有的密钥对,并不需要临时生成。所以需要像下面那样,读取公钥与私钥数据,分别生成PublicKey和PrivateKey对象。
byte[] encodedPublicKey = <编码形式的公钥>; byte[] encodedPrivateKey = <编码形式的私钥>; KeyFactory keyFactory = KeyFactory.getInstance("SM2"); SM2PublicKeySpec publicKeySpec = new SM2PublicKeySpec(encodedPublicKey); PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); SM2PrivateKeySpec privateKeySpec = new SM2PrivateKeySpec(encodedPrivateKey); PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
使用SM2签名算法与使用JDK自带的其它签名算法(如ECDSA)的方式是非常相似的,但在参数设置上需要使用自定义API。
创建Signature实例。
Signature signature = Signature.getInstance("SM2);
使用私钥进行初始化,以准备进行签名操作。
signature.initSign(privateKey);
上面使用的是一种简约的初始化方式,它会使用默认的SM2 ID,即1234567812345678。它还会使用私钥去计算出公钥,因为根据规范,公钥也要参与签名值的计算过程,这是与国际签名算法(如ECDSA)的一个重大不同点。但计算公钥会有一定的开销,对性能会有负面影响。
如果要使用非默认ID,或者不希望额外地计算公钥,则在初始化时之前需要额外设置一个定制的AlgorithmParameterSpec实例,即SM2SignatureParameterSpec。
byte[] altID = <定制化的ID>; ECPublicKey publicKey = <公钥>; SM2SignatureParameterSpec paramSpec = new SM2SignatureParameterSpec(altID, publicKey); signature.setParameter(paramSpec); signature.initSign(privateKey);
参数设置与初始化完成之后,就可以传入被签名的消息数据了。
byte[] message = <被签名的消息数据>; signature.update(message);
生成签名值。
byte[] sign = signature.sign();
SM2签名值使用ASN.1格式进行编码,其长度在71到73字节之间。
使用公钥进行初始化,以准备进行验签操作。
signature.initVerify(publicKey);
传入被签名的消息数据。
byte[] message = <被签名的消息数据>; signature.update(message);
再传入已生成的签名值进行验证,
boolean verified = signature.verify(sign);
如果验证成功,返回true,否则返回false。
必须要注意的是,私钥用于签名,公钥用于验签。关于签名算法API的更详细用法,请参考Signature的官方文档。
出于性能考虑,与其它的非对称加密算法(如RSA和EC)相同,SM2加密算法一般只用于加密少量的关键性数据。
创建Cipher实例,
Cipher cipher = Cipher.getInstance("SM2");
使用公钥对Cipher进行初始化,指定其使用加密模式。
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
传入消息数据生成密文。
byte[] message = <被加密的消息数据>; byte[] ciphertext = cipher.doFinal(message);
使用私钥对Cipher进行初始化,指定其使用解密模式。
cipher.init(Cipher.DECRYPT_MODE, privateKey);
传入密文生成明文。
byte[] cleartext = cipher.doFinal(ciphertext);
必须要注意的是,公钥用于加密,私钥用于解密。关于加密算法API的更详细用法,请参考Cipher的官方文档。
使用SM3算法与使用JDK自带的其它哈希算法(如SHA-256)的方式是完全相同的,仅需要调用JDK API就可以生成消息摘要(哈希值)。
创建MessageDigest实例。
MessageDigest md = MessageDigest.getInstance("SM3");
可以一次性输入全部消息数据,然后生成消息摘要。
byte[] message = <消息数据>; byte[] digest = md.digest(message);
也可以分多次传递消息数据的片断,最后再生成消息摘要。
byte[] messageSegment1 = <消息数据片断1>; byte[] messageSegment2 = <消息数据片断2>; // 多次传入消息数据片断 md.update(messageSegment1); md.update(messageSegment2); // 最后再生成消息摘要 byte[] digest = md.digest();
关于消息摘要算法API的更详细用法,请参考MessageDigest的官方文档。
使用HmacSM3算法与使用JDK自带的其它消息验证码算法(如HmacSHA256)的方式是完全相同的,仅需要调用JDK API就可以生成消息验证码。
准备密钥,其长度为16字节。
byte[] key = <密钥>; SecretKey secretKey = new SecretKeySpec(key, "SM4");
创建Mac实例。
Mac hmac = Mac.getInstance("HmacSM3");
使用密钥对Mac进行初始化。
hmac.init(secretKey);
一次性传入消息数据,并生成消息验证码,其长度固定为32字节。
byte[] message = <消息>; byte[] mac = hmac.doFinal(message);
还可以分多次传入消息数据的片断,最后再生成消息验证码,
byte[] messageSegment1 = <消息数据片断1>; byte[] messageSegment2 = <消息数据片断2>; // 多次传入消息数据片断 hmac.update(messageSegment1); hmac.update(messageSegment2); // 最后再生成消息验证码 byte[] mac = hmac.doFinal();
关于消息验证码算法API的更详细用法,请参考Mac的官方文档。
使用SM4算法与使用JDK自带的其它分组加密算法(如AES)的方式是完全相同的,仅需要调用JDK API就可以进行SM4加密和解密操作。KonaCrypto支持了SM4的四种分组操作模式,包括CBC,CTR,ECB和GCM,同时还支持了PKCS#7填充规范。
准备密钥,其长度为16字节。
byte[] key = <密钥>; SecretKey secretKey = new SecretKeySpec(key, "SM4");
创建Cipher实例。
Cipher cipher = Cipher.getInstance(transformation);
其中的transformation是一个由算法名,分组操作模式和填充规范这三个部分拼接而成的参数组合,各个参数部分之间使用/分隔。
支持如下的参数组合:
构建算法参数。
AlgorithmParameterSpec paramSpec = <算法参数实现类>;
不同的操作模式需要不同类型的算法参数实现类,
使用密钥和算法参数对Cipher进行初始化,指定其使用加密模式。
cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
一次性传入全部的消息数据,并生成密文。
byte[] ciphertext = cipher.doFinal(message);
使用密钥和算法参数对Cipher进行初始化,指定使用解密模式。
cipher.init(Cipher.DECRYPT_MODE, secretKey);
一次性传入全部密文数据,并生成明文。
byte[] cleartext = cipher.doFinal(ciphertext);
另外,还可以分多次传入明文/密文片断,则分多次的获取对应的密文/明文片断,
byte[] input1 = <明文/密文片断1>; byte[] input2 = <明文/密文片断2>; // 多次传入明文/密文片断,并多次生成密文/明文片断 byte[] output1 = cipher.update(input1); byte[] output2 = cipher.update(input2); // 生成最后部分的密文/明文片断 byte[] outputFinal = cipher.doFinal();
关于加密算法API的更详细用法,请参考Cipher的官方文档。