本文不讨论RSA加密原理,只讨论RSA在Android应用中会遇到的坑
一般来说,公钥长这个样子
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApmm6v+lU0mCmulrqYca7
vZQu87/Xd7ii35Gee4vn3MYLH5GPvKkQ1NSi9QFlHEu2Do4w4I0/M1/nKNjsOygm
KSgIZkVK3sXA6JR++6xpnAXZVn/7Go+NLeXq8thBYxOuV2kf3CElFGNdbNfoooaS
ClhT0+9l+Repa8q1dvTpZcbEtIw63pxJ9DvT/T4/DmITieyIy429pnWY8wFtPgI6
a4KEVLvzbO2Ea5B7ZnKADkhHJit1oZATqJXrBl9iDBrstucgxAJTGfHhDsL7/Kwf
Zzoro//RI8w/D5ITRdZDjncCAwEAAQ==
-----END PUBLIC KEY-----
但是后台在拿到公钥时会对这里面所有字符进行Base64加密
最后我们Android端拿到公钥长这个样子
LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQU
FPQ0FnOEFNSUlDQ2dLQ0FnRUFwbW02ditsVTBtQ211bHJxWWNhNwp2WlF1ODcvWGQ3aWkz
NUdlZTR2bjNNWUxINUdQdktrUTFOU2k5UUZsSEV1MkRvNHc0STAvTTEvbktOanNPeWdtCk
tTZ0laa1ZLM3NYQTZKUisrNnhwbkFYWlZuLzdHbytOTGVYcTh0aEJZeE91VjJrZjNDRWxG
R05kYk5mUL1Q0L0RtSVRpZXlJeTQyOXBuV1k4d0Z0UGdJNgphNEtFVkx2emJPMkVhNUI3W
m5LQURraEhKaXQxb1pBVHFKWHJCbDlpREJyc3R1Y2d4QUpUR2ZIaERzTDcvS3dmClp6b3J
vLy9SSTh3L0Q1SVRSZFpEam5jQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tL
Q==
也就是说我们在拿到公钥字符串的时候要先对这串字符串进行Base64解密
现在是这个样子
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApmm6v+lU0mCmulrqYca7
vZQu87/Xd7ii35Gee4vn3MYLH5GPvKkQ1NSi9QFlHEu2Do4w4I0/M1/nKNjsOygm
KSgIZkVK3sXA6JR++6xpnAXZVn/7Go+NLeXq8thBYxOuV2kf3CElFGNdbNfoooaS
ClhT0+9l+Repa8q1dvTpZcbEtIw63pxJ9DvT/T4/DmITieyIy429pnWY8wFtPgI6
a4KEVLvzbO2Ea5B7ZnKADkhHJit1oZATqJXrBl9iDBrstucgxAJTGfHhDsL7/Kwf
Zzoro//RI8w/D5ITRdZDjncCAwEAAQ==
-----END PUBLIC KEY-----
但是注意。在java开发中我们需要把字符串转为Publickey对象。
分析一下结构,要获取公钥对象,就需要对
-----BEGIN PUBLIC KEY-----
这段内容
-----END PUBLIC KEY-----
中间的字符串进行解析,而中间这段字符串还是使用Base64进行加密过的
所以获取公钥要像这样抽蚕剥茧地拿出来
//使用修改过的Base64解密方法以生成字符串,Android的Base64与Java的Base64并不一致
String pk = MyBase64.decode(publicKeyStr);
//对中间部分字符串筛选
pk = pk.replace("-----BEGIN PUBLIC KEY-----","");
pk = pk.replace("-----END PUBLIC KEY-----","");
pk = pk.replace("\r\n","");
//使用Android自带Base64进行解密获取byte[]
byte[] buffer = Base64.decode(pk, Base64.DEFAULT);
//通过证书获取数组
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
Publickey publicKey = keyFactory.generatePublic(keySpec);
/*
* 密钥长度 bit长度
*/
private static int KEY_SIZE = 4096;
/*
* 1 byte = 8 bit
* 每次加密明文最大限制为 KEY_SIZE /8 -11
* 11为Padding长度
*/
private static int BLOCK_SIZE = KEY_SIZE / 8 - 11;
private static int OUTPUT_BLOCK_SIZE = KEY_SIZE / 8; //一次加密后的密文长度
/**
* 用公钥加密
* 每次加密的字节数,不能超过密钥的长度值减去11
*
* @param data
* 需加密数据的byte数据
* 公钥
* @return 加密后的byte型数据
*/
public static byte[] encryptData(byte[] data, PublicKey publicKey)
{
try
{
// private static String RSA = "RSA"; // 单元测试 Java适用
// private static String RSA = "RSA/ECB/PKCS1Padding"; //Android适用
Cipher cipher = Cipher.getInstance(RSA);
// 编码前设定编码方式及密钥
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//记录原明文长度
int dataLen = data.length;
int dateCache = 0;
//加密始起位置
int offset = 0;
//需要加密次数
int count = dataLen / BLOCK_SIZE;
if (dataLen % BLOCK_SIZE != 0) {
count++;
}
// 传入编码数据并返回编码结果
byte[] encryptedData = new byte[count*OUTPUT_BLOCK_SIZE];
for (int i = 0; i < count; i++) {
if ((i+1) != count) { //如果不是最后一段,需要加密长度为BLOCK_SIZE
cipher.doFinal(data, offset*i, BLOCK_SIZE, encryptedData,i*OUTPUT_BLOCK_SIZE);
} else {
cipher.doFinal(data, offset*i,dataLen - (offset*i), encryptedData,i*OUTPUT_BLOCK_SIZE);
}
offset += BLOCK_SIZE;
}
return encryptedData;
} catch (Exception e)
{
e.printStackTrace();
return null;
}
}
在Cipher.getInstance(RSA)中,RSA的值决定你的加密使用的填充模式
在RSA加密中,如长度为4096 bit的密钥,每次只能加密 4096/8 - 11 也就是501 字节长度的明文(但不代表只能加密一次,我们可以对超过501字节长度的明文进行多次加密),对不足501字节的明文,将由你所选用的填充模式决定以哪种方式填充至501字节长度
在Android开发中
加密的时候会在你的明文前面,前向的填充零。解密后的明文也会包括前面填充的零,这是服务器需要注意把解密后的字段前向填充的零去掉,才是真正之前加密的明文。Android数据传输加密(三):RSA加密
如果你的明文不够128字节
加密的时候会在你的明文中随机填充一些数据,所以会导致对同样的明文每次加密后的结果都不一样。对加密后的密文,服务器使用相同的填充方式都能解密。解密后的明文也就是之前加密的明文。
在Java开发中,默认的话就是随机填充,这就是为什么单元测试中能解密,而在Android测试中不能解密的原因。
其他填充模式尚不了解,可以到官网上查询
这个问题也是事故多发地段
详细可以查看 RSA密钥长度、明文长度和密文长度 博客
这里可以简单讲一下
上面讲到字节长度需要-11, 这里的11是指Padding,也就是要通过padding来判断填充模式,以备在解密的时候准确地判断以哪种模式来进行解密
**加密过程
cipher.doFinal(data, offset*i, BLOCK_SIZE, encryptedData,i*OUTPUT_BLOCK_SIZE);
类型 | 参数 | 内容 | 解释 |
---|---|---|---|
byte[] | input | data | 输入的明文byte[] |
int | inputOffset | offset*i | 输入的byte[]起始位置 |
int | inputlen | BLOCK_SIZE | 加密长度 |
byte[] | output | encryptedData | 输出的密文 |
int | outputOffset | encryptedData | 输出密文byte[]的起始位置 |
使用多片加密长明文是在有这个需求的时候才使用,同样也需要服务器解密的配合才适于使用
中间讲到,在解密过程中因为需要对中间字符串的筛选,需要用到能解密出String类型的Base64,而Android本身的Base64只提供byte[]的方法
MyBase64
public class MyBase64 {
//Constructor
public MyBase64() {
}
private static final String base64Code= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
public static String encode(String srcStr) {
//有效值检查
if(srcStr == null || srcStr.length() == 0) {
return srcStr;
}
//将明文的ASCII码转为二进制位字串
char[] srcStrCh= srcStr.toCharArray();
StringBuilder asciiBinStrB= new StringBuilder();
String asciiBin= null;
for(int i= 0; i< srcStrCh.length; i++) {
asciiBin= Integer.toBinaryString((int)srcStrCh[i]);
while(asciiBin.length()< 8) {
asciiBin= "0"+ asciiBin;
}
asciiBinStrB.append(asciiBin);
}
//跟据明文长度在二进制位字串尾部补“0”
while(asciiBinStrB.length()% 6!= 0) {
asciiBinStrB.append("0");
}
String asciiBinStr= String.valueOf(asciiBinStrB);
//将上面得到的二进制位字串转为Value,再跟据Base64编码表将之转为Encoding
char[] codeCh= new char[asciiBinStr.length()/ 6];
int index= 0;
for(int i= 0; i< codeCh.length; i++) {
index= Integer.parseInt(asciiBinStr.substring(0, 6), 2);
asciiBinStr= asciiBinStr.substring(6);
codeCh[i]= base64Code.charAt(index);
}
StringBuilder code= new StringBuilder(String.valueOf(codeCh));
//跟据需要在尾部添加“=”
if(srcStr.length()% 3 == 1) {
code.append("==");
} else if(srcStr.length()% 3 == 2) {
code.append("=");
}
//每76个字符加一个回车换行符(CRLF)
int i= 76;
while(i< code.length()) {
code.insert(i, "\r\n");
i+= 76;
}
code.append("\r\n");
return String.valueOf(code);
}
public static String decode(String srcStr) {
//有效值检查
if(srcStr == null || srcStr.length() == 0) {
return srcStr;
}
//检测密文中“=”的个数后将之删除,同时删除换行符
int eqCounter= 0;
if(srcStr.endsWith("==")) {
eqCounter= 2;
} else if(srcStr.endsWith("=")) {
eqCounter= 1;
}
srcStr= srcStr.replaceAll("=", "");
srcStr= srcStr.replaceAll("\r\n", "");
//跟据Base64编码表将密文(Encoding)转为对应Value,然后转为二进制位字串
char[] srcStrCh= srcStr.toCharArray();
StringBuilder indexBinStr= new StringBuilder();
String indexBin= null;
for(int i= 0; i< srcStrCh.length; i++) {
indexBin= Integer.toBinaryString(base64Code.indexOf((int)srcStrCh[i]));
while(indexBin.length()< 6) {
indexBin= "0"+ indexBin;
}
indexBinStr.append(indexBin);
}
//删除因编码而在尾部补位的“0”后得到明文的ASCII码的二进制位字串
if(eqCounter == 1) {
indexBinStr.delete(indexBinStr.length()- 2, indexBinStr.length());
} else if(eqCounter == 2) {
indexBinStr.delete(indexBinStr.length()- 4, indexBinStr.length());
}
String asciiBinStr= String.valueOf(indexBinStr);
//将上面得到的二进制位字串分隔成字节后还原成明文
String asciiBin= null;
char[] ascii= new char[asciiBinStr.length()/ 8];
for(int i= 0; i< ascii.length; i++) {
asciiBin= asciiBinStr.substring(0, 8);
asciiBinStr= asciiBinStr.substring(8);
ascii[i]= (char)Integer.parseInt(asciiBin, 2);
}
return String.valueOf(ascii);
}
}
这段源码忘记从哪位大佬博客拿过来的,侵立删