pulseengine_mcp_auth/crypto/
hashing.rs1use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
7use rand::RngCore;
8use sha2::{Digest, Sha256};
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13pub struct Salt(pub [u8; 32]);
14
15impl Default for Salt {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl Salt {
22 pub fn new() -> Self {
24 let mut salt = [0u8; 32];
25 rand::thread_rng().fill_bytes(&mut salt);
26 Salt(salt)
27 }
28
29 pub fn from_base64(s: &str) -> Result<Self, HashingError> {
31 let bytes = BASE64
32 .decode(s)
33 .map_err(|e| HashingError::InvalidSalt(format!("Invalid base64: {e}")))?;
34
35 if bytes.len() != 32 {
36 return Err(HashingError::InvalidSalt(format!(
37 "Salt must be 32 bytes, got {}",
38 bytes.len()
39 )));
40 }
41
42 let mut salt = [0u8; 32];
43 salt.copy_from_slice(&bytes);
44 Ok(Salt(salt))
45 }
46
47 pub fn to_base64(&self) -> String {
49 BASE64.encode(&self.0)
50 }
51}
52
53impl fmt::Display for Salt {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{}", self.to_base64())
56 }
57}
58
59#[derive(Debug, thiserror::Error)]
61pub enum HashingError {
62 #[error("Invalid salt: {0}")]
63 InvalidSalt(String),
64
65 #[error("Invalid hash format: {0}")]
66 InvalidHash(String),
67
68 #[error("Hash verification failed")]
69 VerificationFailed,
70}
71
72pub fn generate_salt() -> Salt {
74 Salt::new()
75}
76
77pub fn hash_api_key(api_key: &str, salt: &Salt) -> String {
82 let salted = format!("{}:{}", api_key, salt.to_base64());
84
85 let mut hasher = Sha256::new();
87 hasher.update(salted.as_bytes());
88 let hash = hasher.finalize();
89
90 BASE64.encode(&hash)
92}
93
94pub fn verify_api_key(api_key: &str, stored_hash: &str, salt: &Salt) -> Result<bool, HashingError> {
96 let computed_hash = hash_api_key(api_key, salt);
97
98 use subtle::ConstantTimeEq;
100 let stored_bytes = stored_hash.as_bytes();
101 let computed_bytes = computed_hash.as_bytes();
102
103 if stored_bytes.len() != computed_bytes.len() {
104 return Ok(false);
105 }
106
107 Ok(stored_bytes.ct_eq(computed_bytes).into())
108}
109
110pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
112 use hmac::{Hmac, Mac};
113 type HmacSha256 = Hmac<Sha256>;
114
115 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
116 mac.update(data);
117 mac.finalize().into_bytes().to_vec()
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_salt_generation() {
126 let salt1 = generate_salt();
127 let salt2 = generate_salt();
128
129 assert_ne!(salt1.0, salt2.0);
131
132 let base64 = salt1.to_base64();
134 let salt1_restored = Salt::from_base64(&base64).unwrap();
135 assert_eq!(salt1, salt1_restored);
136 }
137
138 #[test]
139 fn test_api_key_hashing() {
140 let api_key = "test-api-key-12345";
141 let salt = generate_salt();
142
143 let hash1 = hash_api_key(api_key, &salt);
144 let hash2 = hash_api_key(api_key, &salt);
145
146 assert_eq!(hash1, hash2);
148
149 let salt2 = generate_salt();
151 let hash3 = hash_api_key(api_key, &salt2);
152 assert_ne!(hash1, hash3);
153 }
154
155 #[test]
156 fn test_api_key_verification() {
157 let api_key = "test-api-key-12345";
158 let salt = generate_salt();
159 let hash = hash_api_key(api_key, &salt);
160
161 assert!(verify_api_key(api_key, &hash, &salt).unwrap());
163
164 assert!(!verify_api_key("wrong-key", &hash, &salt).unwrap());
166
167 let wrong_salt = generate_salt();
169 assert!(!verify_api_key(api_key, &hash, &wrong_salt).unwrap());
170 }
171
172 #[test]
173 fn test_hmac_sha256() {
174 let key = b"test-key";
175 let data = b"test-data";
176
177 let hmac1 = hmac_sha256(key, data);
178 let hmac2 = hmac_sha256(key, data);
179
180 assert_eq!(hmac1, hmac2);
182
183 let hmac3 = hmac_sha256(b"different-key", data);
185 assert_ne!(hmac1, hmac3);
186 }
187}