但那段程式碼沒有辦法加解密長度稍長的資料(大概是 100 多個 bytes 以上)
稍微查詢了一些資料,目前個人的了解應該是受限於 RSA 是個 block cipher
block cipher 本身的設計就是一次只用來加解密很小的資料。
網路上雖然討論在 Java 中使用 RSA 的文章很多
但多數似乎都假定看的人應該要知道 RSA 的 "一次加密" 只能加密很短的資料,就很少在文章中提到這件事....。
PS. "一次加密" 指的就是執行一次 RSA 的動作。
什麼是 block cipher?
那麼什麼是 block cipher 呢?這跟密碼學的分類法有點關係
可以參考 [1] 的 PDF 檔(由網址看來是稻江技術學院的教學投影片吧)
根據投影片的內容,密碼學可以用不同的方式進行分類,有一種分類方法是依照對明文的處理方式不同來分類
因此可區分為資料區段加密法(block cipher)和資料流加密法(stream cipher)
而兩種方法的差異自 [1] 節錄如下:
(1)「資料區段加密 (block cipher)」
將明文分成數個n個字元或位元的區段,並且對每一個區段資料應用相同的演算法則和鑰匙,數學式表示為 (M為明文,分割成M1、M2… Mn區段)
• E(M,K)=E(M1,K)E(M2,K)… ..E(Mn,K)
(2)「資料流加密 (stream cipher)」
資料流加密並不會將明文切分為區段,而是一次加密資料流的一個位元或是位元組。常見的作法是將較短的加密鑰匙延展成為無限長、近似亂碼的一長串金鑰串流(keystream),再將金鑰串流和原始資料(plain text)經過XOR運算後,產生密文資料(cipher text)。
RSA 的長度限制為何?
有關 RSA 加密的長度限制問題,在 Java 中允許的長度可以參考 [2] 的討論
根據 neilcoffey 的回應,長度限制好像是 (key_size/8)-11
key_size 是金鑰長度(bits),轉成 byte 為單位後扣掉 11 bytes 作為 meta data,剩下的才是放資料的長度。
如何使用 RSA 加解密長資料?
要用 RSA 加解密長資料,其實作法在上面針對 block cipher 的說明中就提到了
要將長資料切割成許多的小資料,然後對每個小資料個別用 RSA 和金鑰去加密
接收端收到密文時,就把密文切割開來之後,每段個別做解密,解完的所有片段組合起來就會變成原本的明文了。
這裡參考的資料為 [3],以下的程式碼是以上一篇文章「在 Java 使用公開金鑰的加密演算法(二)」為基礎,加入 [3] 的程式碼後的結果。
與上篇文一樣,主要分為三個部分,Main.java、myKeyPair.java 和 myEncryption.java,其中 [3] 的程式碼是加在 myEncryption.java 裡。
Main.java
import java.io.UnsupportedEncodingException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Calendar; public class Main { public static void main(String[] args) { myKeyPair adam = new myKeyPair(); PublicKey publicKey = null; PrivateKey privateKey = null; try { String path = "D:/encrypt"; // Load the keys System.out.println("Loaded Key Pair"); KeyPair loadedKeyPair = adam.LoadKeyPair(path, "RSA"); // Load the keys from files publicKey = loadedKeyPair.getPublic(); privateKey = loadedKeyPair.getPrivate(); } catch (Exception e) { e.printStackTrace(); return; } long start, end; // Generate the plain text String plainText = "abc123!@#"; for(int i=0 ; i<10 ; i++) plainText += plainText; System.out.println("Encryption:"); start = Calendar.getInstance().getTimeInMillis(); myEncryption encryption = new myEncryption(); byte[] result = null; try { // Encrypt result = encryption.encryptByRSA(plainText.getBytes("UTF-8"), (RSAPublicKey)publicKey); end = Calendar.getInstance().getTimeInMillis(); System.out.println("Encrypted time: " + (end-start) + "ms\n"); // Decrypt System.out.println("Decryption:"); start = Calendar.getInstance().getTimeInMillis(); byte[] decryptResult = encryption.decryptByRSA(result, (RSAPrivateKey)privateKey); end = Calendar.getInstance().getTimeInMillis(); System.out.println("Decrypted string: " + new String(decryptResult, "UTF-8")); System.out.println("Decrypted time: " + (end-start) + "ms\n"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } }
Main.java 這邊基本上跟前篇比起來沒太大改變,只有在明文的部份用小迴圈加長很多
(二)那篇的程式碼大概是迴圈跑三到四次的字串長度就會 Exception 了,這裡先讓它跑個十次。
另外 String 轉 byte array 的地方額外加上了使用 UTF-8 編碼。
myEncryption.java
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; public class myEncryption { public byte[] encryptByRSA (byte[] data, RSAPublicKey publicKey) { try { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, publicKey); return this.blockCipher(data, cipher, Cipher.ENCRYPT_MODE); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return new byte[0]; } public byte[] decryptByRSA (byte[] data, RSAPrivateKey privateKey) { try { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, privateKey); return this.blockCipher(data, cipher, Cipher.DECRYPT_MODE); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return new byte[0]; } private byte[] blockCipher(byte[] bytes, Cipher cipher, int mode) throws IllegalBlockSizeException, BadPaddingException{ // string initialize 2 buffers. // scrambled will hold intermediate results byte[] scrambled = new byte[0]; // toReturn will hold the total result byte[] toReturn = new byte[0]; // if we encrypt we use 100 byte long blocks. Decryption requires 128 byte long blocks (because of RSA) int length = (mode == Cipher.ENCRYPT_MODE)? 100 : 128; // another buffer. this one will hold the bytes that have to be modified in this step byte[] buffer = new byte[length]; for (int i=0; i< bytes.length; i++){ // if we filled our buffer array we have our block ready for de- or encryption if ((i > 0) && (i % length == 0)){ // execute the operation scrambled = cipher.doFinal(buffer); // add the result to our total result. toReturn = this.appendBytes(toReturn,scrambled); // here we calculate the length of the next buffer required int newlength = length; // if newlength would be longer than remaining bytes in the bytes array we shorten it. if (i + length > bytes.length) newlength = bytes.length - i; // clean the buffer array buffer = new byte[newlength]; } // copy byte into our buffer. buffer[i%length] = bytes[i]; } // this step is needed if we had a trailing buffer. should only happen when encrypting. // example: we encrypt 110 bytes. 100 bytes per run means we "forgot" the last 10 bytes. they are in the buffer array scrambled = cipher.doFinal(buffer); // final step before we can return the modified data. toReturn = this.appendBytes(toReturn,scrambled); return toReturn; } private byte[] appendBytes(byte[] prefix, byte[] suffix) { byte[] toReturn = new byte[prefix.length + suffix.length]; for(int i=0 ; i<prefix.length ; i++) toReturn[i] = prefix[i]; for(int i=0 ; i<suffix.length ; i++) toReturn[i + prefix.length] = suffix[i]; return toReturn; } }
這段可以看出,主要改變是 encryptByRSA() 和 decryptByRSA() 兩個函式都只有初始化 Cipher 而已
剩下的工作全部進入新貼進來的 blockCipher() 函式去進行
而 blockCipher() 函式的動作就是每 100 bytes 進行一次 RSA 加密,再把加密後的 bytes 續接到要輸出的 byte array。
程式輸出如下:
Loaded Key Pair Encryption: Encrypted time: 266ms Decryption: Decrypted time: 594ms
沒有印出加密後的密文和解密後的原始明文是因為字串太長了 XD
想測試的話可以自己加上去,我測下來是可以正確解密的。
另外結果也可以看出很明顯解密時間變長很多!加密時間倒是沒太大變化,讓我有點疑惑 XD...
2014-05-20 補充:
關於 blockCipher() 中第 行的描述,加密時是以每 100 bytes 做一次切割,而解密時則是以每 128 bytes 做切割
意味著 100 bytes 加密後應會產出 128 bytes 的結果,因此解密時要把同樣長度的資料解回來。
不過實務上這可能跟金鑰長度有關,比如說金鑰長度延展成 2048-bit 時,每 100 bytes 加密會產出 256 bytes 的結果。
另外演算法使用的填充方式不同,也會導致一輪加密中能夠加密的長度產生變化。
相關的討論可以參考 [4-5]。
參考資料:
1、密碼學原理與技術
2、Java Encryption: RSA Block Size?
3、Encrypting and decrypting large data using Java and RSA
4、RSA密钥长度、明文长度和密文长度
5、RSA 加密 明文长度 超出
沒有留言:
張貼留言