From 1fcf38db411da65b33aff11062234387efffe65b Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Thu, 1 Oct 2026 10:34:41 +0200 Subject: [PATCH] implement blake2b --- src/blake2b/blake2b.rs | 190 ++++++++++++++++++++++++++++++++++++++++ src/blake2b/compress.rs | 80 +++++++++++++++++ src/blake2b/mix.rs | 14 +++ src/blake2b/mod.rs | 8 ++ src/blake2b/test.rs | 176 +++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 6 files changed, 469 insertions(+) create mode 100644 src/blake2b/blake2b.rs create mode 100644 src/blake2b/compress.rs create mode 100644 src/blake2b/mix.rs create mode 100644 src/blake2b/mod.rs create mode 100644 src/blake2b/test.rs diff --git a/src/blake2b/blake2b.rs b/src/blake2b/blake2b.rs new file mode 100644 index 0000000..4978a85 --- /dev/null +++ b/src/blake2b/blake2b.rs @@ -0,0 +1,190 @@ +use super::compress::{IV, compress}; + +pub const BLOCK_SIZE: usize = 128; +pub const MAX_DIGEST_SIZE: usize = 64; +pub const MAX_KEY_SIZE: usize = 64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Blake2bError { + InvalidDigestLength, + InvalidKeyLength, +} + +#[derive(Debug, Clone)] +pub struct Blake2b { + h: [u64; 8], + t: [u64; 2], + f: [u64; 2], + + buffer: [u8; BLOCK_SIZE], + buffer_len: usize, + + digest_len: usize, +} + +impl Blake2b { + /// Create a BLAKE2b-512 hasher. + pub fn new() -> Self { + Self::with_output_length(MAX_DIGEST_SIZE).expect("64-byte BLAKE2b digest is valid") + } + + /// Create an unkeyed BLAKE2b hasher with a digest of `digest_len` bytes. + /// + /// Valid digest lengths are 1..=64 bytes. + pub fn with_output_length(digest_len: usize) -> Result { + Self::new_inner(digest_len, &[]) + } + + /// Create a keyed BLAKE2b-512 hasher. + /// + /// Valid key lengths are 0..=64 bytes. + pub fn new_keyed(key: &[u8]) -> Result { + Self::new_keyed_with_output_length(MAX_DIGEST_SIZE, key) + } + + /// Create a keyed BLAKE2b hasher with a custom digest length. + pub fn new_keyed_with_output_length( + digest_len: usize, + key: &[u8], + ) -> Result { + Self::new_inner(digest_len, key) + } + + fn new_inner(digest_len: usize, key: &[u8]) -> Result { + if !(1..=MAX_DIGEST_SIZE).contains(&digest_len) { + return Err(Blake2bError::InvalidDigestLength); + } + + if key.len() > MAX_KEY_SIZE { + return Err(Blake2bError::InvalidKeyLength); + } + + /* + * BLAKE2b parameter block, first 64-bit word: + * + * bits 0.. 7 digest length + * bits 8..15 key length + * bits 16..23 fanout = 1 + * bits 24..31 depth = 1 + * + * The remaining parameters use their sequential hashing defaults of zero. + */ + let param0 = (digest_len as u64) | ((key.len() as u64) << 8) | (1u64 << 16) | (1u64 << 24); + + let mut h = IV; + + // Parameter block is XORed into the IV during initialization. + h[0] ^= param0; + + let mut state = Self { + h, + t: [0, 0], + f: [0, 0], + buffer: [0u8; BLOCK_SIZE], + buffer_len: 0, + digest_len, + }; + + /* + * In keyed BLAKE2, the padded key is processed as the first 128 bytes block. + * + * We deliberately leave this block buffered until another block arrives or finalize() is called, so that the last-block flag is handled correctly. + */ + if !key.is_empty() { + state.buffer[..key.len()].copy_from_slice(key); + state.buffer_len = BLOCK_SIZE; + } + + Ok(state) + } + + /// Feed more input into the hash. + pub fn update(&mut self, mut input: &[u8]) { + while !input.is_empty() { + if self.buffer_len == BLOCK_SIZE { + self.increment_counter(BLOCK_SIZE as u64); + + compress(&mut self.h, &self.buffer, self.t, self.f); + + self.buffer = [0u8; BLOCK_SIZE]; + self.buffer_len = 0; + } + + let space = BLOCK_SIZE - self.buffer_len; + let take = space.min(input.len()); + + self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&input[..take]); + + self.buffer_len += take; + input = &input[take..]; + } + } + + /// Finalize the hash and return the digest. + /// + /// This consumes the hasher. + pub fn finalize(mut self) -> Vec { + // Count the final partial block + self.increment_counter(self.buffer_len as u64); + + // Set the final block flag + self.f[0] = u64::MAX; + + // Zero pad the remaining bytes + for byte in &mut self.buffer[self.buffer_len..] { + *byte = 0; + } + + compress(&mut self.h, &self.buffer, self.t, self.f); + + let mut output = Vec::with_capacity(MAX_DIGEST_SIZE); + + for word in self.h { + output.extend_from_slice(&word.to_le_bytes()); + } + + output.truncate(self.digest_len); + output + } + + /// One-shot BLAKE2b-512. + pub fn hash(input: &[u8]) -> Vec { + let mut hasher = Self::new(); + hasher.update(input); + hasher.finalize() + } + + /// One-shot BLAKE2b with a custom digest length. + pub fn hash_with_output_length( + input: &[u8], + digest_len: usize, + ) -> Result, Blake2bError> { + let mut hasher = Self::with_output_length(digest_len)?; + hasher.update(input); + Ok(hasher.finalize()) + } + + /// One-shot keyed BLAKE2b-512. + pub fn hash_keyed(input: &[u8], key: &[u8]) -> Result, Blake2bError> { + let mut hasher = Self::new_keyed(key)?; + hasher.update(input); + Ok(hasher.finalize()) + } + + /// Increment the 128-bit little-endian byte counter. + fn increment_counter(&mut self, amount: u64) { + let (low, overflow) = self.t[0].overflowing_add(amount); + + self.t[0] = low; + + if overflow { + self.t[1] = self.t[1].wrapping_add(1); + } + } +} + +impl Default for Blake2b { + fn default() -> Self { + Self::new() + } +} diff --git a/src/blake2b/compress.rs b/src/blake2b/compress.rs new file mode 100644 index 0000000..c407733 --- /dev/null +++ b/src/blake2b/compress.rs @@ -0,0 +1,80 @@ +use super::mix::g; + +/// Initialization vector for BLAKE2b. +pub(crate) const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, +]; + +/// BLAKE2b message word permutations. +/// +/// There are 12 rounds. BLAKE2b repeats the first two permutations for the final two rounds. +pub(crate) const SIGMA: [[usize; 16]; 12] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], +]; + +/// Compress one 128-byte BLAKE2b block. +/// +/// `t` is the 128-bit byte counter and `f` contains the final-block flags. +#[inline(always)] +pub(crate) fn compress(h: &mut [u64; 8], block: &[u8; 128], t: [u64; 2], f: [u64; 2]) { + let mut m = [0u64; 16]; + + for (i, word) in m.iter_mut().enumerate() { + let start = i * 8; + + let bytes: [u8; 8] = block[start..start + 8] + .try_into() + .expect("BLAKE2b block must contain 128 bytes"); + + *word = u64::from_le_bytes(bytes); + } + + let mut v = [0u64; 16]; + + v[..8].copy_from_slice(h); + v[8..].copy_from_slice(&IV); + + // Inject byte counter. + v[12] ^= t[0]; + v[13] ^= t[1]; + + // Inject finalization flags. + v[14] ^= f[0]; + v[15] ^= f[1]; + + for s in SIGMA { + // Column step + g(&mut v, 0, 4, 8, 12, m[s[0]], m[s[1]]); + g(&mut v, 1, 5, 9, 13, m[s[2]], m[s[3]]); + g(&mut v, 2, 6, 10, 14, m[s[4]], m[s[5]]); + g(&mut v, 3, 7, 11, 15, m[s[6]], m[s[7]]); + + // Diagonal step + g(&mut v, 0, 5, 10, 15, m[s[8]], m[s[9]]); + g(&mut v, 1, 6, 11, 12, m[s[10]], m[s[11]]); + g(&mut v, 2, 7, 8, 13, m[s[12]], m[s[13]]); + g(&mut v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } + + for (hi, (vi, vj)) in h.iter_mut().zip(v[..8].iter().zip(v[8..].iter())) { + *hi ^= *vi ^ *vj; + } +} diff --git a/src/blake2b/mix.rs b/src/blake2b/mix.rs new file mode 100644 index 0000000..c23dc84 --- /dev/null +++ b/src/blake2b/mix.rs @@ -0,0 +1,14 @@ +#[inline(always)] +pub(crate) fn g(v: &mut [u64; 16], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(x); + v[d] = (v[d] ^ v[a]).rotate_right(32); + + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(24); + + v[a] = v[a].wrapping_add(v[b]).wrapping_add(y); + v[d] = (v[d] ^ v[a]).rotate_right(16); + + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(63); +} diff --git a/src/blake2b/mod.rs b/src/blake2b/mod.rs new file mode 100644 index 0000000..caad789 --- /dev/null +++ b/src/blake2b/mod.rs @@ -0,0 +1,8 @@ +mod blake2b; +mod compress; +mod mix; + +#[cfg(test)] +mod test; + +pub use blake2b::{BLOCK_SIZE, Blake2b, Blake2bError, MAX_DIGEST_SIZE, MAX_KEY_SIZE}; diff --git a/src/blake2b/test.rs b/src/blake2b/test.rs new file mode 100644 index 0000000..5213431 --- /dev/null +++ b/src/blake2b/test.rs @@ -0,0 +1,176 @@ +use super::{Blake2b, Blake2bError, MAX_DIGEST_SIZE, MAX_KEY_SIZE}; + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + +#[test] +fn empty() { + assert_eq!( + hex(&Blake2b::hash(b"")), + "786a02f742015903c6c6fd852552d272\ + 912f4740e15847618a86e217f71f5419\ + d25e1031afee585313896444934eb04b\ + 903a685b1448b755d56f701afe9be2ce" + .replace(' ', "") + .replace('\n', "") + ); +} + +#[test] +fn empty_blake2b_384() { + assert_eq!( + hex(&Blake2b::hash_with_output_length(b"", 48).unwrap()), + "b32811423377f52d7862286ee1a72ee5\ + 40524380fda1724a6f25d7978c6fd324\ + 4a6caf0498812673c5e05ef583825100" + .replace(' ', "") + .replace('\n', "") + ); +} + +#[test] +fn abc() { + assert_eq!( + hex(&Blake2b::hash(b"abc")), + "ba80a53f981c4d0d6a2797b69f12f6e94\ + c212f14685ac4b74b12bb6fdbffa2d17\ + d87c5392aab792dc252d5de4533cc951\ + 8d38aa8dbf1925ab92386edd4009923" + .replace(' ', "") + .replace('\n', "") + ); +} + +#[test] +fn quick_brown_fox() { + assert_eq!( + hex(&Blake2b::hash( + b"The quick brown fox jumps over the lazy dog" + )), + "a8add4bdddfd93e4877d2746e62817b116\ + 364a1fa7bc148d95090bc7333b3673f8\ + 2401cf7aa2e4cb1ecd90296e3f14cb541\ + 3f8ed77be73045b13914cdcd6a918" + .replace(' ', "") + .replace('\n', "") + ); +} + +#[test] +fn output_lengths() { + let expected = [ + (1, "6b"), + (2, "ae1e"), + (3, "8c45ed"), + (4, "63906248"), + (8, "d8bb14d833d59559"), + (16, "cf4ab791c62b8d2b2109c90275287816"), + ( + 32, + "bddd813c634239723171ef3fee98579b\ + 94964e3bb1cb3e427262c8c068d52319", + ), + ( + 48, + "6f56a82c8e7ef526dfe182eb5212f7db\ + 9df1317e57815dbda46083fc30f54ee6\ + c66ba83be64b302d7cba6ce15bb556f4", + ), + ( + 64, + "ba80a53f981c4d0d6a2797b69f12f6e94\ + c212f14685ac4b74b12bb6fdbffa2d17\ + d87c5392aab792dc252d5de4533cc951\ + 8d38aa8dbf1925ab92386edd4009923", + ), + ]; + + for (digest_len, expected_hex) in expected { + let expected_hex = expected_hex.replace(' ', "").replace('\n', ""); + + let digest = Blake2b::hash_with_output_length(b"abc", digest_len).unwrap(); + + assert_eq!(digest.len(), digest_len); + assert_eq!(hex(&digest), expected_hex); + } +} + +#[test] +fn keyed() { + let digest = Blake2b::hash_keyed(b"", b"key").unwrap(); + + assert_eq!( + hex(&digest), + "5b3cfd8f422b490b764b55eceb330b500\ + c79cbefa9a928ad00202b8b3c5dd778a\ + 81122570434a2e3b8bfd028d105dfefd\ + 0a9576e88ed66de742ca9fbb5f8d2b6" + .replace(' ', "") + .replace('\n', "") + ); +} + +#[test] +fn keyed_max_key_size() { + let key = [0x42u8; MAX_KEY_SIZE]; + + let digest = Blake2b::hash_keyed(b"abc", &key).unwrap(); + + assert_eq!(digest.len(), MAX_DIGEST_SIZE); +} + +#[test] +fn streaming_matches_one_shot() { + let input: Vec = (0..=255).cycle().take(10_000).collect(); + + let expected = Blake2b::hash(&input); + + let mut hasher = Blake2b::new(); + + for chunk in input.chunks(37) { + hasher.update(chunk); + } + + assert_eq!(hasher.finalize(), expected); +} + +#[test] +fn block_boundaries() { + for len in [0usize, 1, 127, 128, 129, 255, 256, 257, 1023, 1024, 1025] { + let input: Vec = (0..len).map(|value| value as u8).collect(); + + let expected = Blake2b::hash(&input); + + let mut hasher = Blake2b::new(); + + for chunk in input.chunks(17) { + hasher.update(chunk); + } + + assert_eq!(hasher.finalize(), expected, "failed for input length {len}"); + } +} + +#[test] +fn invalid_digest_lengths() { + assert_eq!( + Blake2b::with_output_length(0).unwrap_err(), + Blake2bError::InvalidDigestLength + ); + + assert_eq!( + Blake2b::with_output_length(MAX_DIGEST_SIZE + 1).unwrap_err(), + Blake2bError::InvalidDigestLength + ); +} + +#[test] +fn invalid_key_length() { + let key = vec![0u8; MAX_KEY_SIZE + 1]; + + assert_eq!( + Blake2b::new_keyed(&key).unwrap_err(), + Blake2bError::InvalidKeyLength + ); +} diff --git a/src/main.rs b/src/main.rs index 9363bb0..62e88df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; +mod blake2b; mod file_crypt; mod kdf; mod keccak;