1use base64::Engine;
6use hmac::{Hmac, Mac};
7use sha1::Sha1;
8use sha2::Sha256;
9
10pub fn sign_sha256_hex(secret: &[u8], message: &[u8]) -> String {
13 let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("hmac accepts any key length");
14 mac.update(message);
15 hex_encode(&mac.finalize().into_bytes())
16}
17
18pub fn verify_sha256_hex(secret: &[u8], message: &[u8], signature_hex: &str) -> bool {
21 let Some(signature) = hex_decode(signature_hex) else {
22 return false;
23 };
24 let mut mac = Hmac::<Sha256>::new_from_slice(secret).expect("hmac accepts any key length");
25 mac.update(message);
26 mac.verify_slice(&signature).is_ok()
27}
28
29pub fn sign_sha1_base64(secret: &[u8], message: &[u8]) -> String {
34 let mut mac = Hmac::<Sha1>::new_from_slice(secret).expect("hmac accepts any key length");
35 mac.update(message);
36 base64::engine::general_purpose::STANDARD.encode(mac.finalize().into_bytes())
37}
38
39pub fn verify_sha1_base64(secret: &[u8], message: &[u8], signature_b64: &str) -> bool {
42 let Ok(signature) = base64::engine::general_purpose::STANDARD.decode(signature_b64) else {
43 return false;
44 };
45 let mut mac = Hmac::<Sha1>::new_from_slice(secret).expect("hmac accepts any key length");
46 mac.update(message);
47 mac.verify_slice(&signature).is_ok()
48}
49
50pub(crate) fn hex_encode(bytes: &[u8]) -> String {
52 let mut out = String::with_capacity(bytes.len() * 2);
53 for b in bytes {
54 out.push(char::from_digit((b >> 4) as u32, 16).expect("nibble < 16"));
55 out.push(char::from_digit((b & 0x0f) as u32, 16).expect("nibble < 16"));
56 }
57 out
58}
59
60fn hex_decode(s: &str) -> Option<Vec<u8>> {
63 if !s.len().is_multiple_of(2) {
64 return None;
65 }
66 let bytes = s.as_bytes();
67 let mut out = Vec::with_capacity(s.len() / 2);
68 for pair in bytes.chunks_exact(2) {
69 let hi = (pair[0] as char).to_digit(16)?;
70 let lo = (pair[1] as char).to_digit(16)?;
71 out.push((hi * 16 + lo) as u8);
72 }
73 Some(out)
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn sha256_hex_roundtrip_and_tamper_detection() {
82 let secret = b"whsec_test";
83 let message = b"1717000000.{\"id\":\"evt_1\"}";
84 let signature = sign_sha256_hex(secret, message);
85 assert_eq!(signature.len(), 64);
86 assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
87 assert!(verify_sha256_hex(secret, message, &signature));
88 assert!(!verify_sha256_hex(secret, b"tampered", &signature));
89 assert!(!verify_sha256_hex(b"wrong-secret", message, &signature));
90 assert!(verify_sha256_hex(
92 secret,
93 message,
94 &signature.to_uppercase()
95 ));
96 assert!(!verify_sha256_hex(secret, message, "zz!"));
98 assert!(!verify_sha256_hex(secret, message, ""));
99 }
100
101 #[test]
102 fn sha1_base64_roundtrip_and_tamper_detection() {
103 let secret = b"twilio_auth_token";
104 let message = b"https://example.test/hookBody=value";
105 let signature = sign_sha1_base64(secret, message);
106 assert!(verify_sha1_base64(secret, message, &signature));
107 assert!(!verify_sha1_base64(secret, b"tampered", &signature));
108 assert!(!verify_sha1_base64(secret, message, "not base64!!"));
109 }
110
111 #[test]
115 fn known_answer_vectors() {
116 assert_eq!(
117 sign_sha256_hex(b"Jefe", b"what do ya want for nothing?"),
118 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
119 );
120 use base64::Engine as _;
121 let mac = sign_sha1_base64(b"Jefe", b"what do ya want for nothing?");
122 let raw = base64::engine::general_purpose::STANDARD
123 .decode(&mac)
124 .unwrap();
125 assert_eq!(
126 raw.iter().map(|b| format!("{b:02x}")).collect::<String>(),
127 "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"
128 );
129 }
130}