同样的密钥和明文,为什么各家在线 AES 工具算出来不一样

· 约 6 分钟 🔒 AES 加解密

两个系统对接,一端加密一端解密,结果对不上——这大概是 AES 相关问题里最常见的一类。排查时最容易走的弯路,是怀疑「是不是哪家实现有问题」。

几乎不会是实现的问题。 AES 是 2001 年发布的 FIPS-197 标准,分组变换、密钥扩展、轮数全部写死,任何一个能通过官方测试向量的实现,算出来的结果必然逐字节相同。真正会变的东西全在算法外面。

四个变量,一个都不能差

变量常见取值差错后的表现
密钥的解释方式UTF-8 / Hex / Base64全篇乱码
模式与 IVECB / CBC / CTR / GCM,IV 来源CBC 下仅前 16 字节乱码
填充PKCS7 / 零填充 / 不填充末尾多出或缺少字节
输出编码Base64 / Hex看着完全不像,实则同一串字节

下面逐个拆。

一、密钥:同一串字符可以是三把不同的密钥

这是最高频、也最隐蔽的错因。假设双方约定密钥是 1234567890abcdef

  • UTF-8 理解:16 个 ASCII 字符 → 16 字节 → 合法的 AES-128 密钥
  • Hex 理解:16 个十六进制字符 → 8 字节 → 对 AES 来说长度非法
  • Base64 理解:解出 12 字节 → 同样非法

三种解释指向三串完全不同的字节。双方都觉得「密钥就是那一串啊」,但机器拿到的根本不是一个东西。

排查方法只有一个:把密钥转成 Hex 打印出来逐字节比对。 不要比对字符串,要比对字节。

长度不合法时,各家的补法不一样

AES 只认 16、24、32 三种密钥字节数。给一个 13 字节的密钥,实现必须做出选择:

  • 补零到 16 —— 相当一部分在线工具的做法
  • 截断 —— 超长时砍掉尾部
  • 当作口令走 KDF —— 派生出合法密钥
  • 直接报错

这四种互不兼容。所以「换个工具就能算出来」不代表算得对,只代表那个工具替你静默选了一种补法。本站工具选择直接报错——在联调场景下,一个不吭声的自动补齐会让你把时间花在完全错误的方向上。

密文以 U2FsdGVkX1 开头:那不是裸 AES

如果对方给你的密文 Base64 以 U2FsdGVkX1 开头,先停下。这十个字符解出来是 Salted__,是 OpenSSL enc 的容器格式:

Salted__ (8字节) + 随机盐 (8字节) + 真正的密文

它意味着对方填的是口令不是密钥,真正的 AES 密钥由口令加随机盐派生而来。可以实测一下,同样的口令和明文,两次加密结果完全不同:

$ printf 'hello' | openssl enc -aes-128-cbc -k mypass -a -pbkdf2
U2FsdGVkX1/GZvS3IZoxJbYLfO7vi09+82l6GvIo99U=

$ printf 'hello' | openssl enc -aes-128-cbc -k mypass -a -pbkdf2
U2FsdGVkX18YEybrpf46cREcddc0jI5Ty7ScvD+0hp8=

而裸密钥模式是完全确定的:

$ printf 'hello' | openssl enc -aes-128-cbc \
    -K 00112233445566778899aabbccddeeff \
    -iv 000102030405060708090a0b0c0d0e0f -a
FqK6ViMSEP8/Dbzrx3HmmA==      # 跑一百次都是这个

前端最常见的来源是 CryptoJS。 CryptoJS.AES.encrypt(msg, "字符串") 第二个参数传字符串会走口令模式,传 WordArray 才是裸密钥模式。这两种写法在代码里长得很像,联调时后端怎么都对不上。

如果确实要用口令模式,两端还得对齐 KDF 参数——OpenSSL 1.1.0 之后 enc 的默认摘要从 MD5 改成了 SHA-256,跨版本对接时这一项也会咬人。更省事的做法是两端都换成裸密钥模式,明确给出 Hex 密钥。本站工具只做裸密钥,不做任何口令派生。

二、IV:错了不会全乱,只乱开头

CBC 模式的解密是这样的:每个分组解密后,要和前一个密文分组异或才得到明文。第一个分组没有前驱,用的就是 IV。

所以 IV 错误只影响第一个分组——明文的前 16 字节变成乱码,从第 17 字节开始完全正常。

这个特性极具迷惑性。你会看到一段「开头几个字是乱码、后面完全通顺」的文本,很自然地怀疑是编码问题、是 BOM、是截断。其实是 IV。

反过来这也是个好用的判据:

  • 全篇乱码 → 密钥错
  • 只有前 16 字节乱 → 密钥对,IV 错

IV 从哪来的三种约定

  1. 全零 —— 最省事,也最不该用。同密钥同明文会产生同密文,CBC 相对 ECB 的优势被抵消了
  2. 随机生成,拼在密文最前面 —— 正确做法。解密方先切走前 16 字节当 IV,剩下的才是密文。如果你不知道这个约定,会把 IV 当密文去解,全篇乱码
  3. 取密钥的前 16 字节 —— 老代码里能见到,安全性存疑但确实存在

一个判断技巧:如果密文总字节数减去 16 之后才是 16 的整数倍,那开头 16 字节很可能就是 IV。

GCM 是唯一「本该每次不同」的情况

GCM 要求每次加密用一个不重复的 nonce(推荐 12 字节)。如果工具替你随机生成 nonce,那同样的输入每次产生不同密文是完全正常的。GCM 还会额外输出一个 16 字节的认证标签(tag),解密时缺了它就验不了,很多对接问题是漏传 tag 造成的。

三、填充:只影响最后一个分组

ECB 和 CBC 要求明文长度是分组(16 字节)的整数倍,不够就得补。

填充做法问题
PKCS7缺 n 字节就补 n 个值为 n 的字节,正好整除时补一整个分组事实标准,绝大多数实现的默认值
零填充0x00 到整数倍明文本身以 0x00 结尾时无法还原,且长度已经整除时不补,解密方分不清
不填充不补,要求明文本来就整除长度不对时直接失败

填充不一致的典型表现是:能解密,但末尾多出几个奇怪字节,或者少了几个字符

CFB、OFB、CTR、GCM 是流式的,不分组也就不需要填充,明文多长密文就多长。这也是它们在对接时少一个变量的原因。

四、输出编码:最容易排除的一项

Base64 和 Hex 是同一串字节的两种写法:

Hex     16a2ba56231210ff3f0dbcebc771e698
Base64  FqK6ViMSEP8/Dbzrx3HmmA==

看着毫无关系,其实完全一样。把两端结果都转成 Hex 再比,能一次排除掉这一整类假性差异。

顺带检查 Base64 串里有没有混进换行或空格——从邮件、聊天工具、日志里复制出来的 Base64 经常带看不见的换行,直接解会报错。

一条实用的排查顺序

  1. 密钥转 Hex 逐字节比 —— 排除编码歧义,同时确认字节数合法
  2. 确认模式 —— ECB 还是 CBC 还是 GCM
  3. 确认 IV 来源 —— 固定值?拼在密文头部?看乱码分布判断
  4. 核对填充 —— 默认都用 PKCS7
  5. 两端结果都转 Hex 再比 —— 排除编码差异
  6. 拿官方测试向量做基准 —— 先确认自己这一端是对的,再去查对方

第 6 条常被跳过,但它很关键。没有基准的互相调试,很容易演变成两边同时改参数,越改越远。

用本站工具做对照

AES 加解密支持 AES-128/192/256 以及 DES、3DES,覆盖 ECB、CBC、CFB、OFB、CTR、GCM 六种模式,密钥和 IV 都可以按 UTF-8 / Hex / Base64 三选一输入——这个选择是显式的,不会替你猜。解密结果不是合法 UTF-8 时会明确提示并给出原始 Hex,方便直接比对字节。

实现基于 node-forge,在浏览器本地计算,密钥和明文都不会离开你的设备。AES 的三个密钥长度都用 FIPS-197 附录的官方测试向量验证过,并与 OpenSSL 逐字节交叉核对。

排查时的正确姿势是:先用工具把对方给的密文解出来,确认参数组合;再用同一组参数加密你自己的明文,把结果发过去。中间任何一步对不上,都说明有一个变量还没对齐。

参数对齐之后还有一个更上游的问题:这套参数本身选得对不对。ECB 会泄漏明文结构、CBC 只保密不防篡改、GCM 的 nonce 绝不能重用——见 AES 模式怎么选

❓ 常见问题

AES 是确定性的吗?同样的输入为什么会有不同结果?

AES 算法本身是确定性的——同一把密钥、同一个 IV、同一种模式、同一份明文,算出来必然是同一串密文,全世界任何一个正确实现都一样。所以结果对不上,一定是输入没对齐,而不是算法有随机性。真正会引入差异的有四处:(1) 密钥的解释方式——同样输入 1234567890abcdef,按 UTF-8 理解是 16 字节,按 Hex 理解是 8 字节,这是两把完全不同的密钥;(2) IV 从哪来——有的工具让你填,有的偷偷用全零,有的每次随机生成并拼在密文前面;(3) 填充方式——PKCS7、零填充、不填充,补出来的最后一个分组不同;(4) 输出编码——Base64 和 Hex 是同一串字节的两种写法,看着完全不像。唯一的例外是 GCM 模式:它每次要求一个不重复的 nonce,如果工具替你随机生成,那结果本来就该每次不同。

密文以 U2FsdGVkX1 开头是怎么回事?

这说明对方用的是口令派生模式,不是裸 AESU2FsdGVkX1 是 Base64 解出来的 Salted__ 八个字节,这是 OpenSSL enc 命令的容器格式:Salted__ + 8 字节随机盐 + 密文。它意味着两件事:(1) 你填的不是密钥而是口令,真正的密钥是由口令加随机盐经 KDF 派生出来的;(2) 因为盐每次随机,同样的口令和明文每次加密结果都不同——这不是 bug,是设计。最常见的来源是 CryptoJS:CryptoJS.AES.encrypt("明文", "字符串") 第二个参数传字符串时会走口令模式,传 WordArray 才是裸密钥模式。很多前端代码把这两种写法混着用,联调时后端怎么都对不上。怎么处理:要么两端都用口令模式并对齐 KDF 参数(迭代次数、摘要算法、OpenSSL 1.1 之后默认摘要从 MD5 改成了 SHA-256),要么两端都换成裸密钥模式,明确给出 32 位 Hex 的密钥。本站工具走的是裸密钥模式,不做任何口令派生。

我的密钥不是 16 位,工具报错,别的工具却能算,为什么?

因为那些工具偷偷替你补齐或截断了,而补法各家不同。AES 只接受 16 / 24 / 32 字节三种密钥长度,没有第四种。你给一个 13 字节的密钥,实现必须做点什么才能继续:(1) 补零到 16——CryptoJS 之外的不少在线工具这么干;(2) 截断到 16——超长时直接砍掉后面;(3) 当口令做 KDF——派生出合法长度;(4) 直接报错。这四种做法互不兼容,所以「换个工具就能算」不代表算得对,只代表它悄悄替你选了一种补法。本站工具选择报错,因为在联调场景里,一个静默的补齐会让你花几个小时去查别的地方。正确做法是让密钥本身就是合法长度:用 Hex 输入 32 个字符(16 字节)、48 个字符(24 字节)或 64 个字符(32 字节)。

对方只给了密钥没给 IV,我该填什么?

先别猜,去问。IV 填错的表现很有迷惑性——CBC 模式下 IV 错误只会毁掉第一个分组,也就是明文的前 16 字节,后面全部正确解出。所以你会看到一段开头是乱码、后面完全正常的文本,很容易误判成编码问题。几种常见约定:(1) 全零 IV——最常见的省事做法,也是最不该用的,同密钥同明文会产生同密文,失去 CBC 的意义;(2) IV 拼在密文最前面——传输时把 16 字节 IV 放在密文头部,解密方先切走前 16 字节当 IV。这是正确做法,但如果你不知道这个约定,就会把 IV 当成密文的一部分去解,结果全是乱码;(3) IV 等于密钥的前 16 字节——有些老代码这么写,安全性存疑但确实存在。判断技巧:如果密文字节数减去 16 才是 16 的整数倍,那前 16 字节很可能就是 IV。

解密出来是乱码,怎么定位是哪一步错了?

按「能不能解出整数个分组」把问题分成两类。(1) 报填充错误 / 长度不是分组倍数——说明密文本身就没拿对:可能 Base64 里混进了空格或换行,可能该用 Hex 却按 Base64 解,也可能密文被截断了。先检查密文字节数是不是 16 的整数倍(ECB / CBC 必须是);(2) 能解出来但内容是乱码——说明密钥或 IV 不对。这时看乱码的分布:全篇乱码通常是密钥错,只有开头 16 字节乱、后面正常是 IV 错(CBC 的特性),每隔一段规律性乱码要考虑模式选错了。本站工具在解出的字节不是合法 UTF-8 时会明确提示,并把原始 Hex 显示出来,方便你直接比对字节而不是盯着替换字符猜。

ECB 模式不需要 IV,是不是更省事?

省事,但代价是它基本不该用在真实数据上。ECB 把每个 16 字节分组独立加密,相同的明文分组必然产生相同的密文分组——这会把明文的结构原样泄漏出来。最经典的演示是拿 ECB 加密一张位图,加密后企鹅的轮廓仍然清晰可辨。在业务里的实际后果:(1) 加密后的数据库字段,相同值的密文也相同,攻击者不用解密就能做频次统计和等值关联;(2) 攻击者可以剪切、复制、重排密文分组,拼出一条从未被加密过但能正常解密的记录。什么时候可以用:明文总长不超过一个分组、且每次都不重复(例如加密一个随机 token),此时 ECB 与 CBC 没有实质差别。其余情况一律换掉,需要保密选 CBC,需要保密加防篡改选 GCM。

🔒 打开 AES 加解密 AES/DES/3DES·ECB/CBC/CFB/OFB/CTR/GCM·PKCS7 填充·UTF-8/Hex/Base64 密钥·本地运行不上传

📖 同一工具的其他教程

🔗 相关阅读

全部教程 →