Skip to main content

lit/network/
audit.rs

1/// Audit Log with HMAC Integrity Protection
2/// Provides tamper-evident logging for security events
3use crate::crypto::encryption::restrict_to_owner;
4use hmac::{Hmac, Mac};
5use sha2::Sha256;
6use std::fs;
7use std::io::Write;
8use std::path::PathBuf;
9use zeroize::Zeroizing;
10
11type HmacSha256 = Hmac<Sha256>;
12
13/// Audit log manager with HMAC signing
14pub struct AuditLog {
15    log_path: PathBuf,
16    signing_key: Zeroizing<Vec<u8>>,
17}
18
19impl AuditLog {
20    /// Create or load an audit log with HMAC signing
21    pub fn new(log_path: Option<&str>) -> Result<Self, String> {
22        let log_path = if let Some(path) = log_path {
23            PathBuf::from(shellexpand::tilde(path).as_ref())
24        } else {
25            Self::default_log_path()?
26        };
27
28        // Create log directory if needed
29        if let Some(parent) = log_path.parent() {
30            fs::create_dir_all(parent)
31                .map_err(|e| format!("Failed to create log directory: {}", e))?;
32        }
33
34        // Load or generate signing key
35        let signing_key = Self::get_or_create_signing_key()?;
36
37        // Owner-only on both platforms: 0600 on Unix, an explicit DACL on
38        // Windows. This used to be Unix-only, leaving the log readable by any
39        // local account there.
40        if log_path.exists() {
41            restrict_to_owner(&log_path)?;
42        }
43
44        Ok(AuditLog {
45            log_path,
46            signing_key,
47        })
48    }
49
50    /// Append a signed entry to the audit log
51    pub fn log(&self, event_type: &str, message: &str) -> Result<(), String> {
52        let timestamp = chrono::Utc::now().to_rfc3339();
53        let log_message = format!("{} | {} | {}", timestamp, event_type, message);
54
55        // Create HMAC signature
56        let signature = self.sign_message(&log_message)?;
57
58        // Format: timestamp | event_type | message | signature
59        let signed_entry = format!("{} | {}\n", log_message, hex::encode(signature));
60
61        // Append to log file
62        let mut file = fs::OpenOptions::new()
63            .create(true)
64            .append(true)
65            .open(&self.log_path)
66            .map_err(|e| format!("Failed to open log file: {}", e))?;
67
68        file.write_all(signed_entry.as_bytes())
69            .map_err(|e| format!("Failed to write to log: {}", e))?;
70
71        Ok(())
72    }
73
74    /// Verify the integrity of all log entries
75    pub fn verify(&self) -> Result<VerificationResult, String> {
76        if !self.log_path.exists() {
77            return Ok(VerificationResult {
78                total_entries: 0,
79                valid_entries: 0,
80                invalid_entries: vec![],
81            });
82        }
83
84        let content = fs::read_to_string(&self.log_path)
85            .map_err(|e| format!("Failed to read log file: {}", e))?;
86
87        let mut total = 0;
88        let mut valid = 0;
89        let mut invalid = vec![];
90
91        for (line_num, line) in content.lines().enumerate() {
92            total += 1;
93
94            // Parse line: timestamp | event_type | message | signature
95            let parts: Vec<&str> = line.rsplitn(2, " | ").collect();
96            if parts.len() != 2 {
97                invalid.push((line_num + 1, "Invalid format".to_string()));
98                continue;
99            }
100
101            let signature_hex = parts[0];
102            let message = parts[1];
103
104            // Decode signature
105            let stored_signature = match hex::decode(signature_hex) {
106                Ok(sig) => sig,
107                Err(_) => {
108                    invalid.push((line_num + 1, "Invalid signature encoding".to_string()));
109                    continue;
110                }
111            };
112
113            // Verify signature
114            match self.verify_signature(message, &stored_signature) {
115                Ok(true) => valid += 1,
116                Ok(false) => invalid.push((line_num + 1, "Invalid signature".to_string())),
117                Err(e) => invalid.push((line_num + 1, format!("Verification error: {}", e))),
118            }
119        }
120
121        Ok(VerificationResult {
122            total_entries: total,
123            valid_entries: valid,
124            invalid_entries: invalid,
125        })
126    }
127
128    /// Sign a message with HMAC-SHA256
129    fn sign_message(&self, message: &str) -> Result<Vec<u8>, String> {
130        let mut mac = HmacSha256::new_from_slice(&self.signing_key)
131            .map_err(|e| format!("HMAC initialization failed: {}", e))?;
132
133        mac.update(message.as_bytes());
134        Ok(mac.finalize().into_bytes().to_vec())
135    }
136
137    /// Verify a message signature
138    fn verify_signature(&self, message: &str, signature: &[u8]) -> Result<bool, String> {
139        let mut mac = HmacSha256::new_from_slice(&self.signing_key)
140            .map_err(|e| format!("HMAC initialization failed: {}", e))?;
141
142        mac.update(message.as_bytes());
143
144        match mac.verify_slice(signature) {
145            Ok(_) => Ok(true),
146            Err(_) => Ok(false),
147        }
148    }
149
150    /// Get or create the HMAC signing key
151    fn get_or_create_signing_key() -> Result<Zeroizing<Vec<u8>>, String> {
152        let key_path = Self::signing_key_path()?;
153
154        if key_path.exists() {
155            // Re-apply the restriction on load, not only at creation. A key
156            // written by an older version is still sitting there with whatever
157            // permissions it was given — on this machine, 0644 from 2025 — and
158            // would otherwise keep them for the life of the installation.
159            restrict_to_owner(&key_path)?;
160
161            // Load existing key
162            let key_data =
163                fs::read(&key_path).map_err(|e| format!("Failed to read signing key: {}", e))?;
164            Ok(Zeroizing::new(key_data))
165        } else {
166            // Generate new key
167            use aes_gcm::aead::rand_core::RngCore;
168            use aes_gcm::aead::OsRng;
169
170            let mut key = vec![0u8; 32]; // 256-bit key
171            OsRng.fill_bytes(&mut key);
172
173            // Save key with restrictive permissions
174            fs::write(&key_path, &key)
175                .map_err(|e| format!("Failed to write signing key: {}", e))?;
176
177            // The HMAC key is what makes the audit log tamper-evident, so
178            // anyone who can read it can forge entries. Owner-only on both
179            // platforms now: 0600 on Unix, an explicit DACL on Windows. The
180            // read-only attribute that stood here stopped writes and left the
181            // key readable by any local account — finding I-1.
182            restrict_to_owner(&key_path)?;
183
184            Ok(Zeroizing::new(key))
185        }
186    }
187
188    /// Get default log path
189    fn default_log_path() -> Result<PathBuf, String> {
190        let home = dirs::home_dir().ok_or("Could not determine home directory")?;
191        Ok(home.join(".lit").join("audit.log"))
192    }
193
194    /// Get signing key path
195    fn signing_key_path() -> Result<PathBuf, String> {
196        let home = dirs::home_dir().ok_or("Could not determine home directory")?;
197        Ok(home.join(".lit").join("audit.key"))
198    }
199}
200
201/// Result of audit log verification
202#[derive(Debug)]
203pub struct VerificationResult {
204    pub total_entries: usize,
205    pub valid_entries: usize,
206    pub invalid_entries: Vec<(usize, String)>, // (line_number, error)
207}
208
209impl VerificationResult {
210    /// Check if all entries are valid
211    pub fn is_valid(&self) -> bool {
212        self.invalid_entries.is_empty()
213    }
214
215    /// Get verification summary
216    pub fn summary(&self) -> String {
217        if self.is_valid() {
218            format!(
219                "✓ All {} audit log entries verified successfully",
220                self.total_entries
221            )
222        } else {
223            format!(
224                "✗ Verification failed: {}/{} entries invalid",
225                self.invalid_entries.len(),
226                self.total_entries
227            )
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use tempfile::NamedTempFile;
236
237    #[test]
238    fn test_audit_log_signing() {
239        let temp_log = NamedTempFile::new().unwrap();
240        let log_path = temp_log.path().to_str().unwrap();
241
242        let audit = AuditLog::new(Some(log_path)).unwrap();
243
244        // Log some events
245        audit.log("TEST", "First test event").unwrap();
246        audit.log("TEST", "Second test event").unwrap();
247
248        // Verify
249        let result = audit.verify().unwrap();
250        assert_eq!(result.total_entries, 2);
251        assert_eq!(result.valid_entries, 2);
252        assert!(result.is_valid());
253    }
254
255    #[test]
256    fn test_tamper_detection() {
257        let temp_log = NamedTempFile::new().unwrap();
258        let log_path = temp_log.path().to_str().unwrap();
259
260        let audit = AuditLog::new(Some(log_path)).unwrap();
261
262        // Log an event
263        audit.log("TEST", "Original message").unwrap();
264
265        // Tamper with the log file
266        let mut content = fs::read_to_string(&temp_log).unwrap();
267        content = content.replace("Original message", "Tampered message");
268        fs::write(&temp_log, content).unwrap();
269
270        // Verify should fail
271        let result = audit.verify().unwrap();
272        assert!(!result.is_valid());
273        assert_eq!(result.invalid_entries.len(), 1);
274    }
275
276    #[test]
277    fn test_signing_key_persistence() {
278        let temp_dir = tempfile::tempdir().unwrap();
279        let log_path = temp_dir.path().join("test.log");
280
281        // Create first instance
282        let audit1 = AuditLog::new(Some(log_path.to_str().unwrap())).unwrap();
283        audit1.log("TEST", "Test event").unwrap();
284
285        // Create second instance (should load same key)
286        let audit2 = AuditLog::new(Some(log_path.to_str().unwrap())).unwrap();
287        let result = audit2.verify().unwrap();
288
289        assert!(result.is_valid());
290    }
291}