#![no_std]
#![deny(missing_docs)]
pub const BLOCK_SIZE: usize = 64;
pub const DIGEST_LENGTH: usize = 16;
const STATE_WORDS: usize = 4;
const IV: [u32; STATE_WORDS] = [0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476];
#[cfg(all(target_arch = "aarch64", not(feature = "force-fallback")))]
mod aarch64;
#[cfg(any(
test,
feature = "force-fallback",
not(any(target_arch = "x86_64", target_arch = "aarch64"))
))]
mod fallback;
#[cfg(all(target_arch = "x86_64", not(feature = "force-fallback")))]
mod x86_64;
mod hmac;
pub use hmac::HmacMd5;
pub struct Md5 {
state: [u32; STATE_WORDS],
count: u64,
buf: [core::mem::MaybeUninit<u8>; BLOCK_SIZE],
buf_len: usize,
}
#[allow(clippy::new_without_default)]
impl Md5 {
#[inline(always)]
pub fn new() -> Self {
Self {
state: IV,
count: 0,
buf: [const { core::mem::MaybeUninit::<u8>::uninit() }; BLOCK_SIZE],
buf_len: 0,
}
}
#[inline(always)]
pub(crate) fn from_parts(state: [u32; STATE_WORDS], count_bytes: u64) -> Self {
debug_assert!(count_bytes % (BLOCK_SIZE as u64) == 0);
Self {
state,
count: count_bytes,
buf: [const { core::mem::MaybeUninit::<u8>::uninit() }; BLOCK_SIZE],
buf_len: 0,
}
}
#[inline]
pub fn update(&mut self, mut data: &[u8]) {
self.count = self.count.wrapping_add(data.len() as u64);
if self.buf_len > 0 {
let need = BLOCK_SIZE - self.buf_len;
let take = need.min(data.len());
unsafe {
core::ptr::copy_nonoverlapping(
data.as_ptr(),
self.buf.as_mut_ptr().add(self.buf_len).cast::<u8>(),
take,
);
}
self.buf_len += take;
data = &data[take..];
if self.buf_len == BLOCK_SIZE {
let block: &[u8; BLOCK_SIZE] =
unsafe { &*(self.buf.as_ptr().cast::<[u8; BLOCK_SIZE]>()) };
transform(&mut self.state, block);
self.buf_len = 0;
}
}
while data.len() >= BLOCK_SIZE {
let block: &[u8; BLOCK_SIZE] = unsafe { &*(data.as_ptr().cast::<[u8; BLOCK_SIZE]>()) };
transform(&mut self.state, block);
data = &data[64..];
}
if !data.is_empty() {
unsafe {
core::ptr::copy_nonoverlapping(
data.as_ptr(),
self.buf.as_mut_ptr().cast::<u8>(),
data.len(),
);
}
self.buf_len = data.len();
}
}
#[inline]
pub fn finalize(mut self) -> [u8; DIGEST_LENGTH] {
let bit_count = self.count.wrapping_mul(8);
unsafe {
self.buf
.as_mut_ptr()
.add(self.buf_len)
.cast::<u8>()
.write(0x80);
}
self.buf_len += 1;
if self.buf_len > 56 {
unsafe {
core::ptr::write_bytes(
self.buf.as_mut_ptr().add(self.buf_len).cast::<u8>(),
0,
BLOCK_SIZE - self.buf_len,
);
}
let block: &[u8; BLOCK_SIZE] =
unsafe { &*(self.buf.as_ptr().cast::<[u8; BLOCK_SIZE]>()) };
transform(&mut self.state, block);
self.buf_len = 0;
}
unsafe {
core::ptr::write_bytes(
self.buf.as_mut_ptr().add(self.buf_len).cast::<u8>(),
0,
56 - self.buf_len,
);
core::ptr::copy_nonoverlapping(
bit_count.to_le_bytes().as_ptr(),
self.buf.as_mut_ptr().add(56).cast::<u8>(),
8,
);
}
let block: &[u8; 64] = unsafe { &*(self.buf.as_ptr().cast::<[u8; 64]>()) };
transform(&mut self.state, block);
let mut digest = [0u8; DIGEST_LENGTH];
for (i, word) in self.state.iter().enumerate() {
digest[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes());
}
digest
}
}
#[inline]
pub fn transform(state: &mut [u32; STATE_WORDS], block: &[u8; BLOCK_SIZE]) {
#[cfg(all(target_arch = "x86_64", not(feature = "force-fallback")))]
{
x86_64::transform(state, block);
}
#[cfg(all(target_arch = "aarch64", not(feature = "force-fallback")))]
{
aarch64::transform(state, block);
}
#[cfg(any(
feature = "force-fallback",
not(any(target_arch = "x86_64", target_arch = "aarch64"))
))]
{
fallback::transform(state, block);
}
}
#[inline(always)]
pub fn digest(data: &[u8]) -> [u8; DIGEST_LENGTH] {
let mut m = Md5::new();
m.update(data);
m.finalize()
}
#[cfg(test)]
mod tests {
use super::*;
const VECTORS: &[(&[u8], &str)] = &[
(b"", "d41d8cd98f00b204e9800998ecf8427e"),
(b"a", "0cc175b9c0f1b6a831c399e269772661"),
(b"abc", "900150983cd24fb0d6963f7d28e17f72"),
(b"message digest", "f96b697d7cb7938d525a2f31aaf161d0"),
(
b"abcdefghijklmnopqrstuvwxyz",
"c3fcd3d76192e4007dfb496cca67e13b",
),
(
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
"d174ab98d277d9f5a5611c2c9f419d9f",
),
(
b"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
"57edf4a22be3c955ac49da2e2107b67a",
),
];
fn hex(bytes: &[u8]) -> [u8; 32] {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = [0u8; 32];
for (i, b) in bytes.iter().enumerate() {
out[i * 2] = HEX[(b >> 4) as usize];
out[i * 2 + 1] = HEX[(b & 0x0f) as usize];
}
out
}
#[test]
fn rfc1321_vectors_oneshot() {
for (input, want) in VECTORS {
let got = digest(input);
assert_eq!(
core::str::from_utf8(&hex(&got)).unwrap(),
*want,
"input len {}",
input.len()
);
}
}
#[test]
fn rfc1321_vectors_streaming_byte_by_byte() {
for (input, want) in VECTORS {
let mut h = Md5::new();
for chunk in input.chunks(1) {
h.update(chunk);
}
assert_eq!(
core::str::from_utf8(&hex(&h.finalize())).unwrap(),
*want,
"streaming input len {}",
input.len()
);
}
}
#[test]
fn rfc1321_vectors_streaming_odd_chunks() {
for (input, want) in VECTORS {
let mut h = Md5::new();
for chunk in input.chunks(13) {
h.update(chunk);
}
assert_eq!(core::str::from_utf8(&hex(&h.finalize())).unwrap(), *want,);
}
}
#[test]
fn million_a_test_vector() {
let mut h = Md5::new();
let chunk = [b'a'; 1024];
for _ in 0..1000 {
h.update(&chunk[..1000]);
}
let got = h.finalize();
assert_eq!(
core::str::from_utf8(&hex(&got)).unwrap(),
"7707d6ae4e027c70eea2a935c2296f21",
);
}
#[test]
fn transform_matches_fallback_on_random_blocks() {
let seed: u64 = 0x1234_5678_9abc_def0;
let mut rng_state = seed;
let mut next = || -> u64 {
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 7;
rng_state ^= rng_state << 17;
rng_state.wrapping_mul(0x2545_F491_4F6C_DD1D)
};
for _ in 0..256 {
let mut block = [0u8; BLOCK_SIZE];
for chunk in block.chunks_mut(8) {
chunk.copy_from_slice(&next().to_le_bytes());
}
let init_state = [next() as u32, next() as u32, next() as u32, next() as u32];
let mut s_active = init_state;
transform(&mut s_active, &block);
let mut s_ref = init_state;
fallback::transform(&mut s_ref, &block);
assert_eq!(s_active, s_ref);
}
}
}