1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct SecretRotation {
12 pub timestamp: DateTime<Utc>,
14 pub machine_id: String,
16 pub old_secret_hash: String,
18 pub new_secret_hash: String,
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
24pub struct MachineMastering {
25 pub timestamp: DateTime<Utc>,
27 pub from_machine: String,
29 pub to_machine: String,
31 pub mastered_by: String,
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct ConfigChange {
38 pub timestamp: DateTime<Utc>,
40 pub machine_id: String,
42 pub field: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub old_value: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub new_value: Option<String>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
54pub struct GenerationLogEntry {
55 pub timestamp: DateTime<Utc>,
57 pub machine_id: String,
59 pub count: u64,
61 pub counter_start: u64,
63 pub counter_end: u64,
65}
66
67#[derive(Clone, Debug, Default, Serialize, Deserialize)]
69pub struct History {
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub secret_rotations: Vec<SecretRotation>,
73 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub machine_masterings: Vec<MachineMastering>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub config_changes: Vec<ConfigChange>,
79}
80
81impl History {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn record_secret_rotation(
89 &mut self,
90 machine_id: &[u8; 32],
91 old_secret_hash: &str,
92 new_secret_hash: &str,
93 ) {
94 self.secret_rotations.push(SecretRotation {
95 timestamp: Utc::now(),
96 machine_id: hex::encode(machine_id),
97 old_secret_hash: old_secret_hash.to_string(),
98 new_secret_hash: new_secret_hash.to_string(),
99 });
100 }
101
102 pub fn record_machine_mastering(
104 &mut self,
105 from_machine: &[u8; 32],
106 to_machine: &[u8; 32],
107 mastered_by: &[u8; 32],
108 ) {
109 self.machine_masterings.push(MachineMastering {
110 timestamp: Utc::now(),
111 from_machine: hex::encode(from_machine),
112 to_machine: hex::encode(to_machine),
113 mastered_by: hex::encode(mastered_by),
114 });
115 }
116
117 pub fn record_config_change(
119 &mut self,
120 machine_id: &[u8; 32],
121 field: &str,
122 old_value: Option<&str>,
123 new_value: Option<&str>,
124 ) {
125 self.config_changes.push(ConfigChange {
126 timestamp: Utc::now(),
127 machine_id: hex::encode(machine_id),
128 field: field.to_string(),
129 old_value: old_value.map(String::from),
130 new_value: new_value.map(String::from),
131 });
132 }
133
134 pub fn clear(&mut self, keep_last: Option<usize>) {
136 if let Some(n) = keep_last {
137 let sr_len = self.secret_rotations.len();
138 if sr_len > n {
139 self.secret_rotations = self.secret_rotations.split_off(sr_len - n);
140 }
141
142 let mm_len = self.machine_masterings.len();
143 if mm_len > n {
144 self.machine_masterings = self.machine_masterings.split_off(mm_len - n);
145 }
146
147 let cc_len = self.config_changes.len();
148 if cc_len > n {
149 self.config_changes = self.config_changes.split_off(cc_len - n);
150 }
151 } else {
152 self.secret_rotations.clear();
153 self.machine_masterings.clear();
154 self.config_changes.clear();
155 }
156 }
157
158 pub fn to_json(&self) -> String {
160 serde_json::to_string_pretty(self).unwrap_or_default()
161 }
162}
163
164#[derive(Clone, Debug, Serialize, Deserialize)]
171pub struct AuditInfo {
172 pub created_at: DateTime<Utc>,
174
175 pub created_machine: String,
177
178 #[serde(skip_serializing_if = "Option::is_none")]
180 pub mastered_at: Option<DateTime<Utc>>,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub mastered_for: Option<String>,
185
186 pub generation_count: u64,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub last_generated_at: Option<DateTime<Utc>>,
192
193 #[serde(default, skip_serializing_if = "History::is_empty")]
195 pub history: History,
196
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
199 pub generation_log: Vec<GenerationLogEntry>,
200}
201
202impl History {
203 pub fn is_empty(&self) -> bool {
205 self.secret_rotations.is_empty()
206 && self.machine_masterings.is_empty()
207 && self.config_changes.is_empty()
208 }
209}
210
211impl AuditInfo {
212 pub fn new(machine_id: &[u8; 32]) -> Self {
217 Self {
218 created_at: Utc::now(),
219 created_machine: hex::encode(machine_id),
220 mastered_at: None,
221 mastered_for: None,
222 generation_count: 0,
223 last_generated_at: None,
224 history: History::new(),
225 generation_log: Vec::new(),
226 }
227 }
228
229 pub fn with_timestamp(machine_id: &[u8; 32], created_at: DateTime<Utc>) -> Self {
233 Self {
234 created_at,
235 created_machine: hex::encode(machine_id),
236 mastered_at: None,
237 mastered_for: None,
238 generation_count: 0,
239 last_generated_at: None,
240 history: History::new(),
241 generation_log: Vec::new(),
242 }
243 }
244
245 pub fn record_master(&mut self, target_machine: &[u8; 32]) {
252 self.mastered_at = Some(Utc::now());
253 self.mastered_for = Some(hex::encode(target_machine));
254 }
255
256 pub fn record_master_with_history(
258 &mut self,
259 from_machine: &[u8; 32],
260 target_machine: &[u8; 32],
261 mastered_by: &[u8; 32],
262 ) {
263 self.history
264 .record_machine_mastering(from_machine, target_machine, mastered_by);
265 self.mastered_at = Some(Utc::now());
266 self.mastered_for = Some(hex::encode(target_machine));
267 }
268
269 pub fn record_generation(&mut self, count: u64) {
276 self.generation_count = self.generation_count.saturating_add(count);
277 self.last_generated_at = Some(Utc::now());
278 }
279
280 pub fn record_generation_with_log(
282 &mut self,
283 machine_id: &[u8; 32],
284 count: u64,
285 counter_start: u64,
286 ) {
287 let counter_end = counter_start.saturating_add(count);
288 self.generation_log.push(GenerationLogEntry {
289 timestamp: Utc::now(),
290 machine_id: hex::encode(machine_id),
291 count,
292 counter_start,
293 counter_end,
294 });
295 self.generation_count = self.generation_count.saturating_add(count);
296 self.last_generated_at = Some(Utc::now());
297 }
298
299 pub fn is_mastered(&self) -> bool {
301 self.mastered_for.is_some()
302 }
303
304 pub fn bound_machine(&self) -> Option<&str> {
306 self.mastered_for.as_deref()
307 }
308
309 pub fn created_machine_bytes(&self) -> Option<[u8; 32]> {
311 Self::hex_to_machine_id(&self.created_machine)
312 }
313
314 pub fn mastered_machine_bytes(&self) -> Option<[u8; 32]> {
316 self.mastered_for
317 .as_ref()
318 .and_then(|s| Self::hex_to_machine_id(s))
319 }
320
321 fn hex_to_machine_id(hex_str: &str) -> Option<[u8; 32]> {
323 let bytes = hex::decode(hex_str).ok()?;
324 if bytes.len() != 32 {
325 return None;
326 }
327 let mut arr = [0u8; 32];
328 arr.copy_from_slice(&bytes);
329 Some(arr)
330 }
331
332 pub fn total_codes_from_log(&self) -> u64 {
334 self.generation_log.iter().map(|e| e.count).sum()
335 }
336
337 pub fn get_generation_log(&self) -> &[GenerationLogEntry] {
339 &self.generation_log
340 }
341
342 pub fn get_history(&self) -> &History {
344 &self.history
345 }
346
347 pub fn clear_generation_log(&mut self, keep_last: Option<usize>) {
349 if let Some(n) = keep_last {
350 let len = self.generation_log.len();
351 if len > n {
352 self.generation_log = self.generation_log.split_off(len - n);
353 }
354 } else {
355 self.generation_log.clear();
356 }
357 }
358
359 pub fn clear_history(&mut self, keep_last: Option<usize>) {
361 self.history.clear(keep_last);
362 }
363
364 pub fn export_generation_log(&self) -> String {
366 serde_json::to_string_pretty(&self.generation_log).unwrap_or_default()
367 }
368
369 pub fn export_history(&self) -> String {
371 self.history.to_json()
372 }
373
374 pub fn summary(&self) -> String {
376 let mut lines = Vec::new();
377
378 lines.push(format!(
379 "Created: {}",
380 self.created_at.format("%Y-%m-%d %H:%M:%S UTC")
381 ));
382 lines.push(format!("Created on: {}...", &self.created_machine[..16]));
383
384 if let Some(mastered_at) = &self.mastered_at {
385 lines.push(format!(
386 "Mastered: {}",
387 mastered_at.format("%Y-%m-%d %H:%M:%S UTC")
388 ));
389 }
390
391 if let Some(mastered_for) = &self.mastered_for {
392 lines.push(format!("Bound to: {}...", &mastered_for[..16]));
393 }
394
395 lines.push(format!("Codes generated: {}", self.generation_count));
396
397 if let Some(last) = &self.last_generated_at {
398 lines.push(format!(
399 "Last generated: {}",
400 last.format("%Y-%m-%d %H:%M:%S UTC")
401 ));
402 }
403
404 if !self.generation_log.is_empty() {
405 lines.push(format!(
406 "Generation log entries: {}",
407 self.generation_log.len()
408 ));
409 }
410
411 if !self.history.is_empty() {
412 lines.push(format!(
413 "History: {} rotations, {} masterings, {} config changes",
414 self.history.secret_rotations.len(),
415 self.history.machine_masterings.len(),
416 self.history.config_changes.len()
417 ));
418 }
419
420 lines.join("\n")
421 }
422}
423
424impl Default for AuditInfo {
425 fn default() -> Self {
426 Self {
427 created_at: Utc::now(),
428 created_machine: "0".repeat(64),
429 mastered_at: None,
430 mastered_for: None,
431 generation_count: 0,
432 last_generated_at: None,
433 history: History::new(),
434 generation_log: Vec::new(),
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn test_new_audit() {
445 let machine_id = [0xABu8; 32];
446 let audit = AuditInfo::new(&machine_id);
447
448 assert_eq!(audit.created_machine, "ab".repeat(32));
449 assert!(!audit.is_mastered());
450 assert_eq!(audit.generation_count, 0);
451 assert!(audit.last_generated_at.is_none());
452 }
453
454 #[test]
455 fn test_record_master() {
456 let machine_id = [0xABu8; 32];
457 let mut audit = AuditInfo::new(&machine_id);
458
459 let target_machine = [0xCDu8; 32];
460 audit.record_master(&target_machine);
461
462 assert!(audit.is_mastered());
463 assert!(audit.mastered_at.is_some());
464 assert_eq!(audit.mastered_for, Some("cd".repeat(32)));
465 }
466
467 #[test]
468 fn test_record_generation() {
469 let machine_id = [0xABu8; 32];
470 let mut audit = AuditInfo::new(&machine_id);
471
472 audit.record_generation(100);
473 assert_eq!(audit.generation_count, 100);
474 assert!(audit.last_generated_at.is_some());
475
476 audit.record_generation(50);
477 assert_eq!(audit.generation_count, 150);
478 }
479
480 #[test]
481 fn test_generation_count_overflow() {
482 let machine_id = [0xABu8; 32];
483 let mut audit = AuditInfo::new(&machine_id);
484
485 audit.generation_count = u64::MAX - 10;
486 audit.record_generation(100);
487
488 assert_eq!(audit.generation_count, u64::MAX);
490 }
491
492 #[test]
493 fn test_machine_id_conversion() {
494 let machine_id = [0xABu8; 32];
495 let audit = AuditInfo::new(&machine_id);
496
497 let recovered = audit.created_machine_bytes().unwrap();
498 assert_eq!(recovered, machine_id);
499 }
500
501 #[test]
502 fn test_serialization() {
503 let machine_id = [0xABu8; 32];
504 let mut audit = AuditInfo::new(&machine_id);
505 audit.record_generation(42);
506
507 let json = serde_json::to_string(&audit).unwrap();
508 let recovered: AuditInfo = serde_json::from_str(&json).unwrap();
509
510 assert_eq!(recovered.created_machine, audit.created_machine);
511 assert_eq!(recovered.generation_count, 42);
512 }
513
514 #[test]
515 fn test_summary() {
516 let machine_id = [0xABu8; 32];
517 let mut audit = AuditInfo::new(&machine_id);
518 audit.record_generation(100);
519
520 let summary = audit.summary();
521 assert!(summary.contains("Created:"));
522 assert!(summary.contains("Codes generated: 100"));
523 }
524}