Java AES加密和解密

    1.概述

    對稱密鑰塊密碼在數據加密中起重要作用。這意味著同一密鑰可用於加密和解密。高級加密標準(AES)是一種廣泛使用的對稱密鑰加密算法。

    在本教程中,我們將看到如何使用JDK中的Java密碼體系結構(JCA)來實現AES加密和解密。

    2. AES算法

    AES算法是一種迭代的對稱密鑰塊密碼,它支持128、192和256位的加密密鑰(秘密密鑰),以對128位的塊中的數據進行加密和解密。下圖顯示了高級AES算法:

    高級AES算法

    如果要加密的數據不滿足128位的塊大小要求,則必須對其進行填充。填充是將最後一塊填充為128位的過程。

    3. AES變體

    AES算法具有六種操作模式:

    1. ECB(電子密碼本)
    2. CBC(密碼塊鏈接)
    3. CFB(密碼反饋)
    4. OFB(輸出反饋)
    5. 點擊率(計數器)
    6. GCM(Galois /計數器模式)

    可以應用操作模式以增強加密算法的效果。此外,操作模式可以將分組密碼轉換為流密碼。每種模式都有其優勢和劣勢。讓我們快速回顧一下。

    3.1。ECB

    這種操作模式是最簡單的。明文分為大小為128位的塊。然後,將使用相同的密鑰和算法對每個塊進行加密。因此,對於相同的塊它會產生相同的結果。這是此模式的主要缺點,不建議用於加密。它需要填充數據。

    3.2。CBC

    為了克服ECB的弱點,CBC模式使用初始化向量(IV)來增強加密。首先,CBC將明文塊xor與IV一起使用。然後,它將結果加密到密文塊。在下一個塊中,它將使用加密結果與明文塊進行異或,直到最後一個塊。

    在這種模式下,加密不能並行化,但解密可以並行化。它還需要填充數據。

    3.3。 CFB

    此模式可用作流密碼。首先,它對IV進行加密,然後將其與純文本塊進行異或運算以獲得密文。然後,CFB將加密結果加密以對明文進行異或。它需要IV。

    在這種模式下,解密可以並行化,但加密不能並行化。

    3.4。OFB

    此模式可用作流密碼。首先,它加密IV。然後,它使用加密結果對明文進行異或運算以獲得密文。

    它不需要填充數據,也不會受到噪聲塊的影響。

    3.5。CTR

    該模式將計數器的值用作IV。它與OFB非常相似,但是它每次使用計數器而不是IV對其進行加密。

    此模式具有兩個優點,包括加密/解密並行化,並且一個塊中的噪聲不會影響其他塊。

    3.6。 GCM

    此模式是CTR模式的擴展。 GCM已受到NIST的高度重視並推薦使用。 GCM模型輸出密文和認證標籤。與該算法的其他運算模式相比,此模式的主要優點是效率高。

    在本教程中,我們將使用AES/CBC/PKCS5Padding算法,因為該算法已在許多項目中廣泛使用。

    3.7。加密後的數據大小

    如前所述,AES的塊大小為128位或16個字節。 AES不會更改大小,並且密文大小等於明文大小。另外,在ECB和CBC模式下,我們應該使用類似於PKCS 5.的填充算法PKCS 5.因此,加密後的數據大小為:

    ciphertext_size (bytes) = cleartext_size + (16 - (cleartext_size % 16))

    為了用密文存儲IV,我們需要再增加16個字節。

    4. AES參數

    在AES算法中,我們需要三個參數:輸入數據,秘密密鑰和IV。 ECB模式下不使用IV。

    4.1。輸入數據

    AES的輸入數據可以是基於字符串,文件,對象和密碼的。

    4.2。密鑰

    在AES中生成密鑰的方法有兩種:從隨機數生成或從給定密碼生成。

    在第一種方法中,應該從像SecureRandom類這樣的加密安全(偽)隨機數生成器生成秘密密鑰。

    為了生成密鑰,我們可以使用KeyGenerator類。讓我們定義一種用於生成大小為n (128、192和256)位的AES密鑰的方法:

    public static SecretKey generateKey(int n) throws NoSuchAlgorithmException {
    
     KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    
     keyGenerator.init(n);
    
     SecretKey key = keyGenerator.generateKey();
    
     return key;
    
     }

    在第二種方法中,可以使用基於密碼的密鑰派生功能(例如PBKDF2)從給定的密碼派生AES秘密密鑰。我們還需要一個鹽值來將密碼轉換為密鑰。鹽也是一個隨機值。

    我們可以將SecretKeyFactory類與PBKDF2WithHmacSHA256算法一起使用,以根據給定的密碼生成密鑰。

    讓我們定義一種方法,該方法可通過65,536次迭代和256位密鑰長度從給定密碼生成AES密鑰:

    public static SecretKey getKeyFromPassword(String password, String salt)
    
     throws NoSuchAlgorithmException, InvalidKeySpecException {
    
    
    
     SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    
     KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
    
     SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
    
     .getEncoded(), "AES");
    
     return secret;
    
     }

    4.3。初始化向量(IV)

    IV是偽隨機值,其大小與加密的塊相同。我們可以使用SecureRandom類生成隨機IV。

    讓我們定義一種生成IV的方法:

    public static IvParameterSpec generateIv() {
    
     byte[] iv = new byte[16];
    
     new SecureRandom().nextBytes(iv);
    
     return new IvParameterSpec(iv);
    
     }

    5.加密和解密

    5.1。串

    要實現輸入字符串加密,我們首先需要根據上一節生成密鑰和IV。下一步,我們使用getInstance()方法從Cipher類創建一個實例。

    此外,我們使用帶有秘密密鑰,IV和加密模式的init()方法配置密碼實例。最後,我們通過調用doFinal()方法對輸入字符串進行加密。此方法獲取輸入字節並以字節為單位返回密文:

    public static String encrypt(String algorithm, String input, SecretKey key,
    
     IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    
     InvalidAlgorithmParameterException, InvalidKeyException,
    
     BadPaddingException, IllegalBlockSizeException {
    
    
    
     Cipher cipher = Cipher.getInstance(algorithm);
    
     cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    
     byte[] cipherText = cipher.doFinal(input.getBytes());
    
     return Base64.getEncoder()
    
     .encodeToString(cipherText);
    
     }

    為了解密輸入字符串,我們可以使用DECRYPT_MODE初始化密碼來解密內容:

    public static String decrypt(String algorithm, String cipherText, SecretKey key,
    
     IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
    
     InvalidAlgorithmParameterException, InvalidKeyException,
    
     BadPaddingException, IllegalBlockSizeException {
    
    
    
     Cipher cipher = Cipher.getInstance(algorithm);
    
     cipher.init(Cipher.DECRYPT_MODE, key, iv);
    
     byte[] plainText = cipher.doFinal(Base64.getDecoder()
    
     .decode(cipherText));
    
     return new String(plainText);
    
     }

    讓我們編寫一個用於加密和解密字符串輸入的測試方法:

    @Test
    
     void givenString_whenEncrypt_thenSuccess()
    
     throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    
     BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException {
    
    
    
     String input = "baeldung";
    
     SecretKey key = AESUtil.generateKey(128);
    
     IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    
     String algorithm = "AES/CBC/PKCS5Padding";
    
     String cipherText = AESUtil.encrypt(algorithm, input, key, ivParameterSpec);
    
     String plainText = AESUtil.decrypt(algorithm, cipherText, key, ivParameterSpec);
    
     Assertions.assertEquals(input, plainText);
    
     }

    5.2。文件

    現在,讓我們使用AES算法加密文件。步驟是相同的,但是我們需要一些IO類來處理文件。讓我們加密一個文本文件:

    public static void encryptFile(String algorithm, SecretKey key, IvParameterSpec iv,
    
     File inputFile, File outputFile) throws IOException, NoSuchPaddingException,
    
     NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    
     BadPaddingException, IllegalBlockSizeException {
    
    
    
     Cipher cipher = Cipher.getInstance(algorithm);
    
     cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    
     FileInputStream inputStream = new FileInputStream(inputFile);
    
     FileOutputStream outputStream = new FileOutputStream(outputFile);
    
     byte[] buffer = new byte[64];
    
     int bytesRead;
    
     while ((bytesRead = inputStream.read(buffer)) != -1) {
    
     byte[] output = cipher.update(buffer, 0, bytesRead);
    
     if (output != null) {
    
     outputStream.write(output);
    
     }
    
     }
    
     byte[] outputBytes = cipher.doFinal();
    
     if (outputBytes != null) {
    
     outputStream.write(outputBytes);
    
     }
    
     inputStream.close();
    
     outputStream.close();
    
     }

    請注意,不建議嘗試將整個文件(尤其是大文件)讀入內存。相反,我們一次加密一個緩衝區。

    為了解密文件,我們使用類似的步驟,並使用DECRYPT_MODE初始化密碼, DECRYPT_MODE

    再次,讓我們定義一個用於加密和解密文本文件的測試方法。在這種方法中,我們從測試資源目錄中讀取baeldung.txt文件,將其加密為一個名為baeldung.encrypted的文件,然後將該文件解密為一個新文件:

    @Test
    
     void givenFile_whenEncrypt_thenSuccess()
    
     throws NoSuchAlgorithmException, IOException, IllegalBlockSizeException,
    
     InvalidKeyException, BadPaddingException, InvalidAlgorithmParameterException,
    
     NoSuchPaddingException {
    
    
    
     SecretKey key = AESUtil.generateKey(128);
    
     String algorithm = "AES/CBC/PKCS5Padding";
    
     IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    
     Resource resource = new ClassPathResource("inputFile/baeldung.txt");
    
     File inputFile = resource.getFile();
    
     File encryptedFile = new File("classpath:baeldung.encrypted");
    
     File decryptedFile = new File("document.decrypted");
    
     AESUtil.encryptFile(algorithm, key, ivParameterSpec, inputFile, encryptedFile);
    
     AESUtil.decryptFile(
    
     algorithm, key, ivParameterSpec, encryptedFile, decryptedFile);
    
     assertThat(inputFile).hasSameTextualContentAs(decryptedFile);
    
     }

    5.3。基於密碼

    我們可以使用從給定密碼派生的密鑰進行AES加密和解密。

    為了生成密鑰,我們使用getKeyFromPassword()方法。加密和解密步驟與字符串輸入部分中顯示的步驟相同。然後,我們可以使用實例化的密碼和提供的密鑰來執行加密。

    讓我們寫一個測試方法:

    @Test
    
     void givenPassword_whenEncrypt_thenSuccess()
    
     throws InvalidKeySpecException, NoSuchAlgorithmException,
    
     IllegalBlockSizeException, InvalidKeyException, BadPaddingException,
    
     InvalidAlgorithmParameterException, NoSuchPaddingException {
    
    
    
     String plainText = "www.baeldung.com";
    
     String password = "baeldung";
    
     String salt = "12345678";
    
     IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    
     SecretKey key = AESUtil.getKeyFromPassword(password,salt);
    
     String cipherText = AESUtil.encryptPasswordBased(plainText, key, ivParameterSpec);
    
     String decryptedCipherText = AESUtil.decryptPasswordBased(
    
     cipherText, key, ivParameterSpec);
    
     Assertions.assertEquals(plainText, decryptedCipherText);
    
     }

    5.4。目的

    為了加密Java對象,我們需要使用SealedObject類。該對象應可Serializable 。讓我們從定義Student類開始:

    public class Student implements Serializable {
    
     private String name;
    
     private int age;
    
    
    
     // standard setters and getters
    
     }
    

    接下來,讓我們加密Student對象:

    public static SealedObject encryptObject(String algorithm, Serializable object,
    
     SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    
     NoSuchAlgorithmException, InvalidAlgorithmParameterException,
    
     InvalidKeyException, IOException, IllegalBlockSizeException {
    
    
    
     Cipher cipher = Cipher.getInstance(algorithm);
    
     cipher.init(Cipher.ENCRYPT_MODE, key, iv);
    
     SealedObject sealedObject = new SealedObject(object, cipher);
    
     return sealedObject;
    
     }

    稍後可以使用正確的密碼對加密對象進行解密:

    public static Serializable decryptObject(String algorithm, SealedObject sealedObject,
    
     SecretKey key, IvParameterSpec iv) throws NoSuchPaddingException,
    
     NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException,
    
     ClassNotFoundException, BadPaddingException, IllegalBlockSizeException,
    
     IOException {
    
    
    
     Cipher cipher = Cipher.getInstance(algorithm);
    
     cipher.init(Cipher.DECRYPT_MODE, key, iv);
    
     Serializable unsealObject = (Serializable) sealedObject.getObject(cipher);
    
     return unsealObject;
    
     }

    讓我們寫一個測試用例:

    @Test
    
     void givenObject_whenEncrypt_thenSuccess()
    
     throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
    
     InvalidAlgorithmParameterException, NoSuchPaddingException, IOException,
    
     BadPaddingException, ClassNotFoundException {
    
    
    
     Student student = new Student("Baeldung", 20);
    
     SecretKey key = AESUtil.generateKey(128);
    
     IvParameterSpec ivParameterSpec = AESUtil.generateIv();
    
     String algorithm = "AES/CBC/PKCS5Padding";
    
     SealedObject sealedObject = AESUtil.encryptObject(
    
     algorithm, student, key, ivParameterSpec);
    
     Student object = (Student) AESUtil.decryptObject(
    
     algorithm, sealedObject, key, ivParameterSpec);
    
     assertThat(student).isEqualToComparingFieldByField(object);
    
     }

    六,結論

    總之,我們已經學習瞭如何使用Java中的AES算法來加密和解密輸入數據,例如字符串,文件,對象和基於密碼的數據。此外,我們討論了AES的變化以及加密後數據的大小。