api_keys_simplified/
config.rs1use 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)]
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#[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 pub fn custom(memory_cost: u32, time_cost: u32, parallelism: u32) -> Result<Self> {
103 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 pub fn memory_cost(&self) -> u32 {
117 self.memory_cost
118 }
119
120 pub fn time_cost(&self) -> u32 {
122 self.time_cost
123 }
124
125 pub fn parallelism(&self) -> u32 {
127 self.parallelism
128 }
129
130 pub fn balanced() -> Self {
137 Self {
138 memory_cost: 19_456,
139 time_cost: 2,
140 parallelism: 1,
141 }
142 }
143
144 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}