Skip to main content

cryptography/hash/
sha1.rs

1//! SHA-1 from FIPS 180-4.
2//!
3//! SHA-1 is retained here for compatibility and HMAC support. It is no longer
4//! recommended for collision-sensitive applications due to practical chosen-prefix
5//! collision attacks.
6
7use super::Digest;
8
9// FIPS 180-4 ยง5.3.1 initial hash value H(0) for SHA-1.
10const IV: [u32; 5] = [
11    0x6745_2301,
12    0xEFCD_AB89,
13    0x98BA_DCFE,
14    0x1032_5476,
15    0xC3D2_E1F0,
16];
17
18#[inline]
19fn compress(state: &mut [u32; 5], block: &[u8; 64]) {
20    let mut schedule = [0u32; 80];
21    for (i, chunk) in block.chunks_exact(4).enumerate() {
22        schedule[i] = u32::from_be_bytes(chunk.try_into().unwrap());
23    }
24    for word_idx in 16..80 {
25        schedule[word_idx] = (schedule[word_idx - 3]
26            ^ schedule[word_idx - 8]
27            ^ schedule[word_idx - 14]
28            ^ schedule[word_idx - 16])
29            .rotate_left(1);
30    }
31
32    let mut a_reg = state[0];
33    let mut b_reg = state[1];
34    let mut c_reg = state[2];
35    let mut d_reg = state[3];
36    let mut e_reg = state[4];
37
38    for (round_idx, &schedule_word) in schedule.iter().enumerate() {
39        // FIPS 180-4 round partition:
40        // 0..19: Ch, 0x5A827999
41        // 20..39: Parity, 0x6ED9EBA1
42        // 40..59: Maj, 0x8F1BBCDC
43        // 60..79: Parity, 0xCA62C1D6
44        //
45        // These constants are floor(sqrt(n) * 2^30) for n=2,3,5,10.
46        let (round_mix, round_const) = match round_idx {
47            0..=19 => ((b_reg & c_reg) | ((!b_reg) & d_reg), 0x5A82_7999),
48            20..=39 => (b_reg ^ c_reg ^ d_reg, 0x6ED9_EBA1),
49            40..=59 => (
50                (b_reg & c_reg) | (b_reg & d_reg) | (c_reg & d_reg),
51                0x8F1B_BCDC,
52            ),
53            _ => (b_reg ^ c_reg ^ d_reg, 0xCA62_C1D6),
54        };
55        let next_a = a_reg
56            .rotate_left(5)
57            .wrapping_add(round_mix)
58            .wrapping_add(e_reg)
59            .wrapping_add(round_const)
60            .wrapping_add(schedule_word);
61        e_reg = d_reg;
62        d_reg = c_reg;
63        c_reg = b_reg.rotate_left(30);
64        b_reg = a_reg;
65        a_reg = next_a;
66    }
67
68    state[0] = state[0].wrapping_add(a_reg);
69    state[1] = state[1].wrapping_add(b_reg);
70    state[2] = state[2].wrapping_add(c_reg);
71    state[3] = state[3].wrapping_add(d_reg);
72    state[4] = state[4].wrapping_add(e_reg);
73}
74
75#[derive(Clone)]
76pub struct Sha1 {
77    state: [u32; 5],
78    block: [u8; 64],
79    pos: usize,
80    bit_len: u64,
81}
82
83impl Default for Sha1 {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl Sha1 {
90    pub const BLOCK_LEN: usize = 64;
91    pub const OUTPUT_LEN: usize = 20;
92
93    #[must_use]
94    pub fn new() -> Self {
95        Self {
96            state: IV,
97            block: [0u8; 64],
98            pos: 0,
99            bit_len: 0,
100        }
101    }
102
103    pub fn update(&mut self, mut data: &[u8]) {
104        while !data.is_empty() {
105            let take = (64 - self.pos).min(data.len());
106            self.block[self.pos..self.pos + take].copy_from_slice(&data[..take]);
107            self.pos += take;
108            data = &data[take..];
109
110            if self.pos == 64 {
111                compress(&mut self.state, &self.block);
112                self.block = [0u8; 64];
113                self.pos = 0;
114                self.bit_len = self.bit_len.wrapping_add(512);
115            }
116        }
117    }
118
119    #[must_use]
120    pub fn finalize(mut self) -> [u8; 20] {
121        self.bit_len = self.bit_len.wrapping_add((self.pos as u64) * 8);
122
123        self.block[self.pos] = 0x80;
124        self.pos += 1;
125
126        if self.pos > 56 {
127            self.block[self.pos..].fill(0);
128            compress(&mut self.state, &self.block);
129            self.block = [0u8; 64];
130            self.pos = 0;
131        }
132
133        self.block[self.pos..56].fill(0);
134        self.block[56..].copy_from_slice(&self.bit_len.to_be_bytes());
135        compress(&mut self.state, &self.block);
136
137        let mut out = [0u8; 20];
138        for (chunk, word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
139            chunk.copy_from_slice(&word.to_be_bytes());
140        }
141        out
142    }
143
144    #[must_use]
145    pub fn digest(data: &[u8]) -> [u8; 20] {
146        let mut h = Self::new();
147        h.update(data);
148        h.finalize()
149    }
150
151    fn finalize_into_reset(&mut self, out: &mut [u8; 20]) {
152        self.bit_len = self.bit_len.wrapping_add((self.pos as u64) * 8);
153
154        self.block[self.pos] = 0x80;
155        self.pos += 1;
156
157        if self.pos > 56 {
158            self.block[self.pos..].fill(0);
159            compress(&mut self.state, &self.block);
160            self.block = [0u8; 64];
161            self.pos = 0;
162        }
163
164        self.block[self.pos..56].fill(0);
165        self.block[56..].copy_from_slice(&self.bit_len.to_be_bytes());
166        compress(&mut self.state, &self.block);
167
168        for (chunk, word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
169            chunk.copy_from_slice(&word.to_be_bytes());
170        }
171
172        self.zeroize();
173    }
174}
175
176impl Digest for Sha1 {
177    const BLOCK_LEN: usize = 64;
178    const OUTPUT_LEN: usize = 20;
179
180    fn new() -> Self {
181        Self::new()
182    }
183
184    fn update(&mut self, data: &[u8]) {
185        self.update(data);
186    }
187
188    fn finalize_into(self, out: &mut [u8]) {
189        assert_eq!(out.len(), 20, "wrong digest length");
190        out.copy_from_slice(&self.finalize());
191    }
192
193    fn finalize_reset(&mut self, out: &mut [u8]) {
194        let out: &mut [u8; 20] = out.try_into().expect("wrong digest length");
195        self.finalize_into_reset(out);
196    }
197
198    fn zeroize(&mut self) {
199        crate::ct::zeroize_slice(self.state.as_mut_slice());
200        crate::ct::zeroize_slice(self.block.as_mut_slice());
201        self.pos = 0;
202        self.bit_len = 0;
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    fn hex(bytes: &[u8]) -> String {
211        let mut out = String::with_capacity(bytes.len() * 2);
212        for b in bytes {
213            use core::fmt::Write;
214            let _ = write!(&mut out, "{b:02x}");
215        }
216        out
217    }
218
219    #[test]
220    fn sha1_empty() {
221        assert_eq!(
222            hex(&Sha1::digest(b"")),
223            "da39a3ee5e6b4b0d3255bfef95601890afd80709"
224        );
225    }
226
227    #[test]
228    fn sha1_abc_streaming() {
229        let mut h = Sha1::new();
230        h.update(b"a");
231        h.update(b"b");
232        h.update(b"c");
233        assert_eq!(
234            hex(&h.finalize()),
235            "a9993e364706816aba3e25717850c26c9cd0d89d"
236        );
237    }
238
239    #[test]
240    fn sha1_matches_openssl() {
241        let msg = b"The quick brown fox jumps over the lazy dog";
242        let Some(expected) = crate::test_utils::run_openssl(&["dgst", "-sha1", "-binary"], msg) else {
243            return;
244        };
245        assert_eq!(Sha1::digest(msg).as_slice(), expected.as_slice());
246    }
247}