RSA加/解密的Java实现
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实现起了很大的作用。还有很多博主们的无私奉献,提供了很好的程序参考。不过,资源多起来,如何正确的筛选出有效的资源也成为一件需要好好研究的事情。