2012年6月25日 星期一

在 Java 使用加密演算法(二):使用 RSA 加密與解密(最後修改:2015-10-11)

利用 RSA 加密演算法來加密字串的範例如下~
這裡總共分成兩段程式碼,分別是 Main.java 以及 myEncryption.java
Main.java 是主要程序;myEncryption.java 進行加解密。


Main.java
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Calendar;

import javax.crypto.Cipher;

import org.bouncycastle.util.encoders.Base64;

public class Main {
    private static String ALGORITHM = "RSA/ECB/PKCS1Padding";
    
    public static void main(String[] args) {
        try {
            // Generate key pair, and load it into public key and private key instances.
            KeyPairGenerator keygen = KeyPairGenerator.getInstance("RSA");
            SecureRandom random = new SecureRandom();
            random.setSeed("test".getBytes());
            keygen.initialize(1024, random); // TODO Change length may cause the result incorrect.
            KeyPair keyPair = keygen.generateKeyPair();
            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();
            
            // Tag for timer.
            long start, end;

            // Generate the plain text
            String plainText = "abc123!@#";
    
            System.out.println("Encryption:");
            start = Calendar.getInstance().getTimeInMillis();

            // Initiate the encryptor.
            MyEncryption encryption = new MyEncryption();
            // Initiate a variable for storing the encrypting result.
            byte[] result = null;
            
            try {
                // Encrypt
                result = encryption.cryptByRSA(plainText.getBytes("UTF-8"), publicKey, ALGORITHM, Cipher.ENCRYPT_MODE);
                end = Calendar.getInstance().getTimeInMillis();
                System.out.println("Encrypted result: " + Base64.toBase64String(result));
                System.out.println("Encrypted length: " + result.length);
                System.out.println("Encrypted time: " + (end-start) + "ms\n");
        
                // Decrypt
                System.out.println("Decryption:");
                start = Calendar.getInstance().getTimeInMillis();
                byte[] decryptResult = encryption.cryptByRSA(result, privateKey, ALGORITHM, Cipher.DECRYPT_MODE);
                end = Calendar.getInstance().getTimeInMillis();
                System.out.println("Decrypted string: " + new String(decryptResult, "UTF-8"));
                System.out.println("Decrypted time: " + (end-start) + "ms\n");
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上述程式碼中,42 行呼叫 MyEncryption 進行加密、51 行呼叫 MyEncryption 進行解密。
被加密的字串是 30 行所定義的 String 變數 plainText
而中間同時會計算加密和解密所花費的時間。(因為查到的資料常說 RSA 是速度比較慢的加密方法...)

比較特別的地方是 13 行定義的「RSA/ECB/PKCS1Padding」,是包括指定演算法名稱以及 mode、padding 等
可以參考官方的文件 Standard Algorithm Name Documentation 中的 Implementation Requirement 小節

myEncryption.java
import java.security.InvalidKeyException;
import java.security.Key;
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 MyEncryption0 implements EncryptionItf {
    
    /**
     * Encrypt/Decrypt the data according to the specified way.
     */
    public byte[] cryptByRSA(byte[] data, Key key, String algorithm, int mode) {
        try {
            Cipher cipher = Cipher.getInstance(algorithm);
            
            // Initiate the cipher.
            if (mode == Cipher.ENCRYPT_MODE)
                cipher.init(Cipher.ENCRYPT_MODE, (RSAPublicKey) key);
            else
                cipher.init(Cipher.DECRYPT_MODE, (RSAPrivateKey) key);
            
            // Encrypt/Decrypt the data.
            return cipher.doFinal(data);
        } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | NoSuchAlgorithmException
                | NoSuchPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }
}

另外要注意的是,根據參考連結 2 的描述,因為 RSA 是屬於 block chiper,所以一次只能加密很短的字串
如果加密長度超過 117 bytes,執行時就會丟出 Exception 了。
javax.crypto.IllegalBlockSizeException: Data must not be longer than 117 bytes
      at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:346)
      at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:391)
      at javax.crypto.Cipher.doFinal(Cipher.java:2087)
      at MyEncryption0.cryptByRSA(MyEncryption0.java:28)
      at Main.main(Main.java:40)
PS. 可參考 Provider 原始碼中的註解,部分節錄如下:
We only do one RSA operation per doFinal() call. If the application passes more data via calls to update() or doFinal(), we throw an IllegalBlockSizeException when doFinal() is called (see JCE API spec). Bulk encryption using RSA does not make sense and is not standardized.

要解決此問題,可以透過將資料拆分成很多個小塊來達成,請參閱「在 Java 使用加密演算法(三):使用 RSA 加解密長資料」。

程式碼的執行結果如下:
Encryption:
Encrypted result: R/5rw6C/d34BBDKHpq+cz100K1xQrXe/bsldRgU/is3IOJz13/p7uOlma5rdqfAGBVaE3BGYhGsRcp+tu0VhA/ze+XosQUM4nSFbFI74oxxUVhl35yTmPV/e+CU9ugzi42kSYX/YXbzl702nB/t8/Ub66KiTqu2oQXlpcP9kCw0=
Encrypted length: 128
Encrypted time: 443ms

Decryption:
Decrypted string: abc123!@#
Decrypted time: 11ms

參考資料:
1、使用Java進行RSA加解密
2、JAVA里面RSA加密算法的使用
3、有關印出加密後的資料的討論可參考 Java RSA Encryption (stackoverflow)

4 則留言:

史帝芬 提到...

有一種運用如下:
兩個系統 A、B,要傳輸機密資料,假設 A 為 server,B 為 client,
可以由 A 產生 RSA 的 public key、private key,並將 public key 給 B,
當 B 要向 A 要資料時,將請求命令用 public key 加密傳給 A,
A 收到後將 DES key (或其它對稱的加密方式) 用 private key 加密後傳給 B,
之後 B 和 A 間即可透過 DES key 加密傳輸資料,
DES key 可以每隔一段時間換一次,
反正 B 連上來時,A 就給最新的 DES key。
當有 C、D、E ... 等各系統也要向 A 要資料,
得先向 A 申請取得 RSA public key ...

Unknown 提到...

請問一下
第44行 System.out.println("Encrypted result: " + Base64.toBase64String(result));
是由 import org.bouncycastle.util.encoders.Base64; 所定義的嗎
在執行時 toBase64String那裏都會出錯 實在找不到原因 謝謝

Unknown 提到...

補充一下
她顯示The method toBase64String(byte[]) is undefined for the type Base64

Wayne Yeh 提到...

現在其實我也不太記得了,理論上應該是,不過有可能 API 隨著時間產生版本變更,或者...我當時貼錯了....。

不過如果您的環境是使用 Java 8 的話,現在比較推薦的作法應該是直接使用官方的 Base64 套件吧!
例如:Base64.getEncoder().encodeToString(result)
相關的 Javadoc 可參考:https://docs.oracle.com/javase/8/docs/api/java/util/Base64.html