Skip to main content

kevy_lua/
sha1.rs

1//! Hand-rolled SHA-1 (RFC 3174 / FIPS 180-1).
2//!
3//! Used by the SCRIPT LOAD / EVALSHA cache key and the
4//! `redis.sha1hex` host fn. Kevy's L2 lockdown forbids crates.io
5//! third-party deps; SHA-1 is short enough to write once and lint
6//! against well-known test vectors.
7//!
8//! ## NOT a security primitive
9//!
10//! SHA-1 is broken for collision resistance (SHAttered, 2017+).
11//! That doesn't matter here — kevy uses it as a content-addressed
12//! cache key the same way Redis does, where collisions would only
13//! cause cross-script cache hits (which never produces a security
14//! issue) and Redis itself uses SHA-1 for the same reason.
15//!
16//! If kevy ever needs SHA-1 / SHA-256 for an actual security-bearing
17//! purpose, that's a separate `kevy-crypto` stone, not here.
18//!
19//! ```
20//! use kevy_lua::sha1;
21//! let digest = sha1::sha1(b"abc");
22//! let hex = sha1::hex(&digest);
23//! assert_eq!(&hex, b"a9993e364706816aba3e25717850c26c9cd0d89d");
24//! assert_eq!(sha1::parse_hex(&hex), Some(digest));
25//! ```
26
27/// Compute the SHA-1 of `data`. Returns the 20-byte digest.
28///
29/// ```
30/// let d = kevy_lua::sha1::sha1(b"");
31/// assert_eq!(d[..4], [0xda, 0x39, 0xa3, 0xee]);
32/// ```
33pub fn sha1(data: &[u8]) -> [u8; 20] {
34    let mut h: [u32; 5] = [0x6745_2301, 0xEFCD_AB89, 0x98BA_DCFE, 0x1032_5476, 0xC3D2_E1F0];
35
36    // Pre-processing: append `1` bit, then `0` bits until length ≡ 448 (mod 512),
37    // then 64-bit big-endian original-length-in-bits.
38    let bit_len: u64 = (data.len() as u64) * 8;
39    let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 72);
40    buf.extend_from_slice(data);
41    buf.push(0x80);
42    while buf.len() % 64 != 56 {
43        buf.push(0);
44    }
45    buf.extend_from_slice(&bit_len.to_be_bytes());
46    debug_assert_eq!(buf.len() % 64, 0);
47
48    for chunk in buf.as_chunks::<64>().0 {
49        compress(&mut h, chunk);
50    }
51
52    let mut out = [0u8; 20];
53    for (i, &word) in h.iter().enumerate() {
54        out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
55    }
56    out
57}
58
59/// One 64-byte block of the SHA-1 compression function.
60fn compress(h: &mut [u32; 5], chunk: &[u8]) {
61    let mut w = [0u32; 80];
62    for (i, word) in chunk.as_chunks::<4>().0.iter().enumerate() {
63        w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
64    }
65    for i in 16..80 {
66        w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
67    }
68    let mut a = h[0];
69    let mut b = h[1];
70    let mut c = h[2];
71    let mut d = h[3];
72    let mut e = h[4];
73    for (i, &wi) in w.iter().enumerate() {
74        let (f, k) = match i {
75            0..=19 => ((b & c) | ((!b) & d), 0x5A82_7999),
76            20..=39 => (b ^ c ^ d, 0x6ED9_EBA1),
77            40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDC),
78            _ => (b ^ c ^ d, 0xCA62_C1D6),
79        };
80        let t = a.rotate_left(5).wrapping_add(f).wrapping_add(e).wrapping_add(k).wrapping_add(wi);
81        e = d;
82        d = c;
83        c = b.rotate_left(30);
84        b = a;
85        a = t;
86    }
87    h[0] = h[0].wrapping_add(a);
88    h[1] = h[1].wrapping_add(b);
89    h[2] = h[2].wrapping_add(c);
90    h[3] = h[3].wrapping_add(d);
91    h[4] = h[4].wrapping_add(e);
92}
93
94/// Format a 20-byte SHA-1 digest as 40 lowercase ASCII hex chars.
95///
96/// ```
97/// let mut d = [0u8; 20];
98/// d[0] = 0xAB;
99/// assert_eq!(&kevy_lua::sha1::hex(&d)[..4], b"ab00");
100/// ```
101pub fn hex(digest: &[u8; 20]) -> [u8; 40] {
102    const HEX: &[u8; 16] = b"0123456789abcdef";
103    let mut out = [0u8; 40];
104    for (i, &byte) in digest.iter().enumerate() {
105        out[i * 2] = HEX[(byte >> 4) as usize];
106        out[i * 2 + 1] = HEX[(byte & 0x0f) as usize];
107    }
108    out
109}
110
111/// Parse a 40-character ASCII hex string into a SHA-1 digest.
112/// Returns `None` on malformed input (wrong length or non-hex chars).
113///
114/// ```
115/// use kevy_lua::sha1::parse_hex;
116/// let empty = kevy_lua::sha1::sha1(b"");
117/// assert_eq!(parse_hex(b"DA39A3EE5E6B4B0D3255BFEF95601890AFD80709"), Some(empty));
118/// assert_eq!(parse_hex(b"da39"), None);
119/// assert_eq!(parse_hex(&[b'z'; 40]), None);
120/// ```
121pub fn parse_hex(hex_str: &[u8]) -> Option<[u8; 20]> {
122    if hex_str.len() != 40 {
123        return None;
124    }
125    let mut out = [0u8; 20];
126    for (i, pair) in hex_str.as_chunks::<2>().0.iter().enumerate() {
127        let hi = hex_nibble(pair[0])?;
128        let lo = hex_nibble(pair[1])?;
129        out[i] = (hi << 4) | lo;
130    }
131    Some(out)
132}
133
134fn hex_nibble(b: u8) -> Option<u8> {
135    match b {
136        b'0'..=b'9' => Some(b - b'0'),
137        b'a'..=b'f' => Some(b - b'a' + 10),
138        b'A'..=b'F' => Some(b - b'A' + 10),
139        _ => None,
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    /// RFC 3174 / FIPS 180-1 — standard SHA-1 test vectors.
148    #[test]
149    fn empty_string() {
150        // SHA1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
151        let d = sha1(b"");
152        assert_eq!(hex(&d), *b"da39a3ee5e6b4b0d3255bfef95601890afd80709");
153    }
154
155    #[test]
156    fn abc() {
157        // SHA1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
158        let d = sha1(b"abc");
159        assert_eq!(hex(&d), *b"a9993e364706816aba3e25717850c26c9cd0d89d");
160    }
161
162    #[test]
163    fn quick_brown_fox() {
164        // SHA1("The quick brown fox jumps over the lazy dog")
165        //   = 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
166        let d = sha1(b"The quick brown fox jumps over the lazy dog");
167        assert_eq!(hex(&d), *b"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
168    }
169
170    #[test]
171    fn quick_brown_fox_dot() {
172        // SHA1("The quick brown fox jumps over the lazy cog")
173        //   = de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3
174        let d = sha1(b"The quick brown fox jumps over the lazy cog");
175        assert_eq!(hex(&d), *b"de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3");
176    }
177
178    #[test]
179    fn fips180_56_byte_msg() {
180        // SHA1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
181        //   = 84983e441c3bd26ebaae4aa1f95129e5e54670f1
182        let d = sha1(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq");
183        assert_eq!(hex(&d), *b"84983e441c3bd26ebaae4aa1f95129e5e54670f1");
184    }
185
186    #[test]
187    fn eight_byte_return_1() {
188        // openssl says SHA1("return 1") = e0e1f9fabfc9d4800c877a703b823ac0578ff8db
189        let d = sha1(b"return 1");
190        assert_eq!(hex(&d), *b"e0e1f9fabfc9d4800c877a703b823ac0578ff8db");
191    }
192
193    #[test]
194    fn four_byte_input() {
195        // openssl: SHA1("1234") = 7110eda4d09e062aa5e4a390b0a572ac0d2c0220
196        let d = sha1(b"1234");
197        assert_eq!(hex(&d), *b"7110eda4d09e062aa5e4a390b0a572ac0d2c0220");
198    }
199
200    #[test]
201    fn fips180_one_million_a() {
202        // SHA1("a" × 1_000_000) = 34aa973cd4c4daa4f61eeb2bdbad27316534016f
203        let data = vec![b'a'; 1_000_000];
204        let d = sha1(&data);
205        assert_eq!(hex(&d), *b"34aa973cd4c4daa4f61eeb2bdbad27316534016f");
206    }
207
208    #[test]
209    fn hex_round_trips_through_parse_hex() {
210        let d1 = sha1(b"kevy");
211        let h = hex(&d1);
212        let d2 = parse_hex(&h).expect("valid hex");
213        assert_eq!(d1, d2);
214    }
215
216    #[test]
217    fn parse_hex_rejects_wrong_length() {
218        assert!(parse_hex(b"too short").is_none());
219        assert!(parse_hex(&[b'a'; 41]).is_none());
220    }
221
222    #[test]
223    fn parse_hex_rejects_non_hex_chars() {
224        assert!(parse_hex(b"zzz3e364706816aba3e25717850c26c9cd0d89d").is_none());
225    }
226
227    #[test]
228    fn parse_hex_accepts_uppercase() {
229        let d1 = sha1(b"abc");
230        let lower = hex(&d1);
231        let upper: Vec<u8> = lower.iter().map(|&b| b.to_ascii_uppercase()).collect();
232        let d2 = parse_hex(&upper).expect("upper-hex valid");
233        assert_eq!(d1, d2);
234    }
235}