1 前言
在上个章节介绍了SM2的原理,本章节使用代码实现,考虑到大部分的系统为:B/S框架,实现使用:
1 前端使用:sm-crypto,实现页面的加解密处理
2 后端使用:java,实现与前端加解密互通
3 完整代码见:国密演示: SM算法用法,页面的SM2功能菜单:
2 代码实现
5.1 JS
本章节代码实现使用:sm-crypto库文件,以下展示部分的核心代码实现:
//生产密钥对
function createECKeyPair(){
let keypair = sm2.generateKeyPairHex();
publicKey = keypair.publicKey; // 公钥
privateKey = keypair.privateKey; // 私钥
$("#publicKey").val(publicKey);
$("#PrivateKey").val(privateKey);
}
//公钥加密
function cipherData() {
//获取秘钥
var publicKey = $("#publicKey").val(); // 公钥
//加密原文
const msgString = $("#encode-text").val();
const cipherMode = 1; // 1 - C1C3C2,0 - C1C2C3,默认为1
const encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode); // 加密结果
$("#encData").val(encryptData);
}
//私钥解密
function decDataQ(){
var encryptData= $("#encData").val();//密文
var privateKey = $("#PrivateKey").val(); // 私钥
const cipherMode = 1; // 1 - C1C3C2,0 - C1C2C3,默认为1
const decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode); // 解密结果
$("#decData").val(decryptData);
}
//签名
function signDataJS() {
var msg=$("#encode-text").val();
var privateKey=$("#PrivateKey").val();
// 签名
let sigValueHex = sm2.doSignature(msg, privateKey, {
hash:true,
der:true,
});
$("#signData").val(sigValueHex);
}
5.2 java实现
基于BouncyCastle库实现:
package org.liuy.pki;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.*;
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.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
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.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.Strings;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
/**
* 国密工具包,依赖BC包
* 实现与:sm-crypto.js互通
*
* @author liuy
*
*/
public class SM2Tools {
static {
Security.addProvider(new BouncyCastleProvider());
}
//椭圆曲线ECParameters ASN.1 结构
private static X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
//椭圆曲线公钥或私钥的基本域参数。
private static ECParameterSpec ecParameterSpec = new ECParameterSpec(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
private static ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
//默认国密id
private static String userID="1234567812345678";
/**
* 获取公钥16进制字符串
* @param publicKey
* @return
*/
public static String getPublicKeyHex(PublicKey publicKey) {
String publicKeyHex = null;
if (publicKey instanceof BCECPublicKey) {
//获取65字节非压缩缩的十六进制公钥串(0x04)
publicKeyHex = Hex.toHexString(((BCECPublicKey) publicKey).getQ().getEncoded(false));
}
return publicKeyHex;
}
/**
* 获取16进制私钥字符串
* @param privateKey
* @return
*/
public static String getPrivateKeyHex(PrivateKey privateKey) {
String privateKeyHex=null;
if (privateKey instanceof BCECPrivateKey) {
//获取32字节十六进制私钥串
privateKeyHex = ((BCECPrivateKey) privateKey).getD().toString(16);
}
return privateKeyHex;
}
/**
* @return KeyPair
* @Description 生成秘钥对
* @Author msx
*/
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());
KeyPair kp=kpg.generateKeyPair();
// 通过密钥生成器生成密钥对
return kp;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 公钥加密
* @Author msx
* @param plaintext 明文数据
* @param publicKeyHex 16进制SM2公钥
* @param cipherMode 1 - C1C3C2,0 - C1C2C3,默认为1
* @return String 16进制密文
* @throws InvalidCipherTextException
* @throws UnsupportedEncodingException
*/
public static String encrypt(String plaintext,String publicKeyHex,int cipherMode) {
// 获取一条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(publicKeyHex));
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
SM2Engine sm2Engine = new SM2Engine();
// 设置sm2为加密模式
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
byte[] arrayOfBytes = null;
byte[] in = plaintext.getBytes();
try {
arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
} catch (InvalidCipherTextException e) {
e.printStackTrace();
}
//C1C3C2模式
if(cipherMode==1)
{
arrayOfBytes=changeC1C2C3ToC1C3C2(arrayOfBytes);
return Hex.toHexString(arrayOfBytes);
}
else
{
C1C2C3模式
return Hex.toHexString(arrayOfBytes);
}
}
/**
* 公钥加密
* @Author msx
* @param contentArry 明文数组
* @param publicKey SM2公钥对象
* @param cipherMode 1 - C1C3C2,0 - C1C2C3,默认为1
* @return byte[] 密文数组
* @throws InvalidCipherTextException
* @throws UnsupportedEncodingException
*/
public static byte[] encrypt(byte[] contentArry,PublicKey publicKey,int cipherMode) {
BCECPublicKey localECPublicKey = (BCECPublicKey) publicKey;
ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(localECPublicKey.getQ(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
// 设置sm2为加密模式
sm2Engine.init(true, new ParametersWithRandom(ecPublicKeyParameters, new SecureRandom()));
byte[] arrayOfBytes = null;
try {
arrayOfBytes = sm2Engine.processBlock(contentArry, 0, contentArry.length);
} catch (InvalidCipherTextException e) {
e.printStackTrace();
}
if(cipherMode==1)
{
//C1C3C2模式
arrayOfBytes=changeC1C2C3ToC1C3C2(arrayOfBytes);
}
//C1C2C3模式
return arrayOfBytes;
}
/**
* bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密
* @param c1c3c2
* @return
*/
public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2) {
final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。
final int c3Len = 32; //new SM3Digest().getDigestSize();
byte[] result = new byte[c1c3c2.length];
System.arraycopy(c1c3c2, 0, result, 0, c1Len); //c1: 0->65
System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); //c2
System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); //c3
return result;
}
/**
* bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2
* @param c1c2c3
* @return
*/
public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3) {
final int c1Len = (x9ECParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。
final int c3Len = 32; //new SM3Digest().getDigestSize();
byte[] result = new byte[c1c2c3.length];
System.arraycopy(c1c2c3, 0, result, 0, c1Len); //c1
System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); //c3
System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); //c2
return result;
}
/**
* @Description 私钥解密
* @Author msx
* @param privateKeyHex 16进制私钥
* @param cipherData 16进制密文数据
* @param cipherMode 1 - C1C3C2,0 - C1C2C3,默认为1
* @return String 原文Hex
* @throws InvalidCipherTextException
* @throws UnsupportedEncodingException
*/
public static String decrypt(String cipherDataHex,String privateKeyHex,int cipherMode) {
//兼容smcrypto.js,加密头部增加04
if(!cipherDataHex.startsWith("04"))
{
cipherDataHex="04"+cipherDataHex;
}
byte[] dataByte=Hex.decode(cipherDataHex);
PrivateKey privateKey = getBCECPrivateKeyByPrivateKeyHex(privateKeyHex);
byte[] dataArry= decrypt(dataByte,privateKey,cipherMode);
return Hex.toHexString(dataArry);
}
/**
* @Description 私钥解密
* @Author msx
* @param privateKeyHex 16进制私钥
* @param cipherData 16进制密文数据
* @param cipherMode 1 - C1C3C2,0 - C1C2C3,默认为1
* @return String 原文
* @throws InvalidCipherTextException
* @throws UnsupportedEncodingException
*/
public static byte[] decrypt(byte[] cipherDataArry,PrivateKey privateKey,int cipherMode) {
if(cipherMode==1)
{
cipherDataArry=changeC1C3C2ToC1C2C3(cipherDataArry);
}
BCECPrivateKey localECPrivateKey = (BCECPrivateKey) privateKey;
ECPrivateKeyParameters ecPrivateKeyParameters = new ECPrivateKeyParameters(localECPrivateKey.getD(), ecDomainParameters);
SM2Engine sm2Engine = new SM2Engine();
sm2Engine.init(false, ecPrivateKeyParameters);
try {
return sm2Engine.processBlock(cipherDataArry, 0, cipherDataArry.length);
} catch (Exception e) {
// throw new RuntimeException(e);
return null;
}
}
/**
* @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), ecParameterSpec);
//通过椭圆曲线公钥规范,创建出椭圆曲线公钥对象(可用于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, ecParameterSpec);
//通过椭圆曲线私钥规范,创建出椭圆曲线私钥对象(可用于SM2解密和签名)
return new BCECPrivateKey("EC", ecPrivateKeySpec, BouncyCastleProvider.CONFIGURATION);
}
/**
* Base64 PKCS8 privateKey.getEncoded() 转为BCECPrivateKey
*
* @param pkcs8B64 privateKey.getEncoded() BASE64字符串
* @return BCECPrivateKey SM2私钥对象
*/
public static BCECPrivateKey getBCECPrivateKeyByPKCS8(String pkcs8B64) {
byte[] bytes = Base64.decode(pkcs8B64);
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
BCECPrivateKey privateKey = (BCECPrivateKey) keyFactory.generatePrivate(keySpec);
return privateKey;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
e.printStackTrace();
}
return null;
}
/**
* 私钥签名
* @param privateKeyHex 私钥(16进制)
* @param contentHex 待签名内容(16进制)
* @param userID 国密用户ID数据原文,默认:1234567812345678,参与Z值计算
* @return 16进制签名信息
*/
public static String sign(String privateKeyHex, String contentHex,String userID) throws CryptoException {
//待签名内容转为字节数组
byte[] message = Hex.decode(contentHex);
//获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
//构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
sm2ECParameters.getG(), sm2ECParameters.getN());
BigInteger privateKeyD = new BigInteger(privateKeyHex, 16);
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
//创建签名实例
SM2Signer sm2Signer = new SM2Signer();
//初始化签名实例,带上ID,国密的要求,ID默认值:1234567812345678
try {
sm2Signer.init(true, new ParametersWithID(new ParametersWithRandom(privateKeyParameters, SecureRandom.getInstance("SHA1PRNG")), Strings.toByteArray(userID)));
} catch (NoSuchAlgorithmException e) {
System.out.println("签名时出现异常:"+e);
}
sm2Signer.update(message, 0, message.length);
//生成签名,签名分为两部分r和s,分别对应索引0和1的数组
byte[] signBytes = sm2Signer.generateSignature();
String sign = Hex.toHexString(signBytes);
return sign;
}
private static ECPrivateKeyParameters getPrivateKeyParameters(ECPrivateKey ecPrivateKey) {
ECParameterSpec parameterSpec = ecPrivateKey.getParameters();
ECCurve curve = parameterSpec.getCurve();
ECPoint g = parameterSpec.getG();
BigInteger n = parameterSpec.getN();
BigInteger h = parameterSpec.getH();
ECDomainParameters domainParams = new ECDomainParameters(curve, g, n, h);
return new ECPrivateKeyParameters(ecPrivateKey.getD(), domainParams);
}
public static byte[] sign(PrivateKey privateKey,byte[] contentArry,String userID) throws CryptoException {
ECPrivateKeyParameters privateKeyParameters = getPrivateKeyParameters((ECPrivateKey) privateKey);
SM2Signer signer = new SM2Signer();
ParametersWithRandom pwr = new ParametersWithRandom(privateKeyParameters, new SecureRandom());
CipherParameters param=new ParametersWithID(pwr, userID.getBytes());
signer.init(true, param);
signer.update(contentArry, 0, contentArry.length);
return signer.generateSignature();
}
/**
* 私钥签名,userID为国密局默认参数
* @param privateKey 私钥
* @param contentArry 签名内容
* @return
* @throws CryptoException
*/
public static byte[] sign(PrivateKey privateKey,byte[] contentArry) throws CryptoException {
return sign(privateKey,contentArry,userID);
}
/**
* 国密签名验证
* @param privateKeyHex 私钥(16进制)
* @param contentHex 待签名内容(16进制)
* @return 16进制签名信息
* @throws CryptoException
*/
public static String sign(String privateKeyHex, String contentHex) throws CryptoException {
return sign(privateKeyHex,contentHex,userID);
}
private static ECPublicKeyParameters getPublicKeyParameters(ECPublicKey ecPublicKey) {
ECParameterSpec parameterSpec = ecPublicKey.getParameters();
ECCurve curve = parameterSpec.getCurve();
ECPoint g = parameterSpec.getG();
BigInteger n = parameterSpec.getN();
BigInteger h = parameterSpec.getH();
ECDomainParameters domainParams = new ECDomainParameters(curve, g, n, h);
return new ECPublicKeyParameters(ecPublicKey.getQ(), domainParams);
}
/**
*
* @param publicKeyHex 公钥(16进制)
* @param contentHex 待签名原文(16进制)
* @param signHex 签名值(16进制)
* @param userID 国密用户ID数据原文,默认:1234567812345678,参与Z值计算
* @return
*/
public static boolean verify(String publicKeyHex, String contentHex, String signHex,String userID) {
//待签名内容
byte[] message = Hex.decode(contentHex);
byte[] signData = Hex.decode(signHex);
// 获取一条SM2曲线参数
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
// 构造domain参数
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
sm2ECParameters.getG(),
sm2ECParameters.getN());
//提取公钥点
ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKeyHex));
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
//创建签名实例
SM2Signer sm2Signer = new SM2Signer();
ParametersWithID parametersWithID = new ParametersWithID(publicKeyParameters, Strings.toByteArray(userID));
sm2Signer.init(false, parametersWithID);
sm2Signer.update(message, 0, message.length);
//验证签名结果
boolean verify = sm2Signer.verifySignature(signData);
return verify;
}
public static boolean verify(X509Certificate x509Cert, byte[] contentArry, byte[] signArry,String userID) {
PublicKey publicKey=x509Cert.getPublicKey();
return verify(publicKey, contentArry, signArry, userID);
}
public static boolean verify(X509Certificate x509Cert, byte[] contentArry, byte[] signArry) {
PublicKey publicKey=x509Cert.getPublicKey();
return verify(publicKey, contentArry, signArry, userID);
}
public static boolean verify(PublicKey publicKey, byte[] contentArry, byte[] signArry,String userID) {
ECPublicKeyParameters ecPublicKeyParameters = getPublicKeyParameters((ECPublicKey) publicKey);
SM2Signer signer = new SM2Signer();
CipherParameters param=new ParametersWithID(ecPublicKeyParameters, userID.getBytes());
signer.init(false, param);
signer.update(contentArry, 0, contentArry.length);
return signer.verifySignature(signArry);
}
/**
* 验证签名
* @param publicKey 公钥(16进制)
* @param contentHex 待签名原文(16进制)
* @param signHex 签名值(16进制)
* @return
*/
public static boolean verify(String publicKeyHex, String contentHex, String signHex) {
return verify(publicKeyHex,contentHex,signHex,userID);
}
}