1use chrono::{DateTime, Utc};
22use rand::rngs::StdRng;
23use rand::{Rng, SeedableRng};
24use serde::{Deserialize, Serialize};
25use std::collections::HashMap;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
33#[serde(rename_all = "lowercase")]
34pub enum SecretCharset {
35 #[default]
37 Alphanumeric,
38 Alpha,
40 Numeric,
42 Hex,
44 Base64,
46 UrlSafe,
48}
49
50impl SecretCharset {
51 pub const fn chars(&self) -> &'static [u8] {
53 match self {
54 Self::Alphanumeric => b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
55 Self::Alpha => b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
56 Self::Numeric => b"0123456789",
57 Self::Hex => b"0123456789abcdef",
58 Self::Base64 => b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
59 Self::UrlSafe => b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
60 }
61 }
62
63 pub fn parse(s: &str) -> Option<Self> {
65 match s.to_lowercase().as_str() {
66 "alphanumeric" | "alnum" => Some(Self::Alphanumeric),
67 "alpha" => Some(Self::Alpha),
68 "numeric" | "num" | "digits" => Some(Self::Numeric),
69 "hex" => Some(Self::Hex),
70 "base64" => Some(Self::Base64),
71 "urlsafe" | "url" => Some(Self::UrlSafe),
72 _ => None,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SecretEntry {
84 value: String,
86
87 pub created_at: DateTime<Utc>,
89
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub rotated_at: Option<DateTime<Utc>>,
93
94 #[serde(default)]
96 pub charset: SecretCharset,
97
98 pub length: usize,
100}
101
102impl SecretEntry {
103 pub fn new(value: String, charset: SecretCharset, length: usize) -> Self {
105 Self {
106 value,
107 created_at: Utc::now(),
108 rotated_at: None,
109 charset,
110 length,
111 }
112 }
113
114 pub fn value(&self) -> &str {
116 &self.value
117 }
118
119 pub fn rotate(&mut self, new_value: String) {
121 self.value = new_value;
122 self.rotated_at = Some(Utc::now());
123 }
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct SecretState {
135 #[serde(default)]
137 pub version: u32,
138
139 #[serde(default)]
141 secrets: HashMap<String, SecretEntry>,
142
143 #[serde(skip)]
145 dirty: bool,
146}
147
148impl SecretState {
149 pub const CURRENT_VERSION: u32 = 1;
151
152 pub fn new() -> Self {
154 Self {
155 version: Self::CURRENT_VERSION,
156 secrets: HashMap::new(),
157 dirty: false,
158 }
159 }
160
161 pub fn get(&self, name: &str) -> Option<&SecretEntry> {
163 self.secrets.get(name)
164 }
165
166 pub fn get_value(&self, name: &str) -> Option<&str> {
168 self.secrets.get(name).map(|e| e.value())
169 }
170
171 pub fn insert(&mut self, name: String, entry: SecretEntry) {
173 self.secrets.insert(name, entry);
174 self.dirty = true;
175 }
176
177 pub fn is_dirty(&self) -> bool {
179 self.dirty
180 }
181
182 pub fn mark_clean(&mut self) {
184 self.dirty = false;
185 }
186
187 pub fn names(&self) -> impl Iterator<Item = &str> {
189 self.secrets.keys().map(String::as_str)
190 }
191
192 pub fn iter(&self) -> impl Iterator<Item = (&str, &SecretEntry)> {
194 self.secrets.iter().map(|(k, v)| (k.as_str(), v))
195 }
196
197 pub fn len(&self) -> usize {
199 self.secrets.len()
200 }
201
202 pub fn is_empty(&self) -> bool {
204 self.secrets.is_empty()
205 }
206
207 pub fn rotate(&mut self, name: &str, new_value: String) -> bool {
209 if let Some(entry) = self.secrets.get_mut(name) {
210 entry.rotate(new_value);
211 self.dirty = true;
212 true
213 } else {
214 false
215 }
216 }
217}
218
219impl PartialEq<SecretState> for SecretState {
221 fn eq(&self, other: &SecretState) -> bool {
222 self.version == other.version && self.secrets == other.secrets
223 }
224}
225
226impl PartialEq for SecretEntry {
227 fn eq(&self, other: &Self) -> bool {
228 self.value == other.value && self.charset == other.charset && self.length == other.length
230 }
231}
232
233#[derive(Debug)]
242pub struct SecretGenerator {
243 state: SecretState,
244 rng: StdRng,
245}
246
247impl SecretGenerator {
248 pub fn new() -> Self {
250 Self {
251 state: SecretState::new(),
252 rng: StdRng::from_entropy(),
253 }
254 }
255
256 pub fn with_state(state: SecretState) -> Self {
258 Self {
259 state,
260 rng: StdRng::from_entropy(),
261 }
262 }
263
264 pub fn get_or_generate(&mut self, name: &str, length: usize) -> String {
266 self.get_or_generate_with_charset(name, length, SecretCharset::default())
267 }
268
269 pub fn get_or_generate_with_charset(
271 &mut self,
272 name: &str,
273 length: usize,
274 charset: SecretCharset,
275 ) -> String {
276 if let Some(entry) = self.state.get(name) {
278 return entry.value().to_string();
279 }
280
281 let value = self.generate_random(length, charset);
283 let entry = SecretEntry::new(value.clone(), charset, length);
284 self.state.insert(name.to_string(), entry);
285
286 value
287 }
288
289 fn generate_random(&mut self, length: usize, charset: SecretCharset) -> String {
291 let chars = charset.chars();
292 (0..length)
293 .map(|_| {
294 let idx = self.rng.gen_range(0..chars.len());
295 chars[idx] as char
296 })
297 .collect()
298 }
299
300 pub fn state(&self) -> &SecretState {
302 &self.state
303 }
304
305 pub fn into_state(self) -> SecretState {
307 self.state
308 }
309
310 pub fn is_dirty(&self) -> bool {
312 self.state.is_dirty()
313 }
314
315 pub fn rotate(&mut self, name: &str) -> Option<String> {
317 let entry = self.state.get(name)?;
318 let new_value = self.generate_random(entry.length, entry.charset);
319 self.state.rotate(name, new_value.clone());
320 Some(new_value)
321 }
322}
323
324impl Default for SecretGenerator {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_charset_chars() {
340 assert_eq!(SecretCharset::Numeric.chars(), b"0123456789");
341 assert_eq!(SecretCharset::Hex.chars().len(), 16);
342 assert_eq!(SecretCharset::Alphanumeric.chars().len(), 62);
343 }
344
345 #[test]
346 fn test_charset_parse() {
347 assert_eq!(SecretCharset::parse("hex"), Some(SecretCharset::Hex));
348 assert_eq!(
349 SecretCharset::parse("ALPHANUMERIC"),
350 Some(SecretCharset::Alphanumeric)
351 );
352 assert_eq!(SecretCharset::parse("unknown"), None);
353 }
354
355 #[test]
356 fn test_generator_idempotent() {
357 let mut generator = SecretGenerator::new();
358
359 let secret1 = generator.get_or_generate("test", 16);
360 let secret2 = generator.get_or_generate("test", 16);
361
362 assert_eq!(secret1, secret2);
363 assert_eq!(secret1.len(), 16);
364 }
365
366 #[test]
367 fn test_generator_different_names() {
368 let mut generator = SecretGenerator::new();
369
370 let secret1 = generator.get_or_generate("password1", 16);
371 let secret2 = generator.get_or_generate("password2", 16);
372
373 assert_ne!(secret1, secret2);
374 }
375
376 #[test]
377 fn test_generator_charset() {
378 let mut generator = SecretGenerator::new();
379
380 let hex = generator.get_or_generate_with_charset("hex-token", 32, SecretCharset::Hex);
381 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
382
383 let numeric = generator.get_or_generate_with_charset("pin", 6, SecretCharset::Numeric);
384 assert!(numeric.chars().all(|c| c.is_ascii_digit()));
385 }
386
387 #[test]
388 fn test_state_persistence() {
389 let mut generator1 = SecretGenerator::new();
390 let secret = generator1.get_or_generate("db-password", 24);
391
392 let state = generator1.into_state();
394 let json = serde_json::to_string(&state).unwrap();
395
396 let loaded_state: SecretState = serde_json::from_str(&json).unwrap();
398 let mut generator2 = SecretGenerator::with_state(loaded_state);
399
400 let secret2 = generator2.get_or_generate("db-password", 24);
402 assert_eq!(secret, secret2);
403 assert!(!generator2.is_dirty()); }
405
406 #[test]
407 fn test_rotate() {
408 let mut generator = SecretGenerator::new();
409
410 let original = generator.get_or_generate("api-key", 32);
411 let rotated = generator.rotate("api-key").unwrap();
412
413 assert_ne!(original, rotated);
414 assert_eq!(rotated.len(), 32);
415
416 let current = generator.get_or_generate("api-key", 32);
418 assert_eq!(current, rotated);
419 }
420
421 #[test]
422 fn test_dirty_flag() {
423 let mut generator = SecretGenerator::new();
424 assert!(!generator.is_dirty());
425
426 generator.get_or_generate("new-secret", 16);
427 assert!(generator.is_dirty());
428
429 let mut state = generator.into_state();
430 state.mark_clean();
431 assert!(!state.is_dirty());
432 }
433}