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.    
  5.             String example = "This is an example";  
  6.             byte[] bytes = example.getBytes();  
  7.    
  8.             System.out.println("Text : " + example);  
  9.             System.out.println("Text [Byte Format] : " + bytes);  
  10.             System.out.println("Text [Byte Format] : " + bytes.toString());  
  11.    
  12.             String s = new String(bytes);  
  13.             System.out.println("Text Decryted : " + s);  
  14.    
  15.    
  16.     }  
  17. }  

輸出:
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 ?
  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 ?
  1. // use String to hold cipher binary data  
  2. Base64 base64 = new Base64();   
  3. String cipherTextBase64 = base64.encodeToString(cipherText);  
  4.   
  5. // get cipher binary data back from String  
  6. 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.   
  15. import javax.crypto.Cipher;  
  16.   
  17. import org.apache.commons.codec.binary.Base64;  
  18.   
  19. /** 
  20.  * @author JavaDigest 
  21.  *  
  22.  */  
  23. public class EncryptionUtil {  
  24.   
  25.     /** 
  26.      * String to hold name of the encryption algorithm. 
  27.      */  
  28.     public static final String ALGORITHM = "RSA";  
  29.   
  30.     /** 
  31.      * String to hold name of the encryption padding. 
  32.      */  
  33.     public static final String PADDING = "RSA/NONE/NoPadding";  
  34.   
  35.     /** 
  36.      * String to hold name of the security provider. 
  37.      */  
  38.     public static final String PROVIDER = "BC";  
  39.   
  40.     /** 
  41.      * String to hold the name of the private key file. 
  42.      */  
  43.     public static final String PRIVATE_KEY_FILE = "e:/defonds/work/20150116/private.key";  
  44.   
  45.     /** 
  46.      * String to hold name of the public key file. 
  47.      */  
  48.     public static final String PUBLIC_KEY_FILE = "e:/defonds/work/20150116/public.key";  
  49.   
  50.     /** 
  51.      * Generate key which contains a pair of private and public key using 1024 
  52.      * bytes. Store the set of keys in Prvate.key and Public.key files. 
  53.      *  
  54.      * @throws NoSuchAlgorithmException 
  55.      * @throws IOException 
  56.      * @throws FileNotFoundException 
  57.      */  
  58.     public static void generateKey() {  
  59.         try {  
  60.   
  61.             Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());  
  62.             final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(  
  63.                     ALGORITHM, PROVIDER);  
  64.             keyGen.initialize(256);  
  65.             final KeyPair key = keyGen.generateKeyPair();  
  66.   
  67.             File privateKeyFile = new File(PRIVATE_KEY_FILE);  
  68.             File publicKeyFile = new File(PUBLIC_KEY_FILE);  
  69.   
  70.             // Create files to store public and private key  
  71.             if (privateKeyFile.getParentFile() != null) {  
  72.                 privateKeyFile.getParentFile().mkdirs();  
  73.             }  
  74.             privateKeyFile.createNewFile();  
  75.   
  76.             if (publicKeyFile.getParentFile() != null) {  
  77.                 publicKeyFile.getParentFile().mkdirs();  
  78.             }  
  79.             publicKeyFile.createNewFile();  
  80.   
  81.             // Saving the Public key in a file  
  82.             ObjectOutputStream publicKeyOS = new ObjectOutputStream(  
  83.                     new FileOutputStream(publicKeyFile));  
  84.             publicKeyOS.writeObject(key.getPublic());  
  85.             publicKeyOS.close();  
  86.   
  87.             // Saving the Private key in a file  
  88.             ObjectOutputStream privateKeyOS = new ObjectOutputStream(  
  89.                     new FileOutputStream(privateKeyFile));  
  90.             privateKeyOS.writeObject(key.getPrivate());  
  91.             privateKeyOS.close();  
  92.         } catch (Exception e) {  
  93.             e.printStackTrace();  
  94.         }  
  95.   
  96.     }  
  97.   
  98.     /** 
  99.      * The method checks if the pair of public and private key has been 
  100.      * generated. 
  101.      *  
  102.      * @return flag indicating if the pair of keys were generated. 
  103.      */  
  104.     public static boolean areKeysPresent() {  
  105.   
  106.         File privateKey = new File(PRIVATE_KEY_FILE);  
  107.         File publicKey = new File(PUBLIC_KEY_FILE);  
  108.   
  109.         if (privateKey.exists() && publicKey.exists()) {  
  110.             return true;  
  111.         }  
  112.         return false;  
  113.     }  
  114.   
  115.     /** 
  116.      * Encrypt the plain text using public key. 
  117.      *  
  118.      * @param text 
  119.      *            : original plain text 
  120.      * @param key 
  121.      *            :The public key 
  122.      * @return Encrypted text 
  123.      * @throws java.lang.Exception 
  124.      */  
  125.     public static byte[] encrypt(String text, PublicKey key) {  
  126.         byte[] cipherText = null;  
  127.         try {  
  128.             // get an RSA cipher object and print the provider  
  129.             Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());  
  130.             final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);  
  131.               
  132.             // encrypt the plain text using the public key  
  133.             cipher.init(Cipher.ENCRYPT_MODE, key);  
  134.             cipherText = cipher.doFinal(text.getBytes());  
  135.         } catch (Exception e) {  
  136.             e.printStackTrace();  
  137.         }  
  138.         return cipherText;  
  139.     }  
  140.   
  141.     /** 
  142.      * Decrypt text using private key. 
  143.      *  
  144.      * @param text 
  145.      *            :encrypted text 
  146.      * @param key 
  147.      *            :The private key 
  148.      * @return plain text 
  149.      * @throws java.lang.Exception 
  150.      */  
  151.     public static String decrypt(byte[] text, PrivateKey key) {  
  152.         byte[] dectyptedText = null;  
  153.         try {  
  154.             // get an RSA cipher object and print the provider  
  155.             Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());  
  156.             final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);  
  157.   
  158.             // decrypt the text using the private key  
  159.             cipher.init(Cipher.DECRYPT_MODE, key);  
  160.             dectyptedText = cipher.doFinal(text);  
  161.   
  162.         } catch (Exception ex) {  
  163.             ex.printStackTrace();  
  164.         }  
  165.   
  166.         return new String(dectyptedText);  
  167.     }  
  168.   
  169.     /** 
  170.      * Test the EncryptionUtil 
  171.      */  
  172.     public static void main(String[] args) {  
  173.   
  174.         try {  
  175.   
  176.             // Check if the pair of keys are present else generate those.  
  177.             if (!areKeysPresent()) {  
  178.                 // Method generates a pair of keys using the RSA algorithm and  
  179.                 // stores it  
  180.                 // in their respective files  
  181.                 generateKey();  
  182.             }  
  183.   
  184.             final String originalText = "12345678901234567890123456789012";  
  185.             ObjectInputStream inputStream = null;  
  186.   
  187.             // Encrypt the string using the public key  
  188.             inputStream = new ObjectInputStream(new FileInputStream(  
  189.                     PUBLIC_KEY_FILE));  
  190.             final PublicKey publicKey = (PublicKey) inputStream.readObject();  
  191.             final byte[] cipherText = encrypt(originalText, publicKey);  
  192.   
  193.             // use String to hold cipher binary data  
  194.             Base64 base64 = new Base64();  
  195.             String cipherTextBase64 = base64.encodeToString(cipherText);  
  196.   
  197.             // get cipher binary data back from String  
  198.             byte[] cipherTextArray = base64.decode(cipherTextBase64);  
  199.   
  200.             // Decrypt the cipher text using the private key.  
  201.             inputStream = new ObjectInputStream(new FileInputStream(  
  202.                     PRIVATE_KEY_FILE));  
  203.             final PrivateKey privateKey = (PrivateKey) inputStream.readObject();  
  204.             final String plainText = decrypt(cipherTextArray, privateKey);  
  205.   
  206.             // Printing the Original, Encrypted and Decrypted Text  
  207.             System.out.println("Original=" + originalText);  
  208.             System.out.println("Encrypted=" + cipherTextBase64);  
  209.             System.out.println("Decrypted=" + plainText);  
  210.   
  211.         } catch (Exception e) {  
  212.             e.printStackTrace();  
  213.         }  
  214.     }  
  215. }  


先生成一對密鑰,供以后加解密使用(不需要每次加解密都生成一個密鑰),密鑰長度為 256 位,也就是說生成密文長度都是 32 字節的,支持加密最大長度為 32 字節的明文,因為使用了 nopadding 所以對於同一密鑰同一明文,本文總是生成一樣的密文;然后使用生成的公鑰對你提供的明文信息進行加密,生成 32 字節二進制明文,然后使用 Base64 將二進制密文轉換為字符串保存;之后演示了如何把 Base64 字符串轉換回二進制密文;最后把二進制密文轉換成加密前的明文。以上程序輸出如下:
Original=12345678901234567890123456789012
Encrypted=GTyX3nLO9vseMJ+RB/dNrZp9XEHCzFkHpgtaZKa8aCc=
Decrypted=12345678901234567890123456789012

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM