|
| 1 | +// SHA-1 (FIPS 180-4) is defined with big-endian word/length encoding. |
| 2 | +// Clippy's `big_endian_bytes` lint would incorrectly flag every intentional |
| 3 | +// `to_be_bytes` / `from_be_bytes` call in this file. |
| 4 | +#![allow(clippy::big_endian_bytes)] |
| 5 | + |
| 6 | +/// Block size in bits |
| 7 | +const BLOCK_BITS: usize = 512; |
| 8 | +const BLOCK_BYTES: usize = BLOCK_BITS / 8; |
| 9 | +const BLOCK_WORDS: usize = BLOCK_BYTES / 4; |
| 10 | + |
| 11 | +/// Digest size in bits and bytes |
| 12 | +const DIGEST_BITS: usize = 160; |
| 13 | +const DIGEST_BYTES: usize = DIGEST_BITS / 8; |
| 14 | + |
| 15 | +/// Number of rounds per block |
| 16 | +const ROUNDS: usize = 80; |
| 17 | + |
| 18 | +/// Initial hash values (first 32 bits of the fractional parts of the square roots of the first |
| 19 | +/// five primes) |
| 20 | +const H_INIT: [u32; 5] = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]; |
| 21 | + |
| 22 | +/// Round constants |
| 23 | +const K: [u32; 4] = [0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6]; |
| 24 | + |
| 25 | +/// Nonlinear mixing functions for each of the four 20-round stages |
| 26 | +fn ch(b: u32, c: u32, d: u32) -> u32 { |
| 27 | + (b & c) | ((!b) & d) |
| 28 | +} |
| 29 | + |
| 30 | +fn parity(b: u32, c: u32, d: u32) -> u32 { |
| 31 | + b ^ c ^ d |
| 32 | +} |
| 33 | + |
| 34 | +fn maj(b: u32, c: u32, d: u32) -> u32 { |
| 35 | + (b & c) | (b & d) | (c & d) |
| 36 | +} |
| 37 | + |
| 38 | +/// Selects the mixing function and round constant for a given round index |
| 39 | +fn round_params(t: usize) -> (fn(u32, u32, u32) -> u32, u32) { |
| 40 | + match t { |
| 41 | + 0..=19 => (ch, K[0]), |
| 42 | + 20..=39 => (parity, K[1]), |
| 43 | + 40..=59 => (maj, K[2]), |
| 44 | + 60..=79 => (parity, K[3]), |
| 45 | + _ => unreachable!(), |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +/// Pads the message to a multiple of 512 bits. |
| 50 | +/// |
| 51 | +/// SHA-1 padding appends a single `1` bit, enough `0` bits, and finally the original |
| 52 | +/// message length as a 64-bit big-endian integer, such that the total length is |
| 53 | +/// congruent to 0 mod 512. |
| 54 | +fn pad(message: &[u8]) -> Vec<u8> { |
| 55 | + let bit_len = (message.len() as u64).wrapping_mul(8); |
| 56 | + |
| 57 | + let mut padded = message.to_vec(); |
| 58 | + padded.push(0x80); // append the '1' bit followed by seven '0' bits |
| 59 | + |
| 60 | + // Append zero bytes until length ≡ 56 (mod 64) |
| 61 | + while padded.len() % BLOCK_BYTES != 56 { |
| 62 | + padded.push(0x00); |
| 63 | + } |
| 64 | + |
| 65 | + // Append original length as 64-bit big-endian |
| 66 | + padded.extend_from_slice(&bit_len.to_be_bytes()); |
| 67 | + |
| 68 | + debug_assert!(padded.len().is_multiple_of(BLOCK_BYTES)); |
| 69 | + padded |
| 70 | +} |
| 71 | + |
| 72 | +/// Parses a 64-byte block into sixteen 32-bit big-endian words |
| 73 | +fn parse_block(block: &[u8]) -> [u32; BLOCK_WORDS] { |
| 74 | + debug_assert_eq!(block.len(), BLOCK_BYTES); |
| 75 | + |
| 76 | + let mut words = [0u32; BLOCK_WORDS]; |
| 77 | + for (i, word) in words.iter_mut().enumerate() { |
| 78 | + *word = u32::from_be_bytes(block[i * 4..i * 4 + 4].try_into().unwrap()); |
| 79 | + } |
| 80 | + words |
| 81 | +} |
| 82 | + |
| 83 | +/// Expands sixteen message words into eighty scheduled words using the message schedule |
| 84 | +fn schedule(m: [u32; BLOCK_WORDS]) -> [u32; ROUNDS] { |
| 85 | + let mut w = [0u32; ROUNDS]; |
| 86 | + w[..BLOCK_WORDS].copy_from_slice(&m); |
| 87 | + |
| 88 | + for t in BLOCK_WORDS..ROUNDS { |
| 89 | + w[t] = (w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16]).rotate_left(1); |
| 90 | + } |
| 91 | + w |
| 92 | +} |
| 93 | + |
| 94 | +/// Processes a single 512-bit block, updating the running hash state in place |
| 95 | +fn compress(state: &mut [u32; 5], block: &[u8]) { |
| 96 | + let w = schedule(parse_block(block)); |
| 97 | + |
| 98 | + let [mut a, mut b, mut c, mut d, mut e] = *state; |
| 99 | + |
| 100 | + for (t, &w_t) in w.iter().enumerate() { |
| 101 | + let (f, k) = round_params(t); |
| 102 | + let temp = a |
| 103 | + .rotate_left(5) |
| 104 | + .wrapping_add(f(b, c, d)) |
| 105 | + .wrapping_add(e) |
| 106 | + .wrapping_add(k) |
| 107 | + .wrapping_add(w_t); |
| 108 | + e = d; |
| 109 | + d = c; |
| 110 | + c = b.rotate_left(30); |
| 111 | + b = a; |
| 112 | + a = temp; |
| 113 | + } |
| 114 | + |
| 115 | + state[0] = state[0].wrapping_add(a); |
| 116 | + state[1] = state[1].wrapping_add(b); |
| 117 | + state[2] = state[2].wrapping_add(c); |
| 118 | + state[3] = state[3].wrapping_add(d); |
| 119 | + state[4] = state[4].wrapping_add(e); |
| 120 | +} |
| 121 | + |
| 122 | +/// Computes the SHA-1 digest of the given byte slice, returning a 20-byte array |
| 123 | +pub fn sha1(message: &[u8]) -> [u8; DIGEST_BYTES] { |
| 124 | + let padded = pad(message); |
| 125 | + let mut state = H_INIT; |
| 126 | + |
| 127 | + for block in padded.chunks(BLOCK_BYTES) { |
| 128 | + compress(&mut state, block); |
| 129 | + } |
| 130 | + |
| 131 | + // Serialise the five 32-bit words into twenty bytes (big-endian) |
| 132 | + let mut digest = [0u8; DIGEST_BYTES]; |
| 133 | + for (i, &word) in state.iter().enumerate() { |
| 134 | + digest[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes()); |
| 135 | + } |
| 136 | + digest |
| 137 | +} |
| 138 | + |
| 139 | +#[cfg(test)] |
| 140 | +mod tests { |
| 141 | + use super::*; |
| 142 | + |
| 143 | + /// Convenience macro that generates a named test, hashing `$input` and comparing the |
| 144 | + /// result byte-for-byte against `$expected`. |
| 145 | + macro_rules! sha1_test { |
| 146 | + ($name:ident, $input:expr, $expected:expr) => { |
| 147 | + #[test] |
| 148 | + fn $name() { |
| 149 | + let digest = sha1($input); |
| 150 | + let expected: [u8; DIGEST_BYTES] = $expected; |
| 151 | + assert_eq!(digest, expected); |
| 152 | + } |
| 153 | + }; |
| 154 | + } |
| 155 | + |
| 156 | + // ── NIST FIPS 180-4 / RFC 3174 test vectors ────────────────────────────── |
| 157 | + |
| 158 | + // SHA1("") = da39a3ee 5e6b4b0d 3255bfef 95601890 afd80709 |
| 159 | + sha1_test!( |
| 160 | + sha1_empty, |
| 161 | + b"", |
| 162 | + [ |
| 163 | + 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, |
| 164 | + 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09, |
| 165 | + ] |
| 166 | + ); |
| 167 | + |
| 168 | + // SHA1("abc") = a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d |
| 169 | + sha1_test!( |
| 170 | + sha1_abc, |
| 171 | + b"abc", |
| 172 | + [ |
| 173 | + 0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, |
| 174 | + 0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d, |
| 175 | + ] |
| 176 | + ); |
| 177 | + |
| 178 | + // SHA1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") |
| 179 | + // = 84983e44 1c3bd26e baae4aa1 f95129e5 e54670f1 |
| 180 | + sha1_test!( |
| 181 | + sha1_448_bit, |
| 182 | + b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", |
| 183 | + [ |
| 184 | + 0x84, 0x98, 0x3e, 0x44, 0x1c, 0x3b, 0xd2, 0x6e, 0xba, 0xae, 0x4a, 0xa1, 0xf9, 0x51, |
| 185 | + 0x29, 0xe5, 0xe5, 0x46, 0x70, 0xf1, |
| 186 | + ] |
| 187 | + ); |
| 188 | + |
| 189 | + // SHA1("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") |
| 190 | + // = a49b2446 a02c645b f419f995 b6709125 3a04a259 |
| 191 | + sha1_test!( |
| 192 | + sha1_896_bit, |
| 193 | + b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", |
| 194 | + [ |
| 195 | + 0xa4, 0x9b, 0x24, 0x46, 0xa0, 0x2c, 0x64, 0x5b, 0xf4, 0x19, 0xf9, 0x95, 0xb6, 0x70, |
| 196 | + 0x91, 0x25, 0x3a, 0x04, 0xa2, 0x59, |
| 197 | + ] |
| 198 | + ); |
| 199 | + |
| 200 | + // SHA1("a" × 1 000 000) = 34aa973c d4c4daa4 f61eeb2b dbad2731 6534016f |
| 201 | + // Verifies that the sponge-like multi-block path is exercised correctly. |
| 202 | + #[test] |
| 203 | + fn sha1_million_a() { |
| 204 | + let input = vec![b'a'; 1_000_000]; |
| 205 | + let digest = sha1(&input); |
| 206 | + let expected: [u8; DIGEST_BYTES] = [ |
| 207 | + 0x34, 0xaa, 0x97, 0x3c, 0xd4, 0xc4, 0xda, 0xa4, 0xf6, 0x1e, 0xeb, 0x2b, 0xdb, 0xad, |
| 208 | + 0x27, 0x31, 0x65, 0x34, 0x01, 0x6f, |
| 209 | + ]; |
| 210 | + assert_eq!(digest, expected); |
| 211 | + } |
| 212 | +} |
0 commit comments