Skip to content

Commit 6166ca0

Browse files
feat: add SHA-1 hash function implementation (#1030)
1 parent 3bad194 commit 6166ca0

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
* [ROT13](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/rot13.rs)
5656
* [RSA Cipher](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/rsa_cipher.rs)
5757
* [Salsa](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/salsa.rs)
58+
* [SHA-1](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/sha1.rs)
5859
* [SHA-2](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/sha2.rs)
5960
* [SHA-3](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/sha3.rs)
6061
* [Tea](https://github.com/TheAlgorithms/Rust/blob/master/src/ciphers/tea.rs)

src/ciphers/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod rail_fence;
1919
mod rot13;
2020
mod rsa_cipher;
2121
mod salsa;
22+
mod sha1;
2223
mod sha2;
2324
mod sha3;
2425
mod tea;
@@ -52,6 +53,7 @@ pub use self::rsa_cipher::{
5253
decrypt, decrypt_text, encrypt, encrypt_text, generate_keypair, PrivateKey, PublicKey,
5354
};
5455
pub use self::salsa::salsa20;
56+
pub use self::sha1::sha1;
5557
pub use self::sha2::{sha224, sha256, sha384, sha512, sha512_224, sha512_256};
5658
pub use self::sha3::{sha3_224, sha3_256, sha3_384, sha3_512};
5759
pub use self::tea::{tea_decrypt, tea_encrypt};

src/ciphers/sha1.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)