implement blake2b

This commit is contained in:
sam.hadow committed 2026-10-01 10:34:41 +02:00
1 parent 208e8eda77
commit 1fcf38db41
6 files changed
+469

No files matched your search

+190
View File
@@ -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, Blake2bError> {
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, Blake2bError> {
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, Blake2bError> {
Self::new_inner(digest_len, key)
}
fn new_inner(digest_len: usize, key: &[u8]) -> Result<Self, Blake2bError> {
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<u8> {
// 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<u8> {
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<Vec<u8>, 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<Vec<u8>, 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()
}
}
+80
View File
@@ -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;
}
}
+14
View File
@@ -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);
}
+8
View File
@@ -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};
+176
View File
@@ -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<u8> = (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<u8> = (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
);
}
+1
View File
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use clap::{Parser, Subcommand};
mod blake2b;
mod file_crypt;
mod kdf;
mod keccak;