加密模式深度解析:从CBC、ECB到CTR模式的实战对比与Moshi应用
背景痛点:选模式比写代码还难
第一次把 AES 塞进项目时,我盯着Cipher.getInstance("AES/???/PKCS5Padding")里的???发了半天呆。ECB、CBC、CTR 三个缩写像三胞胎,文档都说自己“安全高效”,可一搜社区,全是“ECB 泄露指纹”“CBC 填充攻击”“CTR 不能重复计数器”的警告。到底该选谁?加解密的接口一样,可背后的坑却完全不同,一旦模式选错,性能、安全、甚至合规审计都得返工。。今天这篇笔记,就把三种模式拆成“乐高积木”,用 Moshi 串起完整流程,让你下次不再纠结。
1. ECB:最简单也最容易翻车
ECB(Electronic Codebook)直接把明文切块,每块独立加密。特点一句话:块与块之间毫无关联。
- 优点:实现简单,无初始向量(IV),可并行,出错只影响单块。
- 隐患:相同明文块 ⇒ 相同密文块,指纹泄露一目了然。下图经典示例——加密企鹅,ECB 后图案依旧可见,安全形象瞬间崩塌。
适用场景:仅用于教学演示或**随机数据(如已加密的密钥)**的二次封装。
2. CBC:老牌“链路”模式
CBC(Cipher Block Chaining)把前一块密文与当前明文异或后再加密,首尾块用随机 IV 拉乱序。
- 关键细节:
- IV 必须每次随机,长度 = 块大小(AES 为 16 B),可附在密文前端。
- 明文需填充到块整数倍,常用 PKCS5Padding / PKCS7Padding。
- 隐患:若攻击者篡改 IV 可翻转明文某比特(位翻转攻击),需做完整性校验(HMAC/SMAC)。
适用场景:文件、磁盘、即时通信等对顺序 & 完整性要求高的业务。
3. CTR:把分组当流加密用
CTR(Counter)让分组算法变身“流密码”。生成递增计数器块序列,用密钥加密计数器得到密钥流,再与明文异或。
- 优点:
- 无填充,明文长度任意字节。
- 加解密完全对称,可预计算密钥流,延迟低。
- 天然并行,吞吐量最高。
- 隐患:计数器不能重复(Nonce+Counter 组合必须唯一),否则“两密文异或 = 两明文异或”直接裸奔。
适用场景:实时音视频、高频 RPC、随机读写数据库字段等低延迟+高并发场景。
4. Moshi 实战:一条 JSON 走三遍
下面用同一份数据User(name, age)跑通 ECB/CBC/CTR,密钥长度统一 128 bit,方便对比。代码基于 JDK17 + Moshi 1.15,仅依赖kotlin-stdlib与bouncycastle提供的 AES 封装(可替换为 javax.crypto)。
4.1 公共部分:密钥生成与 Moshi 序列化
object CryptoUtil { fun generateKey(): SecretKey = KeyGenerator.getInstance("AES") .apply { init(128) }.generateKey() inline fun <reified T> toJson(model: T): String = Moshi.Builder().build().adapter(T::class.java).toJson(model) inline fun <reified T> fromJson(json: String): T? = Moshi.Builder().build().adapter(T::class.java).fromJson(json) }4.2 ECB 实现
fun ecbEncrypt(key: SecretKey, json: String): ByteArray { val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, key) return cipher.doFinal(json.toByteArray(StandardCharsets.UTF_8)) } fun ecbDecrypt(key: SecretKey, data: ByteArray): String { val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, key) return String(cipher.doFinal(data), StandardCharsets.UTF_8) }4.3 CBC 实现(带随机 IV)
fun cbcEncrypt(key: SecretKey, json: String): Pair<ByteArray, ByteArray> { val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) } val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) val cipherText = cipher.doFinal(json.toByteArray(StandardCharsets.UTF_8)) return iv to cipherText // 返回 IV 与密文,方便拼接 } fun cbcDecrypt(key: SecretKey, iv: ByteArray, cipherText: ByteArray): String { val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) return String(cipher.doFinal(cipherText), StandardCharsets.UTF_8) }4.4 CTR 实现(无填充)
fun ctrEncrypt(key: SecretKey, json: String): Pair<ByteArray, ByteArray> { val iv = ByteArray(16).apply { SecureRandom().nextBytes(this) } // 当作Nonce val cipher = Cipher.getInstance("AES/CTR/NoPadding") cipher.init(Cipher.ENCRYPT_MODE, key, IvParameterSpec(iv)) val cipherText = cipher.doFinal(json.toByteArray(StandardCharsets.UTF_8)) return iv to cipherText } fun ctrDecrypt(key: SecretKey, iv: ByteArray, cipherText: ByteArray): String { val cipher = Cipher.getInstance("AES/CTR/NoPadding") cipher.init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) return String(cipher.doFinal(cipherText), StandardCharsets.UTF_8) }4.5 调用示例
val user = User("Alice", 29) val key = CryptoUtil.generateKey() // ECB val ecbBytes = ecbEncrypt(key, toJson(user)) val userEcb = fromJson<User>(ecbDecrypt(key, ecbBytes)) // CBC val (ivCbc, cbcBytes) = cbcEncrypt(key, toJson(user)) val userCbc = fromJson<User>(cbcDecrypt(key, ivCbc, cbcBytes)) // CTR val (ivCtr, ctrBytes) = ctrEncrypt(key, toJson(user)) val userCtr = fromJson<User>(ctrDecrypt(key, ivCtr, ctrBytes))4.6 异常处理最佳实践
- 捕获
BadPaddingException→ 立即返回统一“解密失败”文案,避免旁道攻击。 - 捕获
AEADBadTagException(若启用 GCM)→ 同样不区分原因,直接拒绝。 - 日志只记录调用 ID,绝不打印密钥、IV 或密文片段。
5. 性能横评:跑 1 GB 随机数据
本地 Mac M2 单线程,AES-128,16 KB chunk 平均结果:
- ECB:1.35 GB/s(无额外计算,纯并行)
- CBC:1.10 GB/s(串行链依赖,略慢)
- CTR:1.40 GB/s(可预计算密钥流,最快)
内存占用三者持平;CTR 在多核下优势更明显,适合高并发网关。
6. 安全配置速查表
- ECB:生产环境禁用;若必须兼容老协议,外层再套 HMAC-SHA256。
- CBC:
- IV 用
SecureRandom每次刷新; - 密文末尾追加
HMAC(key, iv+cipher),防篡改; - 拒绝旧版
SSL3填充,用PKCS5Padding即可。
- IV 用
- CTR:
- Nonce 可用
12 B 随机 + 4 B 计数器或8 B 随机 + 8 B 序号,保证(Nonce, Counter)对不重复; - 多设备共享密钥时,用分布式序号生成器或时间戳 + 随机;
- 同样建议加 MAC(如 AES-GCM 内置 TAG)。
- Nonce 可用
7. 那些年我踩过的坑
- “IV 固定写死 16 个 0”→ 明文前缀相同,CBC 也成 ECB。
- CTR 计数器回卷→ 32 位计数器上限 4 GB,大文件溢出后密钥流重复。
- 密文直接
new String(cipherBytes)→ 默认 UTF-8 把随机字节解码成�,再编码就永久丢失。 - 忘记填充→
NoPadding下明文长度非 16 整倍直接抛异常。 - 复用 Cipher 实例→ 多线程并发下计数器/IV 状态错乱,务必
ThreadLocal或每次getInstance。
8. 思考题:你的业务选谁?
假设你在做离线批量加密用户头像文件(平均 2 MB),同时提供在线解密预览接口,QPS 约 5 k,延迟要求 < 20 ms。你会选哪种模式?为什么?(提示:并发、填充、完整性、随机读写)
欢迎在评论区留下你的方案,我们一起 review!
9. 把“豆包”也拉进实时通话
写完这篇,我最大的感受是:“把数据塞进 AES 盒子”只是第一步,真正的挑战是让算法在业务节奏里跑顺。如果你也迷恋“边说话边加密”的实时场景,可以试试从0打造个人豆包实时通话AI动手实验——里面把 ASR→LLM→TTS 整条链路拆成可插拔模块,CTR 加密正好用在语音流低延迟通道,代码里还顺手示范了如何给音频数据加 MAC 校验。整套实验对新手很友好,我跟着跑通只花了不到一小时,推荐你也玩一下,把“加密模式”与“实时通话”两个技能点一次点亮。