use crate::hash::hash;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum OperationType {
KeyGeneration,
KeyImportExport,
KeyRotation,
KeyDeletion,
Signing,
SignatureVerification,
Encryption,
Decryption,
Hashing,
KeyDerivation,
RandomGeneration,
CertificateIssuance,
CertificateRevocation,
AccessControl,
PolicyEnforcement,
AuditAccess,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SeverityLevel {
Debug,
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub id: u64,
pub timestamp: DateTime<Utc>,
pub operation_type: OperationType,
pub severity: SeverityLevel,
pub description: String,
pub metadata: Option<String>,
pub principal: Option<String>,
pub previous_hash: Vec<u8>,
pub entry_hash: Vec<u8>,
}
impl AuditEntry {
fn new(
id: u64,
operation_type: OperationType,
severity: SeverityLevel,
description: String,
metadata: Option<String>,
principal: Option<String>,
previous_hash: Vec<u8>,
) -> Self {
let timestamp = Utc::now();
let mut hash_input = Vec::new();
hash_input.extend_from_slice(&id.to_le_bytes());
hash_input.extend_from_slice(timestamp.to_rfc3339().as_bytes());
hash_input.extend_from_slice(&crate::codec::encode(&operation_type).unwrap());
hash_input.extend_from_slice(&crate::codec::encode(&severity).unwrap());
hash_input.extend_from_slice(description.as_bytes());
if let Some(ref m) = metadata {
hash_input.extend_from_slice(m.as_bytes());
}
if let Some(ref p) = principal {
hash_input.extend_from_slice(p.as_bytes());
}
hash_input.extend_from_slice(&previous_hash);
let entry_hash = hash(&hash_input).to_vec();
Self {
id,
timestamp,
operation_type,
severity,
description,
metadata,
principal,
previous_hash,
entry_hash,
}
}
fn verify(&self, expected_previous_hash: &[u8]) -> bool {
if self.previous_hash != expected_previous_hash {
return false;
}
let mut hash_input = Vec::new();
hash_input.extend_from_slice(&self.id.to_le_bytes());
hash_input.extend_from_slice(self.timestamp.to_rfc3339().as_bytes());
hash_input.extend_from_slice(&crate::codec::encode(&self.operation_type).unwrap());
hash_input.extend_from_slice(&crate::codec::encode(&self.severity).unwrap());
hash_input.extend_from_slice(self.description.as_bytes());
if let Some(ref m) = self.metadata {
hash_input.extend_from_slice(m.as_bytes());
}
if let Some(ref p) = self.principal {
hash_input.extend_from_slice(p.as_bytes());
}
hash_input.extend_from_slice(&self.previous_hash);
let computed_hash = hash(&hash_input);
computed_hash.as_slice() == self.entry_hash.as_slice()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLog {
entries: Vec<AuditEntry>,
current_principal: Option<String>,
retention_days: Option<i64>,
stats: AuditStatistics,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditStatistics {
pub total_entries: u64,
pub by_operation: HashMap<String, u64>,
pub by_severity: HashMap<String, u64>,
}
impl AuditLog {
pub fn new() -> Self {
Self {
entries: Vec::new(),
current_principal: None,
retention_days: None,
stats: AuditStatistics::default(),
}
}
pub fn with_retention(retention_days: i64) -> Self {
Self {
entries: Vec::new(),
current_principal: None,
retention_days: Some(retention_days),
stats: AuditStatistics::default(),
}
}
pub fn set_principal(&mut self, principal: String) {
self.current_principal = Some(principal);
}
pub fn clear_principal(&mut self) {
self.current_principal = None;
}
pub fn log(
&mut self,
operation_type: OperationType,
severity: SeverityLevel,
description: &str,
metadata: Option<&str>,
) {
let id = self.entries.len() as u64;
let previous_hash = self
.entries
.last()
.map(|e| e.entry_hash.clone())
.unwrap_or_else(|| vec![0u8; 32]);
let entry = AuditEntry::new(
id,
operation_type,
severity,
description.to_string(),
metadata.map(|s| s.to_string()),
self.current_principal.clone(),
previous_hash,
);
self.entries.push(entry);
self.stats.total_entries += 1;
*self
.stats
.by_operation
.entry(format!("{:?}", operation_type))
.or_insert(0) += 1;
*self
.stats
.by_severity
.entry(format!("{:?}", severity))
.or_insert(0) += 1;
if let Some(days) = self.retention_days {
self.apply_retention_policy(days);
}
}
fn apply_retention_policy(&mut self, retention_days: i64) {
let cutoff = Utc::now() - Duration::days(retention_days);
self.entries.retain(|entry| entry.timestamp > cutoff);
}
pub fn cleanup(&mut self) {
if let Some(days) = self.retention_days {
self.apply_retention_policy(days);
}
}
pub fn entries(&self) -> &[AuditEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn query_by_operation(&self, operation_type: OperationType) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| e.operation_type == operation_type)
.collect()
}
pub fn query_by_severity(&self, min_severity: SeverityLevel) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| e.severity >= min_severity)
.collect()
}
pub fn query_by_time_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| e.timestamp >= start && e.timestamp <= end)
.collect()
}
pub fn query_by_principal(&self, principal: &str) -> Vec<&AuditEntry> {
self.entries
.iter()
.filter(|e| e.principal.as_ref().is_some_and(|p| p == principal))
.collect()
}
pub fn verify_integrity(&self) -> bool {
if self.entries.is_empty() {
return true;
}
let mut previous_hash = vec![0u8; 32];
for entry in &self.entries {
if !entry.verify(&previous_hash) {
return false;
}
previous_hash = entry.entry_hash.clone();
}
true
}
pub fn statistics(&self) -> &AuditStatistics {
&self.stats
}
pub fn export_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn import_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_bytes(&self) -> Vec<u8> {
crate::codec::encode(self).expect("serialization failed")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Box<dyn std::error::Error>> {
Ok(crate::codec::decode(bytes)?)
}
}
impl Default for AuditLog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_audit_log_basic() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Generated keypair",
None,
);
assert_eq!(log.len(), 1);
assert!(!log.is_empty());
}
#[test]
fn test_audit_log_with_metadata() {
let mut log = AuditLog::new();
log.log(
OperationType::Encryption,
SeverityLevel::Info,
"Encrypted file",
Some("file=test.txt, size=1024"),
);
assert_eq!(
log.entries()[0].metadata,
Some("file=test.txt, size=1024".to_string())
);
}
#[test]
fn test_audit_log_with_principal() {
let mut log = AuditLog::new();
log.set_principal("alice".to_string());
log.log(
OperationType::Signing,
SeverityLevel::Info,
"Signed message",
None,
);
assert_eq!(log.entries()[0].principal, Some("alice".to_string()));
}
#[test]
fn test_integrity_verification() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
log.log(OperationType::Signing, SeverityLevel::Info, "Op 3", None);
assert!(log.verify_integrity());
}
#[test]
fn test_integrity_tamper_detection() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
if let Some(entry) = log.entries.get_mut(0) {
entry.description = "Tampered!".to_string();
}
assert!(!log.verify_integrity());
}
#[test]
fn test_query_by_operation() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 3",
None,
);
let results = log.query_by_operation(OperationType::KeyGeneration);
assert_eq!(results.len(), 2);
}
#[test]
fn test_query_by_severity() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(
OperationType::Encryption,
SeverityLevel::Warning,
"Op 2",
None,
);
log.log(OperationType::Signing, SeverityLevel::Error, "Op 3", None);
log.log(
OperationType::KeyDeletion,
SeverityLevel::Critical,
"Op 4",
None,
);
let results = log.query_by_severity(SeverityLevel::Warning);
assert_eq!(results.len(), 3); }
#[test]
fn test_query_by_principal() {
let mut log = AuditLog::new();
log.set_principal("alice".to_string());
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.set_principal("bob".to_string());
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
log.set_principal("alice".to_string());
log.log(OperationType::Signing, SeverityLevel::Info, "Op 3", None);
let alice_ops = log.query_by_principal("alice");
assert_eq!(alice_ops.len(), 2);
}
#[test]
fn test_query_by_time_range() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
let start = Utc::now();
std::thread::sleep(std::time::Duration::from_millis(10));
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
let end = Utc::now();
let results = log.query_by_time_range(start, end);
assert_eq!(results.len(), 1);
}
#[test]
fn test_statistics() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
log.log(
OperationType::KeyGeneration,
SeverityLevel::Warning,
"Op 3",
None,
);
let stats = log.statistics();
assert_eq!(stats.total_entries, 3);
assert_eq!(*stats.by_operation.get("KeyGeneration").unwrap(), 2);
assert_eq!(*stats.by_severity.get("Info").unwrap(), 2);
}
#[test]
fn test_retention_policy() {
let mut log = AuditLog::with_retention(1);
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Op 1",
None,
);
log.log(OperationType::Encryption, SeverityLevel::Info, "Op 2", None);
assert_eq!(log.len(), 2);
if let Some(entry) = log.entries.get_mut(0) {
entry.timestamp = Utc::now() - Duration::days(2);
}
log.cleanup();
assert_eq!(log.len(), 1);
}
#[test]
fn test_json_export_import() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Test",
None,
);
let json = log.export_json().unwrap();
let restored = AuditLog::import_json(&json).unwrap();
assert_eq!(restored.len(), 1);
assert!(restored.verify_integrity());
}
#[test]
fn test_bincode_serialization() {
let mut log = AuditLog::new();
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Test",
None,
);
let bytes = log.to_bytes();
let restored = AuditLog::from_bytes(&bytes).unwrap();
assert_eq!(restored.len(), 1);
assert!(restored.verify_integrity());
}
#[test]
fn test_multiple_principals() {
let mut log = AuditLog::new();
log.set_principal("alice".to_string());
log.log(
OperationType::KeyGeneration,
SeverityLevel::Info,
"Alice op 1",
None,
);
log.log(
OperationType::Encryption,
SeverityLevel::Info,
"Alice op 2",
None,
);
log.set_principal("bob".to_string());
log.log(
OperationType::Signing,
SeverityLevel::Info,
"Bob op 1",
None,
);
log.clear_principal();
log.log(
OperationType::Decryption,
SeverityLevel::Info,
"System op",
None,
);
assert_eq!(log.query_by_principal("alice").len(), 2);
assert_eq!(log.query_by_principal("bob").len(), 1);
}
#[test]
fn test_all_operation_types() {
let mut log = AuditLog::new();
let operations = [
OperationType::KeyGeneration,
OperationType::KeyImportExport,
OperationType::KeyRotation,
OperationType::KeyDeletion,
OperationType::Signing,
OperationType::SignatureVerification,
OperationType::Encryption,
OperationType::Decryption,
OperationType::Hashing,
OperationType::KeyDerivation,
OperationType::RandomGeneration,
OperationType::CertificateIssuance,
OperationType::CertificateRevocation,
OperationType::AccessControl,
OperationType::PolicyEnforcement,
OperationType::AuditAccess,
OperationType::Other,
];
for op in &operations {
log.log(*op, SeverityLevel::Info, "Test operation", None);
}
assert_eq!(log.len(), operations.len());
assert!(log.verify_integrity());
}
#[test]
fn test_all_severity_levels() {
let mut log = AuditLog::new();
let severities = [
SeverityLevel::Debug,
SeverityLevel::Info,
SeverityLevel::Warning,
SeverityLevel::Error,
SeverityLevel::Critical,
];
for severity in &severities {
log.log(OperationType::Other, *severity, "Test severity", None);
}
assert_eq!(log.len(), severities.len());
assert_eq!(log.query_by_severity(SeverityLevel::Debug).len(), 5);
assert_eq!(log.query_by_severity(SeverityLevel::Info).len(), 4);
assert_eq!(log.query_by_severity(SeverityLevel::Warning).len(), 3);
assert_eq!(log.query_by_severity(SeverityLevel::Error).len(), 2);
assert_eq!(log.query_by_severity(SeverityLevel::Critical).len(), 1);
}
}