哈希算法详解:MD5/SHA/bcrypt
约 1702 字大约 6 分钟
hashalgorithms
2025-08-09
概述
哈希函数将任意长度的输入映射为固定长度的输出(摘要/指纹),是密码学中最基础的原语之一。本文将系统介绍各类哈希算法的原理、安全性分析以及密码存储的最佳实践。
密码学哈希函数的核心性质
哈希算法演进
MD5(已破解 - 禁止用于安全场景)
MD5 产生 128 位摘要。2004 年王小云教授团队证明了 MD5 的碰撞攻击可行性,2008 年已能伪造 SSL 证书。
import hashlib
# 仅用于演示,不要在安全场景使用
md5_hash = hashlib.md5(b"Hello World").hexdigest()
# "b10a8db164e0754105b7a99be72e3fe5" — 32 个十六进制字符 = 128 位
# MD5 碰撞:存在两个不同的文件产生相同的 MD5 值
# 实际攻击已能在数秒内生成碰撞对SHA-1(已弃用)
SHA-1 产生 160 位摘要。Google 在 2017 年 SHAttered 攻击中实际演示了碰撞。
sha1_hash = hashlib.sha1(b"Hello World").hexdigest()
# "0a4d55a8d778e5022fab701977c5d840bbc486d0"
# Git 曾使用 SHA-1 作为对象标识,正在迁移到 SHA-256SHA-2 系列(当前标准)
SHA-256 和 SHA-512 是目前广泛使用的安全哈希算法。
# SHA-256: 256 位输出
sha256 = hashlib.sha256(b"Hello World").hexdigest()
# "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
# SHA-512: 512 位输出,在 64 位处理器上可能更快
sha512 = hashlib.sha512(b"Hello World").hexdigest()
# SHA-224, SHA-384 是截断版本SHA-3 / Keccak
SHA-3 基于海绵结构(Sponge Construction),与 SHA-2 的 Merkle-Damgard 结构完全不同,提供了算法多样性。
# SHA-3 系列
sha3_256 = hashlib.sha3_256(b"Hello World").hexdigest()
sha3_512 = hashlib.sha3_512(b"Hello World").hexdigest()
# SHAKE: 可变长度输出
shake_128 = hashlib.shake_128(b"Hello World").hexdigest(32) # 32 字节输出HMAC 消息认证码
HMAC 使用哈希函数和密钥生成消息认证码,可验证数据的完整性和真实性。
import hmac
import hashlib
key = b"secret-key-should-be-random-and-long"
message = b"Important data to authenticate"
# 生成 HMAC
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
# 验证 HMAC(使用恒定时间比较,防止时序攻击)
def verify_hmac(key: bytes, message: bytes, expected_mac: str) -> bool:
computed = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, expected_mac)
# 错误做法:使用 == 比较(存在时序攻击风险)
# if computed_mac == received_mac: # 不安全!密码哈希函数
通用哈希函数(SHA-256 等)速度太快,不适合密码存储。密码哈希函数故意设计得很慢,以抵抗暴力破解。
bcrypt
import bcrypt
password = b"user_password_123"
# 生成盐并哈希(cost factor 12 表示 2^12 次迭代)
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
# b'$2b$12$LJ3m4ys3Lg.Ry4JEOPSwAeCYQ1z4X7EkD9HVkPcbafi8Nn1Rm5gXS'
# 格式: $算法$cost$22字符salt+31字符hash
# 验证密码
if bcrypt.checkpw(password, hashed):
print("Password matches")scrypt
scrypt 增加了内存硬度(memory-hard),使 GPU/ASIC 攻击成本大增。
import hashlib
password = b"user_password_123"
salt = b"random_salt_value_here"
# n=2^14, r=8, p=1 是推荐参数
derived = hashlib.scrypt(password, salt=salt, n=16384, r=8, p=1, dklen=32)Argon2(推荐首选)
Argon2 是 Password Hashing Competition 的冠军,提供三种变体:
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # 迭代次数
memory_cost=65536, # 内存使用量(KB) = 64 MB
parallelism=4, # 并行线程数
hash_len=32, # 输出长度
type=argon2.Type.ID # Argon2id: 同时抵抗侧信道和 GPU 攻击
)
# 哈希密码
hash_str = ph.hash("user_password_123")
# $argon2id$v=19$m=65536,t=3,p=4$c2FsdHZhbHVl$aGFzaHZhbHVl...
# 验证密码
try:
ph.verify(hash_str, "user_password_123")
# 检查是否需要重新哈希(参数升级时)
if ph.check_needs_rehash(hash_str):
new_hash = ph.hash("user_password_123")
except argon2.exceptions.VerifyMismatchError:
print("Password incorrect")Salt 和 Pepper
Salt(盐)
Salt 是为每个密码随机生成的值,与密码一起哈希。它防止彩虹表攻击和相同密码产生相同哈希。
import os
# 每个用户独立的 salt
salt = os.urandom(16) # 128 位随机盐
# 错误做法
# salt = b"fixed_salt" # 全局固定盐 = 没有盐
# salt = username.encode() # 可预测的盐Pepper(胡椒)
Pepper 是全局密钥,存储在数据库之外(如环境变量或 HSM),即使数据库泄露,攻击者也无法破解密码。
import hmac, hashlib, os
PEPPER = os.environ["PASSWORD_PEPPER"] # 32 字节密钥,存储在密钥管理系统
def hash_password_with_pepper(password: str, pepper: str = PEPPER) -> str:
# 先用 pepper HMAC 处理密码,再送入 Argon2
peppered = hmac.new(
pepper.encode(), password.encode(), hashlib.sha256
).digest()
ph = PasswordHasher()
return ph.hash(peppered)算法对比总表
| 算法 | 输出长度 | 速度 | 安全状态 | 适用场景 |
|---|---|---|---|---|
| MD5 | 128 bit | 极快 | 已破解 | 仅文件校验(非安全) |
| SHA-1 | 160 bit | 快 | 已弃用 | 不应使用 |
| SHA-256 | 256 bit | 快 | 安全 | 数据完整性、签名 |
| SHA-3-256 | 256 bit | 中 | 安全 | 需要算法多样性时 |
| BLAKE2b | 可变 | 极快 | 安全 | 高性能场景 |
| bcrypt | 184 bit | 慢(可调) | 安全 | 密码存储 |
| scrypt | 可变 | 慢(可调) | 安全 | 密码存储 |
| Argon2id | 可变 | 慢(可调) | 最佳 | 密码存储(推荐) |
常见错误与最佳实践
# 错误 1: 使用 MD5/SHA 存储密码
password_hash = hashlib.sha256(password.encode()).hexdigest() # 不安全
# 错误 2: 不使用 salt
hashed = slow_hash(password) # 容易被彩虹表攻击
# 错误 3: 自己实现"加盐"逻辑
hashed = hashlib.sha256(salt + password).hexdigest() # 仍然太快
# 错误 4: 使用 == 比较哈希值(时序攻击)
if computed_hash == stored_hash: # 不安全
pass
# 正确做法: 使用 Argon2id + 自动 salt + 恒定时间比较
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash(password)
ph.verify(stored, password)总结
- 数据完整性用 SHA-256 或 SHA-3
- 消息认证用 HMAC-SHA-256
- 密码存储用 Argon2id(首选)、bcrypt 或 scrypt
- 永远不要使用 MD5/SHA-1 处理安全相关任务
- 每个密码必须有独立的随机 salt
- 使用恒定时间比较函数防止时序攻击
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于