使用JDK的密码流的加密怪癖(以及如何做)
在我們的日常工作中,我們經(jīng)常遇到經(jīng)常性的主題,即將數(shù)據(jù)(例如文件)從一個(gè)位置傳輸?shù)搅硪粋€(gè)位置。 這聽起來像是一個(gè)非常簡單的任務(wù),但讓我們通過聲明這些文件可能包含機(jī)密信息并可以通過非安全的通信渠道進(jìn)行傳輸這一事實(shí),使其變得更加困難。
首先想到的解決方案之一是使用加密算法。 由于文件可能真的很大,可能是數(shù)百兆或數(shù)十千兆字節(jié),所以使用像AES這樣的對稱加密方案可能很有意義。 除了僅加密外,確保數(shù)據(jù)在傳輸過程中不被篡改也將是一件很棒的事情。 幸運(yùn)的是,有一種叫做認(rèn)證加密的東西,它同時(shí)為我們提供了機(jī)密性,完整性和真實(shí)性保證。 Galois /計(jì)數(shù)器模式 ( GCM )是最流行的模式之一,支持身份驗(yàn)證加密 ,可以與AES一起使用。 這些想法使我們使用了足夠強(qiáng)大的加密方案AES256-GCM128 。
如果您使用的是JVM平臺,則應(yīng)該感到幸運(yùn),因?yàn)镴ava密碼體系結(jié)構(gòu) ( JCA )支持AES和GCM 。 話雖這么說,讓我們看看我們能走多遠(yuǎn)。
我們要做的第一件事是生成一個(gè)新的AES256密鑰。 與往常一樣, OWASP對于正確使用JCA / JCE API 提出了許多建議 。
final SecureRandom secureRandom = new SecureRandom(); ???????? final byte [] key = new byte [ 32 ]; secureRandom.nextBytes(key); final SecretKey secretKey = new SecretKeySpec(key, "AES" );另外,要初始化AES / GCM密碼,我們需要生成隨機(jī)初始化向量(或簡稱為IV)。 根據(jù)NIST的建議,其長度應(yīng)為12個(gè)字節(jié) (96位)。
對于IV,建議實(shí)現(xiàn)將支持范圍限制為96位,以提高互操作性,效率和設(shè)計(jì)的簡便性。 –
針對塊密碼模式的建議:伽羅瓦/計(jì)數(shù)器模式(GCM)和GMAC
所以我們在這里:
final byte [] iv = new byte [ 12 ]; secureRandom.nextBytes(iv);準(zhǔn)備好AES密鑰和IV后,我們可以創(chuàng)建一個(gè)密碼實(shí)例并實(shí)際執(zhí)行加密部分。 處理大文件假定依賴于流,因此我們將BufferedInputStream / BufferedOutputStream與CipherOutputStream結(jié)合使用進(jìn)行加密。
public static void encrypt(SecretKey secretKey, byte [] iv, final File input, final File output) throws Throwable { final Cipher cipher = Cipher.getInstance( "AES/GCM/NoPadding" ); final GCMParameterSpec parameterSpec = new GCMParameterSpec( 128 , iv); cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); try ( final BufferedInputStream in = new BufferedInputStream( new FileInputStream(input))) { try ( final BufferedOutputStream out = new BufferedOutputStream( new CipherOutputStream( new FileOutputStream(output), cipher))) { int length = 0 ; byte [] bytes = new byte [ 16 * 1024 ]; while ((length = in.read(bytes)) != - 1 ) { out.write(bytes, 0 , length); } } } }請注意,我們?nèi)绾沃付?biāo)簽大小為128位的 GCM密碼參數(shù),并以加密模式對其進(jìn)行初始化(在處理64Gb以上的文件時(shí)要注意一些GCM限制 )。 除了在解密模式下初始化密碼之外,解密部分沒有什么不同。
public static void decrypt(SecretKey secretKey, byte [] iv, final File input, final File output) throws Throwable { final Cipher cipher = Cipher.getInstance( "AES/GCM/NoPadding" ); final GCMParameterSpec parameterSpec = new GCMParameterSpec( 128 , iv); cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec); ????????try (BufferedInputStream in = new BufferedInputStream( new CipherInputStream( new FileInputStream(input), cipher))) { try (BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(output))) { int length = 0 ; byte [] bytes = new byte [ 16 * 1024 ]; ????????????????while ((length = in.read(bytes)) != - 1 ) { out.write(bytes, 0 , length); } } } }看來我們完成了,對吧? 不幸的是,并不是真的,對小文件進(jìn)行加密和解密只需要花一點(diǎn)時(shí)間,但是處理或多或少的實(shí)際數(shù)據(jù)樣本卻會產(chǎn)生令人震驚的結(jié)果。
處理一個(gè)?42Mb文件通常需要8分鐘(您可能會猜到,文件越大,花費(fèi)的時(shí)間就越長),快速分析顯示,大部分時(shí)間都是在解密數(shù)據(jù)時(shí)花費(fèi)的(請注意,這絕不是基準(zhǔn),僅是測試)。 在這里 , 這里 , 這里和這里 ,尋找可能的罪魁禍?zhǔn)字赋隽薐CA實(shí)現(xiàn)中AES / GCM和CipherInputStream / CipherOutputStream的長期問題清單。
那么還有哪些選擇呢? 似乎有可能犧牲CipherInputStream / CipherOutputStream ,重構(gòu)實(shí)現(xiàn)以直接使用密碼,并使用JCA原語使加密/解密工作。 但是可以說,引入經(jīng)過戰(zhàn)斗測試的BouncyCastle庫是更好的方法。
從實(shí)現(xiàn)的角度來看,解決方案看起來幾乎是相同的。 確實(shí),盡管命名約定沒有改變,但以下代碼段中的CipherOutputStream / CipherInputStream來自BouncyCastle 。
public static void encrypt(SecretKey secretKey, byte [] iv, final File input, final File output) throws Throwable { final GCMBlockCipher cipher = new GCMBlockCipher( new AESEngine()); cipher.init( true , new AEADParameters( new KeyParameter(secretKey.getEncoded()), 128 , iv)); try (BufferedInputStream in = new BufferedInputStream( new FileInputStream(input))) { try (BufferedOutputStream out = new BufferedOutputStream( new CipherOutputStream( new FileOutputStream(output), cipher))) { int length = 0 ; byte [] bytes = new byte [ 16 * 1024 ]; while ((length = in.read(bytes)) != - 1 ) { out.write(bytes, 0 , length); } } } } public static void decrypt(SecretKey secretKey, byte [] iv, final File input, final File output) throws Throwable { final GCMBlockCipher cipher = new GCMBlockCipher( new AESEngine()); cipher.init( false , new AEADParameters( new KeyParameter(secretKey.getEncoded()), 128 , iv)); try (BufferedInputStream in = new BufferedInputStream( new CipherInputStream( new FileInputStream(input), cipher))) { try (BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(output))) { int length = 0 ; byte [] bytes = new byte [ 16 * 1024 ]; ????????????????while ((length = in.read(bytes)) != - 1 ) { out.write(bytes, 0 , length); } } } }使用BouncyCastle加密原語重新運(yùn)行之前的加密/解密測試會產(chǎn)生完全不同的畫面。
公平地說,JVM平臺上的文件加密/解密最初看起來像是一個(gè)已解決的問題,但事實(shí)證明它充滿了令人驚訝的發(fā)現(xiàn)。 盡管如此,由于BouncyCastle的存在 , JCA實(shí)施的一些缺陷得以有效,簡潔地解決。
請?jiān)贕ithub上找到完整的資源。
翻譯自: https://www.javacodegeeks.com/2020/05/the-crypto-quirks-using-jdks-cipher-streams-and-what-to-do-about-that.html
總結(jié)
以上是生活随笔為你收集整理的使用JDK的密码流的加密怪癖(以及如何做)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 十大巅峰网游小说排行榜完结(10本完结高
- 下一篇: log4j 程序日志_使用log4j监视