use core::fmt;
#[allow(clippy::unreadable_literal)]
const H1: [u32; 5] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
#[allow(clippy::unreadable_literal)]
const K1: [u32; 4] = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
#[derive(Clone)]
pub struct Sha1 {
state: [u32; 5],
buffer: [u8; 64],
buf_len: usize,
total_len: u64,
}
impl Sha1 {
#[must_use]
pub fn new() -> Self {
Self {
state: H1,
buffer: [0u8; 64],
buf_len: 0,
total_len: 0,
}
}
#[allow(clippy::missing_panics_doc)]
pub fn update(&mut self, data: &[u8]) {
self.total_len = self.total_len.wrapping_add(data.len() as u64);
let mut offset = 0;
if self.buf_len > 0 {
let need = 64 - self.buf_len;
let take = need.min(data.len());
self.buffer[self.buf_len..self.buf_len + take].copy_from_slice(&data[..take]);
self.buf_len += take;
offset = take;
if self.buf_len == 64 {
let block: [u8; 64] = self.buffer;
Self::compress(&mut self.state, &block);
self.buf_len = 0;
} else {
return;
}
}
while offset + 64 <= data.len() {
let block: [u8; 64] = data[offset..offset + 64].try_into().unwrap();
Self::compress(&mut self.state, &block);
offset += 64;
}
let remaining = data.len() - offset;
if remaining > 0 {
self.buffer[..remaining].copy_from_slice(&data[offset..]);
self.buf_len = remaining;
}
}
#[must_use]
pub fn finalize(mut self) -> [u8; 20] {
let bit_len = self.total_len.wrapping_mul(8);
self.buffer[self.buf_len] = 0x80;
self.buf_len += 1;
if self.buf_len > 56 {
self.buffer[self.buf_len..64].fill(0);
let block: [u8; 64] = self.buffer;
Self::compress(&mut self.state, &block);
self.buf_len = 0;
}
self.buffer[self.buf_len..56].fill(0);
self.buffer[56..64].copy_from_slice(&bit_len.to_be_bytes());
let block: [u8; 64] = self.buffer;
Self::compress(&mut self.state, &block);
let mut out = [0u8; 20];
for (i, word) in self.state.iter().enumerate() {
out[i * 4..(i + 1) * 4].copy_from_slice(&word.to_be_bytes());
}
out
}
#[must_use]
#[inline]
pub fn digest(data: &[u8]) -> [u8; 20] {
let mut hasher = Self::new();
hasher.update(data);
hasher.finalize()
}
#[allow(clippy::many_single_char_names, clippy::needless_range_loop)]
fn compress(state: &mut [u32; 5], block: &[u8; 64]) {
let mut w = [0u32; 80];
for i in 0..16 {
w[i] = u32::from_be_bytes(block[i * 4..(i + 1) * 4].try_into().unwrap());
}
for i in 16..80 {
w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
}
let [mut a, mut b, mut c, mut d, mut e] = *state;
for i in 0..80 {
let (f, k) = match i {
0..20 => ((b & c) | (!b & d), K1[0]),
20..40 => (b ^ c ^ d, K1[1]),
40..60 => ((b & c) | (b & d) | (c & d), K1[2]),
_ => (b ^ c ^ d, K1[3]),
};
let temp = a
.rotate_left(5)
.wrapping_add(f)
.wrapping_add(e)
.wrapping_add(k)
.wrapping_add(w[i]);
e = d;
d = c;
c = b.rotate_left(30);
b = a;
a = temp;
}
state[0] = state[0].wrapping_add(a);
state[1] = state[1].wrapping_add(b);
state[2] = state[2].wrapping_add(c);
state[3] = state[3].wrapping_add(d);
state[4] = state[4].wrapping_add(e);
}
}
impl fmt::Debug for Sha1 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sha1")
.field("total_len", &self.total_len)
.finish_non_exhaustive()
}
}
impl Drop for Sha1 {
#[allow(unsafe_code)]
fn drop(&mut self) {
for word in &mut self.state {
unsafe { core::ptr::write_volatile(word, 0) };
}
crate::crypto::zeroize::zeroize(&mut self.buffer);
}
}
impl Default for Sha1 {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoding::hex_encode;
#[test]
fn sha1_empty() {
let digest = Sha1::digest(b"");
assert_eq!(
hex_encode(&digest),
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
);
}
#[test]
fn sha1_abc() {
let digest = Sha1::digest(b"abc");
assert_eq!(
hex_encode(&digest),
"a9993e364706816aba3e25717850c26c9cd0d89d",
);
}
#[test]
fn sha1_448_bits() {
let msg = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
assert_eq!(msg.len(), 56);
let digest = Sha1::digest(msg);
assert_eq!(
hex_encode(&digest),
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
);
}
#[test]
fn sha1_one_million_a() {
let digest = Sha1::digest(&vec![b'a'; 1_000_000]);
assert_eq!(
hex_encode(&digest),
"34aa973cd4c4daa4f61eeb2bdbad27316534016f",
);
}
#[test]
fn sha1_incremental_split() {
let mut hasher = Sha1::new();
hasher.update(b"a");
hasher.update(b"bc");
assert_eq!(
hex_encode(&hasher.finalize()),
"a9993e364706816aba3e25717850c26c9cd0d89d",
);
}
#[test]
fn sha1_single_byte_updates() {
let mut hasher = Sha1::new();
for &byte in b"abc" {
hasher.update(&[byte]);
}
assert_eq!(
hex_encode(&hasher.finalize()),
"a9993e364706816aba3e25717850c26c9cd0d89d",
);
}
#[test]
fn sha1_default_trait() {
let a = Sha1::new().finalize();
let b = Sha1::default().finalize();
assert_eq!(a, b);
}
#[test]
fn sha1_debug_does_not_leak_state() {
let mut hasher = Sha1::new();
hasher.update(b"secret data that must not appear");
let debug = format!("{hasher:?}");
assert!(debug.contains("Sha1"), "should contain the type name");
assert!(
debug.contains("total_len"),
"should contain total_len field"
);
assert!(!debug.contains("state"), "must not expose state field");
assert!(!debug.contains("buffer"), "must not expose buffer field");
assert!(!debug.contains("buf_len"), "must not expose buf_len field");
assert!(
!debug.contains("67452301"),
"must not expose SHA-1 IV words"
);
}
}