MD5 能转成 SHA-256 吗?为什么不可能,以及正确做法
网上有大量页面承诺能做这件事,其中一些看上去还挺像那么回事。但它们宣传的这个操作并不存在。搞清楚为什么不存在,你才知道接下来该做什么。
简短答案:MD5 转不成 SHA-256,SHA-256 也转不回 MD5
MD5 哈希无法转换成 SHA-256。MD5 摘要是 128 位的输出,它并不包含产生自己的那段输入,而 SHA-256 必须拿到那段输入才能算出任何东西。原始数据一旦不在手上,中间那一步就永久缺失,没有任何工具或库能把它补回来。
同一段输入在三种算法下的结果:
| 算法 | hello 的摘要 | 十六进制字符数 | 位数 |
|---|---|---|---|
| MD5 | 5d41402abc4b2a76b9719d911017c592 | 32 | 128 |
| SHA-1 | aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d | 40 | 160 |
| SHA-256 | 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 | 64 | 256 |
把这三个摘要当成字符串看,它们之间没有共同前缀,也没有共同子串,更谈不上什么算术关系。唯一把它们联系起来的是 hello 这个词,而三者谁都没把它带在身上。想亲眼看着它们随输入各自独立变化,可以用 MD5 哈希生成器 和 SHA-256 哈希生成器 自己算一遍。
反方向也一样。从 SHA-256 转 MD5 之所以失败,原因完全相同:两个函数都是故意销毁信息,所以任何一个摘要都没法倒推回另一个函数所需要的那段输入。
剩下的才是有用的问题。没人是出于研究哈希理论的好奇心去搜这个词的,那你原本想做的到底是什么?实践中无非四件事,每一件都有真正的答案:
- 校验下载文件或比对校验和(checksum)
- 把密码库从 MD5 迁走
- 还原哈希背后的原始输入
- 用另一种算法对同一份数据重新哈希
先看那些承诺「转换」的页面到底在做什么。认出这个套路,你就不会再去相信它们的输出。
「MD5 转 SHA-256 转换器」页面实际在做什么
只有三种可能,没有一种是转换。
它向你索要原文。你粘贴一段字符串,页面同时给出两个摘要,然后宣传文案把这称作转换。这只是一个挂着误导性标签的哈希生成器:什么都没被转换,输入只是被哈希了两次。
它在查表。页面维护着一张「输入—摘要」对照表,数据来自各种字典和泄露的凭据库。粘贴一个原文恰好在表里的 MD5,页面就能找出原文,再用 SHA-256 算一遍结果给你看。这一招对 password、123456 以及一长串常见短字符串有效,对其他任何东西则会静默失败,这也是这类页面经常返回一片空白又不给任何解释的原因。
它什么都没做。相当多的页面只是把你的输入原样回显,或者对你粘贴的那串十六进制算个 SHA-256,却暗示这就是你原始数据的 SHA-256。这两个值并不相同,后文讲双重哈希的那一节会精确展示差在哪里。
检验方法很快:只给页面一个 32 字符的十六进制串,别的什么都不给,然后问一句:它凭什么可能知道原文?如果它照样吐出一个 64 字符的结果并称之为转换,那它要么在查表,要么在骗你。一个从不索要原始数据的哈希转换器,除了碰巧蒙对之外没有任何办法给出正确答案。
这个说法之所以能流传下来,是因为它听上去很像另一件确实可行的事。
哈希、编码与加密:三者中只有两个可逆
开发者往往在同一周里碰上这三样,然后把它们塞进脑子里同一个抽屉。可它们的行为完全不同:
| 操作 | 例子 | 是否可逆 | 需要什么 |
|---|---|---|---|
| 编码(encoding) | Base64、十六进制、URL 编码 | 永远可逆 | 什么都不需要 |
| 加密(encryption) | AES、RSA | 可逆 | 密钥 |
| 哈希(hashing) | MD5、SHA-1、SHA-256 | 永远不可逆 | 给什么都没用 |
培养出错误直觉的是 Base64。任何人都能把 Base64 字符串解码还原成字节,不需要密钥,也不需要授权;把 Base64 转成十六进制更是天天都在做的正当操作。两者不过是同一串字节的两种写法。见过这种操作之后,「MD5 转 SHA-256」听上去就像是同一类换写法。
但它不是。编码保留输入的每一个比特,加密把输入藏在密钥后面同样完整保留,哈希则把其中绝大部分直接扔掉。不管你喂进去的是一个密码还是一个 40 GB 的磁盘镜像,MD5 摘要都是 16 字节。那 40 GB 并没有被巧妙地藏到哪里去,它就是没了。想深入了解哈希与加密的区别,可以看 MD5 哈希生成器的 FAQ,不过这里一句话就够:没有密钥,所以根本没有什么可以解密的。
「MD5 已被攻破」不等于「MD5 可以反解」
这一点连不少态度诚实的文章都会写错。人们会从「MD5 好多年前就被攻破了」直接推到「那现在总该有人能把它还原了吧」。
这里牵涉到两种不同的攻击:
- 碰撞攻击(collision attack):找出两段不同的输入,让它们产生相同的摘要。攻击者两段输入都自己挑,也不在乎它们具体是什么。
- 原像攻击(preimage attack):从一个摘要出发,还原出能产生它的某段输入。能让你把 MD5 转成 SHA-256 的正是这一种,因为它会把可以喂给 SHA-256 的东西还给你。
MD5 的抗碰撞性在 2004 年被攻破,今天在普通硬件上几秒钟就能构造出一次碰撞。这就是 MD5 不适合用于签名、证书以及任何攻击者能施加影响的场景的原因。
MD5 的抗原像性并没有被攻破。已知最好的原像攻击至今仍停留在理论层面,复杂度为 2^123.4(Sasaki 与 Aoki,2009)。这比暴力破解只快了一丝一毫,在任何有实际意义的尺度上都遥不可及。无论 2004 年还是现在,都没有人能反解任意的 MD5 摘要。
所以这两句话可以同时成立,彼此并不矛盾:MD5 已被攻破,MD5 仍然不可反解。被攻破这件事动摇不了上面的结论。如果你要做的其实是给新系统在两种算法之间选型,而不是在它们之间转换,那么 MD5 与 SHA-256 对比 专门讲了这个决策。
你真正想做的:四类任务与各自的正解
| 你实际在做的事 | 真正的答案 |
|---|---|
| 校验下载文件或比对校验和 | 拿文件用你需要的算法重算一遍 |
| 迁移 MD5 密码哈希 | 用 bcrypt 包一层,登录时逐个升级 |
| 还原原始输入 | 那叫破解不叫转换,成败取决于熵 |
| 用另一种算法重新哈希同一份数据 | 回到原始字节 |
任务一:比对校验和或验证下载文件
重算,不要转换。文件就躺在你的磁盘上,这意味着摘要丢掉的那样东西你手里有。直接对它跑你需要的那个算法:
# GNU coreutils
md5sum ubuntu-24.04-desktop-amd64.iso
sha256sum ubuntu-24.04-desktop-amd64.iso
# macOS
md5 ubuntu-24.04-desktop-amd64.iso
shasum -a 256 ubuntu-24.04-desktop-amd64.iso
人们真正卡住的地方是:发布方只给了 MD5,而你所在团队的安全策略要求 SHA-256。就算转换真的可行,转换出来的东西也证明不了任何事:从另一个摘要推导出来的摘要,并不构成发布方对这个文件的任何声明。你需要的是发布方签署过的 SHA-256。如果对方不提供,用 SHA-256 哈希生成器 自己算一个仍然有价值:它是一个稳定的指纹,你可以把它固定下来,在各个镜像之间比对,日后也能靠它发现文件被改过。它只是替代不了发布方的背书。
任务二:把密码库从 MD5 迁走
这是四件事里风险最高的一件,好在解法已经很成熟:先把旧哈希包一层,再在登录时对每个账号做透明升级。用一个批处理任务给所有存量 MD5 哈希套上 bcrypt,此后登录流程先对提交的密码算 MD5,再拿这个值去和 bcrypt 记录比对。
不少旧建议在这里恰好说反了。把 MD5 包进 SHA-256 并不能让密码存储变安全。 SHA-256 是快哈希,和 MD5 属于同一类问题,GPU 扫过这两种哈希的速度一样快。那些让你用数据库内置 SHA2() 函数来解决问题的建议,解的是错误的问题。目标应该是一个刻意设计得很慢的哈希:bcrypt、scrypt 或 Argon2。要存 bcrypt(md5(password)),绝不要存 sha256(md5(password))。三者之间怎么选,bcrypt vs Argon2 vs scrypt 对比 有更细的比较;Bcrypt 生成器与验证器 可以让你在定下 cost 参数之前先试几组。
「密码的 MD5」这句话本身有歧义,两种理解会得出不同的结果,迁移真正翻车的地方就在这里:
const { createHash } = require('node:crypto');
const md5Hex = createHash('md5').update('hello').digest('hex');
const md5Raw = createHash('md5').update('hello').digest(); // the same value as 16 bytes
createHash('sha256').update(md5Hex).digest('hex');
// 4914e23374bb211e3dca0df7636fefffc7fedd94f1340ae81c7d6c07b7113e9b
createHash('sha256').update(md5Raw).digest('hex');
// 88e20f0abb88153e3f0a9683668ccb5b84ed771817dc448a2b73254ed02c8d8c
createHash('sha256').update('hello').digest('hex');
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
三个结果,全都正确,全都不同。对那串 32 字符的十六进制做哈希得到一个值;对它所代表的 16 个原始字节做哈希得到另一个值;对原始密码做哈希又是第三个值。挑定一种约定,把它写下来,然后在迁移脚本和登录流程里用同一种。这里出错会一次性把所有用户锁在门外,而表现出来的症状看着像是 bcrypt 有 bug,不像是一个编码约定的问题。
对 bcrypt 包装来说,十六进制是更安全的约定。原始摘要字节里可能出现零字节,而好几个 bcrypt 实现把输入当作 C 字符串处理,读到零字节就停下。十六进制彻底避开了这个问题,而且 32 个字符远低于 bcrypt 的输入长度上限。大小写也要统一,因为 5D41402A... 和 5d41402a... 在 bcrypt 眼里是两个不同的字符串,尽管它们是同一个摘要。
const bcrypt = require('bcrypt');
const { createHash } = require('node:crypto');
const legacyDigest = (password) =>
createHash('md5').update(password, 'utf8').digest('hex'); // lowercase hex
// Batch job, run once: wrap each stored MD5, then drop the old column.
async function wrapRow(row) {
return bcrypt.hash(row.md5_hash.toLowerCase(), 12);
}
// Login path
async function verify(password, row) {
if (row.scheme === 'bcrypt') {
return bcrypt.compare(password, row.hash);
}
const ok = await bcrypt.compare(legacyDigest(password), row.hash);
if (ok) {
const upgraded = await bcrypt.hash(password, 12);
await saveCredential(row.id, { scheme: 'bcrypt', hash: upgraded }); // your DB write
}
return ok;
}
账号会随着各自的主人登录而逐步迁到纯 bcrypt。给掉队的账号定一个截止时间,过期就强制重置。
任务三:你想还原出原始输入
这件事最好挑明说,至少对自己挑明,因为它决定了该用什么工具、该抱什么预期。从摘要还原输入叫破解,不叫转换;能不能成功和摘要出自哪种算法毫无关系,只取决于输入本身。
短的、人挑出来的常见字符串很快就会失守。字典词、后面缀个数字的人名,以及任何在泄露库里出现过的内容,一次查表或一轮短暂的暴力尝试就能拿下。长的随机字符串则根本破不了,而且这两种情况之间的差距,不是多买几台机器能填平的。
彩虹表(rainbow table)常被笼统地提起。准确的说法是:彩虹表能还原某个输入,当且仅当这个输入没有加盐、并且已经在表里。两个条件缺一不可。加上按用户随机生成的盐,预计算好的表就一文不值了,因为攻击者得为每个盐单独建一张表。而如果被哈希的是一个 128 位的随机令牌,世上没有任何一张表会包含它。密码熵详解 用具体数字标出了这条界线在哪里。
如果那个输入是你自己的密码而你已经想不起来,就把这件事当成重置来处理,而不是找回。如果它属于别人,上面那段话就是能给出的全部诚实回答。
任务四:你需要用另一种算法哈希同一份数据(「md5 转 sha1 转换器」)
这和任务一是同一个问题,只是更一般。请求里是哪一对算法并不重要,因为答案永远是回到原始字节再哈希一次:
sha1sum payload.bin
sha256sum payload.bin
你能找到的每一个「md5 转 sha1 转换器」,要么是在重新哈希,要么是在查表,原因和上面讲的完全一样。如果拿不到原始数据,这件事就是无解的,而这并不是现有工具能力不足。目标算法怎么选,可以参考 SHA-1 vs SHA-256 vs SHA-512 选择指南;SHA-1 哈希生成器 是给那些至今仍要求 SHA-1 的遗留系统准备的,新系统不该再用它。
查表真正管用的那一种情况
有些类别的数据有人专门集中收集,并按各种常见摘要建好索引。恶意软件样本就是标准例子。VirusTotal 这类服务保存着样本本身,所以它们是从同一串字节算出了 MD5、SHA-1 和 SHA-256,并把三者归到同一条记录下。在那里用样本的 MD5 去搜,就能拿到它的 SHA-256。
这是一次数据库连表查询,不是转换。它之所以成立,是因为有人手里有原始文件,并用每种算法都算了一遍,也就是上文说的那个重算步骤,只不过由第三方提前做完了。条件很苛刻:对象必须已经在索引里,而且你得信任建索引的那一方。威胁情报(threat intelligence)的工作流每天都依赖这一点。但它对任意的 MD5 摘要什么都说明不了,对你自己那份数据的哈希也永远帮不上忙。
那些可疑的转换器页面用的也是这套机制。区别在于,恶意软件库会明确告诉你这是查表,并且把记录本身摊开给你看。
自己动手验一遍
要把这件事真正想通,最快的办法是看着同一段输入的两个摘要一起变化,却始终互不相干:
$ printf 'hello' | md5sum
5d41402abc4b2a76b9719d911017c592 -
$ printf 'hello' | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 -
改动其中一个字符,两个输出都会彻底改变,而且改变的方式彼此毫无关联。把同一段文本分别粘进 MD5 哈希生成器 和 SHA-256 哈希生成器 并排看,效果最直观;如果你真正需要的是给 API 请求签名的带密钥摘要,那就用 HMAC 生成器。这三个工具都完全在你的浏览器里运行。
常见问题
MD5 能转成 SHA-256 吗?
不能。MD5 摘要里不含原始输入,而 SHA-256 必须拿到那段输入才能算出任何东西。任何声称能转换的页面,要么是在对你提供的文本重新哈希,要么是在一个预计算好的对照库里查这个值。
为什么那么多网站都提供「MD5 转 SHA-256 转换器」?
因为它们能在这个人们真的会搜的词上拿到排名。你点进去之后,页面要么向你索要原文(那就成了重新哈希而不是转换),要么返回一堆没用的东西。这个说法能一直流传,是因为它背后的误解太普遍了。
SHA-256 能转回 MD5 吗?
不能,而且方向不重要。两者都是单向函数,所以在没有原始输入的情况下,任何一个摘要都变不成另一个。提供反向转换的工具,用的是同样的查表或重新哈希的把戏。
MD5 都被攻破了,那不就意味着它可以被反解吗?
不。MD5 的抗碰撞性在 2004 年被攻破,意思是可以强行构造出两段共用同一摘要的输入。而反解一个摘要属于原像攻击,已知最好的方案至今仍要 2^123.4 次运算。用于签名它已经废了,但依然无法求逆。
怎么把一整个 MD5 密码哈希库迁移到更安全的方案?
在 MySQL 里,UPDATE users SET password = SHA2(password, 256) 这种一行搞定的写法是错的,它哈希的是已经存进去的 MD5 摘要而不是密码;正确做法仍是存 bcrypt(md5(password)),并通过同一层包装来验证,在每个账号下次登录成功时逐条升级。把 MD5 包进 SHA-256 没有用,因为两者都是快哈希,GPU 破解这两种哈希的速度一样快。
为什么双重哈希的结果和我预期的不一样?
对 MD5 的十六进制字符串做哈希,和对它的 16 个原始字节做哈希,得到的 SHA-256 输出不同。两个都是有效的,它们只是不同的输入而已。挑定一种约定,在迁移脚本和登录流程里一模一样地执行。
彩虹表能还原 MD5 哈希背后的输入吗?
只有在输入未加盐、且已经在表里的时候才行,实践中这意味着短字符串或常见字符串。长的随机输入没法用这种方式还原,而给每个用户加上独立的盐,可以彻底废掉预计算表。
有没有 MD5 转 SHA-1 转换器?
没有。同一条不可逆性对 MD5 转 SHA-1 和 MD5 转 SHA-256 一样成立,所以自称的转换器要么拿你贴进去的原文重新哈希,要么在预计算的对照表里查。原始输入还在,用 SHA-1 生成器 重算一遍即可;输入没了,谁也算不出来。