use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::error::{ClusterError, Result};
pub const MAC_LEN: usize = 32;
#[derive(Clone)]
pub struct MacKey([u8; MAC_LEN]);
impl MacKey {
pub fn from_bytes(bytes: [u8; MAC_LEN]) -> Self {
Self(bytes)
}
pub fn random() -> Self {
use rand::RngCore;
let mut out = [0u8; MAC_LEN];
rand::rng().fill_bytes(&mut out);
Self(out)
}
pub fn zero() -> Self {
Self([0u8; MAC_LEN])
}
pub fn as_bytes(&self) -> &[u8; MAC_LEN] {
&self.0
}
pub fn is_zero(&self) -> bool {
self.0 == [0u8; MAC_LEN]
}
}
impl std::fmt::Debug for MacKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_zero() {
write!(f, "MacKey(zero)")
} else {
write!(f, "MacKey(<redacted>)")
}
}
}
pub fn compute_hmac(key: &MacKey, data: &[u8]) -> [u8; MAC_LEN] {
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key.as_bytes())
.expect("HMAC-SHA256 accepts any key length");
mac.update(data);
let out = mac.finalize().into_bytes();
let mut tag = [0u8; MAC_LEN];
tag.copy_from_slice(&out);
tag
}
pub fn verify_hmac(key: &MacKey, data: &[u8], tag: &[u8; MAC_LEN]) -> Result<()> {
let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key.as_bytes())
.expect("HMAC-SHA256 accepts any key length");
mac.update(data);
mac.verify_slice(tag).map_err(|_| ClusterError::Codec {
detail: "frame MAC verification failed".into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_roundtrip() {
let key = MacKey::from_bytes([7u8; MAC_LEN]);
let tag = compute_hmac(&key, b"hello world");
verify_hmac(&key, b"hello world", &tag).unwrap();
}
#[test]
fn hmac_rejects_tampered_data() {
let key = MacKey::from_bytes([7u8; MAC_LEN]);
let tag = compute_hmac(&key, b"hello world");
let err = verify_hmac(&key, b"hello WORLD", &tag).unwrap_err();
assert!(err.to_string().contains("MAC verification failed"));
}
#[test]
fn hmac_rejects_wrong_key() {
let k1 = MacKey::from_bytes([1u8; MAC_LEN]);
let k2 = MacKey::from_bytes([2u8; MAC_LEN]);
let tag = compute_hmac(&k1, b"msg");
assert!(verify_hmac(&k2, b"msg", &tag).is_err());
}
#[test]
fn debug_redacts_key() {
let k = MacKey::from_bytes([0xAA; MAC_LEN]);
let s = format!("{k:?}");
assert!(!s.contains("aa"), "debug leaked key bytes: {s}");
assert!(s.contains("redacted"));
}
#[test]
fn random_keys_differ() {
let k1 = MacKey::random();
let k2 = MacKey::random();
assert_ne!(k1.as_bytes(), k2.as_bytes());
}
#[test]
fn zero_key_reports_zero() {
assert!(MacKey::zero().is_zero());
assert!(!MacKey::random().is_zero());
}
}