想象你是一家餐厅的主厨。每天后厨都会收到千奇百怪的食材——一根葱、半头牛、一桶汤,甚至一张写满字的纸条。你没有时间一一记住它们,于是发明了一台“终极料理机”:不管塞进去什么,它都会在眨眼间把食材打碎、混合、压缩,最后吐出一颗大小完全一样的“浓缩精华丸”。丸子就是食材的指纹:换一根葱,整颗丸子的味道都翻天覆地;而且没有任何人能从丸子还原出原来的食材。这台料理机,就是密码学里的哈希函数;这颗精华丸,就是 SHA 家族的“摘要”。本文将带你拆开这台机器:看它如何用流水线(Merkle-Damgard)消化大块食材,如何用 64 轮搅拌(SHA-256)打出 256 位丸子,又如何用一块魔法海绵(SHA-3)重塑工艺,以及“为什么没人能造出两颗同味丸子”背后那套严密的数学防线。

岗位关联

对 AI 从业者而言,哈希是数据管线里的“信任地基”:训练集去重、模型权重校验、隐私计算中的参数验真,全靠这一颗精华丸。看懂 SHA,你才真正理解 AI 工程中无处不在的“不可篡改”从何而来。

目录
  1. 可视化演示
  2. 哈希函数概述
  3. Merkle-Damgard结构
  4. SHA-256算法
  5. SHA-3海绵构造
  6. 安全特性
  7. 常见攻击
  8. 工程应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

哈希函数将任意长度输入映射为固定长度摘要,SHA-2与SHA-3是主流标准。本文详解Merkle-Damgar... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

哈希函数基本:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

哈希函数概述

回到那间后厨。你的料理机有个神奇属性:它不看食材贵贱,一根葱和一整头牛进去,出来的丸子不仅大小一样,味道也完全无法预测——你没法从丸子的味道反推“这到底是不是牛肉”。更妙的是,只要食材里掺了一粒沙子,出来的丸子就会彻底变味。这台机器就是哈希函数 H,丸子就是摘要 h。

哈希函数的正式定义可以浓缩成一句话:把任意长度的二进制输入,映射为一个固定长度的二进制输出。SHA-256 名字里的 256,指的就是输出恒为 256 位(32 字节)。无论你哈希一个字符还是一部 4K 电影,结果都是 32 字节。这个“固定长度”是一切工程价值的起点——它让复杂度不同的东西有了可比较、可存储的“数字指纹”。

除了定长输出,哈希函数还须满足三个工程特性:确定性(同一输入永远同一输出)、单向性(由摘要反推输入在计算上不可行)、雪崩效应(输入翻转 1 位,输出约一半比特翻转)。正式地,SHA-256 定义了一个从全体二进制串到 256 位空间的映射:

所以,一张照片、一段音频、一份合同,进了这台料理机,出来的都是 32 字节的浓缩精华丸。后面你会发现,所有关于 SHA 的秘密——它怎么消化大块头、怎么防伪造、怎么在几毫秒内从天量输入里给出一颗独一无二的丸子——都藏在这台机器的内部构造里。

H:{0,1}∗→{0,1}256\displaystyle H: \{0,1\}^* \to \{0,1\}^{256}
左边是“任意食材”,右边是“固定 256 位丸子”,箭头就是那台料理机的研磨动作。
  1. 算子 H哈希函数本体,负责把输入“研磨”成输出。
    H\displaystyle H
  2. 映射箭头表示确定性的变换方向——输入决定输出,反之不行。
    →\displaystyle \to
  3. 任意长度集合所有有限长二进制串组成的空间,对应“任意食材”。
    {0,1}∗\displaystyle \{0,1\}^*
  4. 固定长度集合恰好 256 位二进制串的空间,共 2^256 种可能,对应“固定大小的丸子”。
    {0,1}256\displaystyle \{0,1\}^{256}

Merkle-Damgard结构

料理机虽强,胃口却有限——它一次只能吞下一小碗(512 位)。面对整头牛怎么办?大厨的答案是流水线:先把牛肉切成标准大小的薄片(分块),然后一片一片喂。关键诀窍是:上一片榨出的“高汤”绝不倒掉,而是和下一片一起下锅。这样每一片都背负着前面所有片的味道,最后的高汤浓缩了整头牛的精髓。

这就是 Merkle-Damgard(MD)结构,SHA-1 和 SHA-2 家族的共同骨架。它分三步:填充(padding)、分块、迭代压缩。填充规则很讲究:消息末尾先补一个 1,再补 0,直到长度对 512 取模等于 448,最后 64 位用大端序写入原始消息的比特长度。这 64 位“长度支票”是防止歧义的关键——它让“1“和”10”这样的消息在填充后绝不会撞车。

填充完成后,消息被切成 k 个 512 位块 M1..Mk。迭代从初始向量 IV 出发,每一步都把上一块的摘要(即链接值)和当前消息块一起喂给压缩函数 f,像接力一样跑完整个链条:

流水线结构优雅,却留下一个隐患:摘要只依赖“最后状态”,不记录消息总长。就像接力的高汤不贴标签,别人若知道汤味和熬制锅数,就能在你不知情时“续熬”出后续的汤——这就是后文要讲的长度扩展攻击。也正因如此,2015 年标准委员会决定给厨房换一台全新设备:海绵。

hi=f(hi−1,Mi),h0=IV,SHA-256(M)=hk\displaystyle h_i = f(h_{i-1}, M_i),\quad h_0 = IV,\quad \text{SHA-256}(M) = h_k
链接值像接力棒:第一棒从固定的 IV 起跑,每经过一个消息块就换一棒,最后一棒就是整条消息的摘要。
  1. 链接值 h_i第 i 块处理完后的中间摘要,相当于跑完第 i 棒的“接力棒”。
    hi\displaystyle h_i
  2. 压缩函数 f一次搅拌动作,把两个输入(旧状态 + 消息块)压成一个新状态。
    f\displaystyle f
  3. 上一状态 h_{i-1}携带前面所有消息块的混合信息。
    hi−1\displaystyle h_{i-1}
  4. 消息块 M_i第 i 个 512 位分块。
    Mi\displaystyle M_i
  5. 初始向量 IV第一棒的起点,SHA-256 的 IV 是固定的 8 个 32 位常数。
    h0=IV\displaystyle h_0 = IV
  6. 最终摘要处理完最后一块,链接值即为整条消息的 256 位摘要。
    hk\displaystyle h_k

SHA-256算法

现在把料理机的“搅拌程序”拆开。SHA-256 的压缩函数 f 堪称一部精密食谱:桌面上摆着 8 个碗(工作寄存器 a~h,各装 32 位),旁边是 64 罐“秘制香料”(轮常量 K_t),还有一套“提炼工序”——消息调度。16 块基础食材(16 个 32 位字)在工序中提炼成 64 份调味汁 W_t,每轮取一份下锅。

这些常数的来历很有意思:IV 取自前 8 个素数的平方根小数部分,K_t 取自前 64 个素数的立方根小数部分。素数开方的小数毫无规律,相当于把“随机性”钉死进机器里,防止设计者藏后门。先看提炼工序:对 t 从 16 到 63,每个调度字由四个旧字混合而成:

正式搅拌时,每轮把当前调度字和香料加入两个非线性混合函数。左侧的 Ch 是“选择函数”:e 当裁判,e 为 1 取 f,e 为 0 取 g;Maj 是“多数函数”:a、b、c 三票取多数。两者的输出再与旋转混合,构成一轮的核心:

64 轮搅拌下来,8 个寄存器里装满了整块消息的“味道”,任何一丁点食材差异都会在轮与轮之间被 Ch、Maj 和旋转操作反复放大,最终体现在约一半的输出比特上。这就是为什么我们的精华丸如此敏感——不是机器刁钻,而是配方设计得滴水不漏。

Wt=σ1(Wt−2)+Wt−7+σ0(Wt−15)+Wt−16\displaystyle W_t = \sigma_1(W_{t-2}) + W_{t-7} + \sigma_0(W_{t-15}) + W_{t-16}
T1 负责“尝鲜”:用右侧三个寄存器、香料和调味汁算出第一路新味;T2 负责“调和”:用左侧三个寄存器算出第二路新味;两者相加更新寄存器 a,其余寄存器依次左移。
  1. 选择函数 Che 当裁判——e 为 1 取 f,e 为 0 取 g,像“听左边还是听右边”。
    Ch(e,f,g)=(e∧f)⊕(¬e∧g)\displaystyle \mathrm{Ch}(e,f,g) = (e \land f) \oplus (\lnot e \land g)
  2. 多数函数 Maja、b、c 三票取多数,像“少数服从多数”。
    Maj(a,b,c)=(a∧b)⊕(a∧c)⊕(b∧c)\displaystyle \mathrm{Maj}(a,b,c) = (a \land b) \oplus (a \land c) \oplus (b \land c)
  3. 大旋转 Σ132 位字循环右移 6、11、25 位后异或,让比特“满碗乱窜”。
    Σ1(e)=ROTR6(e)⊕ROTR11(e)⊕ROTR25(e)\displaystyle \Sigma_1(e) = \mathrm{ROTR}^{6}(e) \oplus \mathrm{ROTR}^{11}(e) \oplus \mathrm{ROTR}^{25}(e)
  4. 第一路混合 T1把右侧信息、香料、调味汁全部拌在一起。
    T1=h+Σ1(e)+Ch(e,f,g)+Kt+Wt\displaystyle T_1 = h + \Sigma_1(e) + \mathrm{Ch}(e,f,g) + K_t + W_t
  5. 第二路混合 T2把左侧信息拌在一起。
    T2=Σ0(a)+Maj(a,b,c)\displaystyle T_2 = \Sigma_0(a) + \mathrm{Maj}(a,b,c)
  6. 寄存器更新新的 a 等于两路之和,其余寄存器整体右移一位。
    a=T1+T2\displaystyle a = T_1 + T_2

SHA-3海绵构造

2015 年,厨房迎来新设备:一块魔法海绵。它的哲学与流水线截然不同——不切块、不接力,而是先把所有食材汤汁一口气“吸”进海绵里,再由厨师用力一挤,挤出想要的分量。吸多少由胃口定,挤多少由需求定,灵活又干净。

这就是海绵构造(sponge construction),SHA-3(Keccak)的核心。海绵的状态是一个 1600 位的矩阵,分成顶部 r 位(海绵头,rate)和底部 c 位(海绵肚,capacity)。参数 c 是安全性的命门:c 越大越安全,但速度越慢。以 SHA3-256 为例,r=1088,c=512,输出 256 位。

处理分两阶段。吸收阶段:把填充后的消息切成 r 位小块,每块先与海绵头的当前值异或,再对整个状态施加 Keccak-f 置换。挤压阶段:反复取海绵头 r 位作为输出,每取一次再跑一次置换,直到凑够摘要长度。形式化写就是:

Keccak-f 本身也是“配方”:状态被排成 5×5 的 lane 矩阵,每 lane 64 位,共 1600 位。每一轮依次做五种运算:θ(列间线性混合)、ρ(每 lane 循环移位)、π(lane 位置重排)、χ(唯一非线性步骤,行内用非门与异或打乱)、ι(异或轮常量)。24 轮下来,状态里每一个比特都间接依赖了全部输入。

海绵有个流水线羡慕不来的优点:它天然免疫长度扩展攻击——因为摘要不仅由最后状态决定,还和挤压的次数(即输出长度)绑定。就像挤海绵,挤出的每一滴都是“现挤现装”,不存在“续杯”的后门。这也解释了为什么 SHA-3 能和 SHA-2 长期并存:不是更好,而是另一种同样可靠的安全哲学。

S←f(S⊕(Pi∥0c)),z=S[0:r]\displaystyle S \leftarrow f\big(S \oplus (P_i \| 0^c)\big), \quad z = S_{[0:r]}
左边是吸收:消息块按进海绵头,再用 Keccak-f 揉一遍;右边是挤压:从头取 r 位作为输出,取完再揉,直到摘要够长。
  1. 异或按入消息块 P_i 与海绵头逐位异或后“按”进状态——像水渗进海绵。
    S⊕(Pi∥0c)\displaystyle S \oplus (P_i \| 0^c)
  2. 补零保护容量区补 c 个零,保证消息永远只能影响海绵头。
    0c\displaystyle 0^c
  3. 置换 fKeccak-f,把整个 1600 位状态打乱 24 轮。
    S←f(⋯ )\displaystyle S \leftarrow f(\cdots)
  4. 切片输出挤压时取前 r 位作为输出块 z。
    z=S[0:r]\displaystyle z = S_{[0:r]}

安全特性

精华丸为什么值得信赖?因为料理机给了它三重保险。第一重:任何人拿到丸子都无法还原食材(抗原像);第二重:给你一块食材,你无法做出另一块味道一模一样的(抗第二原像);第三重:你无法找到两块不同食材打出同味丸子(抗碰撞)。这三条就是哈希函数安全的基石。

三者的难度天差地别。抗原像是最强的承诺:n 位摘要意味着要暴力尝试 2^n 次才能反推。抗碰撞则因“生日悖论”而脆弱得多:在 365 个生日里找两个同生日的人,只需 23 人;在 2^n 个摘要里找一对碰撞,只需约 2^(n/2) 次尝试。这就是为什么 SHA-256 的碰撞界是 2^128,而不是 2^256:

第三重保障藏在雪崩效应里。哈希设计有一条黄金法则:输入翻转 1 位,输出应有一半比特翻转。这不是玄学,而是压缩函数里旋转、异或、非线性替换层层放大的结果。你的文件在传输中哪怕损坏了 1 个字节,校验时摘要都会面目全非——这让我们能快速、可靠地发现篡改。

三重保险各司其职:抗原像保护“食材秘方”(口令不能反推),抗碰撞保护“身份唯一”(合同不能抵赖),雪崩保护“内容完整”(篡改必现形)。理解了这三重保险,你就理解了为什么整个互联网——从 HTTPS 证书到区块链——都把信任押在这样一颗小小丸子上。

O(2n)(preimage),O(2n/2)(collision)\displaystyle \mathcal{O}(2^n) \quad(\text{preimage}), \qquad \mathcal{O}(2^{n/2}) \quad(\text{collision})
抗原像的攻击成本与摘要空间等量齐观,抗碰撞则被生日悖论砍半——所以密码学里更担心碰撞。
  1. 抗原像成本已知摘要反推消息,随机乱试的期望次数。
    O(2n)\displaystyle \mathcal{O}(2^n)
  2. 摘要空间规模n 位摘要共有 2^n 种取值,n=256 时约 10^77。
    2n\displaystyle 2^n
  3. 碰撞成本生日悖论下找碰撞的期望尝试次数。
    O(2n/2)\displaystyle \mathcal{O}(2^{n/2})
  4. 生日界n=256 时约 2^128,比 2^256 低得多但仍不可行。
    2n/2\displaystyle 2^{n/2}

常见攻击

有保险就有贼。窃贼偷丸子的手段,恰好暴露了哈希算法的软肋。第一种叫生日攻击:贼不偷你的丸子,而是自己批量生产“假食材”,利用生日悖论守株待兔,等着两颗丸子意外相撞——一旦撞上,他就能用假食材骗过校验系统。

生日攻击是把双刃剑:它既是最经典的破解思路,也是密码学家设定安全参数时的“尺子”。对 n 位摘要,碰撞成本约为 2^(n/2),所以 SHA-256 定在 128 位安全级别;而老旧的 MD5 只有 128 位摘要,碰撞成本 2^64——早在上世纪就被攻破。

第二种叫长度扩展攻击:MD 结构的摘要就是“最后一口高汤”。贼只要知道 H(m) 和消息长度,就能在不知道 m 的情况下计算出 H(m ‖ padding ‖ 附加数据)——相当于偷喝了高汤还悄悄续炖了一锅。修复办法有两个:要么用 SHA-3 海绵,要么用 HMAC 把密钥包在哈希外面。第三种是历史教训:SHA-1 的 160 位摘要被 Google 用 SHAttered 攻击在约 2^63 次内找到真实碰撞,2017 年正式退役——这说明“结构相同只是短一点”的想法有多危险。

回顾这几种偷丸子的姿势,你会发现安全不是“够长就行”:结构缺陷(MD 的续炖后门)、参数不足(MD5/SHA-1)、概率杠杆(生日悖论),每一个都在提醒我们——料理机的配方需要不断更新,而 SHA-3 正是这场攻防战的当代答案。

Tcollision≈2n=2 n/2\displaystyle T_{\text{collision}} \approx \sqrt{2^n} = 2^{\,n/2}
找碰撞的尝试次数是摘要空间的平方根——生日悖论是背后的“概率杠杆”。
  1. 碰撞成本 T期望尝试多少次才能找到一对碰撞。
    Tcollision\displaystyle T_{\text{collision}}
  2. 摘要空间n 位摘要的总可能数。
    2n\displaystyle 2^n
  3. 平方根生日悖论把遍历次数从 2^n 压到 2^(n/2)。
    2n\displaystyle \sqrt{2^n}

工程应用

精华丸最终要端上桌,它在后厨有四个高频用途。第一,存口令:餐厅不保存顾客的“口味档案”原文,只存“口味丸子”。用户登录时现榨一颗,对得上就放行——即使数据库被拖库,贼拿到的也只是一堆无法还原的丸子。

第二,验完整性:下载大文件后比对 sha256sum,像验货时掂一掂丸子的重量;CDN 缓存、容器镜像、AI 模型权重,全都靠这颗丸子防篡改。第三,数字签名:先给消息榨丸子,再给丸子盖私钥章——签名的是“摘要”而非整个消息,省时省力又合法。第四,Merkle 树:把海量数据的丸子两两配对上榨,层层聚合出一棵“丸子树”,树根就是整批数据的总指纹:

工程上还有两个讲究。一是防彩虹表:直接存 SHA-256(m) 等于把丸子和食材一一对应写进字典,所以口令存储要用“盐 + 慢哈希”(如 bcrypt、argon2)——盐让字典失效,慢让暴力变慢。二是警惕“快速哈希”:SHA-256 快是优点也是缺点,对攻击者同样是加速器,所以安全场景从不只靠裸哈希。

从一根葱到一颗丸子,从流水线到魔法海绵,SHA 的旅程其实是一道朴素的生活题:如何在不可信的世界里快速建立“不可抵赖、不可篡改、不可还原”的信任。你后厨里的那台料理机,正是现代数字世界的信任地基——现在,你已经知道它每一颗齿轮是如何转动的了。

root=H(H(L1) ∥ H(L2))\displaystyle root = H\big(H(L_1) \,\|\, H(L_2)\big)
四个叶子两两配对,先各自榨丸子、拼接后再榨一次,得到父节点;逐层向上,最终树根浓缩了整棵树上所有数据。
  1. 叶子摘要左侧数据 L1 的摘要。
    H(L1)\displaystyle H(L_1)
  2. 拼接两个摘要首尾相连,得到一个 512 位串。
    H(L1) ∥ H(L2)\displaystyle H(L_1) \,\|\, H(L_2)
  3. 父节点对拼接结果再哈希,得到上一层节点。
    H(H(L1) ∥ H(L2))\displaystyle H\big(H(L_1) \,\|\, H(L_2)\big)
  4. 树根逐层聚合到顶部,root 即全树信任锚点。
    root\displaystyle root

AI 应用场景

训练数据去重与污染检测

阶段:数据准备与治理

解决的问题:LLM 训练集动辄 TB 级,网页抓取数据高度重复、且可能被投毒。逐条比对原文成本极高。用哈希给每一条样本打“指纹丸子”,同一文本的哈希一致,可 O(1) 去重;再对已知恶意样本库取哈希,即可在训练前拦截数据投毒。

具体案例:某大模型团队从 Common Crawl 抓取 50 亿网页,先用 sha256 对 URL 加正文分片去重,把训练集压缩 40%;再用公开毒样本哈希黑名单过滤,阻止了一批刷榜注入文本进入预训练语料。

模型权重分发与供应链安全

阶段:模型部署与 MLOps

解决的问题:开源大模型权重体积大、下载源多,传输中被篡改会导致模型后门。发布方公布 sha256 校验和,部署方下载后本地计算摘要并比对,秒级确认权重与官方一致——就像收到食材先对一对丸子的味道。

具体案例:某公司内网部署私有化大模型,MLOps 流水线在拉取权重后强制校验 sha256,并在模型 registry 里记录摘要;一次供应链投毒事件中,校验环节拦截了被替换的权重文件,避免了一次潜伏后门事故。

联邦学习中的参数验真

阶段:联邦学习聚合

解决的问题:联邦学习中各客户端上传模型更新,恶意客户端可能伪造梯度。服务端对梯度做哈希登记,聚合前校验摘要,防止中间人篡改与重放攻击。

具体案例:医疗影像联邦学习项目中,每轮各医院上传模型参数前先对参数张量计算 SHA-256 摘要并附带在消息中,服务端验真后再进入安全聚合,确保聚合结果可信。

Python 演示

python53 行
import hashlibimport struct  def md_pad(msg):    """SHA-256 填充, 遵循 Merkle-Damgard 规范."""    m = bytearray(msg)    bit_len = len(m) * 8    m.append(0x80)    while len(m) % 64 != 56:        m.append(0)    m += struct.pack(">Q", bit_len)    return bytes(m)  def chain_digest(msg):    """用真实 SHA-256 模拟 MD 迭代, 观察链接值传递."""    blocks = md_pad(msg)    h = b"\x00" * 32    for i in range(0, len(blocks), 64):        h = hashlib.sha256(h + blocks[i:i + 64]).digest()    return h.hex()  def diff_bits(a, b):    return sum(bin(x ^ y).count("1") for x, y in zip(a, b))  def birthday_collision(bits, limit=200000):    """生日攻击演示: 在截断 bits 位摘要中找碰撞."""    seen = {}    mask = (1 << bits) - 1    for i in range(limit):        m = str(i).encode()        h = int.from_bytes(hashlib.sha256(m).digest()[:4], "big") & mask        if h in seen and seen[h] != m:            return seen[h], m        seen[h] = m    return None  if __name__ == "__main__":    msg = b"The secret recipe is love"    flipped = bytearray(msg)    flipped[0] ^= 1    print("原文摘要 :", hashlib.sha256(msg).hexdigest())    print("翻转1位  :", hashlib.sha256(bytes(flipped)).hexdigest())    print("雪崩差异 :", diff_bits(hashlib.sha256(msg).digest(),                                  hashlib.sha256(bytes(flipped)).digest()), "/ 256")    print("MD分块   :", len(md_pad(msg)) // 64, "块")    print("链接链   :", chain_digest(msg))    hit = birthday_collision(16)    print("16位碰撞 :", hit)
目的:演示 SHA-256 的 Merkle-Damgard 填充逻辑、链接值链式传递、雪崩效应,以及生日攻击找碰撞的完整过程。
  1. m.append(0x80)补 1 位作为消息结束标记,对应“食材切块的收尾刀”。
  2. while len(m) % 64 != 56:补 0 直到距块边界差 8 字节,留出长度字段的位置。
  3. m += struct.pack(">Q", bit_len)用大端序写入 64 位原始长度,这是防歧义的“长度支票”。
  4. h = hashlib.sha256(h + blocks[i:i + 64]).digest()上一块的摘要与当前块拼接后再次哈希,模拟链接值接力。
  5. flipped[0] ^= 1翻转第一个字节的最低比特,制造“一颗沙子的差异”。
  6. bin(x ^ y).count("1")统计两个摘要的差异比特数,验证雪崩效应。
  7. mask = (1 << bits) - 1构造截断掩码,把 256 位摘要缩到 bits 位小空间。
  8. if h in seen and seen[h] != m:命中“两食材同丸子”的碰撞判定。

常见误区

误区我经常看到密码“加密”后存进数据库,那哈希应该也是加密的一种吧?既然能加密,那管理员应该能用密钥把密码解出来呀。

正解哈希和加密是两回事。加密是“可逆变换”,有密钥就能还原;哈希是“单向压缩”,根本没有密钥这回事。回到厨房:精华丸是食材被打碎重组后的产物,物理上无法还原成牛排。SHA-256 的设计目标之一就是让反推在计算上不可行。工程上把哈希叫“摘要”而不叫“密文”,正是为了强调这一点。

为何常见:两者都输出“看不懂的乱码”,且常出现在同一张安全流程图里,初学者容易把“变换”简单归为“加密”。

误区SHA-256 有 256 位,找碰撞不就是要试 2^256 次吗?这个数字比宇宙原子还多,所以绝对安全。

正解生日悖论把碰撞成本从 2^256 砍到 2^128。就像一个班级里找两个同生日的人,不需要 365 人,23 人就够了。2^128 虽然仍不可行,但和 2^256 是数量级的差距。更要命的是算法缺陷会进一步降低成本——SHA-1 只有 160 位,实际碰撞成本被压到约 2^63,2017 年被真正攻破。安全级别永远要看“有效攻击成本”,而不是摘要位数。

为何常见:直觉把“查找”当成“遍历整个空间”,忽略了概率的杠杆效应。

误区校验和都是这样的吧,一个字节坏了,对应的那一位校验码就变一下。哈希应该也类似,改 1 位就变 1 位。

正解恰恰相反,哈希追求的是雪崩效应:输入翻转 1 位,输出应当有约一半比特翻转。SHA-256 下就是 256 位中约 128 位变化。这像往一锅汤里加一粒盐,整锅汤的“味道”都变了,而不是只影响某一粒盐所在的位置。雪崩越彻底,攻击者越难以“控制”摘要,也就越难制造碰撞。

为何常见:人们容易用 CRC 校验和的“线性”直觉去套哈希,忽略了密码学哈希刻意设计的非线性扩散。

误区都是哈希嘛,MD5 是 128 位,SHA-1 是 160 位,SHA-256 是 256 位,换个更长的就行了,反正原理一样。

正解输出长度只是参数,结构安全才是根本。MD5 和 SHA-1 与 SHA-256 同属 MD 家族,但 MD5 的碰撞成本降到 2^24 以内(2004 年已被攻破),SHA-1 也在 2017 年被 SHAttered 攻破。而 SHA-3 连结构都换成了海绵构造——它和 SHA-2 是“两种哲学”,不是“长一点的版本”。工程上,校验完整性时可以容忍弱哈希,但签名、证书等安全场景必须用 SHA-256 或 SHA-3。

为何常见:对外只呈现“一长串十六进制”,用户看不到内部结构差异,容易把哈希函数当成可互换的零件。

误区既然哈希不可逆,那我存 SHA-256(密码) 就好了,数据库泄露了攻击者也解不出密码。

正解裸哈希有两个漏洞:一是彩虹表——攻击者预先把常见密码的哈希算好存成字典,查表即得;二是速度——SHA-256 极快,GPU 每秒可算数十亿次,暴力破解 8 位纯数字密码不过是几分钟的事。正确的做法是加盐(每个用户随机盐)并选用慢哈希算法(bcrypt、scrypt、argon2),把单次验证成本拉高到毫秒级,让暴力破解的成本高到不划算。

为何常见:教程常说“只存哈希不存明文”,但没说“快哈希对攻击者也快”这个反直觉事实。

面试题

Q1. SHA-256 输出的摘要长度是多少?

单选题 · easy

  • A.128 位
  • B.160 位
  • C.256 位← 正确答案
  • D.512 位

答案:C。SHA-256 名中的 256 即摘要位数,等于 32 字节。128 位是 MD5,160 位是 SHA-1,512 位是 SHA-512。就像料理机固定吐出一颗 32 字节的精华丸,与食材大小无关。

Q2. 下列哪一项是哈希函数最核心的特性?

单选题 · easy

  • A.可逆性
  • B.单向性← 正确答案
  • C.需要密钥
  • D.输出可预测

答案:B。单向性(抗原像)是哈希区别于加密的本质:精华丸无法还原成食材。可逆的是加密而非哈希,哈希无需密钥,输出应不可预测(雪崩效应)。

Q3. 关于雪崩效应,下列说法正确的是?

单选题 · mid

  • A.输入翻转 1 位,摘要恰好翻转 1 位
  • B.输入翻转 1 位,摘要约一半比特翻转← 正确答案
  • C.输入不变,摘要随机变化
  • D.输入翻转 1 位,摘要全部变为 0

答案:B。雪崩效应要求输入微小变化导致输出约一半比特翻转,SHA-256 下约 128 位变化。这保证攻击者无法通过控制输入来预测或碰撞摘要。

Q4. 在 Merkle-Damgard 结构中,链接变量 h_i 的作用是?

单选题 · mid

  • A.记录消息总长度
  • B.把上一消息块的摘要传给下一块,形成链式依赖← 正确答案
  • C.充当加密密钥
  • D.填充随机噪声

答案:B。链接值像接力棒,h_i = f(h_{i-1}, M_i),把前面所有块的信息层层传递。消息长度由填充阶段的 64 位字段记录,密钥与哈希无关。

Q5. 以下哪些是 SHA-3 海绵构造的特点?

多选题 · mid

  • A.分为吸收与挤压两个阶段← 正确答案
  • B.核心是 Keccak-f 置换← 正确答案
  • C.采用 Merkle-Damgard 迭代
  • D.天然抵抗长度扩展攻击← 正确答案

答案:ABD。海绵构造采用吸收-挤压两阶段,核心置换 Keccak-f,且天然免疫 MD 结构的长度扩展攻击。选项 C 是 SHA-1/SHA-2 家族的结构,与 SHA-3 相反。

Q6. 对 SHA-256 实施生日攻击,期望的碰撞尝试次数约为?

单选题 · mid

  • A.2^64
  • B.2^128← 正确答案
  • C.2^256
  • D.2^512

答案:B。生日悖论把碰撞成本压到 2^(n/2),n=256 时为 2^128。2^64 是 SHA-1 时代的碰撞界,2^256 是抗原像的暴力成本。

Q7. 关于长度扩展攻击,下列说法正确的是?

多选题 · hard

  • A.可利用 SHA-256 等 MD 结构哈希← 正确答案
  • B.需要知道原消息摘要和消息长度← 正确答案
  • C.SHA-3 海绵构造天然免疫该攻击← 正确答案
  • D.需要知道原消息的完整内容

答案:ABC。长度扩展攻击利用 MD 结构“末状态即摘要”的特性,已知 H(m) 和长度即可伪造 H(m ‖ padding ‖ extra),无需知道 m;海绵构造输出与挤压次数绑定,免疫此攻击。D 错误。

Q8. 2017 年 SHA-1 被正式宣布退役(SHAttered)的直接原因是?

单选题 · hard

  • A.输出长度仅 96 位
  • B.研究者找到了真实的碰撞样本← 正确答案
  • C.私钥被大规模泄露
  • D.量子计算机已可破解

答案:B。Google 与 CWI 联合发布了两个内容不同但 SHA-1 摘要相同的 PDF 文件,碰撞成本被压到约 2^63。这证明“同族缩短版”并不安全,推动浏览器全面弃用 SHA-1 证书。

Q9. 工程上存储用户口令,推荐的做法是?

单选题 · easy

  • A.明文存储到数据库
  • B.直接存 SHA-256(口令)
  • C.加盐并使用 bcrypt 等慢哈希← 正确答案
  • D.用固定密钥加密口令

答案:C。慢哈希加盐能同时抵御彩虹表和 GPU 暴力破解。裸 SHA-256 太快、无盐易被预计算,固定密钥加密一旦泄露全部还原,明文存储则完全不可接受。

Q10. 关于 Keccak-f 置换,正确的说法有?

多选题 · extreme

  • A.状态总大小为 1600 位← 正确答案
  • B.状态排成 5×5 的 lane 矩阵← 正确答案
  • C.每一轮包含 θ、ρ、π、χ、ι 五种运算← 正确答案
  • D.χ 是线性运算

答案:ABC。Keccak-f 在 1600 位状态(25 个 64 位 lane)上迭代 24 轮,每轮五种运算;其中 χ 是唯一的非线性步骤,负责提供抗攻击的关键混淆。D 错误。