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