1use crate::error::ConfigError;
2use derive_getters::Getters;
3use lazy_static::lazy_static;
4use regex::Regex;
5use strum::{Display, EnumIter, EnumString};
6use strum::{IntoEnumIterator, IntoStaticStr};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct KeyVersion(u32);
18
19impl KeyVersion {
20 pub const NONE: Self = KeyVersion(0);
23
24 pub const V1: Self = KeyVersion(1);
27
28 pub const V2: Self = KeyVersion(2);
31
32 pub const fn new(version: u32) -> Self {
34 KeyVersion(version)
35 }
36
37 pub const fn number(&self) -> u32 {
39 self.0
40 }
41
42 pub const fn is_versioned(&self) -> bool {
44 self.0 > 0
45 }
46
47 pub fn component(&self) -> String {
50 if self.0 == 0 {
51 String::new()
52 } else {
53 format!("v{}", self.0)
54 }
55 }
56}
57
58impl Default for KeyVersion {
59 fn default() -> Self {
60 KeyVersion::NONE
61 }
62}
63
64impl std::fmt::Display for KeyVersion {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 if self.0 == 0 {
67 write!(f, "unversioned")
68 } else {
69 write!(f, "v{}", self.0)
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, EnumIter, EnumString, Display, IntoStaticStr)]
78pub enum Environment {
79 #[strum(serialize = "dev")]
80 Development,
81 #[strum(serialize = "test")]
82 Test,
83 #[strum(serialize = "staging")]
84 Staging,
85 #[strum(serialize = "live")]
86 Production,
87}
88
89lazy_static! {
90 static ref ENVIRONMENT_VARIANTS: Vec<Environment> = Environment::iter().collect();
91 static ref VERSION_PATTERN: Regex = Regex::new(r"v\d+").unwrap();
93}
94
95impl Environment {
96 pub fn dev() -> Self {
97 Environment::Development
98 }
99 pub fn test() -> Self {
100 Environment::Test
101 }
102 pub fn staging() -> Self {
103 Environment::Staging
104 }
105 pub fn production() -> Self {
106 Environment::Production
107 }
108 pub fn variants() -> &'static [Environment] {
109 &ENVIRONMENT_VARIANTS
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct KeyPrefix(String);
115
116impl KeyPrefix {
117 pub fn new(prefix: impl Into<String>) -> std::result::Result<Self, ConfigError> {
118 let prefix = prefix.into();
119 if prefix.is_empty() || prefix.len() > 20 {
120 return Err(ConfigError::InvalidPrefixLength);
121 }
122 if !prefix
123 .chars()
124 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
125 {
126 return Err(ConfigError::InvalidPrefixCharacters);
127 }
128 if let Some(invalid) = Environment::variants().iter().find(|v| {
129 let s: &'static str = (*v).into();
130 prefix.contains(s)
131 }) {
132 return Err(ConfigError::InvalidPrefixSubstring(invalid.to_string()));
133 }
134
135 if VERSION_PATTERN.is_match(&prefix) {
138 return Err(ConfigError::InvalidPrefixVersionLike);
139 }
140
141 Ok(Self(prefix))
142 }
143
144 pub fn as_str(&self) -> &str {
145 &self.0
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, IntoStaticStr, Default)]
151pub enum Separator {
152 #[strum(serialize = "/")]
153 Slash,
154
155 #[strum(serialize = "-")]
156 #[default]
157 Dash,
158
159 #[strum(serialize = "~")]
160 Tilde,
161}
162
163#[derive(Debug, Clone, Getters)]
164pub struct HashConfig {
165 memory_cost: u32,
166 time_cost: u32,
167 parallelism: u32,
168}
169
170impl HashConfig {
171 pub fn custom(
173 memory_cost: u32,
174 time_cost: u32,
175 parallelism: u32,
176 ) -> std::result::Result<Self, ConfigError> {
177 argon2::Params::new(memory_cost, time_cost, parallelism, None)
180 .map_err(|_| ConfigError::InvalidHashParams)?;
181
182 Ok(Self {
183 memory_cost,
184 time_cost,
185 parallelism,
186 })
187 }
188
189 pub fn balanced() -> Self {
198 Self {
199 memory_cost: 47_104,
200 time_cost: 1,
201 parallelism: 1,
202 }
203 }
204
205 pub fn high_security() -> Self {
213 Self {
214 memory_cost: 65_536,
215 time_cost: 3,
216 parallelism: 4,
217 }
218 }
219}
220
221impl Default for HashConfig {
222 fn default() -> Self {
223 Self::balanced()
224 }
225}
226
227#[derive(Default, Debug, Clone, IntoStaticStr)]
228pub enum ChecksumAlgo {
229 #[default]
233 #[strum(serialize = "b3")]
234 Black3,
235}
236
237#[derive(Debug, Clone, Getters)]
238pub struct KeyConfig {
239 entropy_bytes: usize,
240 checksum_length: usize,
241 separator: Separator,
242 checksum_algorithm: ChecksumAlgo,
243 version: KeyVersion,
244}
245
246impl KeyConfig {
247 pub fn new() -> Self {
248 Self::default()
249 }
250
251 pub fn with_entropy(mut self, bytes: usize) -> std::result::Result<Self, ConfigError> {
252 if bytes < 16 {
253 return Err(ConfigError::EntropyTooLow);
254 }
255 if bytes > 64 {
256 return Err(ConfigError::EntropyTooHigh);
257 }
258 self.entropy_bytes = bytes;
259 Ok(self)
260 }
261
262 pub fn checksum(mut self, bytes: usize) -> Result<Self, ConfigError> {
263 match &self.checksum_algorithm {
264 ChecksumAlgo::Black3 => {
265 if bytes < 32 {
266 return Err(ConfigError::ChecksumLenTooSmall);
267 }
268 if bytes > 64 {
269 return Err(ConfigError::ChecksumLenTooLarge);
270 }
271 }
272 }
273 self.checksum_length = bytes;
274 Ok(self)
275 }
276
277 pub fn disable_checksum(mut self) -> Self {
278 self.checksum_length = 0;
279 self
280 }
281
282 pub fn with_separator(mut self, separator: Separator) -> Self {
283 self.separator = separator;
284 self
285 }
286
287 pub fn with_version(mut self, version: KeyVersion) -> Self {
288 self.version = version;
289 self
290 }
291
292 pub fn balanced() -> Self {
293 Self {
294 entropy_bytes: 24,
295 checksum_length: 20,
296 separator: Separator::default(),
297 checksum_algorithm: ChecksumAlgo::default(),
298 version: KeyVersion::default(),
299 }
300 }
301
302 pub fn high_security() -> Self {
303 Self {
304 entropy_bytes: 64,
305 checksum_length: 32,
306 separator: Separator::default(),
307 checksum_algorithm: ChecksumAlgo::default(),
308 version: KeyVersion::default(),
309 }
310 }
311}
312
313impl Default for KeyConfig {
314 fn default() -> Self {
315 Self::balanced()
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::str::FromStr;
323
324 #[test]
325 fn test_prefix_validation() {
326 assert!(KeyPrefix::new("sk").is_ok());
327 assert!(KeyPrefix::new("api_key").is_ok());
328 assert!(KeyPrefix::new("").is_err());
329 assert!(KeyPrefix::new("invalid-prefix").is_ok());
330 }
331
332 #[test]
333 fn test_prefix_cannot_be_version_like() {
334 assert!(KeyPrefix::new("v1").is_err());
336 assert!(KeyPrefix::new("v2").is_err());
337 assert!(KeyPrefix::new("v42").is_err());
338 assert!(KeyPrefix::new("v100").is_err());
339 assert!(KeyPrefix::new("v0").is_err());
340 assert!(KeyPrefix::new("apiv1").is_err());
341 assert!(KeyPrefix::new("apiv2").is_err());
342 assert!(KeyPrefix::new("myv42key").is_err());
343 assert!(KeyPrefix::new("testv1").is_err());
344 assert!(KeyPrefix::new("v1beta").is_err());
345 assert!(KeyPrefix::new("betav1").is_err());
346 assert!(KeyPrefix::new("keyv123end").is_err());
347
348 assert!(KeyPrefix::new("version").is_ok());
350 assert!(KeyPrefix::new("vault").is_ok());
351 assert!(KeyPrefix::new("v_key").is_ok());
352 assert!(KeyPrefix::new("vkey").is_ok());
353 assert!(KeyPrefix::new("api").is_ok());
354 assert!(KeyPrefix::new("sk").is_ok());
355 assert!(KeyPrefix::new("versionkey").is_ok());
356 assert!(KeyPrefix::new("apiversion").is_ok());
357 assert!(KeyPrefix::new("v").is_ok());
359 }
360
361 #[test]
362 fn test_config_validation() {
363 assert!(KeyConfig::new().with_entropy(32).is_ok());
364 assert!(KeyConfig::new().with_entropy(8).is_err());
365 assert!(KeyConfig::new().with_entropy(128).is_err());
366 }
367
368 #[test]
369 fn test_separator_display() {
370 let slash: &'static str = Separator::Slash.into();
371 let dash: &'static str = Separator::Dash.into();
372 let tilde: &'static str = Separator::Tilde.into();
373 assert_eq!(slash, "/");
374 assert_eq!(dash, "-");
375 assert_eq!(tilde, "~");
376 }
377
378 #[test]
379 fn test_separator_from_str() {
380 assert_eq!(Separator::from_str("/").unwrap(), Separator::Slash);
381 assert_eq!(Separator::from_str("-").unwrap(), Separator::Dash);
382 assert_eq!(Separator::from_str("~").unwrap(), Separator::Tilde);
383 assert!(Separator::from_str(".").is_err());
384 }
385
386 #[test]
387 fn test_separator_default() {
388 assert_eq!(Separator::default(), Separator::Dash);
389 }
390
391 #[test]
392 fn test_key_config_with_separator() {
393 let config = KeyConfig::new().with_separator(Separator::Dash);
394 assert_eq!(config.separator, Separator::Dash);
395
396 let config = KeyConfig::new().with_separator(Separator::Tilde);
397 assert_eq!(config.separator, Separator::Tilde);
398 }
399}