Android RSA加密传输的那些事儿

前言

本文不讨论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; //一次加密后的密文长度
    

 /**
     * 用公钥加密 <br>
     * 每次加密的字节数,不能超过密钥的长度值减去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[]inputdata输入的明文byte[]
intinputOffsetoffset*i输入的byte[]起始位置
intinputlenBLOCK_SIZE加密长度
byte[]outputencryptedData输出的密文
intoutputOffsetencryptedData输出密文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);
	}
}

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值