use crate::{Algorithm, KeyState};
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type")]
pub enum AuditEvent {
KeyCreated {
key_id: String,
algorithm: Algorithm,
version: u32,
},
KeyAccessed {
key_id: String,
operation: String,
},
KeyRotated {
base_id: String,
old_version: u32,
new_version: u32,
},
KeyStateChanged {
key_id: String,
old_state: KeyState,
new_state: KeyState,
},
KeyDeleted {
key_id: String,
version: u32,
},
AuthenticationAttempt {
success: bool,
storage_path: String,
},
EncryptionPerformed {
key_id: String,
data_size: usize,
},
DecryptionPerformed {
key_id: String,
success: bool,
},
ConfigurationChanged {
setting: String,
old_value: String,
new_value: String,
},
ErrorOccurred {
operation: String,
error_type: String,
message: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLogEntry {
pub timestamp: SystemTime,
#[serde(flatten)]
pub event: AuditEvent,
pub context: Option<String>,
}
impl AuditLogEntry {
pub fn new(event: AuditEvent) -> Self {
Self {
timestamp: SystemTime::now(),
event,
context: None,
}
}
pub fn with_context<S: Into<String>>(mut self, context: S) -> Self {
self.context = Some(context.into());
self
}
}
pub trait AuditLogger: Send + Sync {
fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()>;
fn flush(&mut self) -> crate::Result<()>;
}
pub struct NoOpLogger;
impl AuditLogger for NoOpLogger {
fn log(&mut self, _entry: AuditLogEntry) -> crate::Result<()> {
Ok(())
}
fn flush(&mut self) -> crate::Result<()> {
Ok(())
}
}
pub struct FileAuditLogger {
path: PathBuf,
writer: BufWriter<File>,
}
impl FileAuditLogger {
pub fn new<P: AsRef<Path>>(path: P) -> crate::Result<Self> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new().create(true).append(true).open(&path)?;
let writer = BufWriter::new(file);
Ok(Self { path, writer })
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl AuditLogger for FileAuditLogger {
fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()> {
let json = serde_json::to_string(&entry).map_err(|e| {
crate::Error::storage(
"audit_logging",
&format!("failed to serialize audit entry: {}", e),
)
})?;
writeln!(self.writer, "{}", json)
.map_err(|e| crate::Error::storage("", &format!("failed to write audit log: {}", e)))?;
Ok(())
}
fn flush(&mut self) -> crate::Result<()> {
self.writer.flush().map_err(|e| {
crate::Error::storage("audit_flush", &format!("failed to flush audit log: {}", e))
})
}
}
impl Drop for FileAuditLogger {
fn drop(&mut self) {
let _ = self.flush();
}
}
#[derive(Default)]
pub struct MemoryAuditLogger {
entries: Vec<AuditLogEntry>,
}
impl MemoryAuditLogger {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn entries(&self) -> &[AuditLogEntry] {
&self.entries
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn count_event_type(&self, predicate: impl Fn(&AuditEvent) -> bool) -> usize {
self.entries.iter().filter(|e| predicate(&e.event)).count()
}
}
impl AuditLogger for MemoryAuditLogger {
fn log(&mut self, entry: AuditLogEntry) -> crate::Result<()> {
self.entries.push(entry);
Ok(())
}
fn flush(&mut self) -> crate::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_event_serialization() {
let event = AuditEvent::KeyCreated {
key_id: "test-key-123".to_string(),
algorithm: Algorithm::ChaCha20Poly1305,
version: 1,
};
let entry = AuditLogEntry::new(event).with_context("test context");
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("KeyCreated"));
assert!(json.contains("test-key-123"));
assert!(json.contains("test context"));
}
#[test]
fn test_memory_logger() {
let mut logger = MemoryAuditLogger::new();
let event1 = AuditEvent::KeyCreated {
key_id: "key1".to_string(),
algorithm: Algorithm::Aes256Gcm,
version: 1,
};
let event2 = AuditEvent::KeyAccessed {
key_id: "key1".to_string(),
operation: "encrypt".to_string(),
};
logger.log(AuditLogEntry::new(event1)).unwrap();
logger.log(AuditLogEntry::new(event2)).unwrap();
assert_eq!(logger.entries().len(), 2);
let created_count = logger.count_event_type(|e| matches!(e, AuditEvent::KeyCreated { .. }));
assert_eq!(created_count, 1);
}
#[test]
fn test_file_logger() {
use tempfile::tempdir;
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path().join("audit.log");
let mut logger = FileAuditLogger::new(&log_path).unwrap();
let event = AuditEvent::KeyRotated {
base_id: "base-123".to_string(),
old_version: 1,
new_version: 2,
};
logger.log(AuditLogEntry::new(event)).unwrap();
logger.flush().unwrap();
assert!(log_path.exists());
let contents = std::fs::read_to_string(&log_path).unwrap();
assert!(contents.contains("KeyRotated"));
assert!(contents.contains("base-123"));
}
}