diff --git a/src/hash.rs b/src/hash.rs index 43bb2d1..91b6d83 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -78,6 +78,22 @@ pub fn words_to_bytes_le(words: impl AsRef<[u32]>) -> Vec { .collect() } +pub fn bytes_to_words_be(bytes: impl AsRef<[u8]>) -> Vec { + bytes + .as_ref() + .array_chunks::<4>() + .map(|chunk| u32::from_be_bytes(*chunk)) + .collect() +} + +pub fn words_to_bytes_be(words: impl AsRef<[u32]>) -> Vec { + words + .as_ref() + .iter() + .flat_map(|w| w.to_be_bytes()) + .collect() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/hash/md4.rs b/src/hash/md4.rs index c04bdb9..e1242d4 100644 --- a/src/hash/md4.rs +++ b/src/hash/md4.rs @@ -46,21 +46,21 @@ pub fn pad(message: impl AsRef<[u8]>) -> Vec { } // compute an invidiual step in the md4 algorithm -fn step([mut a, b, c, d]: [u32; 4], words: &[u32], index: usize) -> [u32; 4] { +fn step([mut a, b, c, d]: [u32; 4], words: &[u32], i: usize) -> [u32; 4] { // choose function and constant based on which round is currently active - let (constant, round_function) = match index { - 0..=15 => (C1, F), - 16..=31 => (C2, G), - 32..=47 => (C3, H), + let (f, k) = match i { + 0..=15 => (F, C1), + 16..=31 => (G, C2), + 32..=47 => (H, C3), _ => panic!("This function shouldn't be called using an index outside 0..48"), }; // main operation - a = round_function(b, c, d) + a = f(b, c, d) .wrapping_add(a) - .wrapping_add(words[W[index]]) - .wrapping_add(constant) - .rotate_left(S[index]); + .wrapping_add(words[W[i]]) + .wrapping_add(k) + .rotate_left(S[i]); [a, b, c, d] } diff --git a/src/hash/md5.rs b/src/hash/md5.rs index e6476f4..13b043e 100644 --- a/src/hash/md5.rs +++ b/src/hash/md5.rs @@ -70,12 +70,12 @@ fn step([mut a, b, c, d]: [u32; 4], words: &[u32], index: usize) -> [u32; 4] { /// /// ``` pub fn hash(message: impl AsRef<[u8]>) -> Digest<16> { + // the padding function for MD5 is exactly equivalent to the MD4 version, so we reuse it. let padded = pad(message); let buffer = padded.array_chunks::<64>().map(bytes_to_words_le).fold( [A, B, C, D], |[a, b, c, d], words| { - println!("{words:08x?}"); - + // initialize state let mut state = [a, b, c, d]; for i in 0..64 { @@ -84,6 +84,7 @@ pub fn hash(message: impl AsRef<[u8]>) -> Digest<16> { state.rotate_right(1); } + // add the computed state to the buffer [ a.wrapping_add(state[0]), b.wrapping_add(state[1]), diff --git a/src/hash/sha1.rs b/src/hash/sha1.rs index d4d2b89..da51378 100644 --- a/src/hash/sha1.rs +++ b/src/hash/sha1.rs @@ -1,4 +1,62 @@ -use crate::hash::Digest; +use crate::hash::{bytes_to_words_be, words_to_bytes_be, Digest}; + +// based on RFC3174, US Secure Hash Algorithm 1 + +// round functions +const F1: fn(u32, u32, u32) -> u32 = |x, y, z| (x & y) | ((!x) & z); +const F2: fn(u32, u32, u32) -> u32 = |x, y, z| x ^ y ^ z; +const F3: fn(u32, u32, u32) -> u32 = |x, y, z| (x & y) | (x & z) | (y & z); +const F4: fn(u32, u32, u32) -> u32 = |x, y, z| x ^ y ^ z; + +// round constants +const K1: u32 = 0x5a827999; +const K2: u32 = 0x6ed9eba1; +const K3: u32 = 0x8f1bbcdc; +const K4: u32 = 0xca62c1d6; + +// buffer 2 initial constants +const H0: u32 = 0x67452301; +const H1: u32 = 0xefcdab89; +const H2: u32 = 0x98badcfe; +const H3: u32 = 0x10325476; +const H4: u32 = 0xc3d2e1f0; + +fn pad(message: impl AsRef<[u8]>) -> Vec { + let mut message = message.as_ref().to_vec(); + let message_length = message.len().wrapping_mul(8) as u64; + + // push 1 bit (little endian) + message.push(0x80); + + // pad with 0 bits until length is congruent with 64 mod 56 bytes + while (message.len() % 64) < 56 { + message.push(0); + } + + // append the length of the original message (big endian) + message.extend(message_length.to_be_bytes()); + + message +} + +fn step([mut a, b, c, d, e]: [u32; 5], words: &[u32], i: usize) -> [u32; 5] { + let (k, f) = match i { + 0..=19 => (K1, F1), + 20..=39 => (K2, F2), + 40..=59 => (K3, F3), + 60..=79 => (K4, F4), + _ => panic!("step function should not be called with index outside of range 0..80"), + }; + + a = a + .rotate_left(5) + .wrapping_add(f(b, c, d)) + .wrapping_add(e) + .wrapping_add(k) + .wrapping_add(words[i]); + + [e, a, b.rotate_left(30), c, d] +} /// Computes the SHA1 digest of the input bytes. /// @@ -15,7 +73,38 @@ use crate::hash::Digest; /// assert_eq!(digest.to_string(), "a9993e364706816aba3e25717850c26c9cd0d89d") /// ``` pub fn hash(message: impl AsRef<[u8]>) -> Digest<20> { - todo!() + let padded = pad(message); + + let buffer = padded + .array_chunks::<64>() + .map(|chunk| bytes_to_words_be(*chunk)) + .fold([H0, H1, H2, H3, H4], |[a, b, c, d, e], words| { + // extend 16 words to 80 words + + // initialize state + let mut state = [a, b, c, d, e]; + + // perform 80 steps + for i in 0..80 { + state = step(state, &words, i); + } + + // add computed round state to buffer + [ + a.wrapping_add(state[0]), + b.wrapping_add(state[1]), + c.wrapping_add(state[2]), + d.wrapping_add(state[3]), + e.wrapping_add(state[4]), + ] + }); + + let digest = *words_to_bytes_be(buffer) + .array_chunks::<20>() + .next() + .unwrap(); + + Digest(digest) } #[cfg(test)] @@ -23,5 +112,21 @@ mod tests { use super::*; #[test] - fn sha1_hash() {} + fn sha1_pad() { + let expected: [u8; 64] = [ + 0x61, 0x62, 0x63, 0x64, 0x65, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x28, + ]; + + assert_eq!( + expected.to_vec(), + pad([0b01100001, 0b01100010, 0b01100011, 0b01100100, 0b01100101]) + ); + } + + #[test] + fn sha1_hash() { + // panic!(); + } } diff --git a/src/lib.rs b/src/lib.rs index 6e75b5c..40ca6b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,8 @@ #![allow( clippy::unreadable_literal, clippy::missing_panics_doc, - clippy::cast_possible_truncation + clippy::cast_possible_truncation, + clippy::many_single_char_names )] // docs #![doc = include_str!("../README.md")]