用 sha2 计算哈希
你会学到什么
- 哈希(hash)是什么:把任意长度的数据映射成固定长度的“摘要”(digest)。
- 用
sha2的Sha256/Sha512配合Digesttrait 计算摘要。 - 用
hex::encode把二进制摘要转成人类可读的十六进制字符串。 - 写一个完整性校验函数,对比数据的哈希与给定的期望值。
- 两条安全红线:哈希不是存密码的,校验对比不是常数时间的。
什么是哈希
哈希函数有两个关键性质:
- 确定性:同样的输入,永远得到同样的输出。
- 雪崩效应:输入只要变一个比特,输出就会面目全非。
这让哈希非常适合做完整性校验 / 校验和(checksum)。比如你下载一个文件, 官网同时给出它的 SHA-256 值;把下载到的内容重新算一遍哈希再比对,相同就说明 文件完整、没被损坏或篡改。
最小示例
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"abc");
let digest = hasher.finalize(); // 32 字节的二进制摘要
let hex = hex::encode(digest); // 转成 64 个十六进制字符
assert_eq!(hex, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
运行代码
cd examples
cargo run -p rt_49_sha2
cargo test -p rt_49_sha2
代码组织
本章示例按职责拆成了多个文件,更贴近真实项目的组织方式:
49_sha2/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs # mod 声明 + pub use 重导出 + 库级文档
├── hashing.rs # sha256_hex / sha256_str / sha512_hex 计算函数
├── verify.rs # matches 完整性校验函数
└── main.rs # 瘦入口:use 库里的函数,跑一遍演示
每个模块都带有自己的单元测试(#[cfg(test)] mod tests),并且因为哈希是
完全确定的,测试里可以直接断言已知的“测试向量”(known test vector)。
代码讲解
三步走:new / update / finalize
sha2 把每种算法做成一个类型,计算流程固定为三步:
Sha256::new()创建一个 hasher;update(data)喂入数据——可以多次调用,等价于把这些数据拼接起来;finalize()得到固定长度的摘要(SHA-256 是 32 字节)。
finalize() 返回的是一个 GenericArray<u8, _>(一段定长字节数组),直接交给
hex::encode 就能拿到小写十六进制字符串。SHA-256 的摘要总是 64 个十六进制字符,
SHA-512 则是 128 个。
update 可以分块调用这一点很实用:处理大文件时,你可以一边读一边喂,不必把
整个文件读进内存。
Digest trait:换算法,代码不变
注意 sha512_hex 和 sha256_hex 的函数体几乎一字不差,区别只有把 Sha256
换成了 Sha512:
let mut hasher = Sha512::new();
hasher.update(data);
hex::encode(hasher.finalize())
之所以能这样“无痛替换”,是因为 new / update / finalize 都来自同一个
Digest trait。RustCrypto 生态里的一大批哈希 crate(sha2、sha3、blake2
……)都实现了这个 trait,所以掌握了一种用法,其余的全都会用。这正是 trait
作为“共享接口”的威力——把通用的调用方式抽出来,让不同实现可以互换。
校验完整性:matches
verify.rs 里的 matches 把数据重新哈希,再和给定的期望值比较:
pub fn matches(data: &[u8], expected_hex: &str) -> bool {
sha256_hex(data) == expected_hex.to_lowercase()
}
期望值在不同来源里可能写成大写或小写,所以两边都转成小写再比,避免“内容对了 但大小写不同”导致误判。
两条安全红线
这两点务必记住,它们是真实世界里最常见的误用:
- 不要用它存密码。 普通哈希算得太快,攻击者可以每秒尝试上亿次来暴力破解。
存口令请用专门的口令哈希算法,如
argon2或bcrypt——它们刻意算得很慢, 还带“加盐”(salt)。本章的 SHA-256 只适合做完整性校验。 matches的比较不是常数时间的。==一旦发现某个字符不同就提前返回, 这会泄漏“前几位对不对”的时间信息。对抗计时攻击的场景(例如校验消息认证码 MAC)应改用常数时间比较,如subtlecrate 的ConstantTimeEq。本章只做 教学用的完整性校验,普通比较已经够用。
几个可以直接断言的测试向量
哈希是确定的,所以这些值永远成立,可以放心写进测试:
SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
如果不想硬编码 SHA-512 那一长串,也可以退一步只断言“长度等于 128 个十六进制 字符”。
练习
- 给
verify加一个matches_sha512,复用sha512_hex做校验。 - 写一个函数读取一个文件并返回它的 SHA-256(用
update分块喂入,别一次性读完)。 - 把
Sha256换成sha3crate 里的Sha3_256,体会Digesttrait 带来的 “换算法几乎零改动”。
小结
哈希把任意数据压成定长摘要,确定且雪崩,是完整性校验的利器。sha2 + hex
就能算出标准摘要;而 Digest trait 让你在不同哈希算法间自由切换。切记:它不是
密码存储工具,普通比较也不是常数时间的。
下一步
前面的章节都在单线程里处理数据。当数据量大、计算密集时,可以用数据并行把活儿
摊到多个 CPU 核心上。下一章学习用 rayon 做并行迭代。
完整示例代码
下面是 examples/49_sha2/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/49_sha2/src/main.rs
//! 瘦入口:演示用 `sha2` 计算摘要、用 `hex` 编码,并做一次完整性校验。
use rt_49_sha2::{matches, sha256_str, sha512_hex};
fn main() {
println!("=== SHA-256 摘要(十六进制)===");
for text in ["", "abc", "hello", "hellp"] {
println!(" {:>7?} -> {}", text, sha256_str(text));
}
println!("\n=== 雪崩效应:输入差一个字符,输出完全不同 ===");
println!(" hello -> {}", sha256_str("hello"));
println!(" hellp -> {}", sha256_str("hellp"));
println!("\n=== SHA-512(128 个十六进制字符)===");
println!(" abc -> {}", sha512_hex(b"abc"));
println!("\n=== 完整性校验 ===");
let expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
println!(
" matches(b\"abc\", 正确值) = {}",
matches(b"abc", expected)
);
println!(
" matches(b\"abd\", 正确值) = {}",
matches(b"abd", expected)
);
} examples/49_sha2/src/lib.rs
//! 用 `sha2` 计算哈希摘要,并用 `hex` 把摘要编码为十六进制字符串。
//!
//! 哈希(hash)把任意长度的数据映射成固定长度的“摘要”(digest)。
//! 同样的输入永远得到同样的输出,且输入只要变一个比特,输出就会面目全非。
//! 这一性质让哈希非常适合做**完整性校验 / 校验和(checksum)**:
//! 下载文件后比对哈希,就能确认内容没被损坏或篡改。
//!
//! 本章覆盖:
//! - 用 `Sha256` / `Sha512` 配合 `Digest` trait 计算摘要;
//! - 用 `hex::encode` 把二进制摘要转成小写十六进制字符串;
//! - 写一个 `matches` 校验函数,对比数据的哈希与给定的期望值。
//!
//! 重要提醒:
//! - 这里的哈希用于**完整性校验**,**不是**用来存密码的。
//! 存密码请用专门的口令哈希算法(如 `argon2` / `bcrypt`)。
//! - 本章的校验对比**不是常数时间**的,仅作教学,不适合对抗计时攻击的场景。
//!
//! 代码按职责拆分为两个模块:
//! - [`hashing`]:`sha256_hex` / `sha256_str` / `sha512_hex` 等计算函数;
//! - [`verify`]:`matches` 校验函数。
pub mod hashing;
pub mod verify;
pub use hashing::{sha256_hex, sha256_str, sha512_hex};
pub use verify::matches; examples/49_sha2/src/hashing.rs
//! 计算 SHA-2 系列摘要,并编码成十六进制字符串。
//!
//! `sha2` crate 把每种哈希算法做成一个类型(`Sha256`、`Sha512` ……),
//! 它们都实现了同一个 [`Digest`](sha2::Digest) trait。因此“算哈希”的
//! 流程对所有算法完全一致,换算法只需换类型名。
use sha2::{Digest, Sha256, Sha512};
/// 计算一段字节数据的 SHA-256 摘要,返回小写十六进制字符串。
///
/// 计算分三步,所有 RustCrypto 的哈希都是这个套路:
/// 1. `Sha256::new()` 创建一个 hasher;
/// 2. `update(data)` 喂入数据(可多次调用,等价于把数据拼接起来);
/// 3. `finalize()` 得到固定长度的摘要(SHA-256 是 32 字节)。
///
/// 最后用 `hex::encode` 把这 32 个字节转成 64 个十六进制字符。
///
/// ```
/// // SHA-256("abc") 是一个广为人知的测试向量。
/// let h = rt_49_sha2::sha256_hex(b"abc");
/// assert_eq!(h, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
/// ```
pub fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let digest = hasher.finalize(); // GenericArray<u8, 32>
hex::encode(digest)
}
/// 便捷封装:直接对字符串文本计算 SHA-256(按 UTF-8 字节哈希)。
///
/// ```
/// let h = rt_49_sha2::sha256_str("abc");
/// assert_eq!(h.len(), 64); // SHA-256 永远是 64 个十六进制字符
/// ```
pub fn sha256_str(text: &str) -> String {
sha256_hex(text.as_bytes())
}
/// 计算一段字节数据的 SHA-512 摘要,返回小写十六进制字符串。
///
/// 注意代码和 [`sha256_hex`] 几乎一模一样——只是把 `Sha256` 换成了
/// `Sha512`。这正是 [`Digest`](sha2::Digest) trait 的价值:算法可替换,
/// 调用方式不变。SHA-512 的摘要是 64 字节,即 128 个十六进制字符。
///
/// ```
/// let h = rt_49_sha2::sha512_hex(b"abc");
/// assert_eq!(h.len(), 128);
/// ```
pub fn sha512_hex(data: &[u8]) -> String {
let mut hasher = Sha512::new();
hasher.update(data);
let digest = hasher.finalize(); // GenericArray<u8, 64>
hex::encode(digest)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha256_of_empty_input() {
// 空输入的 SHA-256 是一个经典常量。
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
);
}
#[test]
fn sha256_of_abc_matches_known_vector() {
assert_eq!(
sha256_str("abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
);
}
#[test]
fn sha256_is_deterministic() {
// 同样的输入永远得到同样的输出。
assert_eq!(sha256_str("hello"), sha256_str("hello"));
}
#[test]
fn one_bit_change_avalanche() {
// 输入只差一个字符,输出就完全不同(雪崩效应)。
assert_ne!(sha256_str("hello"), sha256_str("hellp"));
}
#[test]
fn sha512_has_128_hex_chars() {
assert_eq!(sha512_hex(b"abc").len(), 128);
}
#[test]
fn update_can_be_called_in_chunks() {
// 分块喂入和一次性喂入结果相同。
let mut hasher = Sha256::new();
hasher.update(b"foo");
hasher.update(b"bar");
let chunked = hex::encode(hasher.finalize());
assert_eq!(chunked, sha256_str("foobar"));
}
} examples/49_sha2/src/verify.rs
//! 校验数据完整性:把数据重新哈希,再和给定的期望摘要对比。
//!
//! 典型场景:你从网上下载了一个文件,页面同时给出它的 SHA-256 值。
//! 把下载到的内容重新算一遍哈希,和页面给的值比一比,相同就说明
//! 文件完整、没被篡改或损坏。
use crate::hashing::sha256_hex;
/// 校验 `data` 的 SHA-256 是否等于 `expected_hex`(不区分大小写)。
///
/// 期望值在不同地方可能写成大写或小写十六进制,所以这里两边都转成
/// 小写再比较,避免“内容对了但大小写不同”导致误判。
///
/// ```
/// let data = b"abc";
/// // 大写期望值同样能通过校验。
/// assert!(rt_49_sha2::matches(
/// data,
/// "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
/// ));
/// assert!(!rt_49_sha2::matches(data, "deadbeef"));
/// ```
///
/// 安全提示:这里用普通的字符串 `==` 比较,**不是常数时间**的——
/// 它一旦发现某个字符不同就提前返回。对抗计时攻击的场景(例如校验
/// 消息认证码 MAC)应改用常数时间比较(如 `subtle` crate 的
/// `ConstantTimeEq`)。本章只做完整性校验教学,普通比较已经够用。
pub fn matches(data: &[u8], expected_hex: &str) -> bool {
sha256_hex(data) == expected_hex.to_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
const ABC_SHA256: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
#[test]
fn matches_correct_lowercase_hash() {
assert!(matches(b"abc", ABC_SHA256));
}
#[test]
fn matches_is_case_insensitive() {
assert!(matches(b"abc", &ABC_SHA256.to_uppercase()));
}
#[test]
fn rejects_wrong_hash() {
assert!(!matches(b"abc", "deadbeef"));
}
#[test]
fn rejects_when_data_tampered() {
// 期望值是 "abc" 的哈希,但数据被改成了 "abd",校验应失败。
assert!(!matches(b"abd", ABC_SHA256));
}
} examples/49_sha2/Cargo.toml
[package]
name = "rt_49_sha2"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
sha2 = "0.10"
hex = "0.4"