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)]
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#[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 pub fn custom(memory_cost: u32, time_cost: u32, parallelism: u32) -> Result<Self> {
100 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 pub fn memory_cost(&self) -> u32 {
114 self.memory_cost
115 }
116
117 pub fn time_cost(&self) -> u32 {
119 self.time_cost
120 }
121
122 pub fn parallelism(&self) -> u32 {
124 self.parallelism
125 }
126
127 pub fn balanced() -> Self {
134 Self {
135 memory_cost: 19_456,
136 time_cost: 2,
137 parallelism: 1,
138 }
139 }
140
141 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}