Java 进行 RSA 加解密时不得不考虑到的那些事儿

Wesley13
• 阅读 1025

Java 进行 RSA 加解密时不得不考虑到的那些事儿 博客分类: java

1. 加密的系统不要具备解密的功能,否则 RSA 可能不太合适

公钥加密,私钥解密。加密的系统和解密的系统分开部署,加密的系统不应该同时具备解密的功能,这样即使黑客攻破了加密系统,他拿到的也只是一堆无法破解的密文数据。否则的话,你就要考虑你的场景是否有必要用 RSA 了。

2. 可以通过修改生成密钥的长度来调整密文长度

生成密文的长度等于密钥长度。密钥长度越大,生成密文的长度也就越大,加密的速度也就越慢,而密文也就越难被破解掉。著名的"安全和效率总是一把双刃剑"定律,在这里展现的淋漓尽致。我们必须通过定义密钥的长度在"安全"和"加解密效率"之间做出一个平衡的选择。

3. 生成密文的长度和明文长度无关,但明文长度不能超过密钥长度

不管明文长度是多少,RSA 生成的密文长度总是固定的。
但 是明文长度不能超过密钥长度。比如 Java 默认的 RSA 加密实现不允许明文长度超过密钥长度减去 11(单位是字节,也就是 byte)。也就是说,如果我们定义的密钥(我们可以通过 java.security.KeyPairGenerator.initialize(int keysize) 来定义密钥长度)长度为 1024(单位是位,也就是 bit),生成的密钥长度就是 1024位 / 8位/字节 = 128字节,那么我们需要加密的明文长度不能超过 128字节 -
11 字节 = 117字节。也就是说,我们最大能将 117 字节长度的明文进行加密,否则会出问题(抛诸如 javax.crypto.IllegalBlockSizeException: Data must not be longer than 53 bytes 的异常)。
而 BC 提供的加密算法能够支持到的 RSA 明文长度最长为密钥长度。

4. byte[].toString() 返回的实际上是内存地址,不是将数组的实际内容转换为 String

警惕 toString 陷阱:Java 中数组的 toString() 方法返回的并非数组内容,它返回的实际上是数组存储元素的类型以及数组在内存的位置的一个标识。
大部分人跌入这个误区而不自知,包括一些写了多年 Java 的老鸟。比如这篇博客《How To Convert Byte[] Array To String In Java》中的代码

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. public class TestByte

  2. {

  3. public static void main(String[] argv) {

  4. String example = "This is an example";

  5. byte[] bytes = example.getBytes();

  6. System.out.println("Text : " + example);

  7. System.out.println("Text [Byte Format] : " + bytes);

  8. System.out.println("Text [Byte Format] : " + bytes.toString());

  9. String s = new String(bytes);

  10. System.out.println("Text Decryted : " + s);

  11. }

  12. }

输出:
Text : This is an example
Text [Byte Format] : [B@187aeca
Text [Byte Format] : [B@187aeca
Text Decryted : This is an example
以及这篇博客《RSA Encryption Example》中的代码

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. final byte[] cipherText = encrypt(originalText, publicKey);
  2. System.out.println("Encrypted: " +cipherText.toString());

输出:
[B@4c3a8ea3
这 些输出其实都是字节数组在内存的位置的一个标识,而不是作者所认为的字节数组转换成的字符串内容。如果我们对密钥以 byte[].toString() 进行持久化存储或者和其他一些字符串打 json 传输,那么密钥的解密者得到的将只是一串毫无意义的字符,当他解码的时候很可能会遇到 "javax.crypto.BadPaddingException" 异常。

5. 字符串用以保存文本信息,字节数组用以保存二进制数据

java.lang.String 保存明文,byte 数组保存二进制密文,在 java.lang.String 和 byte[] 之间不应该具备互相转换。如果你确实必须得使用 java.lang.String 来持有这些二进制数据的话,最安全的方式是使用 Base64(推荐 Apache 的 commons-codec 库的 org.apache.commons.codec.binary.Base64):

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. // use String to hold cipher binary data

  2. Base64 base64 = new Base64();

  3. String cipherTextBase64 = base64.encodeToString(cipherText);

  4. // get cipher binary data back from String

  5. byte[] cipherTextArray = base64.decode(cipherTextBase64);

6. 每次生成的密文都不一致证明你选用的加密算法很安全

一个优秀的加密必须每次生成的密文都不一致,即使每次你的明文一样、使用同一个公钥。因为这样才能把明文信息更安全地隐藏起来。
Java 默认的 RSA 实现是 "RSA/None/PKCS1Padding"(比如 Cipher cipher = Cipher.getInstance("RSA");句,这个 Cipher 生成的密文总是不一致的),Bouncy Castle 的默认 RSA 实现是 "RSA/None/NoPadding"。
为什么 Java 默认的 RSA 实现每次生成的密文都不一致呢,即使每次使用同一个明文、同一个公钥?这是因为 RSA 的 PKCS #1 padding 方案在加密前对明文信息进行了随机数填充。
你可以使用以下办法让同一个明文、同一个公钥每次生成同一个密文,但是你必须意识到你这么做付出的代价是什么。比如,你可能使用 RSA 来加密传输,但是由于你的同一明文每次生成的同一密文,攻击者能够据此识别到同一个信息都是何时被发送。

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
  2. final Cipher cipher = Cipher.getInstance("RSA/None/NoPadding", "BC");

7. 可以通过调整算法提供者来减小密文长度

Java 默认的 RSA 实现 "RSA/None/PKCS1Padding" 要求最小密钥长度为 512 位(否则会报 java.security.InvalidParameterException: RSA keys must be at least 512 bits long 异常),也就是说生成的密钥、密文长度最小为 64 个字节。如果你还嫌大,可以通过调整算法提供者来减小密文长度:

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
  2. final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC");
  3. keyGen.initialize(128);

如此这般得到的密文长度为 128 位(16 个字节)。但是这么干之前请先回顾一下本文第 2 点所述。

8. Cipher 是有状态的,而且是线程不安全的

javax.crypto.Cipher 是有状态的,不要把 Cipher 当做一个静态变量,除非你的程序是单线程的,也就是说你能够保证同一时刻只有一个线程在调用 Cipher。否则你可能会像笔者似的遇到 java.lang.ArrayIndexOutOfBoundsException: too much data for RSA block 异常。遇见这个异常,你需要先确定你给 Cipher 加密的明文(或者需要解密的密文)是否过长;排除掉明文(或者密文)过长的情况,你需要考虑是不是你的 Cipher 线程不安全了。

后记

虽然《RSA Encryption Example》存在一些认识上的误区,但笔者仍然认为它是一篇很不错的入门级文章。结合本文所列内容,笔者将其代码做了一些调整以供参考:

[java] view plain copy print ? Java 进行 RSA 加解密时不得不考虑到的那些事儿 Java 进行 RSA 加解密时不得不考虑到的那些事儿

  1. import java.io.File;

  2. import java.io.FileInputStream;

  3. import java.io.FileNotFoundException;

  4. import java.io.FileOutputStream;

  5. import java.io.IOException;

  6. import java.io.ObjectInputStream;

  7. import java.io.ObjectOutputStream;

  8. import java.security.KeyPair;

  9. import java.security.KeyPairGenerator;

  10. import java.security.NoSuchAlgorithmException;

  11. import java.security.PrivateKey;

  12. import java.security.PublicKey;

  13. import java.security.Security;

  14. import javax.crypto.Cipher;

  15. import org.apache.commons.codec.binary.Base64;

  16. /**

  17. * @author JavaDigest

  18. *

  19. */

  20. public class EncryptionUtil {

  21. /**

  22. * String to hold name of the encryption algorithm.

  23. */

  24. public static final String ALGORITHM = "RSA";

  25. /**

  26. * String to hold name of the encryption padding.

  27. */

  28. public static final String PADDING = "RSA/NONE/NoPadding";

  29. /**

  30. * String to hold name of the security provider.

  31. */

  32. public static final String PROVIDER = "BC";

  33. /**

  34. * String to hold the name of the private key file.

  35. */

  36. public static final String PRIVATE_KEY_FILE = "e:/defonds/work/20150116/private.key";

  37. /**

  38. * String to hold name of the public key file.

  39. */

  40. public static final String PUBLIC_KEY_FILE = "e:/defonds/work/20150116/public.key";

  41. /**

  42. * Generate key which contains a pair of private and public key using 1024

  43. * bytes. Store the set of keys in Prvate.key and Public.key files.

  44. *

  45. * @throws NoSuchAlgorithmException

  46. * @throws IOException

  47. * @throws FileNotFoundException

  48. */

  49. public static void generateKey() {

  50. try {

  51. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

  52. final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(

  53. ALGORITHM, PROVIDER);

  54. keyGen.initialize(256);

  55. final KeyPair key = keyGen.generateKeyPair();

  56. File privateKeyFile = new File(PRIVATE_KEY_FILE);

  57. File publicKeyFile = new File(PUBLIC_KEY_FILE);

  58. // Create files to store public and private key

  59. if (privateKeyFile.getParentFile() != null) {

  60. privateKeyFile.getParentFile().mkdirs();

  61. }

  62. privateKeyFile.createNewFile();

  63. if (publicKeyFile.getParentFile() != null) {

  64. publicKeyFile.getParentFile().mkdirs();

  65. }

  66. publicKeyFile.createNewFile();

  67. // Saving the Public key in a file

  68. ObjectOutputStream publicKeyOS = new ObjectOutputStream(

  69. new FileOutputStream(publicKeyFile));

  70. publicKeyOS.writeObject(key.getPublic());

  71. publicKeyOS.close();

  72. // Saving the Private key in a file

  73. ObjectOutputStream privateKeyOS = new ObjectOutputStream(

  74. new FileOutputStream(privateKeyFile));

  75. privateKeyOS.writeObject(key.getPrivate());

  76. privateKeyOS.close();

  77. } catch (Exception e) {

  78. e.printStackTrace();

  79. }

  80. }

  81. /**

  82. * The method checks if the pair of public and private key has been

  83. * generated.

  84. *

  85. * @return flag indicating if the pair of keys were generated.

  86. */

  87. public static boolean areKeysPresent() {

  88. File privateKey = new File(PRIVATE_KEY_FILE);

  89. File publicKey = new File(PUBLIC_KEY_FILE);

  90. if (privateKey.exists() && publicKey.exists()) {

  91. return true;

  92. }

  93. return false;

  94. }

  95. /**

  96. * Encrypt the plain text using public key.

  97. *

  98. * @param text

  99. *            : original plain text

  100. * @param key

  101. *            :The public key

  102. * @return Encrypted text

  103. * @throws java.lang.Exception

  104. */

  105. public static byte[] encrypt(String text, PublicKey key) {

  106. byte[] cipherText = null;

  107. try {

  108. // get an RSA cipher object and print the provider

  109. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

  110. final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);

  111. // encrypt the plain text using the public key

  112. cipher.init(Cipher.ENCRYPT_MODE, key);

  113. cipherText = cipher.doFinal(text.getBytes());

  114. } catch (Exception e) {

  115. e.printStackTrace();

  116. }

  117. return cipherText;

  118. }

  119. /**

  120. * Decrypt text using private key.

  121. *

  122. * @param text

  123. *            :encrypted text

  124. * @param key

  125. *            :The private key

  126. * @return plain text

  127. * @throws java.lang.Exception

  128. */

  129. public static String decrypt(byte[] text, PrivateKey key) {

  130. byte[] dectyptedText = null;

  131. try {

  132. // get an RSA cipher object and print the provider

  133. Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

  134. final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);

  135. // decrypt the text using the private key

  136. cipher.init(Cipher.DECRYPT_MODE, key);

  137. dectyptedText = cipher.doFinal(text);

  138. } catch (Exception ex) {

  139. ex.printStackTrace();

  140. }

  141. return new String(dectyptedText);

  142. }

  143. /**

  144. * Test the EncryptionUtil

  145. */

  146. public static void main(String[] args) {

  147. try {

  148. // Check if the pair of keys are present else generate those.

  149. if (!areKeysPresent()) {

  150. // Method generates a pair of keys using the RSA algorithm and

  151. // stores it

  152. // in their respective files

  153. generateKey();

  154. }

  155. final String originalText = "12345678901234567890123456789012";

  156. ObjectInputStream inputStream = null;

  157. // Encrypt the string using the public key

  158. inputStream = new ObjectInputStream(new FileInputStream(

  159. PUBLIC_KEY_FILE));

  160. final PublicKey publicKey = (PublicKey) inputStream.readObject();

  161. final byte[] cipherText = encrypt(originalText, publicKey);

  162. // use String to hold cipher binary data

  163. Base64 base64 = new Base64();

  164. String cipherTextBase64 = base64.encodeToString(cipherText);

  165. // get cipher binary data back from String

  166. byte[] cipherTextArray = base64.decode(cipherTextBase64);

  167. // Decrypt the cipher text using the private key.

  168. inputStream = new ObjectInputStream(new FileInputStream(

  169. PRIVATE_KEY_FILE));

  170. final PrivateKey privateKey = (PrivateKey) inputStream.readObject();

  171. final String plainText = decrypt(cipherTextArray, privateKey);

  172. // Printing the Original, Encrypted and Decrypted Text

  173. System.out.println("Original=" + originalText);

  174. System.out.println("Encrypted=" + cipherTextBase64);

  175. System.out.println("Decrypted=" + plainText);

  176. } catch (Exception e) {

  177. e.printStackTrace();

  178. }

  179. }

  180. }

Java 进行 RSA 加解密时不得不考虑到的那些事儿

先 生成一对密钥,供以后加解密使用(不需要每次加解密都生成一个密钥),密钥长度为 256 位,也就是说生成密文长度都是 32 字节的,支持加密最大长度为 32 字节的明文,因为使用了 nopadding 所以对于同一密钥同一明文,本文总是生成一样的密文;然后使用生成的公钥对你提供的明文信息进行加密,生成 32 字节二进制明文,然后使用 Base64 将二进制密文转换为字符串保存;之后演示了如何把 Base64 字符串转换回二进制密文;最后把二进制密文转换成加密前的明文。以上程序输出如下:
Original=12345678901234567890123456789012
Encrypted=GTyX3nLO9vseMJ+RB/dNrZp9XEHCzFkHpgtaZKa8aCc=
Decrypted=12345678901234567890123456789012

参考资料

http://blog.csdn.net/defonds/article/details/42775183

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
RSA加密、解密、签名、验签的原理及方法
一、RSA加密简介  RSA加密是一种非对称加密。可以在不直接传递密钥的情况下,完成解密。这能够确保信息的安全性,避免了直接传递密钥所造成的被破解的风险。是由一对密钥来进行加解密的过程,分别称为公钥和私钥。两者之间有数学相关,该加密算法的原理就是对一极大整数做因数分解的困难性来保证安全性。通常个人保存私钥,公钥是公开的(可能同时多人持有)。
Wesley13 Wesley13
3年前
RSA —— 典型非对称加密算法
RSA——JAVA代码(toc_0)RSA——加密过程图解(toc_1)(图1)构建RSA算法密匙对(toc_2)(图2)甲方向乙方发送RSA加密数据(toc_3)(图3)乙方向甲方发送RSA加密数据(toc_4)RSA——简述(
Wesley13 Wesley13
3年前
RSA加密
rsa加密的解决方案都是需要证书或者文件的,对于服务端提供了公钥,没有给证书或者pem文件的rsa加密:把公钥和你需要编码得Nsstring都转换成NSData用rsa公钥编码你所需要编码得内容最后转换成Nsstring//库 SecKeyWrapper//RSA加密\(NSString\)encryptWithStr
Stella981 Stella981
3年前
OpenSSL和Python实现RSA Key公钥加密私钥解密
基于非对称算法的RSAKey主要有两个用途,数字签名和验证(私钥签名,公钥验证),以及非对称加解密(公钥加密,私钥解密)。本文提供一个基于OpenSSL和Python进行非对称加解密的例子。1\.OpenSSL实现非对称加解密1.1生成私钥,并导出公钥生成2048bit的PEM格式的RSAKey:Key.pem$openssl
Stella981 Stella981
3年前
RSA 加密 解密 (长字符串) JAVA JS版本加解密
系统与系统的数据交互中,有些敏感数据是不能直接明文传输的,所以在发送数据之前要进行加密,在接收到数据时进行解密处理;然而由于系统与系统之间的开发语言不同。本次需求是生成二维码是通过java生成,由php来解密。基于这类需求所以选择了RSA进行加解密。生成RSA公私钥分成三步生成,第1、2步可以满足php的使用,由于java的私钥要转化为PKCS8格式
Stella981 Stella981
3年前
Openssl生成RSA公私钥以及将公钥转换成C#支持的格式
Openssl生成RSA公私钥以及将公钥转换成C支持的格式1.RSA算法介绍RSA算法是一种非对称密码算法,所谓非对称,就是指该算法需要一对密钥,使用其中一个加密,则需要用另一个才能解密。RSA的算法涉及三个参数,n、e、d。其中,n是两个大质数p、q的积,n被称为模数,n的二进
Wesley13 Wesley13
3年前
JAVA_RSA_的加解密
RSA为非对称加密算法。数字签名的过程:1、对明文数据进行HASH加密,不可逆;2、对加密后的数据再用RSA的私钥进行二次加密。数字签名的验证过程:1、对明文数据进行HASH加密,不可逆;2、用RSA的公钥对数字签名后的数据进行解密;3、把1的结果和2的结果进行比较是否相等。RSA加密的过程和解密的过程都需要三步:加/解密、分组、填充。这三部分每
Wesley13 Wesley13
3年前
VC++网络安全编程范例(2)
数字证书采用公钥体制,即利用一对互相匹配的密钥进行加密、解密。每个用户自己设定一把特定的仅为本人所知的私有密钥(私钥),用它进行解密和签名;同时设定一把公共密钥(公钥)并由本人公开,为一组用户所共享,用于加密和验证签名。当发送一份保密文件时,发送方使用接收方的公钥对数据加密,而接收方则使用自己的私钥解密,这样信息就可以安全无误地到达目的地了。通过数字的手段