哈希与加盐 · 网络安全入门
如果用户数据库泄露了,攻击者能看到所有人的密码吗?
课程目录为什么需要它
数据库总会泄露:程序漏洞、备份被偷、员工疏忽。如果密码是明文存的,所有账号会一起沦陷;而且大家习惯重复用密码,他们的邮箱和银行账户也跟着沦陷。正确地存密码,几乎是每个开发者都必须做对的一件安全工作。
直觉理解
哈希就是一段文字的指纹。同样的文字永远得到同样的指纹,改一点点,指纹就完全不同;而且无法从指纹反推出原文。
所以服务器只存哈希值。登录时,把你输入的密码也算一次哈希,再比较两者。但攻击者还可以猜:把最常见的密码都算出哈希,看看有没有对得上的。这就是字典攻击。
盐 Salt 是一段随机字符串,和每个用户的哈希存在一起,计算哈希前先混进密码里。这样两个用户就算密码相同,哈希也不同:猜中一次不再能攻破所有人,提前算好的哈希表也没用了。真实系统还会用故意设计得很慢的哈希,比如 bcrypt、scrypt 或 Argon2,让每次猜测都要花时间。
定义
哈希函数 Hash function
A function that turns any input into a fixed-length value. It is one-way: you can't compute the input from the output. SHA-256 always gives 64 hex characters.把任意输入变成固定长度结果的函数。它是单向的:无法从结果算回输入。SHA-256 的结果永远是 64 个十六进制字符。
盐 Salt
A random value stored with each password hash and combined with the password before hashing, so equal passwords get different hashes.和每个密码哈希存在一起的随机值,计算哈希前先和密码拼在一起,让相同的密码得到不同的哈希。
示例
输入相同,指纹相同
动手试试每个哈希有 64 个字符,这里只打印前 16 个。
import hashlib
def h(text):
return hashlib.sha256(text.encode()).hexdigest()
print(h("sunshine")[:16])
print(h("sunshine")[:16])
print(h("Sunshine")[:16])- L4对文字的字节做哈希,并用十六进制数字显示结果。
- L7密码相同,哈希就相同。登录靠的就是这一点。
- L8只改了一个大写字母,整个哈希都变了。
字典攻击
动手试试攻击者拿到了泄露的哈希,还有一份常见密码清单。弱密码被猜中,随机密码幸存。
import hashlib
def h(text):
return hashlib.sha256(text.encode()).hexdigest()
leaked = {"amy": h("dragon"), "bob": h("x9!Lq2#v")}
wordlist = ["123456", "password", "dragon", "qwerty"]
for user, stored in leaked.items():
found = None
for guess in wordlist:
if h(guess) == stored:
found = guess
print(user, "->", found)- L6攻击者偷到的:用户名和哈希,没有密码。
- L7真实的字典里有上百万个泄露过的密码。
- L11把每个猜测算出哈希,再和存储的哈希比较。
- L13dragon 在清单里,所以 amy 的密码被猜中了。
amy -> dragon - L13bob 的随机密码不在任何清单里。
bob -> None
常见错误
只用一个很快的哈希
SHA-256 的设计目标就是快,这反而帮攻击者每秒试几十亿次。存密码要用慢速、带盐的密码哈希(bcrypt、scrypt、Argon2),并且用可信的库。这一课用 SHA-256 只是为了讲清原理。
所有人共用一个盐
如果所有用户共用同一个盐,相同的密码哈希还是相同,猜中一次照样全部攻破。每个用户都要有自己的随机盐。
小测验
1. 这段代码会输出什么?
import hashlib
def h(text):
return hashlib.sha256(text.encode()).hexdigest()
print(h("salt1" + "pw") == h("salt2" + "pw"))
print(len(h("pw")))查看答案
答案:A
盐不同,同一个密码的哈希也不同;SHA-256 的十六进制结果永远是 64 个字符。
2. 两个用户都用了密码 `letmein`。如果每个用户都有自己的随机盐,攻击者在泄露的表里会看到什么?
查看答案
答案:B
盐不同,哈希就不同。攻击者看不出两人密码一样,只能一个一个地攻击。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出哈希的长度
准确写出这段程序的输出。
- 改需求加上盐
store(pw)存的是不加盐的哈希。修改两个函数,让它们用上盐:store(pw, salt)返回salt + "$" + h(salt + pw)check(pw, record)在"$"处把记录拆成盐和哈希,只有h(salt + pw)等于存储的哈希时才返回True。
- 从零写攻破弱密码
盐能防止猜中一次就攻破所有人,但每个弱密码还是能被单独猜出来。写一个函数
crack(leaked, wordlist):leaked是一个字典:用户 → "salt$hash"- 返回字典
用户 → 密码,包含所有密码在wordlist里的用户;破解不了的用户不放进去。
h函数已经写好了。
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Hash | 哈希 | A fixed-length, one-way fingerprint of some data. |
| Salt | 盐 | A random value mixed into each password before hashing so equal passwords get different hashes. |
| Dictionary attack | 字典攻击 | Guessing passwords by hashing a list of common passwords and comparing. |
| Password hash (bcrypt, Argon2) | 密码哈希(bcrypt、Argon2) | A deliberately slow, salted hash designed for storing passwords. |