How Hash Functions Keep Your Passwords Safe
哈希函数如何保护你的密码
Every time you log in to a website, the server almost never stores or compares your password directly. It stores a hash: a fixed-length string of bytes produced by feeding your password through a one-way mathematical function. When you type your password again, the server hashes the new value and compares the two hashes. If they match, you are in. If a thief copies the database, they get a table of hashes, not passwords — and the harder it is to turn those hashes back into passwords, the safer every user is.
每次你登录一个网站,服务器几乎从不直接存储或比较你的密码。它存储的是一个哈希值:把密码送进一个单向数学函数后,得到的固定长度字节串。下次你再输入密码,服务器把新输入做同样的哈希,然后比对两个哈希值。相同就放行。如果小偷拷走了数据库,他拿到的是一整张哈希表,而不是明文密码;把哈希再变回密码越难,所有用户就越安全。
The three properties of a cryptographic hash
密码学哈希的三个性质
A good cryptographic hash function has three properties that, taken together, make it useful for security.
一个好的密码学哈希函数同时具备三个性质,使它适用于安全场景。
- Deterministic — the same input always produces the same output. Without this, logins would never work.
- 确定性 —— 相同输入永远产生相同输出。否则登录根本无法工作。
- Fast to compute — for general-purpose hashing (file fingerprints, checksums, blockchain), computing a hash must be cheap. This is the property that, paradoxically, makes raw hashes a poor choice for passwords, as we will see.
- 计算快速 —— 在通用哈希(文件指纹、校验和、区块链)中,计算的代价必须低。但讽刺的是,正是这个性质让"裸哈希"不适合做密码哈希,详见后文。
- Avalanche effect — flipping a single bit of the input changes roughly half of the bits of the output, in a way that is unpredictable. This is what makes it impossible to work backwards from the output to find a similar input.
- 雪崩效应 —— 翻转输入中的任意一位,输出中大约一半的位会改变,且方式无法预测。这正是"从输出反推相似输入"做不到的原因。
Two further properties matter for security: pre-image resistance (given a hash, it should be infeasible to find any input that produces it) and collision resistance (it should be infeasible to find any two different inputs that produce the same hash). A function that loses either of these can no longer be considered cryptographically secure.
对安全而言还有两个性质至关重要:抗原像性(给定一个哈希,找到任何能产生它的输入在计算上不可行)和抗碰撞性(找到任何两个不同输入却产生相同哈希在计算上不可行)。任何一项被攻破,该函数就不再能算密码学安全。
The classic algorithms: MD5, SHA-1, SHA-256
经典算法:MD5、SHA-1、SHA-256
Three hash functions dominate two decades of practice, and they sit at very different points on the security timeline.
三个哈希函数主导了过去二十年,它们的安全性时间线差别很大。
- MD5 (1991, 128-bit output) was broken in 2004 when researchers demonstrated chosen-prefix collisions. Today, generating two files with the same MD5 takes seconds on a laptop. Never use MD5 for security.
- MD5(1991 年,128 位输出)于 2004 年被攻破,研究者演示了"选择前缀碰撞"。今天,生成两个 MD5 相同的文件用笔记本电脑只需几秒。绝不要再把 MD5 用于安全场景。
- SHA-1 (1995, 160-bit output) was the default for TLS and Git for years. In 2017, Google's SHAttered attack produced the first practical collision. SHA-1 was formally deprecated by NIST in 2011 and should not appear in new systems.
- SHA-1(1995 年,160 位输出)曾是 TLS 和 Git 的默认算法。2017 年 Google 的 SHAttered 攻击给出了第一个可实用的碰撞。NIST 在 2011 年正式弃用 SHA-1,新系统不应再使用。
- SHA-256 (2001, 256-bit output) is part of the SHA-2 family and remains the workhorse of general-purpose cryptography. It is fast, well-studied, and has no known practical attacks. It is the right choice for file integrity, digital signatures, certificate fingerprints, and HMAC message authentication — but not for password hashing, for the reason in the next section.
- SHA-256(2001 年,256 位输出)属于 SHA-2 系列,是通用密码学的主力。它计算快、被研究透彻、没有已知的实用攻击。对文件完整性、数字签名、证书指纹和 HMAC 消息认证来说它是正确选择 —— 但不适合做密码哈希,理由见下节。
For most applications, SHA-256 is "the safe default" — but for passwords it is the wrong tool, and the rest of this article is about why.
对大多数应用,SHA-256 是"安全默认" —— 但对密码它是错误的工具,下文会解释原因。
The birthday paradox and collision math
生日悖论与碰撞数学
A common question is: "SHA-256 produces 256 bits, so there are 2^256 possible outputs. How can anyone find a collision?" The answer is the birthday paradox: with N possible outputs, the expected number of samples before a collision is about √N, not N. So for SHA-256, the expected number of hashes to find a collision is roughly 2^128 — a big number, but a billion times smaller than the 2^256 you might expect.
一个常见的问题是:"SHA-256 输出 256 位,所以一共有 2^256 种可能,怎么会有人能找到碰撞?" 答案在于生日悖论:对于 N 种可能的输出,找到一次碰撞需要的样本数大约是 √N,而不是 N。因此对 SHA-256,找到一次碰撞的期望样本量大约是 2^128 —— 数字仍然很大,但比你直觉的 2^256 小了 10^30 倍。
This is why output size matters. A hash with 128 bits of output is not "128 bits of security" against collisions; it offers only 64 bits of effective collision resistance. MD5's 128-bit output gives about 64 bits of collision resistance — which is exactly why a laptop can find a collision in seconds. A 256-bit output gives 128 bits, which is still well beyond any attack we know how to build today.
这就是为什么输出长度很关键。128 位的哈希对碰撞攻击并不提供"128 位安全强度",其抗碰撞有效强度只有 64 位。这正是 MD5 128 位输出能被笔记本几秒内碰撞的原因。256 位输出提供 128 位抗碰撞强度,仍远在我们今天能构建的任何攻击能力之外。
For pre-image attacks (given a hash, find an input that produces it), no birthday speed-up applies, and the work is roughly 2^256 for SHA-256 — more than enough. The asymmetry between pre-image and collision resistance is why cryptographers care so much about both, and why MD5 was retired years before anyone demonstrated a pre-image attack against it.
对原像攻击(给定哈希,找到能产生它的输入)不存在生日加速,所需工作量对 SHA-256 仍约 2^256 —— 足够安全。原像与抗碰撞之间的不对称,正是密码学家对两者都极为看重的原因,也解释了为什么 MD5 早在有人演示原像攻击之前很多年就被淘汰了。
Why fast hashes are the wrong tool for passwords
为什么快速哈希不适合做密码哈希
A user logging in cares about latency measured in milliseconds. An attacker cracking hashes cares about throughput measured in billions of hashes per second on consumer GPUs. A function that is "fast" for the user is "fast" for the attacker too. Modern GPUs compute roughly 10 billion SHA-256 hashes per second. A modern rig with eight GPUs can therefore try every possible 8-character lowercase password in under an hour. The 2012 LinkedIn breach, where 6.5 million unsalted SHA-1 hashes fell in days, was an early warning of this exact problem.
登录用户关注的是毫秒级延迟;而攻击者破解哈希关注的是每秒多少十亿次哈希(用消费级 GPU)。对用户"够快"对攻击者同样"够快"。现代 GPU 每秒大约能算 100 亿次 SHA-256;一张装 8 块 GPU 的矿机,1 小时内就能遍历所有 8 位小写字母密码。2012 年的 LinkedIn 数据泄露(6.5 百万个未加盐的 SHA-1 哈希在数天内被破解)正是这一问题的早期警示。
The fix is to pick a hash that is intentionally slow and tunable: a function where each guess costs a millisecond or more of CPU time, and where the cost can be raised over the years as hardware gets faster. The right family of algorithms is what password-security professionals call "memory-hard" or "tunable-cost" hashes, and the three that matter today are bcrypt, scrypt, and Argon2id.
解决方法是选用刻意做得"慢且可调"的哈希:每次猜测需要一毫秒或更多 CPU 时间,并且这个成本可以随硬件进步逐年提高。密码安全领域称这类算法为"内存敏感型"或"可调成本型"哈希,目前最重要的三种是 bcrypt、scrypt 和 Argon2id。
bcrypt, scrypt, Argon2id, salts, and peppers
bcrypt、scrypt、Argon2id:盐与胡椒的正确使用
These three algorithms all have the same basic idea: take a password, combine it with a per-user random "salt", and run the input through a deliberately expensive process. The cost is stored alongside the hash, so it can be increased over time without breaking old data.
这三种算法核心思想一致:把密码与每个用户独立的随机"盐"组合,再送进一个刻意昂贵的流程。成本与哈希一并存储,这样未来可以提高成本而无需废弃旧数据。
- bcrypt (1999) is the oldest of the three. It is based on the Blowfish cipher and exposes a single "cost factor" that doubles the work each time it goes up by 1. A cost of 12 means 4,096 rounds of internal hashing — about a quarter-second on a typical server in 2026. bcrypt is well-supported in every language and remains a respectable default.
- bcrypt(1999 年发布)是三者中最早出现的。它基于 Blowfish 算法,暴露单一的"成本因子",每加 1 工作量翻倍。成本 12 意味着 4,096 轮内部哈希,在 2026 年的普通服务器上大约 1/4 秒。bcrypt 在所有语言中都有良好支持,仍是值得尊重的默认选项。
- scrypt (2009) adds a memory cost on top of time. An attacker cannot reach bcrypt's throughput on a GPU unless they also have the memory to match, which is much more expensive per attempt than raw compute.
- scrypt(2009 年发布)在时间成本之外加入了内存成本。攻击者想用 GPU 跑出 bcrypt 同等的吞吐,就必须配备相应内存,这比单纯堆算力贵得多。
- Argon2id (2015, winner of the Password Hashing Competition) is the current best practice. It has three tunable parameters: time cost, memory cost, and parallelism. It resists both GPU and ASIC attacks, and NIST recommends it in SP 800-63B for new systems.
- Argon2id(2015 年,"密码哈希竞赛"冠军)是当前的最佳实践。它有三个可调参数:时间成本、内存成本、并行度。它能同时抵御 GPU 和 ASIC 攻击,NIST 在 SP 800-63B 中推荐新系统使用它。
A practical starting point in 2026: Argon2id with 1 iteration, 64 MiB of memory, and 1 thread for normal logins; bump to 3 iterations and 256 MiB for high-value accounts. Tune the parameters so that the hash takes roughly 250–500 ms on your production hardware.
2026 年的实际起步配置:普通登录用 Argon2id,参数为 1 次迭代、64 MiB 内存、1 线程;高价值账户上调到 3 次迭代、256 MiB 内存。请在你的生产硬件上微调,使单次哈希大约 250–500 毫秒。
A "salt" is a random string, unique per user, that is mixed into the password before hashing. Its job is to make a database of hashes useless to an attacker who wants to attack many accounts at once. Without a salt, an attacker computes a "rainbow table" of hashes for the trillion most likely passwords, then looks up the table against the database in seconds. With a 16-byte random salt, every user's hash is unique, the table is useless, and the attacker has to spend the full cracking cost on each account individually. A "pepper" is a related but different idea: a secret random string stored separately from the database (for example, in an environment variable or a hardware security module) and mixed into every password. Its job is to defeat an attacker who copies the database but not the application server. Good defaults: 16 bytes of salt per user, generated by a cryptographically secure RNG and stored in plaintext alongside the hash. Pepper should be at least 32 bytes, generated once at deployment, and rotated only when you can re-hash every user's password on next login.
"盐"是为每个用户独立生成的随机字符串,混入密码后再做哈希。它的作用是让攻击者无法用一个数据库同时攻击大量账号。如果没有盐,攻击者可以预先为最常见的上万亿个密码计算一张"彩虹表",再对整个数据库做秒级查表。有了 16 字节的随机盐,每个用户的哈希都不同,彩虹表毫无用处,攻击者必须为每个账号付出完整的破解成本。"胡椒"是相关但不同的概念:一个独立于数据库存储的密钥随机串(例如放在环境变量或硬件安全模块中),对所有密码都混入。它的作用是:当攻击者只偷走了数据库而没拿到应用服务器时,让数据毫无价值。经验性的良好默认:每位用户 16 字节盐,用密码学安全随机数生成,明文与哈希一并存储;胡椒至少 32 字节,在部署时生成一次,只有当你能在用户下次登录时重哈希所有密码时,才考虑轮换胡椒。
Breaches and how to verify credentials safely
数据泄露场景与凭据安全校验
A well-stored password database is a list of usernames, salts, hashes, and per-row parameters. If an attacker copies it, they can attempt to crack each entry in isolation, but the cost is the cost of the hash function per guess, times the number of guesses per account. With Argon2id at the right parameters, a 12-character random password is effectively unrecoverable even for a state-level attacker. If the database is poorly stored — no salt, fast hash, no parameters — the same attacker can crack millions of passwords per hour on commodity hardware. The difference between the two outcomes is the entire reason the right algorithm choice matters. Always check: which hash, which parameters, how was the salt generated, and where is the pepper?
一个良好设计的密码数据库存储的是用户名、盐、哈希和每行的参数。如果攻击者拷走了它,他只能逐条破解,每个账户的代价等于"哈希函数单次成本 × 猜测次数"。参数合适的 Argon2id 下,一个 12 位的随机密码实际上无法被还原,即使对国家级攻击者也是如此。如果数据库存储得很糟 —— 无盐、快速哈希、没有参数 —— 同样的攻击者用消费级硬件每小时就能破解数百万个密码。两种结局之间的差距,正是算法选择重要的原因。请始终确认:用哪种哈希、什么参数、盐如何生成、胡椒放在哪里。
Verification should be a constant-time comparison. A naive if (input == stored) leaks timing information: an attacker who can measure the response time of many login attempts can discover the stored hash one byte at a time. Most languages expose a constant-time comparison function for this — crypto.timingSafeEqual in Node, hmac.compare_digest in Python, MessageDigest.isEqual in Java, subtle.ConstantTimeCompare in the WebCrypto API. Two further practical tips: rate-limit login attempts per account and per IP, and never reveal whether it was the username or the password that was wrong. A vague "invalid credentials" message denies the attacker a free enumeration channel for which accounts exist.
校验应该用常量时间比较。朴素的 if (input == stored) 会泄露时序信息:能多次测量响应时间的攻击者可以一次一字节地"拼"出存储的哈希。大多数语言都提供常量时间比较函数 —— Node 的 crypto.timingSafeEqual、Python 的 hmac.compare_digest、Java 的 MessageDigest.isEqual,以及 Web Crypto API 的 subtle.ConstantTimeCompare。另外两条实操建议:对每个账号、每个 IP 做登录速率限制;不要告诉用户"是用户名错了还是密码错了"。一条含糊的"凭据无效"消息,能阻止攻击者利用错误提示免费枚举哪些账号存在。
Try the tools
试试这些工具
Generate a strong password to feed into these algorithms with the password generator. For a hands-on look at encoding, decode test data with the Base64 tool. Everything runs locally in your browser.
用 密码生成器 产生一个强密码,喂给这些算法做测试;想直观了解编码的话,用 Base64 工具 编解码测试数据。所有处理都在浏览器本地完成。