use base64::Engine;
use hmac::{Hmac, Mac};
use sha1::Sha1;
use sha2::Sha256;
pub fn sign_sha256_hex(secret: &[u8], message: &[u8]) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("hmac accepts any key length");
mac.update(message);
hex_encode(&mac.finalize().into_bytes())
}
pub fn verify_sha256_hex(secret: &[u8], message: &[u8], signature_hex: &str) -> bool {
let Some(signature) = hex_decode(signature_hex) else {
return false;
};
let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("hmac accepts any key length");
mac.update(message);
mac.verify_slice(&signature).is_ok()
}
pub fn sign_sha1_base64(secret: &[u8], message: &[u8]) -> String {
let mut mac = Hmac::<Sha1>::new_from_slice(secret).expect("hmac accepts any key length");
mac.update(message);
base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes())
}
pub fn verify_sha1_base64(secret: &[u8], message: &[u8], signature_b64: &str) -> bool {
let Ok(signature) = base64::engine::general_purpose::STANDARD.decode(signature_b64) else {
return false;
};
let mut mac = Hmac::<Sha1>::new_from_slice(secret).expect("hmac accepts any key length");
mac.update(message);
mac.verify_slice(&signature).is_ok()
}
pub(crate) fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(char::from_digit((b >> 4) as u32, 16).expect("nibble < 16"));
out.push(char::from_digit((b & 0x0f) as u32, 16).expect("nibble < 16"));
}
out
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(s.len() / 2);
for pair in bytes.chunks_exact(2) {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
out.push((hi * 16 + lo) as u8);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha256_hex_roundtrip_and_tamper_detection() {
let secret = b"whsec_test";
let message = b"1717000000.{\"id\":\"evt_1\"}";
let signature = sign_sha256_hex(secret, message);
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
assert!(verify_sha256_hex(secret, message, &signature));
assert!(!verify_sha256_hex(secret, b"tampered", &signature));
assert!(!verify_sha256_hex(b"wrong-secret", message, &signature));
assert!(verify_sha256_hex(
secret,
message,
&signature.to_uppercase()
));
assert!(!verify_sha256_hex(secret, message, "zz!"));
assert!(!verify_sha256_hex(secret, message, ""));
}
#[test]
fn sha1_base64_roundtrip_and_tamper_detection() {
let secret = b"twilio_auth_token";
let message = b"https://example.test/hookBody=value";
let signature = sign_sha1_base64(secret, message);
assert!(verify_sha1_base64(secret, message, &signature));
assert!(!verify_sha1_base64(secret, b"tampered", &signature));
assert!(!verify_sha1_base64(secret, message, "not base64!!"));
}
#[test]
fn known_answer_vectors() {
assert_eq!(
sign_sha256_hex(b"Jefe", b"what do ya want for nothing?"),
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
use base64::Engine as _;
let mac = sign_sha1_base64(b"Jefe", b"what do ya want for nothing?");
let raw = base64::engine::general_purpose::STANDARD
.decode(&mac)
.unwrap();
assert_eq!(
raw.iter().map(|b| format!("{b:02x}")).collect::<String>(),
"effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"
);
}
}