api_keys_simplified/
config.rs

1use crate::error::{ConfigError, Result};
2use lazy_static::lazy_static;
3use strum::{Display, EnumIter, EnumString};
4use strum::{IntoEnumIterator, IntoStaticStr};
5
6/// Deployment environment for API keys (dev/test/staging/live).
7/// Used to visually distinguish keys across different environments and prevent accidental misuse
8/// And allow users to set different Rate limits based on Environment.
9#[derive(Debug, Clone, PartialEq, Eq, EnumIter, EnumString, Display, IntoStaticStr)]
10pub enum Environment {
11    #[strum(serialize = "dev")]
12    Development,
13    #[strum(serialize = "test")]
14    Test,
15    #[strum(serialize = "staging")]
16    Staging,
17    #[strum(serialize = "live")]
18    Production,
19}
20
21lazy_static! {
22    static ref ENVIRONMENT_VARIANTS: Vec<Environment> = Environment::iter().collect();
23}
24
25impl Environment {
26    pub fn dev() -> Self {
27        Environment::Development
28    }
29    pub fn test() -> Self {
30        Environment::Test
31    }
32    pub fn staging() -> Self {
33        Environment::Staging
34    }
35    pub fn production() -> Self {
36        Environment::Production
37    }
38    pub fn variants() -> &'static [Environment] {
39        &ENVIRONMENT_VARIANTS
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct KeyPrefix(String);
45
46impl KeyPrefix {
47    pub fn new(prefix: impl Into<String>, separator: &Separator) -> Result<Self> {
48        let prefix = prefix.into();
49        if prefix.is_empty() || prefix.len() > 20 {
50            return Err(ConfigError::InvalidPrefixLength.into());
51        }
52        if !prefix
53            .chars()
54            .all(|c| c.is_ascii_alphanumeric() || c == '_')
55        {
56            return Err(ConfigError::InvalidPrefixCharacters.into());
57        }
58        let sep_string: &'static str = separator.into();
59        if let Some(invalid) = Environment::variants()
60            .iter()
61            .find(|v| prefix.contains(&format!("{sep_string}{v}{sep_string}")))
62        {
63            return Err(ConfigError::InvalidPrefixSubstring(invalid.to_string()).into());
64        }
65        Ok(Self(prefix))
66    }
67
68    pub fn as_str(&self) -> &str {
69        &self.0
70    }
71}
72
73impl Default for KeyPrefix {
74    fn default() -> Self {
75        Self("key".to_string())
76    }
77}
78
79/// Separator character for API key components (prefix, environment and data).
80#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr, Default)]
81pub enum Separator {
82    #[strum(serialize = "/")]
83    Slash,
84
85    #[strum(serialize = "-")]
86    #[default]
87    Dash,
88
89    #[strum(serialize = "~")]
90    Tilde,
91}
92
93#[derive(Debug, Clone)]
94pub struct HashConfig {
95    memory_cost: u32,
96    time_cost: u32,
97    parallelism: u32,
98}
99
100impl HashConfig {
101    /// Creates a custom HashConfig with validated parameters.
102    pub fn custom(memory_cost: u32, time_cost: u32, parallelism: u32) -> Result<Self> {
103        // Verify parameters are accepted by Argon2 library
104        // Bad idea to do it here.. but we'll keep it here for now
105        argon2::Params::new(memory_cost, time_cost, parallelism, None)
106            .map_err(|_| ConfigError::InvalidHashParams)?;
107
108        Ok(Self {
109            memory_cost,
110            time_cost,
111            parallelism,
112        })
113    }
114
115    /// Returns the memory cost in KiB.
116    pub fn memory_cost(&self) -> u32 {
117        self.memory_cost
118    }
119
120    /// Returns the time cost (number of iterations).
121    pub fn time_cost(&self) -> u32 {
122        self.time_cost
123    }
124
125    /// Returns the parallelism degree.
126    pub fn parallelism(&self) -> u32 {
127        self.parallelism
128    }
129
130    /// Balanced preset for general production use.
131    ///
132    /// - Memory: 19 MB
133    /// - Time: 2 iterations
134    /// - Parallelism: 1 thread
135    /// - Verification time: ~50ms
136    pub fn balanced() -> Self {
137        Self {
138            memory_cost: 19_456,
139            time_cost: 2,
140            parallelism: 1,
141        }
142    }
143
144    /// High security preset for sensitive operations.
145    ///
146    /// - Memory: 64 MB
147    /// - Time: 3 iterations
148    /// - Parallelism: 4 threads
149    /// - Verification time: ~150ms
150    pub fn high_security() -> Self {
151        Self {
152            memory_cost: 65_536,
153            time_cost: 3,
154            parallelism: 4,
155        }
156    }
157}
158
159impl Default for HashConfig {
160    fn default() -> Self {
161        Self::balanced()
162    }
163}
164
165#[derive(Debug, Clone)]
166pub struct KeyConfig {
167    pub entropy_bytes: usize,
168    pub include_checksum: bool,
169    pub hash_config: HashConfig,
170    pub separator: Separator,
171}
172
173impl KeyConfig {
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    pub fn with_entropy(mut self, bytes: usize) -> Result<Self> {
179        if bytes < 16 {
180            return Err(ConfigError::EntropyTooLow.into());
181        }
182        if bytes > 64 {
183            return Err(ConfigError::EntropyTooHigh.into());
184        }
185        self.entropy_bytes = bytes;
186        Ok(self)
187    }
188
189    pub fn with_checksum(mut self, include: bool) -> Self {
190        self.include_checksum = include;
191        self
192    }
193
194    pub fn with_hash_config(mut self, hash_config: HashConfig) -> Self {
195        self.hash_config = hash_config;
196        self
197    }
198
199    pub fn with_separator(mut self, separator: Separator) -> Self {
200        self.separator = separator;
201        self
202    }
203
204    pub fn balanced() -> Self {
205        Self {
206            entropy_bytes: 24,
207            include_checksum: true,
208            hash_config: HashConfig::balanced(),
209            separator: Separator::default(),
210        }
211    }
212
213    pub fn high_security() -> Self {
214        Self {
215            entropy_bytes: 32,
216            include_checksum: true,
217            hash_config: HashConfig::high_security(),
218            separator: Separator::default(),
219        }
220    }
221}
222
223impl Default for KeyConfig {
224    fn default() -> Self {
225        Self::balanced()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use std::str::FromStr;
233
234    #[test]
235    fn test_prefix_validation() {
236        let sep = &Separator::default();
237        assert!(KeyPrefix::new("sk", sep).is_ok());
238        assert!(KeyPrefix::new("api_key", sep).is_ok());
239        assert!(KeyPrefix::new("", sep).is_err());
240        assert!(KeyPrefix::new("invalid-prefix", sep).is_err());
241    }
242
243    #[test]
244    fn test_config_validation() {
245        assert!(KeyConfig::new().with_entropy(32).is_ok());
246        assert!(KeyConfig::new().with_entropy(8).is_err());
247        assert!(KeyConfig::new().with_entropy(128).is_err());
248    }
249
250    #[test]
251    fn test_separator_display() {
252        let slash: &'static str = Separator::Slash.into();
253        let dash: &'static str = Separator::Dash.into();
254        let tilde: &'static str = Separator::Tilde.into();
255        assert_eq!(slash, "/");
256        assert_eq!(dash, "-");
257        assert_eq!(tilde, "~");
258    }
259
260    #[test]
261    fn test_separator_from_str() {
262        assert_eq!(Separator::from_str("/").unwrap(), Separator::Slash);
263        assert_eq!(Separator::from_str("-").unwrap(), Separator::Dash);
264        assert_eq!(Separator::from_str("~").unwrap(), Separator::Tilde);
265        assert!(Separator::from_str(".").is_err());
266    }
267
268    #[test]
269    fn test_separator_default() {
270        assert_eq!(Separator::default(), Separator::Dash);
271    }
272
273    #[test]
274    fn test_key_config_with_separator() {
275        let config = KeyConfig::new().with_separator(Separator::Dash);
276        assert_eq!(config.separator, Separator::Dash);
277
278        let config = KeyConfig::new().with_separator(Separator::Tilde);
279        assert_eq!(config.separator, Separator::Tilde);
280    }
281}