Java 中加密 API 的常見異常
一、簡介
Cipher
對像是一個重要的 Java 類,它幫助我們提供加密和解密功能。
在本文中,我們將了解在使用它加密和解密文本時可能發生的一些常見異常。
2. NoSuchAlgorithmException
:找不到任何支持 X 的提供程序
如果我們運行以下代碼以使用編造的算法獲取Cipher
的實例:
Cipher.getInstance("ABC");
我們將看到堆棧跟踪開始:
java.security.NoSuchAlgorithmException: Cannot find any provider supporting ABC
at javax.crypto.Cipher.getInstance(Cipher.java:543)
這裡到底發生了什麼?
那麼,要使用Cipher.getInstance
,我們需要將算法轉換作為String,
並且這必須是文檔中列出的允許值。如果不是,我們將得到一個NoSuchAlgorithmException
。
如果我們檢查了文檔並且仍然看到這個,我們最好確保檢查轉換是否有錯誤。
我們還可以在轉換中指定算法模式和填充。
讓我們確保這些字段的值也與給定的文檔匹配。否則,我們會看到一個異常:
Cipher.getInstance("AES/ABC"); // invalid, causes exception
Cipher.getInstance("AES/CBC/ABC"); // invalid, causes exception
Cipher.getInstance("AES/CBC/PKCS5Padding"); // valid, no exception
請記住,如果我們不指定這些額外字段,則會使用默認值。
算法模式的默認值為ECB ,填充的默認值為“NoPadding”.
由於 ECB 被認為很弱,我們將要指定一種模式以確保我們不會最終使用它。
總而言之,在解決NoSuchAlgorithmException
時,我們將要檢查我們選擇的轉換的每個部分是否存在於文檔的允許列表中,注意檢查拼寫中的任何拼寫錯誤。
3. IllegalBlockSizeException
:輸入長度不是 X 字節的倍數
我們可能會看到此異常的原因有幾個。
首先,讓我們看看在嘗試解密時拋出的異常,然後在嘗試加密時查看它。
3.1.解密期間IllegalBlockSizeException
下面寫一個很簡單的解密方法:
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherTextBytes);
此代碼的行為將根據傳遞給我們的方法的密文而改變,這可能是我們無法控制的。
有時,我們可能會看到IllegalBlockSizeException:
javax.crypto.IllegalBlockSizeException: Input length not multiple of 16 bytes
at com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1109)
at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at javax.crypto.Cipher.doFinal(Cipher.java:2168)
那麼“塊大小”到底是什麼意思,又是什麼讓它“非法”呢?
要理解這一點,讓我們記住 AES 是Block Cipher的一個例子。
塊密碼通過採用稱為塊的固定長度的位組來工作。
要找出我們算法的一個塊中有多少字節,我們可以使用:
Cipher.getInstance("AES/ECB/PKCS5Padding").getBlockSize();
由此可見, AES 使用的是 16 字節的塊。
這意味著它將使用一個 16 字節的塊,執行相關的算法步驟,然後移動到下一個 16 字節的塊。
簡單地說,非法塊是不包含正確字節數的塊。
通常,當文本長度不是 16 字節的倍數時,這會發生在最後一個塊上。
這通常意味著要解密的文本一開始就沒有正確加密,因此無法解密。
請記住,我們不控制提供給我們的代碼以進行解密的輸入,因此我們必須準備好處理此異常。
因此,像cipher.doFinal
這樣的方法會拋出IllegalBlockSizeException
來強制我們處理這種情況,要么拋出它,要么在try-catch
語句中。否則,代碼將無法編譯。
但是,請記住,大約每 16 次一次,一些錯誤的密文恰好是正確的長度,以避免 AES 的這種異常。
在這種情況下,我們很可能會遇到本文中提到的其他異常之一。
3.2.加密期間IllegalBlockSizeException
現在讓我們在嘗試加密文本“ https://www.baeldung.com/
”時看一下這個異常:
String plainText = "https://www.baeldung.com/";
byte[] plainTextBytes = plainText.getBytes();
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(plainTextBytes);
正如我們在上面看到的,要使 AES 算法起作用,字節數必須是 16 的倍數,而我們的文本不是。因此,運行此代碼會出現與上述相同的異常。
那麼我們是否只能使用 AES 來加密具有 16、32、48……字節的文本?
如果我們想加密一些沒有正確字節數的東西怎麼辦?
嗯,這是我們需要填充數據的地方。
填充數據只是意味著我們要在文本的開頭、中間或結尾添加額外的字節,從而確保數據現在具有正確的字節數。
與算法名稱和模式一樣,我們可以使用一組允許的填充操作列表。
幸運的是,Java 為我們處理了這件事,所以我們不打算在這裡詳細介紹它是如何工作的。
我們要做的就是在我們的Cipher
實例上設置一個填充操作,比如PKCS #5 ,而不是指定“NoPadding”:
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
當然,解密文本的代碼也必須使用相同的填充操作。
3.3.其他故障排除技巧
如果我們開始遇到NoSuchAlgorithmException
或NoSuchPaddingException,
我們將需要檢查Java 文檔以確保我們使用的是有效的填充 - 並且我們的拼寫沒有拼寫錯誤。
如果我們已經這樣做了,那麼可能值得檢查我們正在查看的文檔是否與我們正在使用的 Java 版本相匹配,因為允許的填充操作可以在版本之間更改。本文中提供的鏈接適用於 Java 8。
4. BadPaddingException
:給定的最終塊未正確填充
如果在處理填充時出現問題,代碼將拋出BadPaddingException
,表明這是我們使用的填充的問題。
但是,實際上可能有幾個不同的問題導致我們看到此異常。
4.1.錯誤填充導致BadPaddingException
假設我們的文本“ https://www.baeldung.com/
”是使用ISO 10126填充加密的:
Cipher cipher = Cipher.getInstance("AES/ECB/ISO10126Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherTextBytes = cipher.doFinal(plainTextBytes);
然後,如果我們嘗試使用不同的填充對其進行解密,比如 PKCS #5:
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, encryptionKey);
return cipher.doFinal(cipherTextBytes);
我們的代碼會拋出異常:
javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.
at com.sun.crypto.provider.CipherCore.unpad(CipherCore.java:975)
at com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1056)
at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
at com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
at javax.crypto.Cipher.doFinal(Cipher.java:2168)
然而,當我們看到這個異常時,填充往往不是根本原因。
上面的異常中的一行暗示了這一點,“如果在解密過程中使用了錯誤的密鑰,就會出現此類問題。”
那麼讓我們看看還有什麼可以導致BadPaddingException.
4.2.密鑰錯誤導致BadPaddingException
如堆棧跟踪所示,當我們未使用正確的加密密鑰進行解密時,我們可能會看到此異常:
SecretKey encryptionKey = CryptoUtils.getKeyForText("BaeldungIsASuperCoolSite");
SecretKey differentKey = CryptoUtils.getKeyForText("ThisGivesUsAnAlternative");
Cipher cipher = Cipher.getInstance("AES/ECB/ISO10126Padding");
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey);
byte[] cipherTextBytes = cipher.doFinal(plainTextBytes);
cipher.init(Cipher.DECRYPT_MODE, differentKey);
return cipher.doFinal(cipherTextBytes);
上面的代碼拋出BadPaddingException
而不是InvalidKeyException
,因為這是代碼遇到問題並且無法繼續進行的地方。
這可能是導致此異常的最常見原因。
如果我們看到這個異常,那麼我們必須確保我們使用的是正確的密鑰。
這意味著我們必須使用相同的密鑰進行加密和解密。
4.3.算法錯誤導致BadPaddingException
鑑於以上情況,下一個應該是顯而易見的,但始終值得檢查。
如果我們嘗試使用與數據加密方式不同的算法或算法模式進行解密,我們可能會看到類似的症狀:
Cipher cipher = Cipher.getInstance("AES/ECB/ISO10126Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherTextBytes = cipher.doFinal(plainTextBytes);
cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherTextBytes);
在上面的示例中,數據使用CBC模式加密,但使用 ECB 模式解密,這是行不通的(在大多數情況下)。
通常,我們解決此異常的方法是驗證我們的解密機制的每個組件是否與數據的加密方式相匹配。
5. InvalidKeyException
InvalidKeyException
通常表示我們錯誤地設置了Cipher
對象。
讓我們來看看最常見的原因。
5.1. InvalidKeyException
:缺少參數
我們使用的一些算法需要初始化向量(IV)。
IV 防止重複加密文本,因此某些加密模式(如 CBC)需要它。
讓我們嘗試初始化一個沒有 IV 集的Cipher
實例:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, encryptionKey);
cipher.doFinal(cipherTextBytes);
如果我們運行上面的代碼,那麼我們將看到以下堆棧跟踪:
java.security.InvalidKeyException: Parameters missing
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:469)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:313)
at javax.crypto.Cipher.implInit(Cipher.java:805)
at javax.crypto.Cipher.chooseProvider(Cipher.java:867)
at javax.crypto.Cipher.init(Cipher.java:1252)
at javax.crypto.Cipher.init(Cipher.java:1189)
幸運的是,這個很容易修復,因為我們只需要用 IV 初始化我們的Cipher
:
byte[] ivBytes = new byte[]{'B', 'a', 'e', 'l', 'd', 'u', 'n', 'g', 'I', 's', 'G', 'r', 'e', 'a', 't', '!'};
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, encryptionKey, ivParameterSpec);
byte[] decryptedBytes = cipher.doFinal(cipherTextBytes);
請注意,給定的 IV 必須與用於加密文本的 IV 相同。
關於我們的 IV 的最後一個注意事項是它必須有一定的長度。
如果我們使用 CBC,我們的 IV 必須恰好 16 個字節長。
如果我們嘗試使用不同的字節數,我們將得到一個非常清晰的InvalidAlgorithmParameterException:
java.security.InvalidAlgorithmParameterException: Wrong IV length: must be 16 bytes long
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:525)
at com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:346)
at javax.crypto.Cipher.implInit(Cipher.java:809)
at javax.crypto.Cipher.chooseProvider(Cipher.java:867)
修復只是為了確保我們的 IV 是 16 字節長。
5.2. InvalidKeyException
:無效的 AES 密鑰長度:X 字節
我們將很快介紹這個,因為它與上述情況非常相似。
如果我們嘗試使用長度不正確的密鑰,那麼我們將看到一個簡單的異常:
java.security.InvalidKeyException: Invalid AES key length: X bytes
at com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:87)
at com.sun.crypto.provider.CipherBlockChaining.init(CipherBlockChaining.java:93)
at com.sun.crypto.provider.CipherCore.init(CipherCore.java:591)
我們的密鑰也必須是 16 個字節。
這是因為 Java 默認僅支持 128 位(16 字節)加密。
六,結論
在本文中,我們看到了在加密和解密文本時可能發生的各種異常。
特別是,我們看到異常可能提到一件事的情況,例如填充,但根本原因實際上是另一件事,例如無效的密鑰。
與往常一樣,示例項目在 GitHub 上可用。