1use 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
12pub struct AuditLog {
14 log_path: PathBuf,
15 signing_key: Zeroizing<Vec<u8>>,
16}
17
18impl AuditLog {
19 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 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 let signing_key = Self::get_or_create_signing_key()?;
35
36 #[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); 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 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 let signature = self.sign_message(&log_message)?;
61
62 let signed_entry = format!("{} | {}\n", log_message, hex::encode(signature));
64
65 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 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 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 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 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 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 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 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 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 use aes_gcm::aead::rand_core::RngCore;
166 use aes_gcm::aead::OsRng;
167
168 let mut key = vec![0u8; 32]; OsRng.fill_bytes(&mut key);
170
171 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); 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 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 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 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#[derive(Debug)]
221pub struct VerificationResult {
222 pub total_entries: usize,
223 pub valid_entries: usize,
224 pub invalid_entries: Vec<(usize, String)>, }
226
227impl VerificationResult {
228 pub fn is_valid(&self) -> bool {
230 self.invalid_entries.is_empty()
231 }
232
233 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 audit.log("TEST", "First test event").unwrap();
264 audit.log("TEST", "Second test event").unwrap();
265
266 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 audit.log("TEST", "Original message").unwrap();
282
283 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 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 let audit1 = AuditLog::new(Some(log_path.to_str().unwrap())).unwrap();
301 audit1.log("TEST", "Test event").unwrap();
302
303 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}