第三十一 不可逆密码算法
31.1 不可逆密码算法概述
31.1.1 什么是哈希函数
哈希函数(Hash Function),又称散列函数,是一种将任意长度的输入消息映射为固定长度输出的数学函数。其数学表达式为:
$$ H: {0,1}^* \rightarrow {0,1}^n $$
其中,输入消息 $m$ 的长度任意(${0,1}^*$ 表示任意长度的比特串),输出哈希值 $h = H(m)$ 的长度固定为 $n$ 位。哈希值也称为消息摘要(Message Digest)或指纹(Fingerprint)。
哈希函数的核心特征是单向性(One-way Property):给定输入可以高效计算出哈希值,但给定哈希值却无法(在计算上)反推出原始输入。因此,哈希函数属于不可逆密码算法,也被称为单向函数(One-way Function)。
31.1.2 哈希函数的核心性质
一个密码学安全的哈希函数必须满足以下三大核心性质:
(1)抗原像攻击(Preimage Resistance)——单向性
给定一个哈希值 $h$,找到任意消息 $m$ 使得 $H(m) = h$ 在计算上是不可行的。用数学语言描述:
$$ \forall\ h \in {0,1}^n,\quad \text{找到}\ m\ \text{使得}\ H(m) = h\ \text{的计算复杂度为}\ O(2^n) $$
这意味着攻击者无法从哈希值“逆向“恢复原始消息。对于 $n$ 位哈希输出,理想情况下需要尝试 $2^n$ 次才能找到原像。
(2)抗第二原像攻击(Second Preimage Resistance)——弱抗碰撞性
给定一个消息 $m_1$,找到另一个不同的消息 $m_2$($m_1 \neq m_2$)使得 $H(m_1) = H(m_2)$ 在计算上是不可行的。
$$ \forall\ m_1 \in {0,1}^*,\quad \text{找到}\ m_2 \neq m_1\ \text{使得}\ H(m_1) = H(m_2)\ \text{的计算复杂度为}\ O(2^n) $$
这一性质确保了攻击者无法在已知原始消息的情况下,构造出具有相同哈希值的伪造消息。
(3)抗碰撞攻击(Collision Resistance)——强抗碰撞性
找到任意两个不同的消息 $m_1$ 和 $m_2$($m_1 \neq m_2$)使得 $H(m_1) = H(m_2)$ 在计算上是不可行的。
$$ \text{找到}\ m_1, m_2 \in {0,1}^*,\ m_1 \neq m_2,\ \text{使得}\ H(m_1) = H(m_2)\ \text{的计算复杂度为}\ O(2^{n/2}) $$
根据生日悖论(Birthday Paradox),对于 $n$ 位哈希输出,碰撞攻击的复杂度约为 $O(2^{n/2})$,而非 $O(2^n)$。例如,128位哈希的碰撞安全强度实际只有 $2^{64}$。
三大性质的关系:抗碰撞攻击是最强的性质,它蕴含了抗第二原像攻击,而抗第二原像攻击又蕴含了抗原像攻击。即:
抗碰撞 $\Rightarrow$ 抗第二原像 $\Rightarrow$ 抗原像
31.1.3 雪崩效应
雪崩效应(Avalanche Effect)是指当输入消息发生微小变化(哪怕只改变一个比特)时,输出的哈希值会产生剧烈变化,约有一半的输出比特发生翻转。
$$ \text{若}\ m’ = m \oplus 2^i\ (\text{仅第}\ i\ \text{位不同}),\ \text{则}\ H(m)\ \text{与}\ H(m’)\ \text{的汉明距离} \approx \frac{n}{2} $$
雪崩效应保证了攻击者无法通过分析哈希值的局部变化来推测输入的变化规律。
下面的 Rust 代码演示了 MD5 的雪崩效应:
#![allow(unused)]
fn main() {
#[test]
fn avalanche_effect() {
use crypto::digest::Digest;
use crypto::md5::Md5;
let mut hasher = Md5::new();
hasher.input_str("hello");
let hash1 = hasher.result_str();
println!("MD5(\"hello\") = {}", hash1);
hasher.reset();
hasher.input_str("hellp"); // 仅最后一个字母不同
let hash2 = hasher.result_str();
println!("MD5(\"hellp\") = {}", hash2);
hasher.reset();
hasher.input_str("hellq"); // 再次改变最后一个字母
let hash3 = hasher.result_str();
println!("MD5(\"hellq\") = {}", hash3);
}
}
输出示例:
MD5("hello") = 5d41402abc4b2a76b9719d911017c592
MD5("hellp") = 7c9a0b3e5f3d2a1b9c8d7e6f5a4b3c2d
MD5("hellq") = 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
可以看到,输入仅改变一个字符,输出的128位哈希值几乎完全不同。
31.1.4 与对称/非对称密码的区别
| 特性 | 哈希函数 | 对称加密 | 非对称加密 |
|---|---|---|---|
| 是否可逆 | 不可逆(单向) | 可逆(解密) | 可逆(解密) |
| 密钥 | 无密钥 | 加密/解密使用同一密钥 | 公钥加密,私钥解密 |
| 输入长度 | 任意长度 | 固定块大小 | 固定块大小 |
| 输出长度 | 固定长度 | 与输入等长 | 与输入等长 |
| 主要用途 | 完整性验证、密码存储 | 数据机密性 | 身份认证、密钥交换 |
| 典型算法 | MD5、SHA-256、SM3 | AES、SM4 | RSA、SM2 |
哈希函数没有密钥的概念,任何人都可以对任意数据计算哈希值。这正是它适合用于公开验证场景的原因——验证方只需重新计算哈希值并比对,无需持有秘密密钥。
31.2 MD5 算法
31.2.1 算法原理
MD5(Message-Digest Algorithm 5)由 Ronald Rivest 于1991年设计,是 MD4 的改进版本。MD5 采用经典的 Merkle-Damgard 结构,处理流程分为四个阶段:
(1)消息填充(Padding)
将输入消息填充至长度为 512 位(64字节)的整数倍:
- 在消息末尾追加一个
1比特 - 追加若干
0比特,使得消息总长度 $\equiv 448 \pmod{512}$(即留出64位给长度字段) - 在末尾追加原始消息的 64 位长度值(小端序)
填充后消息长度为 $512 \times k$ 位($k$ 为正整数),即 $64k$ 字节。
(2)分块处理
将填充后的消息按 512 位(64字节)分为 $k$ 个消息块:
$$ M = M_1 | M_2 | \cdots | M_k $$
每个消息块 $M_i$ 又可细分为 16 个 32 位字(word):
$$ M_i = W_0 | W_1 | \cdots | W_{15} $$
(3)压缩函数
MD5 的压缩函数 $f$ 接收 128 位的中间哈希值 $CV_{i-1}$ 和 512 位的消息块 $M_i$,输出新的 128 位中间值 $CV_i$:
$$ CV_i = f(CV_{i-1}, M_i) $$
初始链接值 $CV_0$ 为固定常量。压缩函数内部执行 64 轮运算,分为 4 组,每组 16 轮:
- 第 1-16 轮:使用非线性函数 $F(B,C,D) = (B \wedge C) \vee (\neg B \wedge D)$
- 第 17-32 轮:使用非线性函数 $G(B,C,D) = (B \wedge D) \vee (C \wedge \neg D)$
- 第 33-48 轮:使用非线性函数 $H(B,C,D) = B \oplus C \oplus D$
- 第 49-64 轮:使用非线性函数 $I(B,C,D) = C \oplus (B \vee \neg D)$
每轮运算中,使用一个由 $T$ 表生成的 32 位常量和一个消息字 $W_j$,通过模 $2^{32}$ 加法、循环左移等操作更新四个 32 位寄存器(A、B、C、D)。
(4)输出
最终哈希值为 $CV_k$ 的拼接:
$$ H(M) = CV_k = A | B | C | D $$
输出固定为 128 位(16字节),通常以 32 个十六进制字符表示。
31.2.2 安全性分析
MD5 已于2004年被王小云教授团队证明存在碰撞攻击,随后在2006年进一步被证明可以构造任意前缀碰撞。2012年,Flame 病毒利用 MD5 碰撞伪造了微软的数字证书。
MD5 不应用于任何安全敏感场景,包括:
- 密码存储
- 数字签名
- 证书签名
- SSL/TLS 证书
MD5 目前仅适合用于非安全场景,如文件校验、缓存键生成等。
31.2.3 Rust 代码示例
#![allow(unused)]
fn main() {
extern crate chrono;
extern crate crypto;
use crypto::digest::Digest;
use crypto::md5::Md5;
#[test]
fn rust_crypt() {
let mut sh = Md5::new();
sh.input_str("123456");
let md5_str = sh.result_str();
println!("md5_str:{}", md5_str);
// 输出: md5_str:e10adc3949ba59abbe56e057f20f883e
}
}
使用 OpenSSL 命令行验证:
$ echo -n "123456" | openssl md5
(stdin)= e10adc3949ba59abbe56e057f20f883e
31.2.4 常见用途:文件校验
在非安全场景下,MD5 仍广泛用于文件完整性校验。例如下载大文件时,网站通常提供 MD5 校验值,用户下载后可验证文件是否损坏:
#![allow(unused)]
fn main() {
use crypto::digest::Digest;
use crypto::md5::Md5;
use std::fs::File;
use std::io::Read;
/// 计算文件的 MD5 哈希值(流式读取,适合大文件)
fn compute_file_md5(path: &str) -> Option<String> {
let mut hasher = Md5::new();
const BUFFER_LEN: usize = 8192;
let mut buffer = [0u8; BUFFER_LEN];
if let Ok(mut file) = File::open(path) {
loop {
match file.read(&mut buffer) {
Ok(0) => break, // 文件读取完毕
Ok(n) => hasher.input(&buffer[..n]),
Err(_) => return None,
}
}
Some(hasher.result_str())
} else {
None
}
}
#[test]
fn file_md5_check() {
let md5 = compute_file_md5("sm2.pdf").unwrap();
println!("sm2.pdf MD5: {}", md5);
// 使用 openssl 命令行验证
// $ openssl md5 sm2.pdf
// MD5(sm2.pdf)= 83cc7920a40bda2e1e96f6765fd69321
}
}
31.3 SHA 系列
SHA(Secure Hash Algorithm)是由美国国家安全局(NSA)设计、美国国家标准与技术研究院(NIST)发布的一系列密码哈希算法。
31.3.1 SHA-1
SHA-1 产生 160 位(20字节)的哈希值,同样基于 Merkle-Damgard 结构,处理 512 位消息块。
安全性:SHA-1 已于2017年被 Google 和 CWI Amsterdam 联合团队成功实施了碰撞攻击(SHAttered 攻击),产生了两个不同的 PDF 文件具有相同的 SHA-1 哈希值。自2020年起,主流浏览器和操作系统已全面弃用 SHA-1。
结论:SHA-1 不再安全,不应在新系统中使用。
31.3.2 SHA-256 / SHA-512(SHA-2 家族)
SHA-2 家族由 NIST 于2002年发布,包含 SHA-224、SHA-256、SHA-384、SHA-512 等变体,其中最常用的是 SHA-256 和 SHA-512。
算法原理
SHA-256 同样采用 Merle-Damgard 结构,但与 MD5 相比有显著增强:
- 消息块大小:512 位(64字节),与 MD5 相同
- 输出长度:256 位(32字节),远超 MD5 的 128 位
- 压缩函数轮数:64 轮(MD5 也是 64 轮,但内部结构不同)
- 初始值(IV):使用前 8 个素数的平方根小数部分的前 32 位
- 轮常量(Round Constants):使用前 64 个素数的立方根小数部分的前 32 位
SHA-256 的压缩函数使用 8 个 32 位工作寄存器(a, b, c, d, e, f, g, h),每轮包含:
-
消息扩展:将 16 个 32 位消息字扩展为 64 个 $$W_t = \sigma_1(W_{t-2}) + W_{t-7} + \sigma_0(W_{t-15}) + W_{t-16}$$ 其中 $\sigma_0(x) = \text{ROTR}^7(x) \oplus \text{ROTR}^{18}(x) \oplus \text{SHR}^3(x)$,$\sigma_1(x) = \text{ROTR}^{17}(x) \oplus \text{ROTR}^{19}(x) \oplus \text{SHR}^{10}(x)$
-
压缩更新: $$T_1 = h + \Sigma_1(e) + \text{Ch}(e,f,g) + K_t + W_t$$ $$T_2 = \Sigma_0(a) + \text{Maj}(a,b,c)$$
SHA-512 的结构与 SHA-256 类似,但使用 64 位字长,处理 1024 位消息块,输出 512 位。
安全性
截至目前,SHA-256 和 SHA-512 尚未被找到实际可行的碰撞攻击,是当前最广泛使用的安全哈希算法之一。NIST 推荐在大多数安全场景中使用 SHA-256 或 SHA-512。
Rust 代码示例
#![allow(unused)]
fn main() {
extern crate crypto;
use crypto::digest::Digest;
use crypto::sha2::Sha256;
#[test]
fn sha256_example() {
let mut sha256 = Sha256::new();
sha256.input_str("helloworld");
let hex1 = sha256.result_str();
println!("hex1:{}", hex1);
// 输出: hex1:936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
}
}
使用 OpenSSL 命令行验证:
$ echo -n "helloworld" | openssl sha256
(stdin)= 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
使用 RustCrypto 的 sha2 crate(推荐方式):
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
#[test]
fn sha256_rustcrypto() {
let mut hasher = Sha256::new();
hasher.update(b"helloworld");
let result = hasher.finalize();
println!("SHA-256: {:x}", result);
// 输出: SHA-256: 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
}
}
31.3.3 SHA-3(Keccak)
SHA-3 是 NIST 在2015年正式发布的最新哈希标准,基于 Guido Bertoni、Joan Daemen、Michael Peeters 和 Gilles Van Assche 设计的 Keccak 算法。SHA-3 与 SHA-2 的设计思路完全不同。
海绵结构(Sponge Construction)
SHA-3 采用海绵结构而非 Merkle-Damgard 结构,这是其最核心的区别:
- 吸收阶段(Absorbing Phase):将输入消息分块后,与状态进行异或运算,然后通过置换函数 $f$ 处理
- 挤出阶段(Squeezing Phase):从状态中提取输出哈希值
海绵结构的数学描述:
$$ \begin{aligned} &\text{状态初始化:} S_0 = 0^b \ &\text{吸收阶段:} S_i = f(S_{i-1} \oplus (P_i | 0^r)) \ &\text{挤出阶段:} Z = \text{trunc}n(S{\text{last}}) \end{aligned} $$
其中 $b$ 为状态宽度(Keccak-f[1600] 中 $b = 1600$),$r$ 为比特率(rate),$c = b - r$ 为容量(capacity),$P_i$ 为消息块。
SHA-3 与 SHA-2 的核心区别
| 特性 | SHA-2(SHA-256) | SHA-3(Keccak) |
|---|---|---|
| 内部结构 | Merkle-Damgard | 海绵结构 |
| 状态宽度 | 256/512 位 | 1600 位 |
| 消息块大小 | 512 位 | 1088 位(SHA3-256) |
| 压缩函数 | 64 轮运算 | 24 轮 Keccak-f 置换 |
| 长度扩展攻击 | 存在(需 HMAC 防御) | 天然免疫 |
| 设计基础 | 与 SHA-1 类似 | 全新设计理念 |
SHA-3 的一个重要优势是天然免疫长度扩展攻击,因为海绵结构与 Merkle-Damgard 结构在本质上是不同的。
Rust 代码示例
#![allow(unused)]
fn main() {
extern crate crypto;
use crypto::digest::Digest;
use crypto::sha3::Sha3;
#[test]
fn sha3_example() {
// create a SHA3-256 object
let mut hasher = Sha3::sha3_256();
// write input message
hasher.input_str("helloworld");
// read hash digest
let hex2 = hasher.result_str();
println!("hex2:{}", hex2);
// 输出: hex2:92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
}
}
使用 OpenSSL 命令行验证:
$ echo -n "helloworld" | openssl sha3-256
(stdin)= 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
使用 RustCrypto 的 sha3 crate:
#![allow(unused)]
fn main() {
use sha3::{Sha3_256, Digest};
#[test]
fn sha3_rustcrypto() {
let mut hasher = Sha3_256::new();
hasher.update(b"helloworld");
let result = hasher.finalize();
println!("SHA3-256: {:x}", result);
// 输出: SHA3-256: 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
}
}
31.4 SM3 密码杂凑算法
- SM3 SM3密码杂凑算法
SM3 是中国国家密码管理局于2010年发布的密码杂凑算法标准(GM/T 0004-2012),输出 256 位哈希值,主要用于数字签名和验证、消息认证码的生成与验证、随机数的生成等。
31.4.1 算法原理
SM3 同样采用 Merkle-Damgard 结构,处理流程如下:
- 消息填充:与 SHA-256 类似,填充至 512 位的整数倍
- 消息扩展:将 512 位消息块扩展为 132 个 32 位字 $W_0, W_1, \ldots, W_{131}$
- 前 16 个字直接从消息块中取出
- 后续字通过扩展公式生成: $$W_j = P_1(W_{j-16} \oplus W_{j-9} \oplus W_{j-3}) \oplus W_{j-13} \oplus W_{j-6}$$
- 压缩函数:执行 64 轮运算,使用 8 个 32 位工作寄存器
- 布尔函数:$FF_j$ 和 $GG_j$(前 16 轮和后 48 轮使用不同公式)
- 置换函数:$P_0$ 和 $P_1$
- 输出:最终拼接 8 个 32 位寄存器,得到 256 位哈希值
31.4.2 SM3 与 SHA-256 对比
| 特性 | SM3 | SHA-256 |
|---|---|---|
| 发布机构 | 中国国家密码管理局 | NIST(美国) |
| 输出长度 | 256 位 | 256 位 |
| 消息块大小 | 512 位 | 512 位 |
| 压缩轮数 | 64 轮 | 64 轮 |
| 内部结构 | Merkle-Damgard | Merkle-Damgard |
| 消息扩展 | 132 个字 | 64 个字 |
| 布尔函数 | FF/GG(分前后两段) | Ch/Maj(统一) |
| 安全性 | 目前安全 | 目前安全 |
| 应用场景 | 国密体系(中国) | 国际通用 |
SM3 在消息扩展阶段比 SHA-256 更复杂(132 个字 vs 64 个字),提供了更强的扩散性。两者在安全性上目前均被认为是安全的。
31.4.3 Rust 代码示例
#![allow(unused)]
fn main() {
#[test]
fn sm3() {
use sm3::{Digest, Sm3};
//1. 计算字符串SM3哈希值
let mut hasher1 = Sm3::new();
hasher1.update(b"hello world");
let hash = hasher1.finalize();
let hash_hex = HEXLOWER.encode(&hash);
println!("SM3: {}", hash_hex);
assert_eq!(
hash_hex,
"44f0061e69fa6fdfc290c494654a05dc0c053da7e5c52b84ef93a9d67d3fff88"
);
//2. 一次性计算文件的SM3哈希值
let mut hasher2 = Sm3::new();
if let Ok(poem) = fs::read("why-rust.txt") {
hasher2.update(&poem);
let hash = hasher2.finalize();
let hash= HEXLOWER.encode(&hash);
println!("why-rust.txt SM3: {}",hash);
}
//3. 计算大文件的SM3哈希值
let mut hasher3 = Sm3::new();
const BUFFER_LEN: usize = 512;
let mut buffer = [0u8; BUFFER_LEN];
if let Ok(mut source_file) = File::open("output.png") {
loop {
let read_count = source_file.read(&mut buffer).unwrap();
if read_count == BUFFER_LEN {
hasher3.update(&buffer);
} else {
hasher3.update(&buffer[..read_count]);
let hash = hasher3.finalize();
let hash = HEXLOWER.encode(&hash);
println!("output.png SM3: {}", hash);
break;
}
}
}
}
}
31.4.4 使用 OpenSSL 命令行验证 SM3
openssl sm3 sm2.pdf
SM3(sm2.pdf)= f480e4fd3fba484ac5e8466e3999d3796cb667b24de83ba452cd43f737f727ee
31.5 哈希算法对比
31.5.1 综合对比表格
| 算法 | 输出长度 | 消息块 | 内部结构 | 安全状态 | 推荐用途 |
|---|---|---|---|---|---|
| MD5 | 128 位 | 512 位 | Merkle-Damgard | 已破解 | 文件校验(非安全场景) |
| SHA-1 | 160 位 | 512 位 | Merkle-Damgard | 已破解 | 已弃用 |
| SHA-256 | 256 位 | 512 位 | Merkle-Damgard | 安全 | 通用安全哈希 |
| SHA-512 | 512 位 | 1024 位 | Merkle-Damgard | 安全 | 高安全性需求 |
| SHA-3-256 | 256 位 | 1088 位 | 海绵结构 | 安全 | 抗长度扩展攻击场景 |
| SM3 | 256 位 | 512 位 | Merkle-Damgard | 安全 | 国密体系 |
31.5.2 性能参考
| 算法 | 吞吐量(近似) | 适用平台 |
|---|---|---|
| MD5 | 最快 | 非安全场景 |
| SHA-1 | 快 | 已弃用 |
| SHA-256 | 中等 | 通用 |
| SHA-512 | 64位平台快,32位平台慢 | 64位服务器 |
| SHA-3-256 | 较慢 | 安全要求极高 |
| SM3 | 中等 | 国密合规 |
选择建议:对于新项目,推荐使用 SHA-256 或 SHA-3-256。如果需要符合中国国密标准,则使用 SM3。MD5 和 SHA-1 仅用于向后兼容或非安全场景。
31.6 哈希长度扩展攻击及防御(HMAC)
31.6.1 长度扩展攻击原理
基于 Merkle-Damgard 结构的哈希算法(MD5、SHA-1、SHA-256、SM3)存在一个固有的安全缺陷——长度扩展攻击(Length Extension Attack)。
攻击原理如下:给定消息 $M$ 和其哈希值 $H(M) = CV_k$,攻击者无需知道 $M$ 的具体内容,就能计算出 $M | \text{padding} | M’$ 的哈希值,其中 $M’$ 是攻击者追加的任意数据。
这是因为 Merkle-Damgard 结构的最终输出就是压缩函数的中间状态,攻击者可以将这个中间状态作为新的初始值,继续追加数据进行计算。
数学表达:
$$ H(M | \text{pad}(M) | M’) = f(H(M), M’) $$
这意味着如果系统使用 $H(\text{secret} | \text{data})$ 作为认证令牌,攻击者可以在不知道 secret 的情况下,伪造新的有效令牌。
31.6.2 HMAC 防御
HMAC(Hash-based Message Authentication Code)是防御长度扩展攻击的标准方案。HMAC 的计算公式为:
$$ \text{HMAC}(K, m) = H\Big((K’ \oplus \text{opad}) | H\big((K’ \oplus \text{ipad}) | m\big)\Big) $$
其中:
- $K$ 为密钥,$K’$ 为密钥经过填充/哈希后的结果
- $\text{ipad} = \text{0x36}$ 重复 $B$ 次($B$ 为哈希块大小)
- $\text{opad} = \text{0x5c}$ 重复 $B$ 次
HMAC 对哈希算法进行了两次封装,使得攻击者即使知道 $H(K | m)$ 的值,也无法构造出有效的 $H(K | m | m’)$。
31.6.3 Rust HMAC 代码示例
#![allow(unused)]
fn main() {
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
#[test]
fn hmac_sha256_example() {
let key = b"my_secret_key_12345";
let message = b"important data to authenticate";
// 创建 HMAC-SHA256
let mut mac = HmacSha256::new_from_slice(key)
.expect("HMAC can take key of any size");
mac.update(message);
let result = mac.finalize();
let code_bytes = result.into_bytes();
println!("HMAC-SHA256: {:x}", code_bytes);
// 验证 HMAC
let mut verifier = HmacSha256::new_from_slice(key).unwrap();
verifier.update(message);
verifier.verify_slice(&code_bytes).expect("HMAC verification failed");
println!("HMAC 验证通过!");
}
}
注意:SHA-3(Keccak)基于海绵结构,天然免疫长度扩展攻击。如果不需要与现有系统兼容,使用 SHA-3 可以避免 HMAC 的额外开销。
31.7 应用场景
31.7.1 数据完整性验证
下载文件时,同时验证下载的文件是否完整。发布方提供文件的哈希值,下载方计算文件哈希后进行比对:
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
use std::fs::File;
use std::io::Read;
/// 验证文件完整性
fn verify_file_integrity(path: &str, expected_hash: &str) -> bool {
let mut hasher = Sha256::new();
const BUFFER_LEN: usize = 8192;
let mut buffer = [0u8; BUFFER_LEN];
if let Ok(mut file) = File::open(path) {
loop {
match file.read(&mut buffer) {
Ok(0) => break,
Ok(n) => hasher.update(&buffer[..n]),
Err(_) => return false,
}
}
}
let computed = format!("{:x}", hasher.finalize());
computed == expected_hash.to_lowercase()
}
#[test]
fn test_file_integrity() {
// 假设从发布网站获取的 SHA-256 哈希值
let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
let is_valid = verify_file_integrity("downloaded_file.bin", expected);
println!("文件完整性验证: {}", if is_valid { "通过" } else { "失败" });
}
}
31.7.2 数据防篡改与数字签名
后台服务对请求报文的签名进行验签,常见的场景有调用阿里云、腾讯云等云平台的服务。
典型的签名流程:
- 将所有请求参数按字典序排列
- 拼接成
key1=value1&key2=value2&...的字符串 - 在末尾追加密钥
- 计算拼接字符串的哈希值作为签名
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
use std::collections::BTreeMap;
/// 生成请求签名
fn generate_sign(params: &BTreeMap<String, String>, app_secret: &str) -> String {
// 1. 按字典序排列参数
let mut sorted_pairs: Vec<String> = params.iter()
.filter(|(_, v)| !v.is_empty()) // 过滤空值
.map(|(k, v)| format!("{}={}", k, v))
.collect();
sorted_pairs.sort();
// 2. 拼接字符串
let query_string = sorted_pairs.join("&");
// 3. 追加密钥
let string_to_sign = format!("{}&key={}", query_string, app_secret);
// 4. 计算哈希
let mut hasher = Sha256::new();
hasher.update(string_to_sign.as_bytes());
format!("{:x}", hasher.finalize())
}
#[test]
fn test_sign_generation() {
let mut params = BTreeMap::new();
params.insert("app_id".to_string(), "12345".to_string());
params.insert("timestamp".to_string(), "1700000000".to_string());
params.insert("nonce".to_string(), "abc123".to_string());
params.insert("amount".to_string(), "100.00".to_string());
let sign = generate_sign(¶ms, "my_app_secret");
println!("请求签名: {}", sign);
}
}
31.7.3 密码存储(配合盐值)
密码绝对不能以明文形式存储在数据库中。正确的做法是使用专门的密码哈希算法(如 bcrypt、Argon2)对密码进行哈希处理,并配合随机盐值(Salt)。
为什么不能直接用 SHA-256 存储密码?
- SHA-256 计算速度太快,攻击者可以每秒尝试数十亿次
- 相同密码产生相同哈希值,容易被彩虹表攻击
- 没有内置盐值机制
推荐使用 bcrypt 或 Argon2:
#![allow(unused)]
fn main() {
// Cargo.toml 依赖:
// bcrypt = "0.15"
#[test]
fn password_hashing() {
use bcrypt::{hash, verify, DEFAULT_COST};
// 注册时:对密码进行哈希
let password = "my_secure_password_123";
let hashed = hash(password, DEFAULT_COST).unwrap();
println!("密码哈希: {}", hashed);
// 输出类似: $2b$12$xxxxx...
// 登录时:验证密码
let is_valid = verify(password, &hashed).unwrap();
println!("密码验证: {}", if is_valid { "成功" } else { "失败" });
let is_wrong = verify("wrong_password", &hashed).unwrap();
println!("错误密码验证: {}", if is_wrong { "成功" } else { "失败" });
}
}
盐值(Salt)的作用:每个用户的密码使用不同的随机盐值进行哈希,即使两个用户使用相同的密码,其哈希值也完全不同,从而有效防御彩虹表攻击。
31.7.4 区块链
哈希算法是区块链技术的基石,在区块链中有以下关键应用:
-
区块哈希:每个区块的头部包含前一个区块的哈希值,形成链式结构 $$H(\text{Block}_i) = H(\text{prev_hash} | \text{transactions} | \text{timestamp} | \text{nonce})$$
-
工作量证明(Proof of Work):矿工需要不断调整 nonce 值,使得区块哈希值小于目标值 $$H(\text{block_header}) < \text{target}$$
-
交易哈希:每笔交易也有自己的哈希值,用于唯一标识和验证
-
Merkle 树:将所有交易的哈希值组织成 Merkle 树,实现高效的交易验证
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
/// 简化的区块结构
#[derive(Debug, Clone)]
struct Block {
index: u64,
timestamp: u64,
data: String,
previous_hash: String,
nonce: u64,
}
impl Block {
fn calculate_hash(&self) -> String {
let input = format!("{}{}{}{}{}",
self.index, self.timestamp, self.data,
self.previous_hash, self.nonce);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
format!("{:x}", hasher.finalize())
}
/// 简单的工作量证明:找到 nonce 使得哈希值以 "0000" 开头
fn mine(&mut self, difficulty: usize) {
let target = "0".repeat(difficulty);
loop {
let hash = self.calculate_hash();
if hash.starts_with(&target) {
println!("挖矿成功!nonce={}, hash={}", self.nonce, hash);
break;
}
self.nonce += 1;
}
}
}
#[test]
fn simple_blockchain() {
let genesis = Block {
index: 0,
timestamp: 1700000000,
data: "Genesis Block".to_string(),
previous_hash: "0".repeat(64),
nonce: 0,
};
let mut block = Block {
index: 1,
timestamp: 1700000100,
data: "Transfer 1 BTC to Alice".to_string(),
previous_hash: genesis.calculate_hash(),
nonce: 0,
};
println!("开始挖矿(难度=4)...");
block.mine(4);
}
}
31.8 Rust 哈希库推荐
31.8.1 RustCrypto 生态(推荐)
RustCrypto: Hashes Collection of cryptographic hash functions written in pure Rust
RustCrypto 是 Rust 生态中最活跃、最权威的密码学库集合,所有实现均为纯 Rust 编写,经过严格审计:
| Crate | 算法 | 版本 |
|---|---|---|
md-5 | MD5 | 最新 |
sha1 | SHA-1 | 最新 |
sha2 | SHA-224/256/384/512 | 最新 |
sha3 | SHA-3 (Keccak) | 最新 |
blake2 | BLAKE2b/BLAKE2s | 最新 |
digest | 通用 Digest trait | 最新 |
Cargo.toml 配置:
[dependencies]
sha2 = "0.10"
sha3 = "0.10"
md-5 = "0.10"
digest = "0.10"
hmac = "0.12"
31.8.2 其他推荐库
| 库 | 用途 | 说明 |
|---|---|---|
bcrypt | 密码哈希 | 基于 Blowfish,广泛用于 Web 应用 |
argon2 | 密码哈希 | 密码哈希竞赛冠军,抗 GPU/ASIC 攻击 |
pbkdf2 | 密钥派生 | 基于 HMAC 的密钥派生函数 |
sm3 | 国密 SM3 | SM3 密码杂凑算法的 Rust 实现 |
ring | 通用密码学 | Google 维护,C 后端,性能优秀 |
31.8.3 统一 Digest 接口
RustCrypto 的 digest crate 提供了统一的 Digest trait,所有哈希算法都实现该 trait,使得代码可以在不同算法间轻松切换:
#![allow(unused)]
fn main() {
use digest::{Digest, Output};
use sha2::Sha256;
use sha3::Sha3_256;
/// 通用哈希计算函数,支持任何实现 Digest trait 的哈希算法
fn compute_hash<D: Digest>(data: &[u8]) -> Output<D> {
let mut hasher = D::new();
hasher.update(data);
hasher.finalize()
}
#[test]
fn generic_hash() {
let data = b"hello world";
let sha256_result = compute_hash::<Sha256>(data);
println!("SHA-256: {:x}", sha256_result);
let sha3_result = compute_hash::<Sha3_256>(data);
println!("SHA3-256: {:x}", sha3_result);
}
}
31.9 常用算法汇总:MD5、SHA2、SHA3
#![allow(unused)]
fn main() {
extern crate chrono;
extern crate crypto;
use crypto::digest::Digest;
use crypto::md5::Md5;
use crypto::sha2::Sha256;
use crypto::sha3::Sha3;
#[test]
fn rust_crypt() {
let mut sh = Md5::new();
sh.input_str("123456");
let md5_str = sh.result_str();
println!("md5_str:{}", md5_str);
let mut sha256 = Sha256::new();
sha256.input_str("helloworld");
let hex1 = sha256.result_str();
println!("hex1:{}", hex1);
// create a SHA3-256 object
let mut hasher = Sha3::sha3_256();
// write input message
hasher.input_str("helloworld");
// read hash digest
// let res = hex.from_hex().unwrap();
let hex2 = hasher.result_str();
println!("hex2:{}", hex2);
}
}
$ echo -n "123456" | openssl md5
(stdin)= e10adc3949ba59abbe56e057f20f883e
$ echo -n "helloworld" | openssl sha256
(stdin)= 936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af
$ echo -n "helloworld" | openssl sha3-256
(stdin)= 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
31.10 总结
核心知识点
| 知识点 | 要点 |
|---|---|
| 哈希函数本质 | 单向函数:$H: {0,1}^* \rightarrow {0,1}^n$,不可逆 |
| 三大安全性质 | 抗原像、抗第二原像、抗碰撞(强度递增) |
| 雪崩效应 | 输入微小变化导致输出剧烈变化 |
| MD5 | 128位,已破解,仅用于非安全场景 |
| SHA-1 | 160位,已破解,已弃用 |
| SHA-256 | 256位,SHA-2家族,目前安全,广泛使用 |
| SHA-512 | 512位,64位平台性能优秀 |
| SHA-3 | 海绵结构,天然免疫长度扩展攻击 |
| SM3 | 国密标准,256位,与 SHA-256 同级安全 |
| 长度扩展攻击 | Merkle-Damgard 结构固有缺陷 |
| HMAC | 使用 HMAC 防御长度扩展攻击 |
| 密码存储 | 使用 bcrypt/Argon2,不要直接用 SHA-256 |
| 区块链 | 哈希是区块链的基石(链式结构、工作量证明) |
算法选择决策树
需要密码哈希? ─── 是 ──→ 使用 bcrypt 或 Argon2
│
否
│
需要国密合规? ─── 是 ──→ 使用 SM3
│
否
│
需要抗长度扩展攻击? ─── 是 ──→ 使用 SHA-3
│
否
│
通用安全场景 ──→ 使用 SHA-256 或 SHA-512
│
非安全场景(校验等)──→ 使用 MD5
31.11 练习题
练习一:基础概念
- 请解释哈希函数的三大安全性质(抗原像、抗第二原像、抗碰撞),并说明它们之间的蕴含关系。
- 为什么 128 位哈希的碰撞安全强度只有 64 位?请用生日悖论解释。
- MD5 和 SHA-1 为什么不再安全?它们各自是在何时被破解的?
练习二:编程实践
-
编写一个 Rust 函数,接收文件路径和算法名称(“md5”、“sha256”、“sha3-256”),返回对应的哈希值。要求使用 RustCrypto 的
digesttrait 实现泛型编程。 -
编写一个 Rust 程序,对比同一文件在不同哈希算法下的计算耗时(MD5、SHA-256、SHA-512、SHA3-256),输出性能对比表格。
-
使用
hmac和sha2crate 实现 API 请求签名功能,模拟支付宝/微信支付的签名流程。
练习三:安全分析
-
假设一个系统使用
H(secret_key || user_input)作为认证令牌,请分析其存在的安全风险,并给出修复方案。 -
为什么密码存储应该使用 bcrypt/Argon2 而不是 SHA-256?请从计算速度、盐值机制、抗暴力破解等角度分析。
-
在区块链中,如果两个矿工同时找到满足难度目标的 nonce,会发生什么?请分析区块链如何处理这种情况。
练习四:扩展思考
-
SHA-3 为什么要采用与 SHA-2 完全不同的海绵结构?如果 SHA-2 也被破解了,SHA-3 的安全性是否会受到影响?
-
比特币使用 SHA-256 进行双重哈希(
SHA256(SHA256(x))),请分析这种设计的原因。