文件哈希(SHA-256)

由 Rust + WebAssembly 手写 SHA-256,文件完全在本地计算,不会上传

—
文件

将任意文件拖放到此处,或点击选择

大文件按 4 MB 分块流式计算,内存占用很小

计算完成后会再用浏览器内置的 Web Crypto 算一遍,交叉验证结果并对比速度

📖 原理说明

SHA-256 把任意长度的数据压缩成一个 256 位(64 个十六进制字符)的「指纹」。输入哪怕只改变 1 个比特,输出也会面目全非;而且无法从指纹反推原文,也几乎不可能找到两份指纹相同的不同数据。下载文件后核对校验值、Git 的对象 ID、比特币挖矿用的都是这类哈希函数。这里的 SHA-256 完全按照 FIPS 180-4 标准用 Rust 手写,文件只在你的浏览器里计算,不会上传。

🧮算法原理

Hi = Hi−1 + compress(Hi−1, Mi)Merkle–Damgård 结构:逐块压缩,状态向前传递
分块与填充
消息被切成 512 位(64 字节)的块。最后补一个 0x80 字节、若干个 0,再以大端序写入原始消息的比特长度(64 位),使总长度正好是 64 字节的整数倍。如果最后一块剩余空间不够放下长度字段,就要多补一整块,测试里 448 位的 NIST 向量正是这种情况。
消息扩展
每个 64 字节的块先拆成 16 个 32 位字,再用移位、循环右移和异或扩展成 64 个字 W₀…W₆₃。
64 轮压缩
8 个 32 位工作变量 a…h 经过 64 轮混合,每一轮用到选择函数 Ch、多数函数 Maj、两组循环右移组合 Σ₀/Σ₁,以及一个轮常数 Kᵢ(前 64 个素数立方根的小数部分)。所有加法都按 2³² 取模,在 Rust 里写成 wrapping_add。
流式计算
哈希状态只有 8 个字加一个不满 64 字节的缓冲区,所以可以一块一块地喂数据:sha256_init → 多次 sha256_update → sha256_finish。无论文件多大,内存占用都只取决于每次读取的块大小(4 MB)。

🔄Rust 与 JavaScript 的分工

  1. JS文本模式:每次输入都用 TextEncoder 把文字转成 UTF-8 字节,调用一次性函数 sha256_hex()。
  2. JS文件模式:用 File.slice() 每次读取 4 MB,转成 Uint8Array。
  3. Rust每块调用 sha256_update(块),Rust 在全局状态中累积压缩结果;全部读完后调用 sha256_finish(),完成填充并返回十六进制字符串。
  4. JSJS 再调用浏览器内置的 crypto.subtle.digest('SHA-256', …) 计算一遍,比较两者的结果和速度。

「生成并计算」会用 xorshift 生成确定性的伪随机数据,每次运行的哈希值都相同,方便在没有大文件时测试。

⚡性能要点

  • 参考数据(一台 Linux 测试机,Chromium 152,256 MB 测试数据):WASM 约 300 MB/s,Web Crypto 约 1,380 MB/s。
  • Web Crypto 快 4 倍多并不奇怪:它调用的是浏览器内置的原生加密库,还能用上 CPU 专门的 SHA 硬件指令(x86 的 SHA-NI、ARM 的 SHA2 扩展);WASM 目前没有这类指令,只能用普通的整数运算。
  • 那为什么还要在 WASM 里实现?Web Crypto 只提供少数几种标准算法,而且只能一次性处理整个缓冲区,没有流式接口。需要其他哈希算法(如 BLAKE3、xxHash),或者要分块处理超大文件时,WASM 是更灵活的选择。
  • Rust 实现中,完整的 64 字节块直接从输入切片读取(chunks_exact),不经过缓冲区复制;只有块边界上不满 64 字节的部分才进入内部缓冲。

源码crates/tools/sha256/src/lib.rswww/sha256/index.js