需求描述
小件员app端拨打消费者手机号,基于小件员手机号,消费者手机号,绑定一个中间隐私号码,通过拨打隐私小号来建立二者的通话,保护客户隐私。
隐私小号绑定由另外一个公司提供,我们称为公司B。我们公司称为公司A。
公司A将消费者手机号通过私钥加密。公司B将消费者手机号通过公钥加密。
加解密
加密
公司A将加密后的数据转为字符串透传给公司B
new String(EncodeDecodeUtil.encryptByPrivateKey(bindVirtualNumberParam.getCalledNum().getBytes(), privateKey),
StandardCharsets.UTF_8)
解密
公司B将解密后的数据转为字符串,然后绑定隐私小号响应给公司A
new String(EncodeDecodeUtil.decryptByPublicKey(param.getCalledNum().getBytes(), publicKey),
StandardCharsets.UTF_8)
工具类
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import javax.crypto.Cipher;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
/**
* @author 会灰翔的灰机
* @date 2020/11/20
*/
@Slf4j
public class EncodeDecodeUtil {
public static final String RSA = "RSA";
private final static String MOBILE_STR = "0123456789ABCDEF";
/**
* 私钥加密
*
* @param data 待加密数据
* @param key 密钥
* @return byte[] 加密数据
*/
public static byte[] encryptByPrivateKey(byte[] data, String key) {
try {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(key));
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data);
} catch (Exception e) {
log.error("encryptByPrivateKey failed", e);
}
return data;
}
/**
* 公钥解密
*
* @param data 待解密数据
* @param key 密钥
* @return byte[] 解密数据
*/
public static byte[] decryptByPublicKey(byte[] data, String key) {
try {
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(key));
PublicKey pubKey = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, pubKey);
return cipher.doFinal(data);
} catch (Exception e) {
log.error("decryptByPublicKey failed", e);
}
return data;
}
/**
* 将字节数组转换为十六进制字符串
* @param bytearray :
* @return :
*/
public static String byteArrayToHexString(byte[] bytearray) {
StringBuilder strDigest = new StringBuilder();
for (byte b : bytearray) {
strDigest.append(byteToHexString(b));
}
return strDigest.toString();
}
/**
* 将字节转换为十六进制字符串
* @param ib :
* @return :
*/
private static String byteToHexString(byte ib) {
char[] digit = MOBILE_STR.toCharArray();
char[] ob = new char[2];
ob[0] = digit[(ib >>> 4) & 0X0F];
ob[1] = digit[ib & 0X0F];
return new String(ob);
}
/**
* 16进制字符串转为字节数组
* @param hex :
* @return :
*/
public static byte[] hexStringToByte(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] character = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(character[pos]) << 4 | toByte(character[pos + 1]));
}
return result;
}
public static int toByte(char c) {
return (byte) MOBILE_STR.indexOf(c);
}
public static void initKey() throws Exception {
//通过SPI接口获取密钥生成器实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA);
//初始化密钥生成器
keyPairGenerator.initialize(512);
//生成密钥对
KeyPair keyPair = keyPairGenerator.generateKeyPair();
//公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
//私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 公钥、私钥转字符串,便于动态配置
System.out.printf("publicKey=%s, privateKey=%s%n", Base64.encodeBase64String(publicKey.getEncoded()), Base64.encodeBase64String(privateKey.getEncoded()));
}
public static void main(String[] args) throws Exception {
// 离线执行初始化公钥秘钥,并将公钥提供给公司B
initKey();
}
}
问题
IllegalBlockSizeException
该异常很直白,数据超出长度限制64字节
2020-11-23 14:53:19.304 [23333333] [HSFBizProcessor-DEFAULT-6-thread-2] ERROR ?:? - decryptByPublicKey failed
javax.crypto.IllegalBlockSizeException: Data must not be longer than 64 bytes
at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:344)
at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389)
at javax.crypto.Cipher.doFinal(Cipher.java:2165)
at com.dianwoda.virtual.number.util.EncodeDecodeUtil.decryptByPublicKey(EncodeDecodeUtil.java:56)
at com.dianwoda.virtual.number.provider.VirtualNumberProviderImpl.bindVirtualNumber(VirtualNumberProviderImpl.java:62)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
咋办?看源码吧,堆栈中的具体类:sun.security.mscapi.RSACipher
private byte[] doFinal() throws BadPaddingException, IllegalBlockSizeException {
if (this.bufOfs > this.buffer.length) {
throw new IllegalBlockSizeException("Data must not be longer than " + (this.buffer.length - this.paddingLength) + " bytes");
} ...
}
可以看到我们实际可以使用的数据长度不一定是buffer的长度,还需要考虑paddingLength长度。这两个长度是否可以调整?两个字段的默认配置是多少?
buffer长度
buffer长度=密钥长度/8
除以8的原因是密钥的长度为bit单位,数据单位为字节byte
// sun.security.mscapi.RSACipher
// 1. 参数 var1:模式,例如:解密模式=Cipher.DECRYPT_MODE
// 2. 参数 var2:密钥,例如:非对称加密的公钥、密钥
private void init(int var1, java.security.Key var2) throws InvalidKeyException {
boolean var3;
...
if (var2 instanceof PublicKey) {
...
this.outputSize = this.publicKey.length() / 8;
} else {
...
this.outputSize = this.privateKey.length() / 8;
}
this.bufOfs = 0;
this.buffer = new byte[this.outputSize];
}
paddingLength长度
paddingLength长度逻辑
- 如果是javax.crypto.Cipher#ENCRYPT_MODE、javax.crypto.Cipher#WRAP_MODE模式,则为11
- 如果是javax.crypto.Cipher#DECRYPT_MODE、javax.crypto.Cipher#UNWRAP_MODE模式,则为0
// sun.security.mscapi.RSACipher
// 1. 参数 var1:模式,例如:解密模式=Cipher.DECRYPT_MODE
// 2. 参数 var2:秘钥,例如:非对称加密的公钥、秘钥
private void init(int var1, java.security.Key var2) throws InvalidKeyException {
boolean var3;
switch(var1) {
case 1:
case 3:
this.paddingLength = 11;
var3 = true;
break;
case 2:
case 4:
this.paddingLength = 0;
var3 = false;
break;
default:
throw new InvalidKeyException("Unknown mode: " + var1);
}
...
}
所以如果是解密我们可以使用的数据长度为公钥长度,加密长度为私钥长度-11。因为我们的解密数据长度为512/8-0=64 bytes。与报错相符
解决方案
加大密钥长度
直接增加一倍至1024,不可行**,**因为随着key增长,加密后的数据也随之增长,依然触发了长度限制
keyPairGenerator.initialize(1024);
二次加密
- 使用对称加密算法生成一个"对称密钥"
- 使用"对称密钥"加密数据
- 使用RSA算法加密"对称密钥"
- 发送**加密后的****“对称密钥”**以及数据
- 使用RSA算法解密加密后的****"对称密钥"
- 使用解密后的"对称密钥"解密数据
分段加密
时间复杂度高不建议使用
private static String encryptByPublicKey(byte[] data, String publicKey, int maxEncryptBlockSize) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
PublicKey key = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(RSA);
cipher.init(Cipher.ENCRYPT_MODE, key);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] encryptedData;
try {
int offSet = 0;
byte[] cache;
int i = 0;
// 数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > maxEncryptBlockSize) {
cache = cipher.doFinal(data, offSet, maxEncryptBlockSize);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * maxEncryptBlockSize;
}
encryptedData = out.toByteArray();
} finally {
out.close();
}
return Base64.encodeBase64String(encryptedData);
}
数据转16进制字符串
减小数据长度。对加密后的数据不要直接转String,而是转16进制字符串。对于数据小,并且超长不多,且所有数据长度固定,可以通过该方式处理。如果数据长度不可控该方法也是不可行的。
加密侧使用如下代码进行byte数组转字符串
private final static String MOBILE_STR = "0123456789ABCDEF";
/**
* 将字节数组转换为十六进制字符串
* @param bytearray :
* @return :
*/
public static String byteArrayToHexString(byte[] bytearray) {
StringBuilder strDigest = new StringBuilder();
for (byte b : bytearray) {
strDigest.append(byteToHexString(b));
}
return strDigest.toString();
}
/**
* 将字节转换为十六进制字符串
* @param ib :
* @return :
*/
private static String byteToHexString(byte ib) {
char[] digit = MOBILE_STR.toCharArray();
char[] ob = new char[2];
ob[0] = digit[(ib >>> 4) & 0X0F];
ob[1] = digit[ib & 0X0F];
return new String(ob);
}
解密侧使用下面的代码将数据字符串转为byte数组
private final static String MOBILE_STR = "0123456789ABCDEF";
/**
* 16进制字符串转为字节数组
* @param hex :
* @return :
*/
public static byte[] hexStringToByte(String hex) {
int len = (hex.length() / 2);
byte[] result = new byte[len];
char[] character = hex.toCharArray();
for (int i = 0; i < len; i++) {
int pos = i * 2;
result[i] = (byte) (toByte(character[pos]) << 4 | toByte(character[pos + 1]));
}
return result;
}
public static int toByte(char c) {
return (byte) MOBILE_STR.indexOf(c);
}