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》中的代碼
public class TestByte
{    
    public static void main(String[] argv) {
 
            String example = "This is an example";
            byte[] bytes = example.getBytes();
 
            System.out.println("Text : " + example);
            System.out.println("Text [Byte Format] : " + bytes);
            System.out.println("Text [Byte Format] : " + bytes.toString());
 
            String s = new String(bytes);
            System.out.println("Text Decryted : " + s);
 
 
    }
}

輸出:
Text : This is an example
Text [Byte Format] : [B@187aeca
Text [Byte Format] : [B@187aeca
Text Decryted : This is an example
以及這篇博客《RSA Encryption Example》中的代碼
final byte[] cipherText = encrypt(originalText, publicKey);
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):
      // use String to hold cipher binary data
      Base64 base64 = new Base64(); 
      String cipherTextBase64 = base64.encodeToString(cipherText);
      
      // get cipher binary data back from String
      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 來加密傳輸,但是由於你的同一明文每次生成的同一密文,攻擊者能夠據此識別到同一個信息都是何時被髮送。

Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
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 個字節。如果你還嫌大,可以通過調整算法提供者來減小密文長度:

Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC");
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》存在一些認識上的誤區,但筆者仍然認爲它是一篇很不錯的入門級文章。結合本文所列內容,筆者將其代碼做了一些調整以供參考:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
 
import javax.crypto.Cipher;
 
import org.apache.commons.codec.binary.Base64;
 
/**
 * @author JavaDigest
 * 
 */
public class EncryptionUtil {
 
    /**
     * String to hold name of the encryption algorithm.
     */
    public static final String ALGORITHM = "RSA";
 
    /**
     * String to hold name of the encryption padding.
     */
    public static final String PADDING = "RSA/NONE/NoPadding";
 
    /**
     * String to hold name of the security provider.
     */
    public static final String PROVIDER = "BC";
 
    /**
     * String to hold the name of the private key file.
     */
    public static final String PRIVATE_KEY_FILE = "e:/defonds/work/20150116/private.key";
 
    /**
     * String to hold name of the public key file.
     */
    public static final String PUBLIC_KEY_FILE = "e:/defonds/work/20150116/public.key";
 
    /**
     * Generate key which contains a pair of private and public key using 1024
     * bytes. Store the set of keys in Prvate.key and Public.key files.
     * 
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws FileNotFoundException
     */
    public static void generateKey() {
        try {
 
            Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
            final KeyPairGenerator keyGen = KeyPairGenerator.getInstance(
                    ALGORITHM, PROVIDER);
            keyGen.initialize(256);
            final KeyPair key = keyGen.generateKeyPair();
 
            File privateKeyFile = new File(PRIVATE_KEY_FILE);
            File publicKeyFile = new File(PUBLIC_KEY_FILE);
 
            // Create files to store public and private key
            if (privateKeyFile.getParentFile() != null) {
                privateKeyFile.getParentFile().mkdirs();
            }
            privateKeyFile.createNewFile();
 
            if (publicKeyFile.getParentFile() != null) {
                publicKeyFile.getParentFile().mkdirs();
            }
            publicKeyFile.createNewFile();
 
            // Saving the Public key in a file
            ObjectOutputStream publicKeyOS = new ObjectOutputStream(
                    new FileOutputStream(publicKeyFile));
            publicKeyOS.writeObject(key.getPublic());
            publicKeyOS.close();
 
            // Saving the Private key in a file
            ObjectOutputStream privateKeyOS = new ObjectOutputStream(
                    new FileOutputStream(privateKeyFile));
            privateKeyOS.writeObject(key.getPrivate());
            privateKeyOS.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
 
    }
 
    /**
     * The method checks if the pair of public and private key has been
     * generated.
     * 
     * @return flag indicating if the pair of keys were generated.
     */
    public static boolean areKeysPresent() {
 
        File privateKey = new File(PRIVATE_KEY_FILE);
        File publicKey = new File(PUBLIC_KEY_FILE);
 
        if (privateKey.exists() && publicKey.exists()) {
            return true;
        }
        return false;
    }
 
    /**
     * Encrypt the plain text using public key.
     * 
     * @param text
     *            : original plain text
     * @param key
     *            :The public key
     * @return Encrypted text
     * @throws java.lang.Exception
     */
    public static byte[] encrypt(String text, PublicKey key) {
        byte[] cipherText = null;
        try {
            // get an RSA cipher object and print the provider
            Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
            final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);
            
            // encrypt the plain text using the public key
            cipher.init(Cipher.ENCRYPT_MODE, key);
            cipherText = cipher.doFinal(text.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cipherText;
    }
 
    /**
     * Decrypt text using private key.
     * 
     * @param text
     *            :encrypted text
     * @param key
     *            :The private key
     * @return plain text
     * @throws java.lang.Exception
     */
    public static String decrypt(byte[] text, PrivateKey key) {
        byte[] dectyptedText = null;
        try {
            // get an RSA cipher object and print the provider
            Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
            final Cipher cipher = Cipher.getInstance(PADDING, PROVIDER);
 
            // decrypt the text using the private key
            cipher.init(Cipher.DECRYPT_MODE, key);
            dectyptedText = cipher.doFinal(text);
 
        } catch (Exception ex) {
            ex.printStackTrace();
        }
 
        return new String(dectyptedText);
    }
 
    /**
     * Test the EncryptionUtil
     */
    public static void main(String[] args) {
 
        try {
 
            // Check if the pair of keys are present else generate those.
            if (!areKeysPresent()) {
                // Method generates a pair of keys using the RSA algorithm and
                // stores it
                // in their respective files
                generateKey();
            }
 
            final String originalText = "12345678901234567890123456789012";
            ObjectInputStream inputStream = null;
 
            // Encrypt the string using the public key
            inputStream = new ObjectInputStream(new FileInputStream(
                    PUBLIC_KEY_FILE));
            final PublicKey publicKey = (PublicKey) inputStream.readObject();
            final byte[] cipherText = encrypt(originalText, publicKey);
 
            // use String to hold cipher binary data
            Base64 base64 = new Base64();
            String cipherTextBase64 = base64.encodeToString(cipherText);
 
            // get cipher binary data back from String
            byte[] cipherTextArray = base64.decode(cipherTextBase64);
 
            // Decrypt the cipher text using the private key.
            inputStream = new ObjectInputStream(new FileInputStream(
                    PRIVATE_KEY_FILE));
            final PrivateKey privateKey = (PrivateKey) inputStream.readObject();
            final String plainText = decrypt(cipherTextArray, privateKey);
 
            // Printing the Original, Encrypted and Decrypted Text
            System.out.println("Original=" + originalText);
            System.out.println("Encrypted=" + cipherTextBase64);
            System.out.println("Decrypted=" + plainText);
 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

先生成一對密鑰,供以後加解密使用(不需要每次加解密都生成一個密鑰),密鑰長度爲 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


原文:https://blog.csdn.net/defonds/article/details/42775183 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章