但那段程式碼沒有辦法加解密長度稍長的資料(大概是 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 加密 明文长度 超出
沒有留言:
張貼留言