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

时间:2022-02-14 21:39:14

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》中的代码

  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》中的代码

  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):

  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 来加密传输,但是由于你的同一明文每次生成的同一密文,攻击者能够据此识别到同一个信息都是何时被发送。

  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 个字节。如果你还嫌大,可以通过调整算法提供者来减小密文长度:

  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》存在一些认识上的误区,但笔者仍然认为它是一篇很不错的入门级文章。结合本文所列内容,笔者将其代码做了一些调整以供参考:

  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. }

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

参考资料