1use crate::util;
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4use std::fmt;
5use zeroize::Zeroize;
6
7type HmacSha256 = Hmac<Sha256>;
8
9#[derive(Clone)]
11pub struct Token {
12 pub key: String,
13 secret: SecretString,
14}
15
16#[derive(Clone, Zeroize, ZeroizeOnDrop)]
18struct SecretString(String);
19
20impl Token {
21 pub fn new(key: impl Into<String>, secret: impl Into<String>) -> Self {
23 Self {
24 key: key.into(),
25 secret: SecretString(secret.into()),
26 }
27 }
28
29 pub fn sign(&self, data: &str) -> String {
31 let mut mac = HmacSha256::new_from_slice(self.secret.0.as_bytes())
32 .expect("HMAC can take key of any size");
33 mac.update(data.as_bytes());
34
35 format!("{:x}", mac.finalize().into_bytes())
37 }
38
39 pub fn verify(&self, data: &str, signature: &str) -> bool {
41 let expected = self.sign(data);
42 util::secure_compare(&expected, signature)
43 }
44
45 pub(crate) fn secret_string(&self) -> String {
47 self.secret.0.clone()
48 }
49}
50
51impl fmt::Debug for Token {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.debug_struct("Token")
54 .field("key", &self.key)
55 .field("secret", &"[REDACTED]")
56 .finish()
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn test_sign_and_verify() {
66 let token = Token::new("test_key", "test_secret");
67 let data = "test_data";
68 let signature = token.sign(data);
69
70 assert!(token.verify(data, &signature));
71 assert!(!token.verify("other_data", &signature));
72 assert!(!token.verify(data, "wrong_signature"));
73 }
74
75 #[test]
76 fn test_hmac_consistency() {
77 let token = Token::new("key", "secret");
78 let data = "some data to sign";
79
80 let sig1 = token.sign(data);
81 let sig2 = token.sign(data);
82
83 assert_eq!(sig1, sig2, "HMAC should be deterministic");
84 }
85
86 #[test]
87 fn test_debug_redaction() {
88 let token = Token::new("public_key", "secret_key");
89 let debug_str = format!("{:?}", token);
90
91 assert!(debug_str.contains("public_key"));
92 assert!(debug_str.contains("[REDACTED]"));
93 assert!(!debug_str.contains("secret_key"));
94 }
95}