1use hmac::{Hmac, Mac};
4use rand::Rng;
5use sha2::{Digest, Sha256};
6
7type HmacSha256 = Hmac<Sha256>;
8
9pub fn sha256_hex(value: &str) -> String {
13 let hash = Sha256::digest(value.as_bytes());
14 hex::encode(hash)
15}
16
17pub fn hmac_sha256_hex(key: &str, value: &str) -> String {
21 #[allow(clippy::expect_used)]
24 let mut mac = HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC accepts any key length");
25 mac.update(value.as_bytes());
26 hex::encode(mac.finalize().into_bytes())
27}
28
29pub fn hmac_sha256_verify(key: &str, value: &str, signature_hex: &str) -> Result<(), CryptoError> {
31 let mut mac = HmacSha256::new_from_slice(key.as_bytes())
32 .map_err(|e| CryptoError::InvalidKey(e.to_string()))?;
33 mac.update(value.as_bytes());
34 let expected =
35 hex::decode(signature_hex).map_err(|e| CryptoError::InvalidHex(e.to_string()))?;
36 mac.verify_slice(&expected)
37 .map_err(|_| CryptoError::SignatureMismatch)
38}
39
40pub fn generate_opaque_token(prefix: &str, byte_length: usize) -> String {
44 let mut bytes = vec![0u8; byte_length];
45 rand::rng().fill(&mut bytes[..]);
46 format!("{}{}", prefix, hex::encode(bytes))
47}
48
49pub fn generate_state_nonce(byte_length: usize) -> String {
51 generate_opaque_token("", byte_length)
52}
53
54#[derive(Debug, thiserror::Error)]
56pub enum CryptoError {
57 #[error("invalid key: {0}")]
59 InvalidKey(String),
60
61 #[error("invalid hex: {0}")]
63 InvalidHex(String),
64
65 #[error("signature mismatch")]
67 SignatureMismatch,
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn sha256_hex_deterministic() {
76 let h1 = sha256_hex("hello");
77 let h2 = sha256_hex("hello");
78 assert_eq!(h1, h2);
79 assert_eq!(h1.len(), 64);
80 }
81
82 #[test]
83 fn sha256_hex_different_inputs() {
84 assert_ne!(sha256_hex("hello"), sha256_hex("world"));
85 }
86
87 #[test]
88 fn hmac_roundtrip() {
89 let sig = hmac_sha256_hex("secret-key", "test-message");
90 assert!(hmac_sha256_verify("secret-key", "test-message", &sig).is_ok());
91 }
92
93 #[test]
94 fn hmac_wrong_key_fails() {
95 let sig = hmac_sha256_hex("key1", "message");
96 assert!(hmac_sha256_verify("key2", "message", &sig).is_err());
97 }
98
99 #[test]
100 fn hmac_wrong_message_fails() {
101 let sig = hmac_sha256_hex("key", "msg1");
102 assert!(hmac_sha256_verify("key", "msg2", &sig).is_err());
103 }
104
105 #[test]
106 fn generate_opaque_token_prefix() {
107 let token = generate_opaque_token("tok_", 16);
108 assert!(token.starts_with("tok_"));
109 assert_eq!(token.len(), 4 + 32);
110 }
111
112 #[test]
113 fn generate_state_nonce_length() {
114 let nonce = generate_state_nonce(16);
115 assert_eq!(nonce.len(), 32);
116 }
117
118 #[test]
119 fn hmac_invalid_hex_fails() {
120 assert!(hmac_sha256_verify("key", "msg", "not-hex").is_err());
121 }
122}