安全

哈希入门

理解哈希:密码学哈希函数的特性、它与加密和编码的区别、sha256sum 与 Get-FileHash 命令,以及 MD5、SHA-1、SHA-256 的现状和口令的哈希处理。

· 4 分钟阅读 · 难度 初级

哈希是将一个给定值转换为另一个值的过程。哈希函数依据一种数学算法来生成这个新值。哈希函数的结果被称为哈希值,或简称哈希。

良好的哈希函数通常采用一种单向的哈希算法:也就是说,哈希无法被重新转换回原始值。

哈希函数产出什么

哈希函数接受任意大小的数据作为输入——一个单词、一个口令、一个数吉字节的文件——并输出一段固定长度的比特序列。这个输出在不同文档中有多种名称:哈希、指纹、摘要,或英文中的 digest。它的长度取决于所选的算法,而绝不取决于输入的大小。

SHA-256(Secure Hash Algorithm,产生 256 位的变体)始终产生 256 位,即 32 个字节。这 32 个字节几乎总是以十六进制显示,这种记法用取自 0 到 9 和 a 到 f 的两个字符来表示每个字节。因此一个 SHA-256 指纹表现为一串 64 个字符的字符串。

$ printf 'bonjour' | sha256sum
2cb4b1431b84ec15d35ed83bb927e27e8967d75f4bcd9cc4b25c8d879ae23e18  -

$ printf 'Bonjour' | sha256sum
9172e8eec99f144f72eca9a568759580edadb2cfd154857f07e657569493bc44  -

这两条命令在任何具备 sha256sum 命令的 Linux 系统上都能原样重现,常见发行版都是如此。这里使用 printf 命令而非 echo,是因为它不会添加换行符:这个多余的字符会成为被哈希数据的一部分,从而完全改变结果。输出末尾的短横线表示数据来自标准输入而非文件。

这个例子中有两点值得注意。第一:相同的输入无论在这台机器上还是另一台机器上,都会永远给出相同的输出。第二:从小写变为大写,即第一个字节中仅一个比特的差异,就产生了一个与前一个看不出任何关联的指纹。

密码学哈希函数应具备的四项特性

并非任何把数据缩减为一个短值的函数都是密码学的。CRC32(32 位的 Cyclic Redundancy Check)能很好地检测传输错误,但刻意制造两个 CRC32 相同的文件,用一台笔记本电脑就能办到。「密码学」这一限定词意味着四项特性。

确定性

相同的输入总是产生相同的指纹。正是这一点使得可以比较两个指纹,从而断定两份数据相同。要注意真正被哈希的是什么:是字节,而不是含义。以 UTF-8 编码的带重音文本,与以 ISO-8859-1 编码的同一文本,会给出两个不同的指纹;以 Windows 行尾(回车再换行)保存的文本文件,与以 Unix 行尾保存的同一文件不同。一个对不上的指纹,往往正是这样解释的。

抗原像性

给定一个指纹,要找回一份能产生该指纹的数据,必须是不可行的。这正是引言所称的单向性。不存在逆运算:已知的唯一途径是不断尝试各种输入,直到找回该指纹,对 SHA-256 而言这约需 2256 次尝试。

这一保证有一个被初学者低估的局限:它针对的是函数,而不是数据。如果输入属于一个很小或可预测的集合——一个出生日期、一个电话号码、一个电子邮件地址、一个常见口令——攻击者无需破解任何东西。他把这些候选项一一列举、哈希,再比对。对一份可猜到的数据做哈希并不能保护它。

这一特性有一个变体,即抗第二原像性:从一份已知数据出发,要构造出另一份不同的、却能产生相同指纹的数据,必须是不可行的。

抗碰撞性

碰撞是指一对具有相同指纹的不同输入。碰撞必然存在:可能的输入数量无限,而输出数量有限。因此该特性并不要求碰撞不存在,而是要求人们无法制造出碰撞。

查找一个碰撞的代价明显低于查找一个原像的代价,这是由于所谓的生日悖论:对于一个 n 位的指纹,约需 2n/2 次尝试。因此对 SHA-256 而言,抗碰撞的实际安全性约为 2128,而非 2256。正是这个减半的界限,解释了为何过短的指纹已被弃用。

雪崩效应

改动输入中的一个比特,就应当改变输出中约一半的比特,且没有可资利用的规律。上面 bonjour 和 Bonjour 的例子就说明了这一点。其实际后果很重要:无法从两个指纹的相似程度推断出任何东西。两个几乎相同的文件,其指纹完全不相似。指纹比较是一种非此即彼的回答,绝不是一种衡量接近程度的尺度。

哈希、加密、编码:三种不同的操作

这是学习之初最常见的混淆,而它对一个应用的安全有实实在在的后果。这三种操作都把一份数据转换成另一串字符,但它们用途不同,也不提供相同的保证。

  • 编码改变数据的表示形式。任何人都能将其还原,无需密钥,这正是它的目的。例如 Base64 用 64 个可打印字符来表示任意字节,以便让它们通过电子邮件或 JSON 文档。编码不提供任何保密性。
  • 加密使数据对没有密钥的人不可读,对拥有密钥的人则完全可读。按其设计,它在两个方向上都是可逆的。AES(Advanced Encryption Standard)和 ChaCha20 是加密算法。
  • 哈希对任何人都不可逆,即使是计算出该指纹的人也不行。既没有密钥,也没有解密。其输出大小固定,与输入无关。
$ printf 'bonjour' | base64
Ym9uam91cg==

$ echo 'Ym9uam91cg==' | base64 -d
bonjour

因此有两种常见说法应予摒弃。「加密的口令」并未描述任何可用的东西:如果一个口令能被找回,那就是它被加了密,而密钥被存放在某处,这又把问题归结为密钥的存储。而一个用 Base64 编码的口令就是一个明文口令,只是肉眼看起来不那么易读罢了。

在 Linux 和 Windows 下计算指纹

在 Linux 下

$ printf 'bonjour\n' > exemple.txt

$ sha256sum exemple.txt
9cec0af545144159bac85c7b908d5e0b9b0ef961497401c5ad8da26f065ad926  exemple.txt

$ md5sum exemple.txt
94baaad4d1347ec6e15ae35c88ee8bc8  exemple.txt

$ openssl dgst -sha256 exemple.txt
SHA2-256(exemple.txt)= 9cec0af545144159bac85c7b908d5e0b9b0ef961497401c5ad8da26f065ad926

GNU coreutils 软件包几乎存在于所有发行版上——基于 BusyBox 构建的系统,如 Alpine,只提供一个功能受限的等价物——它提供了 sha256sum、sha512sum、sha1sum、md5sum 和 b2sum。openssl dgst 命令涵盖更多算法,例如 openssl dgst -sha3-256。它打印的标签取决于所安装的版本:较新的版本写作 SHA2-256(...),而旧版本写作 SHA256(...)。机器上实际可用的算法列表可用 openssl dgst -list 获得。

在 Windows 下

PS> Get-FileHash .\exemple.txt

Algorithm  Hash                                                              Path
---------  ----                                                              ----
SHA256     9CEC0AF545144159BAC85C7B908D5E0B9B0EF961497401C5AD8DA26F065AD926  C:\...\exemple.txt

PS> Get-FileHash .\exemple.txt -Algorithm MD5

PowerShell 的 Get-FileHash 命令默认使用 SHA-256;-Algorithm 参数尤其接受 SHA1、SHA256、SHA384、SHA512 和 MD5。它的输出为大写,而 sha256sum 写成小写:这是同一个值,比较时只需忽略大小写即可。经典的命令行方式也有一个变体,即 certutil -hashfile exemple.txt SHA256。

一点方法上的提醒:用肉眼比较 64 个十六进制字符,或只看开头和结尾,都不构成一次验证。比较应当由机器来做,如同下一节所示。

哈希实际有什么用

验证一次下载

发布者在其文件旁附上一个指纹文件,常命名为 SHA256SUMS,其中每个文件占一行。sha256sum 命令能读回这种格式并自行完成比较,使用 -c 选项。

$ sha256sum exemple.txt > SHA256SUMS

$ sha256sum -c SHA256SUMS
exemple.txt: Réussi

所显示的词语取决于系统的语言:配置为英语的系统会打印 OK。若存在差异,命令会报告失败并返回一个非零的返回码,从而可以在脚本中使用它。

这一验证的作用范围值得理解。它证明所收到的文件确实是那个已公布指纹所对应的文件。因此它可以防范意外损坏:中断的传输、有缺陷的镜像、损坏的介质。而面对一个控制着服务器的对手,它并不足够:能替换文件的人,通常也能替换显示指纹的那个页面。正因如此,严肃的项目会发布指纹文件的电子签名,可用通过另一渠道获得的公钥来验证。

通过内容识别一个文件

指纹构成一种内容标识:由于 SHA-256 尚无已知碰撞,实践上可以断定两个名称不同、SHA-256 指纹相同的文件具有相同的内容。这一特性被版本管理器 Git 所利用——它以指纹来命名其内部对象——也被那些避免重复存储相同数据块的备份系统,以及恶意软件的指纹库所利用。

签署一份文档

电子签名并不针对文档本身,而是针对其指纹:指纹很短、大小固定、计算迅速,而签名运算则代价高昂。这种构造解释了为何抗碰撞性并非一种理论上的顾虑:如果攻击者能制造出两份指纹相同的文档,他就让人签署第一份,再拿出第二份,配上同样有效的签名。

认证一条消息

HMAC(keyed-Hash Message Authentication Code,带密钥的消息认证码)把一个哈希函数和一个密钥结合起来,产生一个既能证明消息完整性、又能证明对密钥知情的值。不应通过简单地拼接密钥和消息来即兴构造它:SHA-256 和 SHA-512 建立在所谓 Merkle-Damgård 结构之上,易受长度扩展攻击(同一家族中经截断的变体,如 SHA-384,则可幸免),该攻击使第三方能够在不知道密钥的情况下延长消息并重新计算出一个有效的指纹。HMAC 正是为阻止这种情况而设计的。SHA-3 和 BLAKE2 不存在这一弱点。

各算法在实践中的现状

MD5(Message-Digest Algorithm 5,128 位)在抗碰撞性方面已被攻破:制造两个指纹相同的输入这件事在 2004 年就已被证明,如今在一台普通机器上几秒钟就能算出。一旦对手能够影响被哈希的内容,MD5 就不应再使用:签名、更新的完整性校验、安全语境下的文件识别。

SHA-1(160 位)走上了同样的道路:一个完整碰撞于 2017 年以 SHAttered 之名公布,其形式是两个指纹相同的不同 PDF 文件,随后 2020 年又出现了选择前缀碰撞,更加接近真实的伪造场景。SHA-1 已从公共证书颁发机构的证书中移除,并被禁用于新的签名;它在一些老旧系统中仍有残留。

一点澄清可避免误解:这两种情况下,倒下的都是抗碰撞性,而非抗原像性。从一个 MD5 指纹找回数据,通过直接计算仍然遥不可及——但当数据可猜到时,这并不妨碍通过枚举将其找回,如前文所述。这一细微差别并不为使用 MD5 开脱;它只是说明什么被攻破了、什么没有。

迄今有三个家族在使用上尚无已公布的保留意见(此处为撰写之时,即 2026 年,所知研究的状况)。SHA-2 尤其包括 SHA-224、SHA-256、SHA-384 和 SHA-512,其中未知任何碰撞,SHA-256 构成完整性用途下合理的默认选择。SHA-3 源自 Keccak 算法、于 2015 年标准化,基于一种不同的、所谓海绵的构造;它并非旨在替代 SHA-2,而是提供一个不会与之共享潜在弱点的后备方案。BLAKE2 和 BLAKE3 在软件中运行迅速,尤其见于备份和去重工具;Linux 下可用的 b2sum 命令默认计算一个 512 位的 BLAKE2b。

口令这一特殊情形

为何裸用 SHA-256 并不合适

口令从不以明文存储。存储的是一个指纹,每次登录时都重新计算所输入口令的指纹,以与已记录的那个作比较。初学者的本能反应是使用 SHA-256。这会导致一个几小时就能被打开的数据库。

原因在于 SHA-256 的一项优点在此变成了缺点:它的快速。一块游戏显卡每秒计算数十亿个 SHA-256 指纹——这是 2026 年所观察到的量级,且随每一代硬件而上升。由于用户实际选择的口令集中在一个有限的候选集合上,遍历数亿个已泄露口令的列表是瞬间的事。再加上确定性带来的影响:两个口令相同的账户呈现相同的指纹,这在数据库中一目了然,直接指明了最普遍的那些口令。最后,彩虹表以浓缩形式保存了预先算好的计算链,能够以存储换计算时间来找回一个口令,而无需从头重做攻击。

盐

盐是一个随机值,每个账户各不相同,在注册时抽取。它并不保密,与指纹并排存放于同一个数据库中。哈希函数被应用于盐与口令的组合。由此产生两种效果:预计算的表变得无法使用,因为每个盐都需要一张;两个共用同一口令的账户会得到不同的指纹,从而消除了上文所述的直接读取。反之,盐并不会拖慢针对单一、特定账户的攻击。

胡椒

胡椒是一个保密值,对整个应用相同,在哈希之前同样掺入口令,但它不存储在数据库中:它驻留于应用服务器的配置中,甚至驻留于一个硬件安全模块中。如果只有数据库泄露——SQL 注入的常见情形——攻击者便不具备测试其候选项所需的要素。其代价在于运维层面:更改胡椒会使所有现存指纹失效,因此需要预先规划如何轮换它。胡椒是一道额外的防线,绝非盐或某个适配函数的替代品。

专用函数

对快速这一问题的正确回应,不是拼凑出 SHA-256 的重复计算,而是采用一个为存储口令而设计、其计算代价可调的函数。

  • bcrypt 派生自 Blowfish 加密算法,由一个代价因子来调节,每递增一次就使计算时间翻倍。需要知道的一个特点:它只考虑输入的前 72 个字节。
  • scrypt 被设计为在内存和计算时间上都代价高昂,以妨碍在显卡或专用电路上进行的攻击,对后者而言内存是稀缺资源。
  • Argon2 于 2015 年赢得了 Password Hashing Competition。它有三种变体:Argon2d、Argon2i 和 Argon2id;默认推荐的是 Argon2id。可调节三个参数:所用内存、遍数以及并行度。
  • PBKDF2(Password-Based Key Derivation Function 2)较为古老,基于对一个带密钥的伪随机函数(实践中为 HMAC)的重复。它对专用硬件的抵抗不如前几者,但仍为某些合规规范所要求。

参数的设定取决于硬件和负载:不存在一个通用的取值,而从一篇文章照抄来的数字会很快过时。可行的规则是在目标服务器上进行测量,取应用在连接高峰时所能承受的最高代价,随后定期重新评估这一测量结果。对所有这些函数都适用的两点是:使用语言标准库所提供的实现,而不要自己去写一个;以及用一个恒定时间的比较函数来比较指纹,以免响应时长向攻击者透露信息。

注意事项与后续学习

日常使用需记住的命令仅寥寥数行:Linux 下的 sha256sum fichier 和 sha256sum -c SHA256SUMS,Windows 下的 Get-FileHash chemin,以及当需要一个不太常见的算法时的 openssl dgst -sha256 fichier。

  • 指纹不是秘密:它保护完整性,绝不保护保密性。公布一份敏感数据的指纹,当该数据可猜到时,往往等同于公布了该数据本身。
  • 指纹要通过自动比较来验证,而不是看开头和结尾的几个字符。
  • 一个与预期不同的指纹并不总是表明遭到攻击:文本的编码、行尾、把归档文件与其解压后的内容相比较,都是最常见的原因。
  • 一个未签名的指纹文件只能证明不存在意外损坏。
  • 一旦对手能选择被哈希的内容,MD5 和 SHA-1 就应被摒弃;SHA-256 是完整性用途的默认选择。
  • 对于口令,任何快速的函数都不合适:应使用 Argon2id、scrypt 或 bcrypt,并为每个账户配一个唯一的盐。

这一学习路径的自然延续,涉及三个都以哈希为基础的主题:电子签名与公钥的验证、HMAC 类型的消息认证码及由此派生的令牌,以及把口令转换为加密密钥的密钥派生函数。

本文的原始文本未被网络存档保留:页面的抓取在正文之前就中断了。仅其引言部分留存,此处作为开篇沿用。其余内容于 2026 年 9 月 9 日重新撰写,随后逐点校对并修正。