哈希:为什么改一个字母,会得到完全不同的乱码
哈希函数是一台把任意输入搅成固定长度指纹的确定性搅拌机。它刻意让输入的每一比特都影响输出的每一比特——所以改一个字母,不是"变一点点",而是整体重排。
一句话结论
哈希函数是一台确定性的、定长的、不可逆的搅拌机:
- 同样的输入,永远得到同样的输出(确定性);
- 不管喂进去一个字还是一部电影,都只吐出 256 个 0 和 1(定长);
- 而且它被刻意设计成:输入改动一位,输出改动一半(雪崩效应)。
第三条不是副作用,它是设计目标。“改一个字母得到完全不同的乱码”,原因全在这里。
直觉铺垫:一台搅拌机
想象一台果汁机:
- 确定性:同样的苹果、同样的转速,打出来永远是同一个味道。“baihe.org” 这个字符串,在世界上任何一台电脑上算 SHA-256,结果都一样。
- 定长输出:不管倒进去一颗樱桃还是一整筐,机器只倒出一小杯 —— 256 个比特。这意味着信息必然丢失:输入有无穷多种,输出只有 种,撞车(碰撞)一定存在。这是鸽巢原理,不是设计缺陷。
- 雪崩:往果汁里多放一粒盐,整杯味道都不一样。而且不是”多了一点咸味”,是你没法从味道上判断”是不是只放了一粒盐”。
- 不可逆:给你一杯打好的果汁,你无法倒推出原来放了几个苹果、有没有放盐。
现在问关键的一句:为什么不做得温和一点?值得这么极端吗?
值得,而且必须。
假设哈希是”温和”的:改一个字母,输出只变一两个比特。那我就能这样破译:
- 猜一个输入,算出哈希,看它和目标哈希差几个比特;
- 改掉一个字母,再看差几个比特;
- 哪个改动让差距变小,就往哪个方向继续改。
这是一架可以一步步爬上去的梯子,就像” hotter / colder “那个游戏。只要输出肯告诉攻击者”你更接近了”,密码就会被爬出来。
所以哈希函数必须把梯子抽掉:无论你怎么微调输入,输出都像重新掷了一次硬币。你得不到任何”更接近了”的信号,只能从头瞎猜。
这就是雪崩效应的真实身份 —— 它不是哈希的一个特性,它是哈希之所以能当密码用的前提。
推导过程
形式化地写一句话
左边的 表示”任意长度”,右边是固定的 256 位。所以 SHA-256 的输出永远是 32 字节,写成十六进制是 64 个字符,无论你哈希的是 "a" 还是整部《红楼梦》。
SHA-256 到底在干什么
- 切块与填充:把输入切成 512 比特一块;最后一块不够就补齐,并在末尾附上原始长度。
- 初始常量:8 个 32 位工作变量,取自前 8 个质数(2、3、5、7、11、13、17、19)平方根的小数部分前 32 位(
0x6a09e667…)。为什么用这种看似随手抓来的数?为了证明没有后门:这些数不是我挑的,没人能事先在里面动手脚。密码学里叫它 nothing-up-my-sleeve number。 - 64 轮搅拌:每一轮做三件事 ——
- 非线性混合: 与 ;
- 循环移位:,;
- 模 加法:进位会自然地把低位的影响推到高位;每轮还要加一个固定常量(取自前 64 个质数立方根的小数部分)。
- 反馈:这一轮算出的 8 个变量,直接成为下一轮的输入。所以任何一点差异,会被一轮一轮反复放大。
香农在 1949 年就给这两样武器起了名字:混淆(confusion) 让输入与输出的关系复杂到看不懂,扩散(diffusion) 让一个比特的影响铺满整个输出。SHA-256 的 64 轮,就是把这两件事重复 64 次。
为什么非要非线性不可
假设哈希是线性的:每个输出位都是若干输入位的异或组合。那么 256 个输出位就是 256 个线性方程 —— 你只要收集 256 组输入/输出,用高斯消元就能把整个函数解出来,然后随意构造碰撞。
所以 SHA-256 里必须塞进 、 和模加法的进位:它们不是线性的。顺便一提,异或 本身是线性运算,光靠它一个是不够的 —— 这也是为什么”把输入全部异或一遍”不能当哈希用。
单向性:请注意,它没有被证明过
必须诚实地说一句:至今没有任何哈希函数被真正”证明”不可逆。 我们对 SHA-256 的信心,来自”全世界的聪明人找了几十年,还没找到比暴力搜索更快的逆运算”。
这是经验,不是定理。所以密码学才会定期让哈希函数退役(MD5 → SHA-1 → SHA-2 → SHA-3),不是因为旧函数被证明有洞,而是因为攻击方法一直在进步。
交互演示
先看真实的 SHA-256。改动下面任意一个字符,数一数有多少个比特跟着翻了:
无论你改的是第一个字母还是最后一个,翻转数都稳定在 128 附近(256 的一半)。这不是巧合,这是设计指标:每一个输出比特,被翻转的概率都应该是 50%,不多不少。
那么这个”一半”是怎么长出来的?下面用一个教学用的、简化到只剩骨架的混合函数来演示。真实 SHA-256 的每一轮更强、共 64 轮,但道理完全一样:
第 1 轮时只有孤零零一个点被改写。随着轮数增加,差异向两侧蔓延,很快就铺满整行。当所有输出位都被改写、而且改写方向看起来像随机时,你就再也看不出”原文到底改了哪一位” —— 单向性就是这么来的。
常见误解
- “哈希就是加密” —— 不是。加密要能解开(有密钥),哈希故意让你解不开。所以网站应该哈希存储你的密码,而不是加密存储:它根本不需要知道你的明文,只需要在你登录时比对哈希值。遇到”客服能告诉你密码是什么”的网站,它一定是存错了。
- “哈希不会撞车” —— 会。输入无限、输出有限,鸽巢原理保证必然撞车,只是 大到撞不上而已。哈希的安全性不是”绝对不撞”,是”撞不起”。
- “SHA-256 提供 256 位安全” —— 抗碰撞只有 128 位。因为生日攻击:找碰撞不需要遍历 次,只需要约 次。这就是生日悖论的力量: 个人里有两人同生日的概率超过一半,靠的完全是同一件事。
- “MD5 只是老了一点,还能凑合用” —— 不能。MD5 的碰撞在 2004 年就被实际构造出来,SHA-1 在 2017 年被真实碰撞攻破(SHAttered)。它们现在只能用于检测传输中的意外损坏,不能用于任何安全场景。
- “加盐就是给密码加个固定字符串” —— 盐必须每个用户随机生成、各不相同。加盐防的不是”某一个人被破解”,而是”一张预计算的彩虹表打穿整个数据库”。
- “哈希值一样就说明文件没被篡改” —— 只能防意外损坏,防不住攻击者:他改完文件重新算一遍哈希贴上去就行了。要真正防篡改,需要带密钥的 HMAC,或者非对称的数字签名。
延伸引用
- 横切原理 · 熵与信息 —— 哈希是一次有损压缩,它到底丢掉了什么?
- 计算 · 从哈希到数字签名 —— 哈希 + 私钥 = 不可抵赖
- 数学 · 生日悖论:为什么 23 个人就有一半概率撞生日 —— 以及为什么答案是 而不是
- 外部:FIPS PUB 180-4,SHA-2 标准原文
本文是「白盒」的第一篇样板文。发现错误、或者觉得哪一段没讲清楚,欢迎在 GitHub 上提 PR。