use blake3;
use subtle::ConstantTimeEq;
#[inline]
fn hash_api_key(key: &str) -> [u8; 32] {
blake3::hash(key.as_bytes()).into()
}
pub const MIN_API_KEY_LENGTH: usize = 32;
#[must_use]
#[inline]
pub fn validate_api_key(provided: &str, expected: &str) -> bool {
if provided.len() < MIN_API_KEY_LENGTH || expected.len() < MIN_API_KEY_LENGTH {
tracing::warn!(
"API key validation failed: key length ({}) below minimum required ({} chars)",
provided.len().min(expected.len()),
MIN_API_KEY_LENGTH
);
return false;
}
let provided_hash = hash_api_key(provided);
let expected_hash = hash_api_key(expected);
provided_hash.ct_eq(&expected_hash).into()
}
#[must_use]
pub fn validate_api_key_multiple(provided: &str, expected_keys: &[&str]) -> bool {
let provided_hash = hash_api_key(provided);
for expected_key in expected_keys {
let expected_hash = hash_api_key(expected_key);
if provided_hash.ct_eq(&expected_hash).into() {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
const TEST_KEY_1: &str = "test_key_1234567890abcdef1234567890abc";
const TEST_KEY_2: &str = "test_key_0000000000000000111111111111a";
const TEST_KEY_3: &str = "demo_key_1234567890abcdef1234567890abc";
#[test]
fn test_validate_correct_key() {
assert!(validate_api_key(TEST_KEY_1, TEST_KEY_1));
}
#[test]
fn test_validate_incorrect_key() {
assert!(!validate_api_key(TEST_KEY_2, TEST_KEY_1));
}
#[test]
fn test_validate_prefix_mismatch() {
assert!(!validate_api_key(TEST_KEY_3, TEST_KEY_1));
}
#[test]
fn test_validate_suffix_mismatch() {
let wrong_suffix = "test_key_1234567890abcdef1234567890abx";
assert!(!validate_api_key(wrong_suffix, TEST_KEY_1));
}
#[test]
fn test_validate_empty_keys() {
assert!(!validate_api_key("", ""));
assert!(!validate_api_key("key", ""));
assert!(!validate_api_key("", "key"));
}
#[test]
fn test_validate_short_keys_rejected() {
let short_key = "test_key_short";
let long_key = TEST_KEY_1;
assert!(!validate_api_key(short_key, long_key));
assert!(!validate_api_key(long_key, short_key));
assert!(!validate_api_key(short_key, short_key));
}
#[test]
fn test_validate_case_sensitive() {
let lower = "test_key_abcdefghijklmnopqrstuvwxyz1234";
let upper = "SK_LIVE_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234";
assert!(!validate_api_key(lower, upper));
}
#[test]
fn test_validate_multiple_keys_first_match() {
let provided = "test_key_key1_abcdefghijklmnopqrstuv";
let valid_keys = vec![
"test_key_key1_abcdefghijklmnopqrstuv",
"test_key_key2_abcdefghijklmnopqrstuv",
"test_key_key3_abcdefghijklmnopqrstuv",
];
assert!(validate_api_key_multiple(provided, &valid_keys));
}
#[test]
fn test_validate_multiple_keys_middle_match() {
let provided = "test_key_key2_abcdefghijklmnopqrstuv";
let valid_keys = vec![
"test_key_key1_abcdefghijklmnopqrstuv",
"test_key_key2_abcdefghijklmnopqrstuv",
"test_key_key3_abcdefghijklmnopqrstuv",
];
assert!(validate_api_key_multiple(provided, &valid_keys));
}
#[test]
fn test_validate_multiple_keys_last_match() {
let provided = "test_key_key3_abcdefghijklmnopqrstuv";
let valid_keys = vec![
"test_key_key1_abcdefghijklmnopqrstuv",
"test_key_key2_abcdefghijklmnopqrstuv",
"test_key_key3_abcdefghijklmnopqrstuv",
];
assert!(validate_api_key_multiple(provided, &valid_keys));
}
#[test]
fn test_validate_multiple_keys_no_match() {
let provided = "test_key_wrong_bcdefghijklmnopqrstuv";
let valid_keys = vec![
"test_key_key1_abcdefghijklmnopqrstuv",
"test_key_key2_abcdefghijklmnopqrstuv",
"test_key_key3_abcdefghijklmnopqrstuv",
];
assert!(!validate_api_key_multiple(provided, &valid_keys));
}
#[test]
fn test_validate_multiple_keys_empty_list() {
let provided = "test_key_key1_abcdefghijklmnopqrstuv";
let valid_keys: Vec<&str> = vec![];
assert!(!validate_api_key_multiple(provided, &valid_keys));
}
#[test]
fn test_timing_attack_resistance() {
let correct_key = "test_key_1234567890abcdef1234567890abc";
let wrong_prefix = "xk_live_1234567890abcdef1234567890abc";
let wrong_suffix = "test_key_1234567890abcdef1234567890abx";
for _ in 0..1000 {
let _ = validate_api_key(wrong_prefix, correct_key);
let _ = validate_api_key(wrong_suffix, correct_key);
}
let start = Instant::now();
for _ in 0..10000 {
let _ = validate_api_key(wrong_prefix, correct_key);
}
let prefix_time = start.elapsed();
let start = Instant::now();
for _ in 0..10000 {
let _ = validate_api_key(wrong_suffix, correct_key);
}
let suffix_time = start.elapsed();
let diff_ns = (prefix_time.as_nanos() as i128 - suffix_time.as_nanos() as i128).abs();
let avg_diff_ns = diff_ns / 10000;
println!(
"Average timing difference: {}ns per comparison",
avg_diff_ns
);
assert!(
avg_diff_ns < 500,
"Timing difference too large: {}ns (threshold: 500ns). \
This suggests potential timing attack vulnerability.",
avg_diff_ns
);
}
#[test]
fn test_blake3_hash_consistency() {
let key = "test_key_test";
let hash1 = hash_api_key(key);
let hash2 = hash_api_key(key);
assert_eq!(hash1, hash2);
}
#[test]
fn test_blake3_hash_collision_resistance() {
let key1 = "test_key_1234567890abcdef";
let key2 = "test_key_1234567890abcdeg";
let hash1 = hash_api_key(key1);
let hash2 = hash_api_key(key2);
assert_ne!(hash1, hash2);
}
#[test]
fn test_long_keys() {
let long_key = "test_key_".to_string() + &"a".repeat(1000);
assert!(validate_api_key(&long_key, &long_key));
}
#[test]
fn test_special_characters() {
let key = "test_key_!@#$%^&*()_+-={}[]|:;<>?,./";
assert!(validate_api_key(key, key));
}
#[test]
fn test_unicode_keys() {
let key = "test_key_你好世界🔒abcdefghijklmnopqrst";
assert!(validate_api_key(key, key));
}
}