1. 前言
本篇博客是工作经验总结,如果您发现此篇博客有疏漏或有待改进之处,欢迎评论区交流。
2. 数字信封
数字信封使用的是接收者的非对称密钥对。即:用接收者的公钥加密,且只能由接收者的私钥解密。其实现过程如下:
(1)信息发送者发送信息时,首先生成一个共享的对称密钥,用该对称密钥加密要发送的明文,得到数据密文;
(2)信息发送者用接收者的公钥加密此对称密钥,形成对称密钥密文;
(3)信息发送者将(1)和(2)的结果结合在一起,形成数字信封,一并传给信息接收者。信息接收者接到数据后,使用自己的私钥解密对称密钥密文,得到共享的对称密钥;再用此对称密钥解密数据密文,得到真正的原始明文;
3. 数字信封和数字签名
简要说明数字信封和数字签名的区别。数字签名和数字信封都使用非对称密钥算法,但实现过程相反,使用的密钥对不同。
(1)数字签名使用的是发送方的密钥对,发送方用自己的私钥进行加密,接收方用发送方的公钥进行解密,这是一个一对多的关系,任何拥有发送方公钥的人都可以验证数字签名的正确性。
(2)数字信封使用的是接收方的密钥对,这是多对一的关系,任何知道接收方公钥的人都可以向接收方发送加密信息,只有惟一拥有接收方私钥者才能对信息解密。
(3)数字签名只采用了非对称密钥加密算法和数字摘要技术,能够保证发送信息的完整性(但未保证信息的机密性);数字信封采用了对称密钥加密算法和非对称密钥加密算法相结合的方法,能保证信息传输的机密性。
4. 数字信封代码实现
4.1. 需要引入的包
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency><dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.8</version> </dependency>
4.2 实现思路
本示例的对称加密使用SM4算法,非对称加密使用SM2算法,算法库使用BC库
创建数字信封实现思路:
1. 根据传递的对称密钥算法,如SM4/CBC/PKCS5PADDING, 生成对称密钥;
2. 使用对称密钥加密数据原文得到数据密文;
3. 使用接收方公钥加密对称密钥得到对称密钥密文;
4. 按照数据密文|对称密钥密文|对称加密算法类型|IV值的形式把结果拼接起来。
解密数字信封实现思路:
1. 传递上述第4步中的结果和私钥到解密数字信封接口中;
2. 获取其中的数据密文、对称密钥密文、对称加密算法类型和IV值;
3. 使用私钥解密对称密钥密文,得到对称密钥;
4. 使用对称密钥、对称密钥算法和IV值解密数据密文得到数据原文。
考虑下数字信封的实际应用场景是什么?
4.3 具体实现
4.3.1 代码实现
代码中有相应的注释,此处就不再阐述代码逻辑了。
package com.example.enveloped;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Objects;
/**
* <p>本示例的对称加密使用SM4算法,非对称加密使用SM2算法,算法库使用BC库</p>
*
*
* <p>创建数字信封实现思路:
*
* <p>1. 根据传递的对称密钥算法,如SM4/CBC/PKCS5PADDING, 生成对称密钥;
* <p>2. 使用对称密钥加密数据原文得到数据密文;
* <p>3. 使用接收方公钥加密对称密钥得到对称密钥密文;
* <p>4. 按照数据密文|对称密钥密文|对称加密算法类型|IV值的形式把结果拼接起来。
*
* <p>解密数字信封实现思路:
* <p>1. 传递上述第4步中的结果和私钥到解密数字信封接口中;
* <p>2. 获取其中的数据密文、对称密钥密文、对称加密算法类型和IV值;
* <p>3. 使用私钥解密对称密钥密文,得到对称密钥;
* <p>4. 使用对称密钥、对称密钥算法和IV值解密数据密文得到数据原文。
*
*
* <p>考虑下数字信封的实际应用场景是什么?
*/
public class CreateEnveloped {
// iv值的长度(字节)
public static final int IV_LENGTH = 16;
// 默认iv值
public static final String IV = "cPqChVEW9OpnJQTkcObQvA==";
// 加密模式
public static final String ECB = "ECB";
public static final String CBC = "CBC";
public static final String BASE64_SEPARATOR = "|";
private static final String KEY_ALGORITHM = "EC";
/**
* 解开数字信封
*
* @param envelopedData 数字信封数据
* @param priKey 私钥
* @return 信封里面的数据
*/
public static String openEnvelope(String envelopedData, String priKey) {
// 获取数据密文、密钥密文、加密算法和IV值
Base64.Decoder decoder = Base64.getDecoder();
String[] parts = envelopedData.split("\\|");
byte[] encryptData = decoder.decode(parts[0]);
byte[] encryptKeyBytes = decoder.decode(parts[1]);
String encAlgorithm = new String(decoder.decode(parts[2]));
byte[] iv = decoder.decode(parts[3]);
// 私钥解密对称密钥密钥
PrivateKey privateKey = getPrivateKey(priKey);
// 此处使用了hutool工具提供的国密SM2工具类
SM2 sm2 = new SM2(privateKey, null);
byte[] key = sm2.decrypt(encryptKeyBytes, KeyType.PrivateKey);
// 使用对称密钥解密数据密文
byte[] plainTextBytes;
if (encAlgorithm.contains(CBC)) {
plainTextBytes = SM4Util.decrypt(encAlgorithm, key, iv, encryptData);
}else {
plainTextBytes = SM4Util.decrypt(encAlgorithm, key, null, encryptData);
}
return new String(plainTextBytes);
}
/**
* 创建数字信封
*
* @param encAlgorithm 对称加密算法(由使用者自己指定)
* @param pubKey 接收方的公钥
* @param plainText 数据原文
* @return 封装的数字信封数据
*/
public static String createEnvelope(String encAlgorithm, String pubKey, String plainText) {
// 原文
byte[] dataToUse = plainText.getBytes();
// 生成用于加密数据的对称密钥
String alg = encAlgorithm.substring(0, encAlgorithm.indexOf("/"));
Key key = SM4Util.genKey(alg);
// 使用对称密钥加密明文数据
byte[] ivToUse;
byte[] encryptData;
if (encAlgorithm.contains(CBC)) {
ivToUse = getIv(IV_LENGTH);
encryptData = SM4Util.encrypt(encAlgorithm, key.getEncoded(), ivToUse, dataToUse);
} else {
ivToUse = Base64.getDecoder().decode(IV);
encryptData = SM4Util.encrypt(encAlgorithm, key.getEncoded(), null, dataToUse);
}
// 通过接收方的公钥加密对称密钥
PublicKey publicKey = getPublicKey(pubKey);
// 此处使用了hutool工具提供的国密SM2工具类
SM2 sm2 = new SM2(null, publicKey);
byte[] encryptKeyBytes = sm2.encrypt(key.getEncoded(), KeyType.PublicKey);
return assembleEnvelop(encryptData, encryptKeyBytes, encAlgorithm.getBytes(), ivToUse);
}
/**
* 组装数字信封数据
*
* @param encryptData 加密后的数据密文
* @param encryptAsymmetricKey 加密后的对称密钥密文
* @param encAlg 对称加密算法
* @param ivToUse IV值(如果使用者使用了CBC加密模式,则随机生成一个IV值;如果使用者使用了ECB加密模式,则给一个默认的IV值。这样做是为了让结果统一)
* @return
*/
private static String assembleEnvelop(byte[] encryptData, byte[] encryptAsymmetricKey, byte[] encAlg, byte[] ivToUse) {
Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(encryptData) + BASE64_SEPARATOR +
encoder.encodeToString(encryptAsymmetricKey) + BASE64_SEPARATOR +
encoder.encodeToString(encAlg) + BASE64_SEPARATOR +
encoder.encodeToString(ivToUse);
}
/**
* 根据十六进制的公钥字符串,获取公钥对象
*
* @param pubKey 十六进制的公钥字符串
* @return 公钥对象
*/
private static PublicKey getPublicKey(String pubKey) {
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(HexUtil.decodeHex(pubKey));
KeyFactory keyFactory;
PublicKey publicKey;
try {
keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME);
publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
return publicKey;
}
/**
* 根据十六进制的私钥字符串,获取私钥对象
*
* @param priKey 十六进制的私钥对象
* @return 私钥对象
*/
private static PrivateKey getPrivateKey(String priKey) {
PrivateKey privateKey;
try {
byte[] privateKeyBytes = HexUtil.decodeHex(priKey);
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
return privateKey;
}
/**
* 获取指定长度的随机数,此处充当IV值
*
* @param length 随机数长度
* @return 随机数
*/
private static byte[] getIv(int length) {
byte[] randomData = new byte[length];
RandomUtil.getSecureRandom().nextBytes(randomData);
return randomData;
}
}
// SM4工具类
class SM4Util {
public static final int SYM_KEY_SIZE = 128;
public static final String SM4 = "SM4";
// 使用BC库
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 生成指定算法的对称密钥 - 对象
*
* @param algorithmType 对称算法,如SM4
* @return 对称密钥对象
*/
public static Key genKey(String algorithmType) {
KeyGenerator keyGenerator;
try {
keyGenerator = KeyGenerator.getInstance(algorithmType, BouncyCastleProvider.PROVIDER_NAME);
keyGenerator.init(SYM_KEY_SIZE);
return keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException(e.getMessage());
}
}
/**
* 对称加密
*
* @param cipherAlgorithm 对称加密算法,如 SM4/CBC/PKCS5PADDING
* @param key 对称密钥
* @param iv iv值
* @param originalText 原文字节数组
* @return 密文字节数组
*/
public static byte[] encrypt(String cipherAlgorithm, byte[] key, byte[] iv, byte[] originalText) {
byte[] encryptData;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm, BouncyCastleProvider.PROVIDER_NAME);
Key secretKey = new SecretKeySpec(key, SM4);
if (Objects.isNull(iv)) {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
} else {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
}
encryptData = cipher.doFinal(originalText);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException |
BadPaddingException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
throw new RuntimeException(e.getMessage());
}
return encryptData;
}
/**
* 对称解密
*
* @param cipherAlgorithm 对称解密算法,如 SM4/CBC/PKCS5PADDING
* @param key 对称密钥
* @param iv iv值
* @param encryptText 密文字节数组
* @return 原文字节数组
*/
public static byte[] decrypt(String cipherAlgorithm, byte[] key, byte[] iv, byte[] encryptText) {
byte[] encryptData;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm, BouncyCastleProvider.PROVIDER_NAME);
Key secretKey = new SecretKeySpec(key, SM4);
if (Objects.isNull(iv)) {
cipher.init(Cipher.DECRYPT_MODE, secretKey);
} else {
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
}
encryptData = cipher.doFinal(encryptText);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException |
BadPaddingException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
throw new RuntimeException(e.getMessage());
}
return encryptData;
}
}
4.3.2 测试代码
package com.example.enveloped;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.crypto.symmetric.SM4;
import org.junit.Test;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
public class EnvelopedTest {
public static void main(String[] args) {
//printKeyPair();
String privateKey = "308193020100301306072a8648ce3d020106082a811ccf5501822d04793077020101042073f9ec0500d25e731f505de740f30a71e095a482101e7471b52cbb9e06e1813fa00a06082a811ccf5501822da1440342000452127847a8b7107848e5020295d08541e72551490e061928ce641c87714706f75211f964265a79ca92a49bc0e8b24de848876f01fdd368cd6fbdc24151cb8e8f";
String publicKey = "3059301306072a8648ce3d020106082a811ccf5501822d0342000452127847a8b7107848e5020295d08541e72551490e061928ce641c87714706f75211f964265a79ca92a49bc0e8b24de848876f01fdd368cd6fbdc24151cb8e8f";
String envelope = CreateEnveloped.createEnvelope("SM4/CBC/PKCS5PADDING", publicKey, "123QAZ");
System.out.println("封装的数字信封:" + envelope);
String result = CreateEnveloped.openEnvelope(envelope, privateKey);
System.out.println("解密的数字信封:" + result);
}
private static void printKeyPair() {
SM2 sm2 = new SM2();
String privateKeyHex = HexUtil.encodeHexStr(sm2.getPrivateKey().getEncoded());
String publicKeyHex = HexUtil.encodeHexStr(sm2.getPublicKey().getEncoded());
System.out.println("priKey: " + privateKeyHex);
System.out.println("pubKey: " + publicKeyHex);
}
}