1use 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
13pub struct AuditLog {
15 log_path: PathBuf,
16 signing_key: Zeroizing<Vec<u8>>,
17}
18
19impl AuditLog {
20 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 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 let signing_key = Self::get_or_create_signing_key()?;
36
37 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 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 let signature = self.sign_message(&log_message)?;
57
58 let signed_entry = format!("{} | {}\n", log_message, hex::encode(signature));
60
61 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 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 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 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 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 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 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 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 restrict_to_owner(&key_path)?;
160
161 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 use aes_gcm::aead::rand_core::RngCore;
168 use aes_gcm::aead::OsRng;
169
170 let mut key = vec![0u8; 32]; OsRng.fill_bytes(&mut key);
172
173 fs::write(&key_path, &key)
175 .map_err(|e| format!("Failed to write signing key: {}", e))?;
176
177 restrict_to_owner(&key_path)?;
183
184 Ok(Zeroizing::new(key))
185 }
186 }
187
188 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 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#[derive(Debug)]
203pub struct VerificationResult {
204 pub total_entries: usize,
205 pub valid_entries: usize,
206 pub invalid_entries: Vec<(usize, String)>, }
208
209impl VerificationResult {
210 pub fn is_valid(&self) -> bool {
212 self.invalid_entries.is_empty()
213 }
214
215 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 audit.log("TEST", "First test event").unwrap();
246 audit.log("TEST", "Second test event").unwrap();
247
248 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 audit.log("TEST", "Original message").unwrap();
264
265 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 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 let audit1 = AuditLog::new(Some(log_path.to_str().unwrap())).unwrap();
283 audit1.log("TEST", "Test event").unwrap();
284
285 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}