CipherInputStream 看上去只是“解密版的 InputStream”,但认证加密的边界、截断与内存行为全不一样。这篇用真实的篡改、截断、关闭和大文件实验量出它的保证和限制。
1 MiB 明文的 GCM 密文 1048592 字节(多了 16 字节的标签) GCM 未篡改:读到 1048576 字节,正常到达 EOF GCM 密文倒数第 100 个字节翻转 1 位:读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch GCM 密文第 10 个字节翻转 1 位:读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch GCM 标签的最后 1 位翻转:读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch CBC(PKCS5Padding,无认证)密文倒数第 100 个字节翻转 1 位:读到 1048576 字节,正常到达 EOF CBC 最后一个密文块里翻转 1 位:读到 1048576 字节后抛出 IOException ← BadPaddingException:Given final block not properly padded. Such issues can arise if a bad key is used during decryption. GCM 被篡改的流第一次 read():IOException ← AEADBadTagException:Tag mismatch(第一个字节就读不到,说明明文在标签验证通过之前不会交给调用者)
GCM 密文去掉最后 16 字节(标签):读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch GCM 密文去掉最后 1 字节:读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch GCM 密文去掉前半(只留后 1000 字节):读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch GCM 密文只剩 10 字节(比标签短):读到 0 字节后抛出 IOException ← AEADBadTagException:Input data too short to contain an expected tag length of 16bytes GCM 空密文(0 字节):读到 0 字节后抛出 IOException ← AEADBadTagException:Input data too short to contain an expected tag length of 16bytes CBC 密文(PKCS5Padding)完整:读到 1048576 字节,正常到达 EOF CBC 密文在块边界截掉最后 1 个块(16 字节):读到 1048560 字节后抛出 IOException ← BadPaddingException:Given final block not properly padded. Such issues can arise if a bad key is used during decryption. CBC/NoPadding 密文在块边界截掉最后 1 个块:读到 1048560 字节,正常到达 EOF(没有任何错误,明文悄悄少了 16 字节) CBC 密文截掉最后 5 字节(不在块边界):读到 1048560 字节后抛出 IOException ← IllegalBlockSizeException:Input length must be multiple of 16 when decrypting with padded cipher
被篡改的 GCM 流没有读取就 close():没有异常 未篡改的 GCM 流读了 100 字节就 close():没有异常 被篡改的流读 100 字节:IOException ← AEADBadTagException:Tag mismatch 该流再 close():没有异常 markSupported() = false;available() 在读取前 = 0;skip(1000) = 0 CipherOutputStream(GCM)写 1000 字节后 flush() 但没有 close():文件 992 字节(= 62 个完整的 16 字节分组;剩下 8 字节密文和 16 字节标签要等 close() 才写出,完整的文件是 1016 字节) close() 之后文件 1016 字节 写 1000 字节后既不 flush 也不 close:文件 992 字节(同样只有完整分组) 不 close 的文件用 CipherInputStream 解密:读到 0 字节后抛出 IOException ← AEADBadTagException:Tag mismatch
GCM 加密 10 MiB 成功(-Xmx128m):密文文件 10 MiB GCM 用 CipherInputStream 解密 10 MiB:成功,读到 10 MiB,耗时 130 ms CBC 加密 10 MiB 成功(-Xmx128m):密文文件 10 MiB CBC 用 CipherInputStream 解密 10 MiB:成功,读到 10 MiB,耗时 32 ms GCM 加密 20 MiB 成功(-Xmx128m):密文文件 20 MiB GCM 用 CipherInputStream 解密 20 MiB:成功,读到 20 MiB,耗时 253 ms CBC 加密 20 MiB 成功(-Xmx128m):密文文件 20 MiB CBC 用 CipherInputStream 解密 20 MiB:成功,读到 20 MiB,耗时 45 ms GCM 加密 30 MiB 成功(-Xmx128m):密文文件 30 MiB GCM 用 CipherInputStream 解密 30 MiB:成功,读到 30 MiB,耗时 367 ms CBC 加密 30 MiB 成功(-Xmx128m):密文文件 30 MiB CBC 用 CipherInputStream 解密 30 MiB:成功,读到 30 MiB,耗时 55 ms GCM 加密 40 MiB 成功(-Xmx128m):密文文件 40 MiB GCM 处理 40 MiB(-Xmx128m):OutOfMemoryError:Java heap space CBC 加密 40 MiB 成功(-Xmx128m):密文文件 40 MiB CBC 用 CipherInputStream 解密 40 MiB:成功,读到 40 MiB,耗时 68 ms GCM 加密 50 MiB 成功(-Xmx128m):密文文件 50 MiB GCM 处理 50 MiB(-Xmx128m):OutOfMemoryError:Java heap space CBC 加密 50 MiB 成功(-Xmx128m):密文文件 50 MiB CBC 用 CipherInputStream 解密 50 MiB:成功,读到 50 MiB,耗时 77 ms GCM 加密 100 MiB 成功(-Xmx128m):密文文件 100 MiB GCM 处理 100 MiB(-Xmx128m):OutOfMemoryError:Java heap space CBC 加密 100 MiB 成功(-Xmx128m):密文文件 100 MiB CBC 用 CipherInputStream 解密 100 MiB:成功,读到 100 MiB,耗时 128 ms GCM 加密 200 MiB 成功(-Xmx128m):密文文件 200 MiB GCM 处理 200 MiB(-Xmx128m):OutOfMemoryError:Java heap space CBC 加密 200 MiB 成功(-Xmx128m):密文文件 200 MiB CBC 用 CipherInputStream 解密 200 MiB:成功,读到 200 MiB,耗时 231 ms