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/// Compute the SHA-1 of `data`. Returns the 20-byte digest.
20pub fn sha1(data: &[u8]) -> [u8; 20] {
21    let mut h: [u32; 5] = [
22        0x6745_2301,
23        0xEFCD_AB89,
24        0x98BA_DCFE,
25        0x1032_5476,
26        0xC3D2_E1F0,
27    ];
28
29    // Pre-processing: append `1` bit, then `0` bits until length ≡ 448 (mod 512),
30    // then 64-bit big-endian original-length-in-bits.
31    let bit_len: u64 = (data.len() as u64) * 8;
32    let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 72);
33    buf.extend_from_slice(data);
34    buf.push(0x80);
35    while buf.len() % 64 != 56 {
36        buf.push(0);
37    }
38    buf.extend_from_slice(&bit_len.to_be_bytes());
39    debug_assert_eq!(buf.len() % 64, 0);
40
41    for chunk in buf.chunks_exact(64) {
42        compress(&mut h, chunk);
43    }
44
45    let mut out = [0u8; 20];
46    for (i, &word) in h.iter().enumerate() {
47        out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
48    }
49    out
50}
51
52/// One 64-byte block of the SHA-1 compression function.
53fn compress(h: &mut [u32; 5], chunk: &[u8]) {
54    let mut w = [0u32; 80];
55    for (i, word) in chunk.chunks_exact(4).enumerate() {
56        w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
57    }
58    for i in 16..80 {
59        w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
60    }
61    let mut a = h[0];
62    let mut b = h[1];
63    let mut c = h[2];
64    let mut d = h[3];
65    let mut e = h[4];
66    for (i, &wi) in w.iter().enumerate() {
67        let (f, k) = match i {
68            0..=19 => ((b & c) | ((!b) & d), 0x5A82_7999),
69            20..=39 => (b ^ c ^ d, 0x6ED9_EBA1),
70            40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDC),
71            _ => (b ^ c ^ d, 0xCA62_C1D6),
72        };
73        let t = a
74            .rotate_left(5)
75            .wrapping_add(f)
76            .wrapping_add(e)
77            .wrapping_add(k)
78            .wrapping_add(wi);
79        e = d;
80        d = c;
81        c = b.rotate_left(30);
82        b = a;
83        a = t;
84    }
85    h[0] = h[0].wrapping_add(a);
86    h[1] = h[1].wrapping_add(b);
87    h[2] = h[2].wrapping_add(c);
88    h[3] = h[3].wrapping_add(d);
89    h[4] = h[4].wrapping_add(e);
90}
91
92/// Format a 20-byte SHA-1 digest as 40 lowercase ASCII hex chars.
93pub fn hex(digest: &[u8; 20]) -> [u8; 40] {
94    const HEX: &[u8; 16] = b"0123456789abcdef";
95    let mut out = [0u8; 40];
96    for (i, &byte) in digest.iter().enumerate() {
97        out[i * 2] = HEX[(byte >> 4) as usize];
98        out[i * 2 + 1] = HEX[(byte & 0x0f) as usize];
99    }
100    out
101}
102
103/// Parse a 40-character ASCII hex string into a SHA-1 digest.
104/// Returns `None` on malformed input (wrong length or non-hex chars).
105pub fn parse_hex(hex_str: &[u8]) -> Option<[u8; 20]> {
106    if hex_str.len() != 40 {
107        return None;
108    }
109    let mut out = [0u8; 20];
110    for (i, pair) in hex_str.chunks_exact(2).enumerate() {
111        let hi = hex_nibble(pair[0])?;
112        let lo = hex_nibble(pair[1])?;
113        out[i] = (hi << 4) | lo;
114    }
115    Some(out)
116}
117
118fn hex_nibble(b: u8) -> Option<u8> {
119    match b {
120        b'0'..=b'9' => Some(b - b'0'),
121        b'a'..=b'f' => Some(b - b'a' + 10),
122        b'A'..=b'F' => Some(b - b'A' + 10),
123        _ => None,
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    /// RFC 3174 / FIPS 180-1 — standard SHA-1 test vectors.
132    #[test]
133    fn empty_string() {
134        // SHA1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709
135        let d = sha1(b"");
136        assert_eq!(
137            hex(&d),
138            *b"da39a3ee5e6b4b0d3255bfef95601890afd80709"
139        );
140    }
141
142    #[test]
143    fn abc() {
144        // SHA1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d
145        let d = sha1(b"abc");
146        assert_eq!(
147            hex(&d),
148            *b"a9993e364706816aba3e25717850c26c9cd0d89d"
149        );
150    }
151
152    #[test]
153    fn quick_brown_fox() {
154        // SHA1("The quick brown fox jumps over the lazy dog")
155        //   = 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
156        let d = sha1(b"The quick brown fox jumps over the lazy dog");
157        assert_eq!(
158            hex(&d),
159            *b"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
160        );
161    }
162
163    #[test]
164    fn quick_brown_fox_dot() {
165        // SHA1("The quick brown fox jumps over the lazy cog")
166        //   = de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3
167        let d = sha1(b"The quick brown fox jumps over the lazy cog");
168        assert_eq!(
169            hex(&d),
170            *b"de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3"
171        );
172    }
173
174    #[test]
175    fn fips180_56_byte_msg() {
176        // SHA1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")
177        //   = 84983e441c3bd26ebaae4aa1f95129e5e54670f1
178        let d = sha1(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq");
179        assert_eq!(
180            hex(&d),
181            *b"84983e441c3bd26ebaae4aa1f95129e5e54670f1"
182        );
183    }
184
185    #[test]
186    fn eight_byte_return_1() {
187        // openssl says SHA1("return 1") = e0e1f9fabfc9d4800c877a703b823ac0578ff8db
188        let d = sha1(b"return 1");
189        assert_eq!(
190            hex(&d),
191            *b"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
192        );
193    }
194
195    #[test]
196    fn four_byte_input() {
197        // openssl: SHA1("1234") = 7110eda4d09e062aa5e4a390b0a572ac0d2c0220
198        let d = sha1(b"1234");
199        assert_eq!(
200            hex(&d),
201            *b"7110eda4d09e062aa5e4a390b0a572ac0d2c0220"
202        );
203    }
204
205    #[test]
206    fn fips180_one_million_a() {
207        // SHA1("a" × 1_000_000) = 34aa973cd4c4daa4f61eeb2bdbad27316534016f
208        let data = vec![b'a'; 1_000_000];
209        let d = sha1(&data);
210        assert_eq!(
211            hex(&d),
212            *b"34aa973cd4c4daa4f61eeb2bdbad27316534016f"
213        );
214    }
215
216    #[test]
217    fn hex_round_trips_through_parse_hex() {
218        let d1 = sha1(b"kevy");
219        let h = hex(&d1);
220        let d2 = parse_hex(&h).expect("valid hex");
221        assert_eq!(d1, d2);
222    }
223
224    #[test]
225    fn parse_hex_rejects_wrong_length() {
226        assert!(parse_hex(b"too short").is_none());
227        assert!(parse_hex(&[b'a'; 41]).is_none());
228    }
229
230    #[test]
231    fn parse_hex_rejects_non_hex_chars() {
232        assert!(parse_hex(b"zzz3e364706816aba3e25717850c26c9cd0d89d").is_none());
233    }
234
235    #[test]
236    fn parse_hex_accepts_uppercase() {
237        let d1 = sha1(b"abc");
238        let lower = hex(&d1);
239        let upper: Vec<u8> = lower
240            .iter()
241            .map(|&b| b.to_ascii_uppercase())
242            .collect();
243        let d2 = parse_hex(&upper).expect("upper-hex valid");
244        assert_eq!(d1, d2);
245    }
246}