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

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?

  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] : [[email protected]
Text [Byte Format] : [[email protected]
Text Decryted : This is an example
以及这篇博客《RSA Encryption Example》中的代码

[java] view plain copy

print?

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

输出:
[[email protected]
这些输出其实都是字节数组在内存的位置的一个标识,而不是作者所认为的字节数组转换成的字符串内容。如果我们对密钥以 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?

  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?

  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?

  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?

  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

参考资料

时间: 2024-10-22 03:41:14

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

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

[转] Java 进行 RSA 加解密时不得不考虑到的那些事儿 1. 加密的系统不要具备解密的功能,否则 RSA 可能不太合适 公钥加密,私钥解密.加密的系统和解密的系统分开部署,加密的系统不应该同时具备解密的功能,这样即使黑客攻破了加密系统,他拿到的也只是一堆无法破解的密文数据.否则的话,你就要考虑你的场景是否有必要用 RSA 了. 2. 可以通过修改生成密钥的长度来调整密文长度 生成密文的长度等于密钥长度.密钥长度越大,生成密文的长度也就越大,加密的速度也就越慢,而密文也就越难被破解掉.著名

Java 进行 RSA 加解密的例子

加密是保证数据安全的手段之一.加密是将纯文本数据转换为难以理解的密文:解密是将密文转换回纯文本.数据的加解密属于密码学的范畴.通常,加密和解密都需要使用一些秘密信息,这些秘密信息叫做密钥,将纯文本转为密文或者转回的时候都要用到这些密钥.对称加密指的是发送者和接收者共用同一个密钥的加解密方法.非对称加密(又称公钥加密)指的是需要一个私有密钥一个公开密钥,两个不同的密钥的加解密体系.尽管不同,这个密钥对的这两个部分在算法上是有关联的.一个密钥将纯文本加密,另一个将密文解密.没有一个密钥能够把加密和加

java中RSA加解密的实现

今天在做RSA加密的时候遇到了一个这样的错误:ArrayIndexOutOfBoundsException: too much data for RSA block 查询相关资料后得知该错误是加密数据过长导致的. 加密数据长度 <= 模长-11 解决办法:将要加密的数据截取后分段加密 1.密钥长度rsa算法初始化的时候一般要填入密钥长度,在96-1024bits间(1)为啥下限是96bits(12bytes)?因为加密1byte的明文,需要至少1+11=12bytes的密钥(不懂?看下面的明文长

用Java实现RSA加解密及签名和验签(1)——.pem文件格式秘钥

一.***.pem文件格式的秘钥(获取秘钥:可通过文件读取内容或者直接打开文件复制内容),我这里是打开文件复制秘钥直接使用 1.准备秘钥对,通过openssl生成秘钥对,生成秘钥可参考:https://www.cnblogs.com/ouyanxia/p/12427955.html A_RSA_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDnPzYKf20JIza

用Java实现RSA加解密及签名和验签(2)——.pfx文件格式秘钥

注:.pfx 主要用于windows平台,浏览器可以使用,也是包含证书和私钥,获取私钥需要密码才可以 .pfx文件生成的方式可参考:https://www.cnblogs.com/ouyanxia/p/12427955.html 1.准备好pfx秘钥文件(alias默认是1) path=/RSA/other/openssl.pfx pwd=秘钥的秘钥(生成秘钥时记得存好) alias=1 cerPath=/RSA/other/openssl.cer 2.编写RSAUtil import java

全面解决.Net与Java互通时的RSA加解密问题,使用PEM格式的密钥文件

一.缘由 RSA是一种常用的非对称加密算法.所以有时需要在不用编程语言中分别使用RSA的加密.解密.例如用Java做后台服务端,用C#开发桌面的客户端软件时.由于 .Net.Java 的RSA类库存在很多细节区别,尤其是它们支持的密钥格式不同.导致容易出现“我加密的数据对方不能解密,对方加密的数据我不能解密,但是自身是可以正常加密解密”等情况.虽然网上已经有很多文章讨论 .Net与Java互通的RSA加解密,但是存在不够全面.需要第三方dll.方案复杂 等问题.于是我仔细研究了这一课题,得到了一

java rsa加解密算法的实现

RSAUtils:RSA加解密的实现 package com.rsa.test; import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import java.security.Key; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import jav

java RSA加解密以及用途

在公司当前版本的中间件通信框架中,为了防止非授权第三方和到期客户端的连接,我们通过AES和RSA两种方式的加解密策略进行认证.对于非对称RSA加解密,因为其性能耗费较大,一般仅用于认证连接,不会用于每次报文本身的加解密(这一般使用AES/DES加密),对于较为安全的支付通道,则一般是约定定期交换加解密密钥,交换过程本身的报文则是通过RSA进行加解密的.这样就在单纯的对称加密的基础上提供了更好的保障,只要签名复杂,定期的更新足以使得破坏的成本高昂到超过破解的成本. 一般来说,公钥会发布给客户端,客

java 使用pem密钥进行RSA加解密

1.使用openssl生成私钥和公钥 openssl下载地址:http://www.openssl.org/source openssl生成私钥命令:  genrsa -out rsa_private_key.pem 1024 openssl生成公钥命令:  rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem 2.此时在openssl安装目录下的bin文件夹可以看到 rsa_private_key.pem 和 rsa_publi