AES-256 解密失败排查:从密文格式到 IV、Padding 和 Key 派生
排查 AES-256 解密失败时,先确认密钥长度、IV、mode、padding、密文编码和 OpenSSL passphrase 输出差异。本文按真实接口兼容场景说明为什么同样叫 AES-256 仍会解不开,以及如何逐项验证参数、密文表示、库默认值和样例输入输出。并记录每一步的输入输出。
AES-256 解密失败时,错误通常不会告诉你真正原因。Node 里可能是 bad decrypt,Java 里可能是 padding exception,前端工具里可能只是空白或乱码。排查时不要先怀疑算法“玄学”,先把加密配方拆开:密文字节怎么表示、key 是原始 32 字节还是口令、有没有 IV、mode 和 padding 是否一致。
可以用 AES Encrypt 复现小样例,但不要把生产密文、客户数据或真实长期密钥直接粘进去。更好的做法是用同样配置造一段假数据,先证明配置能闭环。
先判断失败发生在哪一层
解密失败大致分三类。第一类是输入根本不是预期的密文字节,例如把 Base64URL 当 Base64、把 hex 当普通文本、复制时少了一段。第二类是 AES 参数不一致,例如 CBC 用成 CTR、PKCS7 用成 NoPadding、IV 填错。第三类是 key 来源不一致,例如一端使用 32 字节 raw key,另一端把同一个字符串当 passphrase 再派生。
判断顺序很重要。先确认密文能被正确还原成字节,再检查 AES 参数,最后再怀疑上游密文损坏。否则你会在错误的层面上反复试 mode 和 padding。
密文格式:Base64、hex 和外层包装
很多线上值看起来像一段随机字符串,但表示方式不同。Base64 常见字符包括 +、/、=;hex 通常只包含 0-9a-f 且长度为偶数;Base64URL 会把 + 和 / 换成 - 和 _,结尾也可能没有 =。
如果日志里是这样的值:
"ciphertext":"U2FsdGVkX19k4k...=="
真正要解密的是 JSON 字段里的字符串,不是带引号、字段名或反斜杠转义的整段日志。先把外层 JSON 解析出来,再判断内部值的编码。只有需要单独验证编码时,再用 Base64 Encoder 做转换;Base64 转出来的字节仍然不是明文,它只是 AES 解密的输入。
Key:32 字节 raw key 不等于 32 个字符的口令
AES-256 的 “256” 指 key 是 256 bit,也就是 32 字节。问题在于很多文档写 “key”,但没有说明它是 raw key、hex key、Base64 key,还是用户输入的 passphrase。
例如 0123456789abcdef0123456789abcdef 如果按 UTF-8 字符串使用,是 32 字节;如果按 hex 解析,则只代表 16 字节。这两种解释会得到完全不同的 key。再比如 my-secret-password 不是 AES-256 key,它通常需要通过 PBKDF2、EVP_BytesToKey 或其他 KDF 派生成真正 key 和 IV。
排查时记录 key 的来源和解释方式:
- 环境变量里存的是普通字符串、hex,还是 Base64。
- 代码是否对 key 做过 hash、PBKDF2 或截断。
- 另一端是否把 passphrase 自动派生成 key。
- staging 和 production 是否用了不同 secret。
如果小样例在本地能解开,线上密文解不开,优先确认是不是拿错环境 secret,而不是继续换 padding。
IV、mode、padding 必须成套匹配
CBC、CTR、CFB、OFB、ECB 不是“解不开时换一个试试”的选项。加密时用了什么,解密时就必须用什么。CBC 通常还需要 IV;CTR 也依赖 nonce/counter;ECB 不使用 IV,但也不应该因为工具要求字段就随便填一个。
padding 只在需要块对齐的模式里常见,例如 CBC + PKCS7。错误边界也比较明显:如果 key、IV、mode 都正确,但最后一步报 padding 错,可能是 padding 设置错,也可能是密文最后一个 block 被截断,不能只看错误名就下结论。
一个实用判断是:用相同配置加密 hello,马上再解密。如果这个 round-trip 失败,说明你还没把工具参数填对;如果 round-trip 成功,但第三方密文失败,再对照第三方文档和样例。
OpenSSL Salted 输出和 passphrase 派生
看到 Base64 解码后以 Salted__ 开头,通常说明它是 OpenSSL 风格的 salted passphrase 输出。这个格式不是“普通 AES-CBC 密文前面多几个字符”那么简单,它暗示 key 和 IV 可能是从 passphrase 与 salt 派生出来的。
典型错误是:一端使用 CryptoJS 的 passphrase 模式生成 U2FsdGVkX1...,另一端却按固定 raw key + IV 去解。两边都写着 AES-256-CBC,但 key 派生不同,结果仍然对不上。
如果你遇到这种情况,不要手动删掉 Salted__ 后继续试。应该确认:salt 是否包含在密文里、KDF 算法是什么、迭代次数是多少、digest 是 MD5 还是 SHA-256、IV 是派生得到还是单独传输。
复制和传输边界也会破坏密文
密文失败不一定是密码学参数错。工单、Excel、日志系统、URL query 和聊天软件都可能改写字符串:去掉末尾 =、把 + 变成空格、自动换行、截断长字段、加入不可见字符。
排查这类问题时,把收到的值和原始发送值放到 Text Diff 里比较,重点看长度、开头、结尾和换行。Base64 长度变化、hex 少一个字符、复制时多了引号,都会导致 AES 解密失败。
一套可执行排查流程
- 从协议或代码里确认密文是 Base64、Base64URL、hex 还是原始文本包装。
- 去掉 JSON、header 前缀、引号和日志转义,只保留真正密文值。
- 确认 key 是 raw bytes、hex、Base64 还是 passphrase 派生结果。
- 固定 mode、padding、IV,不要一次改多个变量。
- 用假明文做 round-trip,证明当前工具设置能加密再解密。
- 用第三方官方样例验证;样例能通而真实值失败,再查截断、环境 secret 和复制改写。
- 记录最终配方:算法、mode、padding、key 格式、IV 格式、密文编码、KDF。
FAQ
为什么解密出来是乱码,而不是报错?
可能是 padding 没有校验、模式类似流式输出,或者工具把错误字节强行按文本显示。乱码不代表“快成功了”,仍然要回到 key、IV、mode、padding 和编码逐项确认。
只有 key 没有 IV 能解密吗?
取决于原始模式。ECB 不需要 IV;CBC、CTR、CFB、OFB 通常需要相同 IV 或 nonce。原来使用随机 IV 时,IV 必须随密文保存或通过协议传递。
AES-256 密文能靠暴力尝试恢复吗?
现实排查中不应该这样做。正确路径是找到加密配方和 key 来源。缺少 key、IV 或派生规则时,解密失败不能证明工具有问题。