use std::fmt;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
pub const PROBE_ALG: &str = "HMAC-SHA256";
pub const PROBE_INPUT: &str = "nonce";
pub const PROBE_ENCODING: &str = "hex";
#[derive(Clone)]
pub struct ProbeVerifier {
key: Vec<u8>,
}
impl ProbeVerifier {
pub fn new(key: &[u8]) -> Self {
Self { key: key.to_vec() }
}
pub fn expected_hex(&self, nonce: &str) -> Option<String> {
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).ok()?;
mac.update(nonce.as_bytes());
Some(hex::encode(mac.finalize().into_bytes()))
}
pub fn verify(&self, nonce: &str, candidate_hex: &str) -> bool {
let Ok(candidate) = hex::decode(candidate_hex) else {
return false;
};
let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(&self.key) else {
return false;
};
mac.update(nonce.as_bytes());
mac.verify_slice(&candidate).is_ok()
}
}
impl fmt::Debug for ProbeVerifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProbeVerifier").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::ProbeVerifier;
#[test]
fn expected_round_trips_through_verify() {
let verifier = ProbeVerifier::new(b"shared-secret");
let tag = verifier.expected_hex("nonce-abc").unwrap();
assert!(verifier.verify("nonce-abc", &tag));
}
#[test]
fn expected_is_deterministic() {
let verifier = ProbeVerifier::new(b"shared-secret");
assert_eq!(
verifier.expected_hex("nonce-abc").unwrap(),
verifier.expected_hex("nonce-abc").unwrap()
);
}
#[test]
fn wrong_key_nonce_or_encoding_fails_closed() {
let verifier = ProbeVerifier::new(b"shared-secret");
let tag = verifier.expected_hex("nonce-abc").unwrap();
assert!(!verifier.verify("nonce-xyz", &tag));
assert!(!ProbeVerifier::new(b"other-secret").verify("nonce-abc", &tag));
assert!(!verifier.verify("nonce-abc", "zz-not-hex"));
assert!(!verifier.verify("nonce-abc", ""));
}
#[test]
fn debug_redacts_key() {
let shown = format!("{:?}", ProbeVerifier::new(b"top-secret-key"));
assert!(!shown.contains("top-secret-key"));
assert!(!shown.contains("116")); }
}