Android RSA加密传输的那些事儿

文章目录

      • 前言
      • 正文来了
      • Java获取公钥对象
      • 加密 请注意你的填充模式和明文长度
        • 选择你的填充模式
        • 明文长度
      • 源码

前言

本文不讨论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对象。

Java获取公钥对象

分析一下结构,要获取公钥对象,就需要对

-----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开发中

  • 如果默认填写“RSA”的话,那就是选择不填充

加密的时候会在你的明文前面,前向的填充零。解密后的明文也会包括前面填充的零,这是服务器需要注意把解密后的字段前向填充的零去掉,才是真正之前加密的明文。Android数据传输加密(三):RSA加密

  • 选择“RSA/ECB/PKCS1Padding”填充模式

如果你的明文不够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);
	}
}

这段源码忘记从哪位大佬博客拿过来的,侵立删

你可能感兴趣的:(Android开发记录)