宝塔拨测博客拨测技术博客
宝塔拨测

AES 加密结果每次都不一样?U2FsdGVkX1 开头的密文与密钥派生讲清

同一段明文、同一个密钥,连加密两次却得到两个完全不同的 Base64 串——这是很多人第一次用 AES 工具时的第一反应:是不是坏了,还是我填错了?都不是。差在每次都会新生成的一个 8 字节随机盐上。

打开 AES/DES 加解密工具,在「AES 加解密」Tab 的明文框填 hello、密钥框填 secret,密文框立刻出现结果;再点一次同样的内容,结果又是一串新的。下面两张截图来自同一次运行:

明文 hello、密钥 secret,第一次加密得到 U2FsdGVkX18sxkzueDbezK2h8wIEBr38VfBCXx5DSog=

同一组输入再算一次,得到的是:

U2FsdGVkX19YLdjGPz8dqWzIPTniE1LWPcVkiNKC860=

两个串长度一样、开头一样,内容完全不同。把第一个串原样贴回解密框、密钥仍填 secret,明文框给出 hello。

把第一次的密文和密钥 secret 贴进解密框,还原出 hello

U2FsdGVkX1 开头,是 Base64 后的 Salted__

把 U2FsdGVkX18sxkzueDbezK2h8wIEBr38VfBCXx5DSog= 做一次 Base64 解码,得到 32 个字节,结构是这样:

字节位置 长度 内容
0–7 8 字节 ASCII 字符串 Salted__
8–15 8 字节 本次加密随机生成的盐
16–31 16 字节 真正的密文(hello 只有 5 字节,填充到 16 字节)

U2FsdGVkX1 就是 Salted__ 这 8 个字节在 Base64 里的前 11 个字符(单独编码 Salted__ 得到的是 U2FsdGVkX18=,接上后面的盐以后等号被后续字符顶掉了)。它只说明「这个串带盐」,不是密钥,也不是校验值。后面紧跟的 8 字节才是盐,两个密文之所以不同,差的就是这 8 个字节。

这是 OpenSSL 的 enc 命令留下的老格式,也叫「带盐的 OpenSSL 格式」。它的好处是盐跟着密文走:解密方不需要另外拿到盐,从密文里读出来就行。

解密能还原,是因为盐和口令一起参与派生

AES 需要两样东西:32 字节密钥和 16 字节 IV。用户只给了一个口令 secret,缺的部分由密钥派生函数补出来——把口令和刚从密文里读出的 8 字节盐拼在一起,用 MD5 反复哈希拼接,先填满 32 字节密钥,再接着填 16 字节 IV(EVP_BytesToKey,这个格式只做一轮)。同样的口令配同样的盐,派生出的密钥和 IV 就完全一样,所以密文能解回 hello;盐变了,密钥跟着变,密文自然也不同。

这一步决定了「密钥一样却解不开」的根本原因:两边算的不是同一把密钥。下面是本机用 OpenSSL 3.5.7 对同一个密文做的六组尝试,只有第一组还原出 hello:

解密参数 结果
-aes-256-cbc -md md5 还原出 hello
-aes-256-cbc -md sha256 bad decrypt
-aes-128-cbc -md md5 bad decrypt
-aes-192-cbc -md md5 bad decrypt
-aes-256-ecb -md md5 bad decrypt
-aes-256-cbc -md md5 -pbkdf2 bad decrypt

能用的那一组命令是:

echo 'U2FsdGVkX18sxkzueDbezK2h8wIEBr38VfBCXx5DSog=' | openssl enc -d -aes-256-cbc -a -k secret -md md5

少写 -md md5 或加上 -pbkdf2 都会失败,因为老格式用的是 MD5 版 EVP_BytesToKey、只做一轮;OpenSSL 3 会把这套派生方式标记为 deprecated,提示改用 -iter 或 -pbkdf2,但默认行为仍是老的——所以命令行上既能看到告警,又能解出结果。这就是「同一句口令,换个库就解不开」最常见的一层原因。

和调用方对不上,按这四个检查点排

检查点 这个工具的实际行为 对不上时会怎样
算法与模式 实测为 AES-256-CBC,PKCS7 填充 对方用 GCM、CFB 或 AES-128,解出乱码或报错
密钥派生 口令 + 8 字节盐经 MD5 派生 32 字节密钥与 16 字节 IV 对方把口令当原始密钥,或改用 PBKDF2,必然失败
密文结构 Salted__ 头 + 8 字节盐 + 密文 对方按裸密文处理,前 16 字节会被当成数据
编码 Base64 对方按十六进制读,第一步就解不出来

页面只有「AES 加解密」和「DES(TripleDES)加解密」两个 Tab,没有模式、填充、密钥长度的选项——这意味着它不是通用的 AES 调试器,而是一个固定参数的 OpenSSL 兼容工具。要验证 GCM、CBC 与 IV 的区别,得在本机用 openssl 或代码库跑,不能拿它当参照。

密文比明文长是正常的:填充与分组

AES 的分组固定 16 字节,明文不够一块就补齐,补齐的字节数还要单独占一块。实测三组长度:

明文 明文字节 密文串 Base64 长度 解码后字节 减去 16 字节头
hello 5 44 32 16
你好 6 44 32 16
100 个 a 100 172 128 112

5 字节的 hello 变成 16 字节密文,是因为要补 11 个字节的填充;100 字节补到 112 字节,正好 7 个分组。所以「密文长度不是 16 的倍数」「Base64 不是 4 的倍数」这类判断可以直接用:解不开之前先看长度对不对。

解不开时它报什么,不报什么

密钥填错会明确报错。把 secret 改成 secret9 去解第一段密文,密文框变红,下方给出「无法解密:密钥错误或密文无效」,明文框为空:

密钥改成 secret9,解密框变红并提示「无法解密:密钥错误或密文无效」

但空白输入不会报错:明文为空时加密结果为空,密钥为空时结果也为空,界面上没有任何提示。写脚本批量调用时,别把「空结果」当成加密成功。

DES(TripleDES)这一 Tab 该不该用

切到第二个 Tab,同样 hello 配 secret,得到 U2FsdGVkX1/giVbrAbYDnpRh2bmCxGnZ,解码后 24 字节:16 字节头加 8 字节密文,分组是 64 位,能原样解回 hello。

DES(TripleDES)Tab:明文 hello、密钥 secret,密文同样是 U2FsdGVkX1 开头

能用,但不建议用于新系统。3DES 的分组只有 64 位,按生日界,同一把密钥下加密到几十 GB 量级就有分组碰撞风险,这就是 SWEET32(CVE-2016-2183);NIST 已在 2023 年正式弃用 3DES。

它在 OpenSSL 3 里还没被移出默认支持:本机 3.5.7 直接用 -des-ede3-cbc 就能解出 hello,不像单 DES(-des-cbc)那样报 unsupported、必须加载 legacy provider 才能用。

echo 'U2FsdGVkX1/giVbrAbYDnpRh2bmCxGnZ' | openssl enc -d -des-ede3-cbc -a -k secret -md md5

要兼容旧数据可以解,新加密用 AES-256-GCM:GCM 除了保密还带完整性校验,密文被改一个字节就会验证失败,而 CBC 只会解出乱码。

生产密钥别贴进在线工具

工具页写明「所有加解密操作在浏览器本地完成,不会上传到服务器」,测试期间页面发出的请求只有自身静态资源、登录态查询和访问统计,没有把明文或口令提交出去的接口。但「本地计算」不等于「可以放心贴生产密钥」:浏览器扩展、录屏、剪贴板历史都可能把内容带出去,真正的生产密钥应该留在密钥管理系统里,本机运算用命令或代码库完成:

# 加密(每次自动生成盐,输出 OpenSSL 格式)
echo -n 'hello' | openssl enc -aes-256-cbc -a -salt -k secret -md md5

# 解密
echo 'U2FsdGVkX18sxkzueDbezK2h8wIEBr38VfBCXx5DSog=' | openssl enc -d -aes-256-cbc -a -k secret -md md5

新项目直接用带认证的模式(AES-GCM)和正规的密钥管理,不要沿用口令派生 + CBC 这套老格式:它能解老数据,但不该成为新系统的加密方案。

常见问题

同样的明文和密钥,AES 加密两次结果不一样,是工具坏了吗?

不是。每次点加密都会生成一个新的 8 字节随机盐(salt),盐和密文一起放进结果串里,所以同样的 hello 配 secret 加密两次会得到两个不同的 Base64。盐不需要保密,因为它就明文写在密文开头;解密时从密文里读回来,就能派生出同一把密钥和同一个 IV,照样解得出原来的 hello。

密文开头的 U2FsdGVkX1 是什么?

是 base64 编码后的 Salted__。OpenSSL 的 enc 命令在带盐加密时,会在密文前写 8 字节 ASCII 的 Salted__,再跟 8 字节随机盐,然后才是真正的密文,整体再做一次 Base64。所以 U2FsdGVkX1 这 11 个字符只是「这里有盐」的标记,后面那一段才是盐值。

为什么我把密文贴到别的程序里解不开?

先对齐四件事:算法与模式(实测这个工具是 AES-256-CBC 加 PKCS7 填充)、密钥派生方式(口令经 EVP_BytesToKey 的 MD5 版本派生 32 字节密钥,不是把口令直接当密钥)、密文结构(Salted__ 头加 8 字节盐,不是裸密文)、编码(Base64 不是十六进制)。四项里错一项,结果就是乱码或 bad decrypt。

密钥填 16 位,为什么用 AES-128 解不开?

因为这个输入框收的是口令(passphrase),不是 16 字节原始密钥。口令会先经过密钥派生函数拉长成固定长度:AES 取 32 字节密钥加 16 字节 IV,跟口令本身有多少个字符无关。实测把工具给出的密文用 aes-128-cbc 解,直接 bad decrypt;只有 aes-256-cbc 配 -md md5 能还原出 hello。

解密时提示「无法解密:密钥错误或密文无效」是怎么回事?

两种情况会报这一句:口令不对,或者密文被截断、改过、不是这个工具产出的 OpenSSL 格式。实测把密钥从 secret 改成 secret9,解密框变红并给出这句提示,结果区为空。另外空白输入不会报错:明文为空或密钥为空时,加密结果同样是空,不要把它当成加密成功。

DES(TripleDES)还能用来加密业务数据吗?

不建议。这个 Tab 用的是 3DES(OpenSSL 里的 des-ede3-cbc),分组只有 64 位,按生日界在几十 GB 数据量级就有碰撞风险(SWEET32,CVE-2016-2183),NIST 也已在 2023 年弃用。它在 OpenSSL 3 里仍能直接解(单 DES 才需要 legacy provider),但 64 位分组加生日界决定了它已不该用于新数据。新系统用 AES-GCM。

AES3DES对称加密OpenSSLSalted__宝塔拨测