Skip to main content

modern_crypto_toolkit/
hash.rs

1//! Deterministic hashing and HMAC with safe, modern defaults.
2
3use std::fs::File;
4use std::io::Read;
5
6use blake2::{Blake2b512, Blake2s256};
7use digest::{Digest, DynDigest};
8use hmac::{Hmac, Mac};
9use sha2::{Sha256, Sha384, Sha512};
10use sha3::{Sha3_256, Sha3_384, Sha3_512};
11use subtle::ConstantTimeEq;
12
13use crate::error::{Error, Result};
14
15macro_rules! hmac_with {
16    ($key:expr, $data:expr, $alg:ty) => {{
17        let mut mac = Hmac::<$alg>::new_from_slice($key)
18            .map_err(|_| Error::InvalidKey("HMAC key length invalid".to_string()))?;
19        mac.update($data);
20        Ok(hex::encode(mac.finalize().into_bytes()))
21    }};
22}
23
24/// Supported digest algorithms. Parsing is centralized here so that
25/// `string`, `file`, and `hmac` cannot drift apart.
26#[derive(Clone, Copy, Debug)]
27enum Algorithm {
28    Sha256,
29    Sha384,
30    Sha512,
31    Sha3_256,
32    Sha3_384,
33    Sha3_512,
34    Blake2b,
35    Blake2s,
36}
37
38impl Algorithm {
39    fn parse(name: &str) -> Result<Self> {
40        match name.to_lowercase().as_str() {
41            "sha-256" | "sha256" => Ok(Algorithm::Sha256),
42            "sha-384" | "sha384" => Ok(Algorithm::Sha384),
43            "sha-512" | "sha512" => Ok(Algorithm::Sha512),
44            "sha3-256" | "sha3_256" => Ok(Algorithm::Sha3_256),
45            "sha3-384" | "sha3_384" => Ok(Algorithm::Sha3_384),
46            "sha3-512" | "sha3_512" => Ok(Algorithm::Sha3_512),
47            "blake2b" => Ok(Algorithm::Blake2b),
48            "blake2s" => Ok(Algorithm::Blake2s),
49            _ => Err(Error::UnsupportedAlgorithm(name.to_string())),
50        }
51    }
52
53    fn new_digest(&self) -> Box<dyn DynDigest> {
54        match self {
55            Algorithm::Sha256 => Box::new(Sha256::new()),
56            Algorithm::Sha384 => Box::new(Sha384::new()),
57            Algorithm::Sha512 => Box::new(Sha512::new()),
58            Algorithm::Sha3_256 => Box::new(Sha3_256::new()),
59            Algorithm::Sha3_384 => Box::new(Sha3_384::new()),
60            Algorithm::Sha3_512 => Box::new(Sha3_512::new()),
61            Algorithm::Blake2b => Box::new(Blake2b512::new()),
62            Algorithm::Blake2s => Box::new(Blake2s256::new()),
63        }
64    }
65
66    fn hmac(&self, key: &[u8], data: &[u8]) -> Result<String> {
67        match self {
68            Algorithm::Sha256 => hmac_with!(key, data, Sha256),
69            Algorithm::Sha384 => hmac_with!(key, data, Sha384),
70            Algorithm::Sha512 => hmac_with!(key, data, Sha512),
71            Algorithm::Sha3_256 => hmac_with!(key, data, Sha3_256),
72            Algorithm::Sha3_384 => hmac_with!(key, data, Sha3_384),
73            Algorithm::Sha3_512 => hmac_with!(key, data, Sha3_512),
74            // BLAKE2 is already keyed-capable and is not exposed for HMAC.
75            Algorithm::Blake2b | Algorithm::Blake2s => {
76                Err(Error::UnsupportedAlgorithm("blake2 hmac".to_string()))
77            }
78        }
79    }
80}
81
82/// Returns a safe, deterministic hex-encoded hash of `data`.
83pub fn string(data: &str, algorithm: &str) -> Result<String> {
84    let mut hasher = Algorithm::parse(algorithm)?.new_digest();
85    hasher.update(data.as_bytes());
86    Ok(hex::encode(hasher.finalize()))
87}
88
89/// Returns the hex-encoded hash of a file.
90pub fn file(path: &str, algorithm: &str) -> Result<String> {
91    let mut hasher = Algorithm::parse(algorithm)?.new_digest();
92    let mut file = File::open(path).map_err(|e| Error::Internal(e.to_string()))?;
93
94    let mut buf = [0u8; 8192];
95    loop {
96        let n = file.read(&mut buf).map_err(|e| Error::Internal(e.to_string()))?;
97        if n == 0 {
98            break;
99        }
100        hasher.update(&buf[..n]);
101    }
102    Ok(hex::encode(hasher.finalize()))
103}
104
105/// Returns a hex-encoded HMAC of `data` using `key` and `algorithm`.
106///
107/// `key` and `data` accept any `AsRef<[u8]>`, so both text (`&str`) and
108/// binary inputs (`&[u8]`, `Vec<u8>`, byte arrays) are supported.
109pub fn hmac(
110    key: impl AsRef<[u8]>,
111    data: impl AsRef<[u8]>,
112    algorithm: &str,
113) -> Result<String> {
114    Algorithm::parse(algorithm)?.hmac(key.as_ref(), data.as_ref())
115}
116
117/// Verifies a hex-encoded HMAC in constant time.
118pub fn verify_hmac(
119    mac: &str,
120    key: impl AsRef<[u8]>,
121    data: impl AsRef<[u8]>,
122    algorithm: &str,
123) -> Result<bool> {
124    let expected = hmac(key, data, algorithm)?;
125    let a = hex::decode(mac).map_err(|e| Error::Hex(e.to_string()))?;
126    let b = hex::decode(expected).map_err(|e| Error::Hex(e.to_string()))?;
127    if a.len() != b.len() {
128        return Ok(false);
129    }
130    Ok(a.as_slice().ct_eq(b.as_slice()).into())
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_sha256() {
139        let got = string("hello world", "sha-256").unwrap();
140        assert_eq!(got.len(), 64);
141    }
142
143    #[test]
144    fn test_hmac() {
145        let mac = hmac("key", "message", "sha-256").unwrap();
146        assert!(verify_hmac(&mac, "key", "message", "sha-256").unwrap());
147        assert!(!verify_hmac(&mac, "key", "tampered", "sha-256").unwrap());
148    }
149
150    #[test]
151    fn test_hmac_binary_inputs() {
152        let key = [0x00u8, 0xff, 0x10];
153        let data = [0xdeu8, 0xad, 0xbe, 0xef];
154        let mac = hmac(key, data, "sha-256").unwrap();
155        assert!(verify_hmac(&mac, key, data, "sha-256").unwrap());
156        assert!(!verify_hmac(&mac, key, b"other", "sha-256").unwrap());
157    }
158
159    #[test]
160    fn test_rejects_weak() {
161        assert!(string("test", "md5").is_err());
162        assert!(string("test", "sha-1").is_err());
163        assert!(string("test", "sha1").is_err());
164        assert!(file("nonexistent", "md5").is_err());
165        assert!(hmac("k", "d", "sha-1").is_err());
166        assert!(hmac("k", "d", "blake2b").is_err());
167    }
168}