api_keys_simplified/
domain.rs

1use crate::{
2    config::{Environment, KeyConfig, KeyPrefix, Separator},
3    error::Result,
4    generator::KeyGenerator,
5    hasher::KeyHasher,
6    secure::SecureString,
7    validator::KeyValidator,
8};
9
10/// Represents a generated API key with its hash.
11///
12/// The key field is stored in a `SecureString` which automatically zeros
13/// its memory on drop, preventing potential memory disclosure.
14#[derive(Clone)]
15pub struct ApiKey {
16    key: SecureString,
17    hash: String,
18}
19
20// Custom Debug implementation to prevent accidental key logging
21impl std::fmt::Debug for ApiKey {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        f.debug_struct("ApiKey")
24            .field("key", &"[REDACTED]")
25            .field("hash", &self.hash)
26            .finish()
27    }
28}
29
30impl ApiKey {
31    pub fn generate(
32        prefix: impl Into<String>,
33        environment: impl Into<Environment>,
34        config: KeyConfig,
35    ) -> Result<Self> {
36        let prefix = KeyPrefix::new(prefix, &config.separator)?;
37
38        let key = KeyGenerator::generate(prefix, environment.into(), &config)?;
39        let hash = KeyHasher::hash(&key, &config.hash_config)?;
40
41        Ok(Self { key, hash })
42    }
43
44    pub fn generate_default(
45        prefix: impl Into<String>,
46        environment: impl Into<Environment>,
47    ) -> Result<Self> {
48        Self::generate(prefix, environment, KeyConfig::default())
49    }
50
51    pub fn generate_high_security(
52        prefix: impl Into<String>,
53        environment: impl Into<Environment>,
54    ) -> Result<Self> {
55        Self::generate(prefix, environment, KeyConfig::high_security())
56    }
57
58    pub fn verify(provided_key: impl AsRef<str>, stored_hash: impl AsRef<str>) -> Result<bool> {
59        KeyValidator::verify(provided_key.as_ref(), stored_hash.as_ref())
60    }
61
62    pub fn verify_checksum(key: impl AsRef<str>) -> Result<bool> {
63        KeyGenerator::verify_checksum(key.as_ref())
64    }
65
66    /// Returns a reference to the secure API key.
67    ///
68    /// To access the underlying string, use `.as_ref()` on the returned `SecureString`:
69    ///
70    /// ```rust
71    /// # use api_keys_simplified::{ApiKey, Environment};
72    /// # let api_key = ApiKey::generate_default("sk", Environment::production()).unwrap();
73    /// let key_str: &str = api_key.key().as_ref();
74    /// ```
75    ///
76    /// # Security Note
77    ///
78    /// The key is stored in secure memory that is automatically zeroed on drop.
79    /// Be careful NOT to clone or log the value unnecessarily.
80    pub fn key(&self) -> &SecureString {
81        &self.key
82    }
83
84    pub fn hash(&self) -> &str {
85        &self.hash
86    }
87
88    pub fn parse_prefix(key: &SecureString, separator: Separator) -> Result<String> {
89        KeyGenerator::parse_key(key.as_ref(), separator).map(|(prefix, _)| prefix)
90    }
91
92    pub fn parse_environment(key: &SecureString, separator: Separator) -> Result<String> {
93        KeyGenerator::parse_key(key.as_ref(), separator).map(|(_, env)| env)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_full_lifecycle() {
103        let api_key = ApiKey::generate_default("sk", Environment::production()).unwrap();
104
105        let key_str = api_key.key();
106        let hash_str = api_key.hash();
107
108        assert!(key_str.as_ref().starts_with("sk-live-"));
109        assert!(hash_str.starts_with("$argon2id$"));
110
111        assert!(ApiKey::verify(key_str.as_ref(), hash_str).unwrap());
112        assert!(!ApiKey::verify("wrong_key", hash_str).unwrap());
113    }
114
115    #[test]
116    fn test_different_presets() {
117        let balanced = ApiKey::generate_default("pk", Environment::test()).unwrap();
118        let high_sec = ApiKey::generate_high_security("sk", Environment::Production).unwrap();
119
120        assert!(!balanced.key().is_empty());
121        assert!(high_sec.key().len() > balanced.key().len());
122    }
123
124    #[test]
125    fn test_parsing() {
126        let key = SecureString::from("sk/live/abc123xyz789");
127        let prefix = ApiKey::parse_prefix(&key, Separator::Slash).unwrap();
128        let env = ApiKey::parse_environment(&key, Separator::Slash).unwrap();
129
130        assert_eq!(prefix, "sk");
131        assert_eq!(env, "live");
132    }
133
134    #[test]
135    fn test_custom_config() {
136        let config = KeyConfig::new()
137            .with_entropy(32)
138            .unwrap()
139            .with_checksum(true);
140
141        let key = ApiKey::generate("custom", Environment::production(), config).unwrap();
142        assert!(ApiKey::verify_checksum(key.key()).unwrap());
143    }
144}