Skip to main content

perfgate_sha256/
lib.rs

1//! Minimal SHA-256 implementation for perfgate fingerprinting.
2//!
3//! This crate provides a `#![no_std]`-compatible SHA-256 hash function
4//! that returns a hexadecimal string. It's designed for fingerprinting
5//! and identification purposes, not for cryptographic security.
6//!
7//! Part of the [perfgate](https://github.com/EffortlessMetrics/perfgate) workspace.
8//!
9//! # Features
10//!
11//! - `std` (default): Enable `std` support
12//! - Without `std`: Uses `alloc::string::String`
13//!
14//! # Example
15//!
16//! ```
17//! use perfgate_sha256::sha256_hex;
18//!
19//! let hash = sha256_hex(b"hello");
20//! assert_eq!(hash, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
21//!
22//! let empty_hash = sha256_hex(b"");
23//! assert_eq!(empty_hash, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
24//! ```
25
26#![no_std]
27
28#[cfg(feature = "std")]
29extern crate std;
30
31extern crate alloc;
32
33use alloc::string::String;
34
35/// SHA-256 round constants.
36const K: [u32; 64] = [
37    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
38    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
39    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
40    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
41    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
42    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
43    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
44    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
45];
46
47/// Initial hash values (first 32 bits of the fractional parts of the
48/// square roots of the first 8 primes 2..19).
49const H0: [u32; 8] = [
50    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
51];
52
53/// Compute SHA-256 hash and return as lowercase hexadecimal string.
54///
55/// # Arguments
56///
57/// * `data` - Input bytes to hash
58///
59/// # Returns
60///
61/// A 64-character lowercase hexadecimal string representing the SHA-256 hash.
62///
63/// # Example
64///
65/// ```
66/// use perfgate_sha256::sha256_hex;
67///
68/// let hash = sha256_hex(b"hello world");
69/// assert_eq!(hash.len(), 64);
70/// assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
71/// ```
72pub fn sha256_hex(data: &[u8]) -> String {
73    let mut h = H0;
74
75    let ml = (data.len() as u64) * 8;
76    let mut padded = alloc::vec::Vec::with_capacity(data.len() + 65);
77    padded.extend_from_slice(data);
78    padded.push(0x80);
79
80    while (padded.len() % 64) != 56 {
81        padded.push(0x00);
82    }
83
84    padded.extend_from_slice(&ml.to_be_bytes());
85
86    for chunk in padded.chunks(64) {
87        let mut w = [0u32; 64];
88
89        for (i, word_bytes) in chunk.chunks(4).enumerate() {
90            w[i] = u32::from_be_bytes([word_bytes[0], word_bytes[1], word_bytes[2], word_bytes[3]]);
91        }
92
93        for i in 16..64 {
94            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
95            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
96            w[i] = w[i - 16]
97                .wrapping_add(s0)
98                .wrapping_add(w[i - 7])
99                .wrapping_add(s1);
100        }
101
102        let mut a = h[0];
103        let mut b = h[1];
104        let mut c = h[2];
105        let mut d = h[3];
106        let mut e = h[4];
107        let mut f = h[5];
108        let mut g = h[6];
109        let mut hh = h[7];
110
111        for i in 0..64 {
112            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
113            let ch = (e & f) ^ ((!e) & g);
114            let temp1 = hh
115                .wrapping_add(s1)
116                .wrapping_add(ch)
117                .wrapping_add(K[i])
118                .wrapping_add(w[i]);
119            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
120            let maj = (a & b) ^ (a & c) ^ (b & c);
121            let temp2 = s0.wrapping_add(maj);
122
123            hh = g;
124            g = f;
125            f = e;
126            e = d.wrapping_add(temp1);
127            d = c;
128            c = b;
129            b = a;
130            a = temp1.wrapping_add(temp2);
131        }
132
133        h[0] = h[0].wrapping_add(a);
134        h[1] = h[1].wrapping_add(b);
135        h[2] = h[2].wrapping_add(c);
136        h[3] = h[3].wrapping_add(d);
137        h[4] = h[4].wrapping_add(e);
138        h[5] = h[5].wrapping_add(f);
139        h[6] = h[6].wrapping_add(g);
140        h[7] = h[7].wrapping_add(hh);
141    }
142
143    let mut result = String::with_capacity(64);
144    for val in h.iter() {
145        let _ = core::fmt::write(&mut result, core::format_args!("{:08x}", val));
146    }
147    result
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use alloc::vec;
154    use alloc::vec::Vec;
155    use proptest::prelude::*;
156
157    fn is_valid_hex(s: &str) -> bool {
158        s.len() == 64 && s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
159    }
160
161    #[test]
162    fn nist_empty_string() {
163        let hash = sha256_hex(b"");
164        assert_eq!(
165            hash,
166            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
167        );
168    }
169
170    #[test]
171    fn nist_abc() {
172        let hash = sha256_hex(b"abc");
173        assert_eq!(
174            hash,
175            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
176        );
177    }
178
179    #[test]
180    fn nist_hello() {
181        let hash = sha256_hex(b"hello");
182        assert_eq!(
183            hash,
184            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
185        );
186    }
187
188    #[test]
189    fn nist_hello_world() {
190        let hash = sha256_hex(b"hello world");
191        assert_eq!(
192            hash,
193            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
194        );
195    }
196
197    #[test]
198    fn nist_abc_long() {
199        let input = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
200        let hash = sha256_hex(input);
201        assert_eq!(
202            hash,
203            "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
204        );
205    }
206
207    #[test]
208    fn nist_one_million_a() {
209        let input: Vec<u8> = vec![b'a'; 1_000_000];
210        let hash = sha256_hex(&input);
211        assert_eq!(
212            hash,
213            "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"
214        );
215    }
216
217    #[test]
218    fn single_byte_zero() {
219        let hash = sha256_hex(b"\x00");
220        assert_eq!(
221            hash,
222            "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"
223        );
224    }
225
226    #[test]
227    fn single_byte_ff() {
228        let hash = sha256_hex(b"\xff");
229        assert_eq!(
230            hash,
231            "a8100ae6aa1940d0b663bb31cd466142ebbdbd5187131b92d93818987832eb89"
232        );
233    }
234
235    #[test]
236    fn large_input_1mb() {
237        let input: Vec<u8> = (0..=255u8).cycle().take(1_048_576).collect();
238        let hash = sha256_hex(&input);
239        assert_eq!(hash.len(), 64);
240        assert!(is_valid_hex(&hash));
241    }
242
243    #[test]
244    fn output_length_is_64() {
245        assert_eq!(sha256_hex(b"").len(), 64);
246        assert_eq!(sha256_hex(b"a").len(), 64);
247        assert_eq!(sha256_hex(b"abc").len(), 64);
248        assert_eq!(sha256_hex(&vec![0u8; 1000]).len(), 64);
249    }
250
251    #[test]
252    fn output_is_lowercase_hex() {
253        let hash = sha256_hex(b"test");
254        assert!(is_valid_hex(&hash), "Output should be lowercase hex");
255    }
256
257    proptest! {
258        #![proptest_config(ProptestConfig::with_cases(200))]
259
260        #[test]
261        fn determinism(bytes in proptest::collection::vec(any::<u8>(), 0..1000)) {
262            let hash1 = sha256_hex(&bytes);
263            let hash2 = sha256_hex(&bytes);
264            prop_assert_eq!(hash1, hash2, "Same input should produce same output");
265        }
266
267        #[test]
268        fn length_invariant(bytes in proptest::collection::vec(any::<u8>(), 0..1000)) {
269            let hash = sha256_hex(&bytes);
270            prop_assert_eq!(hash.len(), 64, "Output should always be 64 hex chars");
271        }
272
273        #[test]
274        fn hex_charset(bytes in proptest::collection::vec(any::<u8>(), 0..1000)) {
275            let hash = sha256_hex(&bytes);
276            prop_assert!(
277                hash.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
278                "Output should only contain [0-9a-f]"
279            );
280        }
281
282        #[test]
283        fn different_inputs_different_outputs(
284            a in proptest::collection::vec(any::<u8>(), 1..100),
285            b in proptest::collection::vec(any::<u8>(), 1..100)
286        ) {
287            prop_assume!(a != b);
288            let hash_a = sha256_hex(&a);
289            let hash_b = sha256_hex(&b);
290            prop_assert_ne!(hash_a, hash_b, "Different inputs should produce different outputs");
291        }
292    }
293}