Skip to main content

cryptography/hash/
md5.rs

1//! MD5 from RFC 1321.
2//!
3//! MD5 is retained for legacy compatibility. It is broken for collision
4//! resistance and should not be used for new integrity designs. It also keeps
5//! the Merkle-Damgaard length-extension property, so plain `MD5(key || msg)` is
6//! not a secure MAC construction.
7
8use super::Digest;
9
10// RFC 1321 §3.3 initial state words (A, B, C, D).
11const IV: [u32; 4] = [0x6745_2301, 0xEFCD_AB89, 0x98BA_DCFE, 0x1032_5476];
12
13// RFC 1321 §3.4 per-step left-rotation schedule.
14const S: [u32; 64] = [
15    7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
16    14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
17    21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
18];
19
20// RFC 1321 table T[i] = floor(2^32 * abs(sin(i+1))) for i=0..63.
21const K: [u32; 64] = [
22    0xd76a_a478,
23    0xe8c7_b756,
24    0x2420_70db,
25    0xc1bd_ceee,
26    0xf57c_0faf,
27    0x4787_c62a,
28    0xa830_4613,
29    0xfd46_9501,
30    0x6980_98d8,
31    0x8b44_f7af,
32    0xffff_5bb1,
33    0x895c_d7be,
34    0x6b90_1122,
35    0xfd98_7193,
36    0xa679_438e,
37    0x49b4_0821,
38    0xf61e_2562,
39    0xc040_b340,
40    0x265e_5a51,
41    0xe9b6_c7aa,
42    0xd62f_105d,
43    0x0244_1453,
44    0xd8a1_e681,
45    0xe7d3_fbc8,
46    0x21e1_cde6,
47    0xc337_07d6,
48    0xf4d5_0d87,
49    0x455a_14ed,
50    0xa9e3_e905,
51    0xfcef_a3f8,
52    0x676f_02d9,
53    0x8d2a_4c8a,
54    0xfffa_3942,
55    0x8771_f681,
56    0x6d9d_6122,
57    0xfde5_380c,
58    0xa4be_ea44,
59    0x4bde_cfa9,
60    0xf6bb_4b60,
61    0xbebf_bc70,
62    0x289b_7ec6,
63    0xeaa1_27fa,
64    0xd4ef_3085,
65    0x0488_1d05,
66    0xd9d4_d039,
67    0xe6db_99e5,
68    0x1fa2_7cf8,
69    0xc4ac_5665,
70    0xf429_2244,
71    0x432a_ff97,
72    0xab94_23a7,
73    0xfc93_a039,
74    0x655b_59c3,
75    0x8f0c_cc92,
76    0xffef_f47d,
77    0x8584_5dd1,
78    0x6fa8_7e4f,
79    0xfe2c_e6e0,
80    0xa301_4314,
81    0x4e08_11a1,
82    0xf753_7e82,
83    0xbd3a_f235,
84    0x2ad7_d2bb,
85    0xeb86_d391,
86];
87
88#[inline]
89fn compress(state: &mut [u32; 4], block: &[u8; 64]) {
90    let mut schedule = [0u32; 16];
91    for (i, chunk) in block.chunks_exact(4).enumerate() {
92        schedule[i] = u32::from_le_bytes(chunk.try_into().expect("4-byte chunk"));
93    }
94
95    let mut a_reg = state[0];
96    let mut b_reg = state[1];
97    let mut c_reg = state[2];
98    let mut d_reg = state[3];
99
100    for round_idx in 0..64 {
101        let (mix, word_idx) = match round_idx {
102            0..=15 => ((b_reg & c_reg) | ((!b_reg) & d_reg), round_idx),
103            16..=31 => (
104                (d_reg & b_reg) | ((!d_reg) & c_reg),
105                (5 * round_idx + 1) % 16,
106            ),
107            32..=47 => (b_reg ^ c_reg ^ d_reg, (3 * round_idx + 5) % 16),
108            _ => (c_reg ^ (b_reg | (!d_reg)), (7 * round_idx) % 16),
109        };
110
111        let tmp = d_reg;
112        d_reg = c_reg;
113        c_reg = b_reg;
114        b_reg = b_reg.wrapping_add(
115            a_reg
116                .wrapping_add(mix)
117                .wrapping_add(K[round_idx])
118                .wrapping_add(schedule[word_idx])
119                .rotate_left(S[round_idx]),
120        );
121        a_reg = tmp;
122    }
123
124    state[0] = state[0].wrapping_add(a_reg);
125    state[1] = state[1].wrapping_add(b_reg);
126    state[2] = state[2].wrapping_add(c_reg);
127    state[3] = state[3].wrapping_add(d_reg);
128}
129
130#[derive(Clone)]
131pub struct Md5 {
132    state: [u32; 4],
133    block: [u8; 64],
134    pos: usize,
135    bit_len: u64,
136}
137
138impl Default for Md5 {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144impl Md5 {
145    pub const BLOCK_LEN: usize = 64;
146    pub const OUTPUT_LEN: usize = 16;
147
148    #[must_use]
149    pub fn new() -> Self {
150        Self {
151            state: IV,
152            block: [0u8; 64],
153            pos: 0,
154            bit_len: 0,
155        }
156    }
157
158    pub fn update(&mut self, mut data: &[u8]) {
159        while !data.is_empty() {
160            let take = (64 - self.pos).min(data.len());
161            self.block[self.pos..self.pos + take].copy_from_slice(&data[..take]);
162            self.pos += take;
163            data = &data[take..];
164
165            if self.pos == 64 {
166                compress(&mut self.state, &self.block);
167                self.block = [0u8; 64];
168                self.pos = 0;
169                self.bit_len = self.bit_len.wrapping_add(512);
170            }
171        }
172    }
173
174    #[must_use]
175    pub fn finalize(mut self) -> [u8; 16] {
176        self.bit_len = self.bit_len.wrapping_add((self.pos as u64) * 8);
177
178        self.block[self.pos] = 0x80;
179        self.pos += 1;
180
181        if self.pos > 56 {
182            self.block[self.pos..].fill(0);
183            compress(&mut self.state, &self.block);
184            self.block = [0u8; 64];
185            self.pos = 0;
186        }
187
188        self.block[self.pos..56].fill(0);
189        self.block[56..].copy_from_slice(&self.bit_len.to_le_bytes());
190        compress(&mut self.state, &self.block);
191
192        let mut out = [0u8; 16];
193        for (chunk, word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
194            chunk.copy_from_slice(&word.to_le_bytes());
195        }
196        out
197    }
198
199    #[must_use]
200    pub fn digest(data: &[u8]) -> [u8; 16] {
201        let mut h = Self::new();
202        h.update(data);
203        h.finalize()
204    }
205
206    fn finalize_into_reset(&mut self, out: &mut [u8; 16]) {
207        self.bit_len = self.bit_len.wrapping_add((self.pos as u64) * 8);
208
209        self.block[self.pos] = 0x80;
210        self.pos += 1;
211
212        if self.pos > 56 {
213            self.block[self.pos..].fill(0);
214            compress(&mut self.state, &self.block);
215            self.block = [0u8; 64];
216            self.pos = 0;
217        }
218
219        self.block[self.pos..56].fill(0);
220        self.block[56..].copy_from_slice(&self.bit_len.to_le_bytes());
221        compress(&mut self.state, &self.block);
222
223        for (chunk, word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
224            chunk.copy_from_slice(&word.to_le_bytes());
225        }
226
227        self.zeroize();
228    }
229}
230
231impl Digest for Md5 {
232    const BLOCK_LEN: usize = 64;
233    const OUTPUT_LEN: usize = 16;
234
235    fn new() -> Self {
236        Self::new()
237    }
238
239    fn update(&mut self, data: &[u8]) {
240        self.update(data);
241    }
242
243    fn finalize_into(self, out: &mut [u8]) {
244        assert_eq!(out.len(), 16, "wrong digest length");
245        out.copy_from_slice(&self.finalize());
246    }
247
248    fn finalize_reset(&mut self, out: &mut [u8]) {
249        let out: &mut [u8; 16] = out.try_into().expect("wrong digest length");
250        self.finalize_into_reset(out);
251    }
252
253    fn zeroize(&mut self) {
254        crate::ct::zeroize_slice(self.state.as_mut_slice());
255        crate::ct::zeroize_slice(self.block.as_mut_slice());
256        self.pos = 0;
257        self.bit_len = 0;
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    fn hex(bytes: &[u8]) -> String {
266        let mut out = String::with_capacity(bytes.len() * 2);
267        for b in bytes {
268            use core::fmt::Write;
269            let _ = write!(&mut out, "{b:02x}");
270        }
271        out
272    }
273
274    #[test]
275    fn md5_empty() {
276        assert_eq!(hex(&Md5::digest(b"")), "d41d8cd98f00b204e9800998ecf8427e");
277    }
278
279    #[test]
280    fn md5_streaming_abc() {
281        let mut h = Md5::new();
282        h.update(b"a");
283        h.update(b"b");
284        h.update(b"c");
285        assert_eq!(hex(&h.finalize()), "900150983cd24fb0d6963f7d28e17f72");
286    }
287
288    #[test]
289    fn md5_known_vector() {
290        assert_eq!(
291            hex(&Md5::digest(b"message digest")),
292            "f96b697d7cb7938d525a2f31aaf161d0"
293        );
294    }
295
296    #[test]
297    fn md5_matches_openssl() {
298        let msg = b"The quick brown fox jumps over the lazy dog";
299        let Some(expected) = crate::test_utils::run_openssl(&["dgst", "-md5", "-binary"], msg) else {
300            return;
301        };
302        assert_eq!(Md5::digest(msg).as_slice(), expected.as_slice());
303    }
304}