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 》中的代碼
輸出:
Text : This is an example
Text [Byte Format] : [B@187aeca
Text [Byte Format] : [B@187aeca
Text Decryted : This is an example
以及這篇博客《 RSA Encryption Example 》中的代碼
輸出:
[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):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 來加密傳輸,但是由於你的同一明文每次生成的同一密文,攻擊者能夠據此識別到同一個信息都是何時被發送。
7. 可以通過調整算法提供者來減小密文長度
Java 默認的 RSA 實現 "RSA/None/PKCS1Padding" 要求最小密鑰長度為 512 位(否則會報 java.security.InvalidParameterException: RSA keys must be at least 512 bits long 異常),也就是說生成的密鑰、密文長度最小為 64 個字節。如果你還嫌大,可以通過調整算法提供者來減小密文長度:如此這般得到的密文長度為 128 位(16 個字節)。但是這么干之前請先回顧一下本文第 2 點所述。
8. Cipher 是有狀態的,而且是線程不安全的
javax.crypto.Cipher 是有狀態的,不要把 Cipher 當做一個靜態變量,除非你的程序是單線程的,也就是說你能夠保證同一時刻只有一個線程在調用 Cipher。否則你可能會像筆者似的遇到 Java.lang.ArrayIndexOutOfBoundsException: too much data for RSA block 異常。遇見這個異常,你需要先確定你給 Cipher 加密的明文(或者需要解密的密文)是否過長;排除掉明文(或者密文)過長的情況,你需要考慮是不是你的 Cipher 線程不安全了。
后記
雖然《 RSA Encryption Example 》存在一些認識上的誤區,但筆者仍然認為它是一篇很不錯的入門級文章。結合本文所列內容,筆者將其代碼做了一些調整以供參考:先生成一對密鑰,供以后加解密使用(不需要每次加解密都生成一個密鑰),密鑰長度為 256 位,也就是說生成密文長度都是 32 字節的,支持加密最大長度為 32 字節的明文,因為使用了 nopadding 所以對於同一密鑰同一明文,本文總是生成一樣的密文;然后使用生成的公鑰對你提供的明文信息進行加密,生成 32 字節二進制明文,然后使用 Base64 將二進制密文轉換為字符串保存;之后演示了如何把 Base64 字符串轉換回二進制密文;最后把二進制密文轉換成加密前的明文。以上程序輸出如下:
Original=12345678901234567890123456789012
Encrypted=GTyX3nLO9vseMJ+RB/dNrZp9XEHCzFkHpgtaZKa8aCc=
Decrypted=12345678901234567890123456789012
參考資料
- http://www.bouncycastle.org/wiki/display/JA1/Frequently+Asked+Questions
- http://stackoverflow.com/questions/1536054/how-to-convert-byte-array-to-string-and-vice-versa
- http://www.experts-exchange.com/Security/Encryption/Q_26980724.html
- http://stackoverflow.com/questions/17497426/why-does-rsa-produce-different-results-with-same-key-and-message
