Zydecx's Site

Debug code, debug life, debug today!

RSA加/解密的Java实现

Time: , by zydecx

RSA算法是一种常用的非对称加密算法。所谓非对称性,是指加/解密的过程中需要两个不同的密钥——公钥和私钥,通过公钥对数据加密,通过私钥对密文解密。RSA算法的可靠性是建立在大数因数分解的难度上的。在数论中,对极大整数做因数分解的难度极大,因此决定了RSA算法的高可靠性。若想了解RSA算法的数学原理,可参考WIKI:RSA加密算法RSA算法原理

本文介绍在Java中实现RSA的加/解密操作。(测试环境:Windows OS, jdk_1.7.0_25)

1、获得公钥和私钥

使用RSA算法加/解密前,需要先获得公钥和私钥。代码实现如下:

			KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
			keyPairGen.initialize(KEY_SIZE);
			KeyPair keyPair = keyPairGen.generateKeyPair();
			
			RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
			RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

获得密钥过程中,执行keyPairGen.initialize(KEY_SIZE);方法初始化密钥大小为KEY_SIZE。这里的KEY_SIZE即为RSA算法中所采用的大数的比特位数,KEY_SIZE越大,安全性越好。一般取值1024,对安全性要求更高可取2048。2009年12月,768位的大数被成功因数分解,因此,KEY_SIZE过小可能存在被攻击破解的可能。

密钥的存储与恢复

应用时,需要对密钥做存储。存储密钥有两种方式,一种是利用标准流输入/输出方式,将密钥对象写入到本地文件;一种是保存密钥的模和指数,使用时利用这两个参数恢复密钥(上文的设置的KEY_SIZE其实就是模的大小,若想更好的理解这里的指数的概念,建议阅读前文中推荐的RSA算法原理介绍)。

方法一:将密钥对象写入到本地文件

该方法的代码实现如下所示(以公钥的写入和读取为例)。

写入密钥:

	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(publicKeyPath));
	oos.writeObject(publicKey);
	oos.flush();
	oos.close();
					

读取密钥:

	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(publicKeyPath));
	RSAPublicKey publicKey = (RSAPublicKey) ois.readObject();
	ois.close();
					

方法二:保存密钥的模和指数

获得密钥后,保存密钥的模和指数,即可在之后通过这两个参数恢复密钥。该方法的代码实现如下所示(以公钥的写入和读取为例)。

获得公钥的模和指数:

	String modules = publicKey.getModulus().toString();
	String exponent = publicKey.getPublicExponent().toString();
					

通过模和指数还原公钥:

	BigInteger bModules = new BigInteger(modules);
	BigInteger bExponent = new BigInteger(exponent);
	RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(bModules, bExponent);
	KeyFactory keyFactory = KeyFactory.getInstance("RSA");
	RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);
					

2、通过公钥加密

加密时,首先通过步骤1获得公钥,然后获得Cipher对象,并用公钥进行加密初始化,代码如下:

			//Cipher cipher = Cipher.getInstance("RSA");
			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.ENCRYPT_MODE, publicKey);
获得Cipher对象时,传入加密算法变换式,以得到指定加密算法的Cipher对象。加密算法变换式的完整格式为“加密算法/分组密码工作模式/填充模式”;也可以直接传入“加密算法”,Java将使用默认的分组密码工作模式(ECB)和填充模式(PKCS1Padding)。以上面代码为例,注释部分与未注释部分效果相同,当采用注释语句来获得Cipher对象,Java自动匹配成“RSA/ECB/PKCS1Padding”返回Cipher对象。Oracle JCA使用手册有对此的详细介绍。Oracle JCA标准算法名手册中罗列了Java中的实现的加密算法变换式。

初始化完成后,需要对明文进行预处理,包括将明文转换为byte数组,并确定明文的分块大小和分块数量。RSA算法要求待加密的数据比特位数不能超过RSA的的位数,因此当明文较大时,还需要对明文分块加密,分块的大小不超过模的大小;此外,明文的位数是不固定的,而分块加密后的密文位数是固定的,为了能在解密的时候准确获得明文的实际位数,需对明文按一定规则填充。以PKCS1Padding填充模式为例,它需要在每个分块中使用11字节的填充位。因此,对于一个模位数为1024且采用PKCS1Padding的RSA算法,分块大小为1024/8=128字节,其中11字节为填充位,分块中的明文数据最多为128-11=117字节。当使用不同的填充模式时,由于填充位的不同,分块中可用的明文数据大小也可能会有不同。

该部分代码如下:

			byte[] bytes = plainText.getBytes("UTF-8");
		
			int blockSize = key.getModulus().bitLength() / 8 - 11;	// 待加密数据长度 <= 模长-11(PKCS1Padding算法填充位),超过大小进行分块加密
			int bytesLength = bytes.length;
			int blockNumber = bytesLength / blockSize;
			int bytesRemainLength = bytesLength % blockSize;
			
			int blockOutSize = key.getModulus().bitLength() / 8;
			int blockOutNumber = bytesRemainLength > 0 ? blockNumber + 1 : blockNumber;
			byte[] bytesOut = new byte[blockOutNumber * blockOutSize];
		

接下来便是加密过程。加密时,先对blockNumber个分块进行加密,最后对剩余的字节加密。实现代码如下:

			int byteOutOffset = 0;
			for (int i = 0; i < blockNumber; i++) {
				byteOutOffset += cipher.doFinal(bytes, i * blockSize, blockSize, bytesOut, byteOutOffset);
			}
			if (bytesRemainLength > 0) {
				byteOutOffset += cipher.doFinal(bytes, blockNumber * blockSize, bytesRemainLength, bytesOut, byteOutOffset);	// 返回值不确定,与bytesRemainLength大小有关
			}
		

最后是对密文格式化处理,将byte数组转换为字符串,以便于传输或保存。这里实现的方式就比较多样化了,既可以自己定义转换规则,也可以使用Base64编码规则。这里使用了org.apache.commons.codec.binary.Base64类(不建议使用JDK自带的BASE64类,不排除不同版本的JDK会有不同实现方式导致兼容性问题)对密文编码,编码部分的代码如下:

			String cipherText = new String(Base64.encodeBase64(bytesOut), "UTF-8");
		

生成的cipherText即可用于传输或保存,等待对端解密。

3、通过私钥解密

解密是加密的逆过程,因此整个过程是类似的,这里不再赘述,解密部分程序实现如下:

			Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			
			byte[] bytes = Base64.decodeBase64(cipherText.getBytes("UTF-8"));
			
			int blockSize = key.getModulus().bitLength() / 8;
			int bytesLength = bytes.length;
			int blockNumber = bytesLength / blockSize;
			
			int blockOutSize = key.getModulus().bitLength() / 8;
			byte[] bytesOut = new byte[blockNumber * blockOutSize];
			
			int byteOutOffset = 0;
			for (int i = 0; i < blockNumber; i++) {
				byteOutOffset += cipher.doFinal(bytes, i * blockSize, blockSize, bytesOut, byteOutOffset);	// 返回值不确定,可参考加密过程,此处与其相反
			}
			if (byteOutOffset < bytesOut.length) {
				bytesOut = Arrays.copyOf(bytesOut, byteOutOffset);
			}
			
			String plainText = new String(bytesOut, "UTF-8");
		

4、写在后面

这篇文章整理起来比我想象的要难得多。之前已经凭着网上资料完成了一个RSA加/解密的初版。但此次整理时,又发现了很多新问题,尤其11字节填充位和分块加/解密部分。以前只是对网上代码简单的复制、整合,理解并不是很深。想要更好理解RSA加/解密过程,还是要好好研究一下它的数学理论,至少对程序中的出现的模、指数等概念会有个清晰的概念。当然,数论真心太难,看了最开始推荐的几篇文章,唯一的感觉就是——作者讲的都是些什么!

网络确实是一个很好的学习资源。在学习RSA算法的时候,Wiki和Java官方文档对帮助我理解这个算法和Java实现起了很大的作用。还有很多博主们的无私奉献,提供了很好的程序参考。不过,资源多起来,如何正确的筛选出有效的资源也成为一件需要好好研究的事情。


This is a magic phrase. You CANNOT see it(I'll really FULE you if you do that), but it does work. Why? You may feel confused. OK, at least it doesn't afftect your experience and it works. That is what we call MAGICE!