rust_keyvault/
audit.rs

1//! Audit logging for key operations
2
3use crate::{Algorithm, KeyState};
4use serde::{Deserialize, Serialize};
5use std::fs::{File, OpenOptions};
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9
10/// Types of auditable events
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "event_type")]
13pub enum AuditEvent {
14    /// Key was created
15    KeyCreated {
16        /// The ID of the created key
17        key_id: String,
18        /// The algorithm used for the key
19        algorithm: Algorithm,
20        /// The version number of the key
21        version: u32,
22    },
23
24    /// Key was retrieved/accessed
25    KeyAccessed {
26        /// The ID of the accessed key
27        key_id: String,
28        /// The operation performed (e.g., "encrypt", "decrypt", "sign", "verify")
29        operation: String,
30    },
31
32    /// Key was rotated to a new version
33    KeyRotated {
34        /// The base ID of the key being rotated
35        base_id: String,
36        /// The version number before rotation
37        old_version: u32,
38        /// The version number after rotation
39        new_version: u32,
40    },
41
42    /// Key state changed
43    KeyStateChanged {
44        /// The ID of the key whose state changed
45        key_id: String,
46        /// The state before the change
47        old_state: KeyState,
48        /// The state after the change
49        new_state: KeyState,
50    },
51
52    /// Key was deleted
53    KeyDeleted {
54        /// The ID of the deleted key
55        key_id: String,
56        /// The version number of the deleted key
57        version: u32,
58    },
59
60    /// Authentication attempt (password-based unlock)
61    AuthenticationAttempt {
62        /// Whether the authentication was successful
63        success: bool,
64        /// The storage path being accessed
65        storage_path: String,
66    },
67
68    /// Encryption operation performed
69    EncryptionPerformed {
70        /// The ID of the key used for encryption
71        key_id: String,
72        /// The size of data encrypted in bytes
73        data_size: usize,
74    },
75
76    /// Decryption operation performed
77    DecryptionPerformed {
78        /// The ID of the key used for decryption
79        key_id: String,
80        /// Whether the decryption was successful
81        success: bool,
82    },
83
84    /// Configuration changed
85    ConfigurationChanged {
86        /// The name of the configuration setting that changed
87        setting: String,
88        /// The previous value
89        old_value: String,
90        /// The new value
91        new_value: String,
92    },
93
94    /// Error occurred
95    ErrorOccurred {
96        /// The operation that was being performed
97        operation: String,
98        /// The type of error that occurred
99        error_type: String,
100        /// Detailed error message
101        message: String,
102    },
103}
104
105/// Audit log entry with metadata
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct AuditLogEntry {
108    /// When the event occurred
109    pub timestamp: SystemTime,
110
111    /// The event details
112    #[serde(flatten)]
113    pub event: AuditEvent,
114
115    /// Optional context/metadata
116    pub context: Option<String>,
117}
118
119impl AuditLogEntry {
120    /// Create a new audit log entry
121    pub fn new(event: AuditEvent) -> Self {
122        Self {
123            timestamp: SystemTime::now(),
124            event,
125            context: None,
126        }
127    }
128
129    /// Add context to the log entry
130    pub fn with_context<S: Into<String>>(mut self, context: S) -> Self {
131        self.context = Some(context.into());
132        self
133    }
134}
135
136/// Trait for audit logging backends
137pub trait AuditLogger: Send + Sync {
138    /// Log an audit event
139    fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()>;
140
141    /// Flush any buffered logs
142    fn flush(&mut self) -> crate::Result<()>;
143}
144
145/// No-op logger for testing or when auditing is disabled
146pub struct NoOpLogger;
147
148impl AuditLogger for NoOpLogger {
149    fn log(&mut self, _entry: AuditLogEntry) -> crate::Result<()> {
150        Ok(())
151    }
152
153    fn flush(&mut self) -> crate::Result<()> {
154        Ok(())
155    }
156}
157
158/// File-based JSON audit logger
159pub struct FileAuditLogger {
160    path: PathBuf,
161    writer: BufWriter<File>,
162}
163
164impl FileAuditLogger {
165    /// Create a new file-based audit logger
166    pub fn new<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
167        let path = path.as_ref().to_path_buf();
168
169        // Create parent directories if needed
170        if let Some(parent) = path.parent() {
171            std::fs::create_dir_all(parent)?;
172        }
173
174        let file = OpenOptions::new().create(true).append(true).open(&path)?;
175
176        let writer = BufWriter::new(file);
177
178        Ok(Self { path, writer })
179    }
180
181    /// Get the path to the audit log file
182    pub fn path(&self) -> &Path {
183        &self.path
184    }
185}
186
187impl AuditLogger for FileAuditLogger {
188    fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()> {
189        let json = serde_json::to_string(&entry).map_err(|e| {
190            crate::Error::storage(
191                "audit_logging",
192                &format!("failed to serialize audit entry: {}", e),
193            )
194        })?;
195
196        writeln!(self.writer, "{}", json)
197            .map_err(|e| crate::Error::storage("", &format!("failed to write audit log: {}", e)))?;
198
199        Ok(())
200    }
201
202    fn flush(&mut self) -> crate::Result<()> {
203        self.writer.flush().map_err(|e| {
204            crate::Error::storage("audit_flush", &format!("failed to flush audit log: {}", e))
205        })
206    }
207}
208
209impl Drop for FileAuditLogger {
210    fn drop(&mut self) {
211        let _ = self.flush();
212    }
213}
214
215/// In-memory audit logger for testing
216#[derive(Default)]
217pub struct MemoryAuditLogger {
218    entries: Vec<AuditLogEntry>,
219}
220
221impl MemoryAuditLogger {
222    /// Create a new in-memory audit logger
223    pub fn new() -> Self {
224        Self {
225            entries: Vec::new(),
226        }
227    }
228
229    /// Get all logged entries
230    pub fn entries(&self) -> &[AuditLogEntry] {
231        &self.entries
232    }
233
234    /// Clear all logged entries
235    pub fn clear(&mut self) {
236        self.entries.clear();
237    }
238
239    /// Count entries of a specific type
240    pub fn count_event_type(&self, predicate: impl Fn(&AuditEvent) -> bool) -> usize {
241        self.entries.iter().filter(|e| predicate(&e.event)).count()
242    }
243}
244
245impl AuditLogger for MemoryAuditLogger {
246    fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()> {
247        self.entries.push(entry);
248        Ok(())
249    }
250
251    fn flush(&mut self) -> crate::Result<()> {
252        Ok(())
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_audit_event_serialization() {
262        let event = AuditEvent::KeyCreated {
263            key_id: "test-key-123".to_string(),
264            algorithm: Algorithm::ChaCha20Poly1305,
265            version: 1,
266        };
267
268        let entry = AuditLogEntry::new(event).with_context("test context");
269
270        let json = serde_json::to_string(&entry).unwrap();
271        assert!(json.contains("KeyCreated"));
272        assert!(json.contains("test-key-123"));
273        assert!(json.contains("test context"));
274    }
275
276    #[test]
277    fn test_memory_logger() {
278        let mut logger = MemoryAuditLogger::new();
279
280        let event1 = AuditEvent::KeyCreated {
281            key_id: "key1".to_string(),
282            algorithm: Algorithm::Aes256Gcm,
283            version: 1,
284        };
285
286        let event2 = AuditEvent::KeyAccessed {
287            key_id: "key1".to_string(),
288            operation: "encrypt".to_string(),
289        };
290
291        logger.log(AuditLogEntry::new(event1)).unwrap();
292        logger.log(AuditLogEntry::new(event2)).unwrap();
293
294        assert_eq!(logger.entries().len(), 2);
295
296        let created_count = logger.count_event_type(|e| matches!(e, AuditEvent::KeyCreated { .. }));
297        assert_eq!(created_count, 1);
298    }
299
300    #[test]
301    fn test_file_logger() {
302        use tempfile::tempdir;
303
304        let temp_dir = tempdir().unwrap();
305        let log_path = temp_dir.path().join("audit.log");
306
307        let mut logger = FileAuditLogger::new(&log_path).unwrap();
308
309        let event = AuditEvent::KeyRotated {
310            base_id: "base-123".to_string(),
311            old_version: 1,
312            new_version: 2,
313        };
314
315        logger.log(AuditLogEntry::new(event)).unwrap();
316        logger.flush().unwrap();
317
318        // Verify file was created and contains data
319        assert!(log_path.exists());
320        let contents = std::fs::read_to_string(&log_path).unwrap();
321        assert!(contents.contains("KeyRotated"));
322        assert!(contents.contains("base-123"));
323    }
324}