前置知识
数据签名:私钥签名,然后公钥进行验签。
私钥签名的过程:先利用摘要算法对明文数据生成数字摘要,然后使用私钥对数字摘要进行签名(加密),生成的值称为数字签名,最后把 数字签名+明文数据 发送给对方。
公钥验签的过程:利用公钥对数字签名进行验证(解密),拿到解密后的数字摘要,然后利用相同的摘要算法对数据明文生成数字摘要,然后比对解密后的数字摘要和自己生成的数字摘要是否一致,如果一致则说明明文数据没有被篡改过,反之就是被篡改过。
备注1:私钥仅被服务端锁持有,而公钥被多个客户端锁持有。
备注2:常见的摘要算法有MD5、SHA1、SHA256,生成的摘要值长度分别为128位、160位、256位比特。
场景需求
需求1:服务端明文数据进行加密,只有拥有公钥的客户端能对数据进行解密,而且要求具备不可否认的效果,即如果客户端能解密成功,必须代表这些数据是来自服务端(有点签名、验证的味道)。
需求2:客户端加密的数据只能由服务端进行解密,其他客户端无法解密。
解决方案
方案猜想1(RSA)
方案猜想1:利用RSA 私钥、公钥来解决。服务端私钥对明文数据进行加密,客户端公钥进行解密;客户端公钥对明文数据进行加密,服务端私钥对密文数据进行解密。该猜想是否可行?
上面方案猜想理论上可以满足需求,但是实际无法解决。
猜想实际存在问题1:RSA非对称密钥对数据加解密速度很慢,尤其是数据量大的情况。可以参考 Https采用非对称加密+对称加密的原因。
猜想实际存在问题2:RSA加解密对数据明文长度是有限制的,这也是致命问题。
RSA密钥长度和明文长度的关系
在RSA 1024位密钥长度时,最大明文长度限制是117个字节
在RSA 2048位密钥长度时,最大明文长度限制是245个字节
在RSA 4096位密钥长度时,最大明文长度限制是512个字节
综上所述,方案猜想一不成立!
方案猜想2(RSA+AES+Base64)
步骤一:服务端生成随机AES对称密钥,然后利用AES对称密钥对明文数据进行加密形成数据密文,然后利用RSA私钥对AES对称密钥进行加密,形成对称密钥密文。然后将这两者先进行Base64编码,最后以 “冒号” 进行拼接,然后发给客户端。
步骤二:客户端收到密文后,先用**“冒号”进行分割成两部分,然后别进行进行Base64解码,对称密钥密文和数据密文两部分。客户端先利用公钥对对称密钥密文进行解密,拿到对称密钥,然后利用对称密钥对数据密文进行解密,拿到数据明文。
客户端数据加密只能服务端解密流程也是基本类似。只不过此时是客户端生成随机对称密钥,利用公钥对对称密钥进行加密,服务端使用私钥对对称密钥密文进行解密,拿到对称密钥。
好处1:对称密钥加解密速度比非对称密钥快得多。这里 利用AES对称密钥对较长的明文数据进行加解密,利用RSA对较短的对称密钥进行加解密。
好处2:AES对称密钥长度较短,无需担心 RSA加解密明文数据长度的限制。
好处3:Base64对数据进行编码,可以将一些不可打印字符转为常见的可打印字符。这样的话就无需考虑加密后的数据存在特殊字符之类的问题。
扩展:Base64的64个可打印字符如下:
实施过程(RSA+AES256+Base64)
OpenSsl生成RSA私钥和公钥文件
#生成2048位的密钥
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
#从私钥中提取公钥
openssl rsa -pubout -in private_key.pem -out public_key.pem
查看生成的文件
私钥文件:
公钥文件:
注:私钥和公钥的数据内容默认都是按Base64进行了编码,所以我们这边才能肉眼查看文件内容。
代码案例
我这里把生成后的私钥和公钥放在D盘根目录下,大家可以根据实际业务来修改。
package com.jxf;
import org.springframework.util.Assert;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RSASignatureExample {
// 公钥和私钥文件存放路径
private static final String PRIVATE_KEY_FILE = "d:/private_key.pem";
private static final String PUBLIC_KEY_FILE = "d:/public_key.pem";
private static final String IV = "1234567890123456";
private static String defaultCharset = "utf-8";
public static PrivateKey loadPrivateKey() throws Exception {
byte[] keyBytes = Files.readAllBytes(Paths.get(PRIVATE_KEY_FILE));
String keyStr = new String(keyBytes)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] encoded = Base64.getDecoder().decode(keyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
public static PublicKey loadPublicKey() throws Exception {
byte[] keyBytes = Files.readAllBytes(Paths.get(PUBLIC_KEY_FILE));
String keyStr = new String(keyBytes)
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(keyStr));
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
// 使用私钥签名(数据签名,这里没有使用到该方法,可以跳过该方法)
public static byte[] signData(String data, PrivateKey privateKey) throws Exception {
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initSign(privateKey);
signer.update(data.getBytes());
return signer.sign();
}
// 使用公钥验签(数据验签,这里没有使用到该方法,可以跳过该方法)
public static boolean verifySignature(String data, byte[] signature, PublicKey publicKey) throws Exception {
Signature signer = Signature.getInstance("SHA256withRSA");
signer.initVerify(publicKey);
signer.update(data.getBytes());
return signer.verify(signature);
}
public static String encryptData(String rowData, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
// 生成随机的AES密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); // 可以选择128, 192, 或 256位
SecretKey secretKey = keyGenerator.generateKey();
// aes对数据加密
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// iv参数可以理解为对称加密的盐值
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes(defaultCharset));
aesCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
byte[] encryptDataBytes = aesCipher.doFinal(rowData.getBytes(defaultCharset));
String encryptDataBase64 = Base64.getEncoder().encodeToString(encryptDataBytes);
// rsa对密钥进行加密
Cipher rsaCipher = Cipher.getInstance("RSA");
rsaCipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptAesSecretBytes = rsaCipher.doFinal(secretKey.getEncoded());
String encryptAesSecretBase64 = Base64.getEncoder().encodeToString(encryptAesSecretBytes);
System.out.println("AES length:" + secretKey.getEncoded().length);
// 将解密后的密钥 和 加密数据进行拼接
return encryptAesSecretBase64 + ":" + encryptDataBase64;
}
public static String decryptData(String encryptedData, Key key) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, UnsupportedEncodingException, InvalidAlgorithmParameterException {
String[] split = encryptedData.split(":");
Assert.isTrue(split.length == 2, "密文格式有误!");
// 获取加密密钥 和 加密数据
String encryptedAesSecretBase64 = split[0];
String encryptedDataBase64 = split[1];
// rsa对密钥密文进行解密,拿到对称密钥
Cipher rasCipher = Cipher.getInstance("RSA");
rasCipher.init(Cipher.DECRYPT_MODE, key);
byte[] aesSecretBytes = rasCipher.doFinal(Base64.getDecoder().decode(encryptedAesSecretBase64));
SecretKey aesSecret = new SecretKeySpec(aesSecretBytes, "AES");
System.out.println("AES length:" + aesSecretBytes.length);
// 利用对称密钥对数据密文进行解密
Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// iv参数可以理解为对称加密的盐值
IvParameterSpec ivParameterSpec = new IvParameterSpec(IV.getBytes(defaultCharset));
aesCipher.init(Cipher.DECRYPT_MODE, aesSecret, ivParameterSpec);
byte[] rowData = aesCipher.doFinal(Base64.getDecoder().decode(encryptedDataBase64));
return new String(rowData, defaultCharset);
}
public static void main(String[] args) {
try {
// 加载私钥和公钥
PrivateKey privateKey = loadPrivateKey();
PublicKey publicKey = loadPublicKey();
// 数据加密(可以选择私钥加密公钥加密 或者 公钥解密私钥加密)
String encryptData = encryptData("你好!111111111111111111111111111111111111", privateKey);
System.out.println(encryptData);
// 数据解密
String decryptData = decryptData(encryptData, publicKey);
System.out.println(decryptData);
} catch (Exception e) {
e.printStackTrace();
}
}
}