use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use rand::RngCore;
use sha2::{Digest, Sha256};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Salt(pub [u8; 32]);
impl Default for Salt {
fn default() -> Self {
Self::new()
}
}
impl Salt {
pub fn new() -> Self {
let mut salt = [0u8; 32];
rand::thread_rng().fill_bytes(&mut salt);
Salt(salt)
}
pub fn from_base64(s: &str) -> Result<Self, HashingError> {
let bytes = BASE64
.decode(s)
.map_err(|e| HashingError::InvalidSalt(format!("Invalid base64: {e}")))?;
if bytes.len() != 32 {
return Err(HashingError::InvalidSalt(format!(
"Salt must be 32 bytes, got {}",
bytes.len()
)));
}
let mut salt = [0u8; 32];
salt.copy_from_slice(&bytes);
Ok(Salt(salt))
}
pub fn to_base64(&self) -> String {
BASE64.encode(&self.0)
}
}
impl fmt::Display for Salt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base64())
}
}
#[derive(Debug, thiserror::Error)]
pub enum HashingError {
#[error("Invalid salt: {0}")]
InvalidSalt(String),
#[error("Invalid hash format: {0}")]
InvalidHash(String),
#[error("Hash verification failed")]
VerificationFailed,
}
pub fn generate_salt() -> Salt {
Salt::new()
}
pub fn hash_api_key(api_key: &str, salt: &Salt) -> String {
let salted = format!("{}:{}", api_key, salt.to_base64());
let mut hasher = Sha256::new();
hasher.update(salted.as_bytes());
let hash = hasher.finalize();
BASE64.encode(&hash)
}
pub fn verify_api_key(api_key: &str, stored_hash: &str, salt: &Salt) -> Result<bool, HashingError> {
let computed_hash = hash_api_key(api_key, salt);
use subtle::ConstantTimeEq;
let stored_bytes = stored_hash.as_bytes();
let computed_bytes = computed_hash.as_bytes();
if stored_bytes.len() != computed_bytes.len() {
return Ok(false);
}
Ok(stored_bytes.ct_eq(computed_bytes).into())
}
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
use hmac::{Hmac, Mac};
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_salt_generation() {
let salt1 = generate_salt();
let salt2 = generate_salt();
assert_ne!(salt1.0, salt2.0);
let base64 = salt1.to_base64();
let salt1_restored = Salt::from_base64(&base64).unwrap();
assert_eq!(salt1, salt1_restored);
}
#[test]
fn test_api_key_hashing() {
let api_key = "test-api-key-12345";
let salt = generate_salt();
let hash1 = hash_api_key(api_key, &salt);
let hash2 = hash_api_key(api_key, &salt);
assert_eq!(hash1, hash2);
let salt2 = generate_salt();
let hash3 = hash_api_key(api_key, &salt2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_api_key_verification() {
let api_key = "test-api-key-12345";
let salt = generate_salt();
let hash = hash_api_key(api_key, &salt);
assert!(verify_api_key(api_key, &hash, &salt).unwrap());
assert!(!verify_api_key("wrong-key", &hash, &salt).unwrap());
let wrong_salt = generate_salt();
assert!(!verify_api_key(api_key, &hash, &wrong_salt).unwrap());
}
#[test]
fn test_hmac_sha256() {
let key = b"test-key";
let data = b"test-data";
let hmac1 = hmac_sha256(key, data);
let hmac2 = hmac_sha256(key, data);
assert_eq!(hmac1, hmac2);
let hmac3 = hmac_sha256(b"different-key", data);
assert_ne!(hmac1, hmac3);
}
}