bigywater.tech
Java · 410

CipherInputStream:加密流的结束和认证边界

CipherInputStream 看上去只是“解密版的 InputStream”,但认证加密的边界、截断与内存行为全不一样。这篇用真实的篡改、截断、关闭和大文件实验量出它的保证和限制。

0 字节
GCM 密文被篡改(任何位置)时,CipherInputStream 在异常前交给调用者的明文字节数
1048576 字节
CBC 密文被篡改(中间)时读出的明文字节数——篡改没有被发现
30 → 40 MiB
-Xmx128m 下 GCM 用 CipherInputStream 解密成功与 OutOfMemoryError 的分界(CBC 200 MiB 仍成功)

篡改

实测 · 本机执行输出
  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

close 与 flush

实测 · 本机执行输出
  被篡改的 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

分步演示

CipherInputStream
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12(SunJCE);AES-128,全零密钥与随机明文(只用于实验);GCM 标签 128 位、IV 12 字节固定(只用于实验,真实使用必须每次不同)
    • 没有验证:JDK 17 / 27 的行为差异(JDK 对 GCM 解密的缓冲策略各版本不同)、Cipher.update 的逐块用法、ChaCha20-Poly1305 的流式行为、BouncyCastle 的 CipherInputStream、分块加密(STREAM / 每块一个标签)的实现与性能
    京ICP备2026067502号-1京公网安备11010502063487号