Skip to main content

lit/network/
audit.rs

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