1use crate::audit_log::AuditLog;
2use crate::auditor::ProofAuditor;
3use crate::crypto::hash_bytes;
4use crate::proof::{Action, ActionType, IntegrityProof};
5use std::fs::{remove_file, OpenOptions};
6use std::io::{Read, Seek, SeekFrom, Write};
7use std::path::{Path, PathBuf};
8use thiserror::Error;
9use uuid::Uuid; #[derive(Debug, Error)]
12pub enum ExecutorError {
13 #[error("Auditor error: {0}")]
14 AuditorError(#[from] crate::auditor::AuditorError),
15
16 #[error("Audit log error: {0}")]
17 AuditLogError(#[from] crate::audit_log::AuditError),
18
19 #[error("Action mismatch: expected hash {expected:?}, found {found:?}")]
20 ActionMismatch { expected: [u8; 32], found: [u8; 32] },
21
22 #[error("Action type mismatch")]
23 TypeMismatch,
24
25 #[error("IO error: {0}")]
26 IoError(#[from] std::io::Error),
27
28 #[error("Integrity violation: written data doesn't match expected hash")]
29 IntegrityViolation,
30
31 #[error("Permission denied: {0}")]
32 PermissionDenied(String),
33
34 #[error("Path traversal attempt detected: {0}")]
35 PathTraversal(String),
36}
37
38pub type Result<T> = std::result::Result<T, ExecutorError>;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ExecutionResult {
42 Success,
43 Denied { reason: String },
44}
45
46pub struct SecureExecutor {
47 auditor: ProofAuditor,
48 #[allow(dead_code)] audit_log: AuditLog,
50 storage_root: PathBuf, }
52
53impl SecureExecutor {
54 pub fn new(auditor: ProofAuditor, audit_log: AuditLog, storage_root: PathBuf) -> Result<Self> {
56 if !storage_root.exists() {
58 std::fs::create_dir_all(&storage_root)?;
59 }
60 let canonical_root = storage_root.canonicalize()?;
61
62 Ok(SecureExecutor {
63 auditor,
64 audit_log,
65 storage_root: canonical_root,
66 })
67 }
68
69 fn sanitize_path(&self, unsafe_path_str: &str) -> Result<PathBuf> {
72 let path = Path::new(unsafe_path_str);
73
74 if path.is_absolute() {
76 return Err(ExecutorError::PermissionDenied(
77 "Absolute paths are not allowed. Use paths relative to storage root.".into(),
78 ));
79 }
80
81 let full_candidate = self.storage_root.join(path);
83
84 let parent = full_candidate
87 .parent()
88 .ok_or_else(|| ExecutorError::PermissionDenied("Invalid path structure".into()))?;
89
90 if !parent.exists() {
91 return Err(ExecutorError::PermissionDenied(
92 "Target directory does not exist".into(),
93 ));
94 }
95
96 let canonical_parent = parent.canonicalize()?;
98
99 if !canonical_parent.starts_with(&self.storage_root) {
101 return Err(ExecutorError::PathTraversal(format!(
102 "Resolved path {:?} escapes storage root",
103 canonical_parent
104 )));
105 }
106
107 let filename = full_candidate
109 .file_name()
110 .ok_or_else(|| ExecutorError::PermissionDenied("Missing filename".into()))?;
111
112 Ok(canonical_parent.join(filename))
113 }
114
115 pub fn execute_with_proof(
116 &mut self,
117 action: &Action,
118 proof: &IntegrityProof,
119 ) -> Result<ExecutionResult> {
120 self.auditor.verify_proof(proof)?;
122
123 if proof.action_hash != action.hash() {
124 return Err(ExecutorError::ActionMismatch {
126 expected: action.hash(),
127 found: proof.action_hash,
128 });
129 }
130
131 let exec_result = match &action.action_type {
133 ActionType::Write => self.execute_write_atomic(action)?, ActionType::Delete => self.execute_delete(action)?,
135 ActionType::Read => self.execute_read(action)?,
136 _ => ExecutionResult::Denied {
137 reason: "Not implemented".into(),
138 },
139 };
140
141 Ok(exec_result)
144 }
145
146 fn execute_write_atomic(&self, action: &Action) -> Result<ExecutionResult> {
148 let safe_target_path = self.sanitize_path(&action.target)?;
150
151 let content = action
152 .payload
153 .as_ref()
154 .ok_or_else(|| ExecutorError::PermissionDenied("No content".into()))?;
155
156 let temp_filename = format!(
159 ".tmp_{}_{}",
160 Uuid::new_v4(),
161 safe_target_path.file_name().unwrap().to_string_lossy()
162 );
163 let temp_path = safe_target_path.parent().unwrap().join(temp_filename);
164
165 {
166 let mut file = OpenOptions::new()
168 .write(true)
169 .create_new(true) .open(&temp_path)?;
171
172 file.write_all(content)?;
173 file.sync_all()?; file.seek(SeekFrom::Start(0))?;
179
180 let mut written_content = Vec::new();
181 file.read_to_end(&mut written_content)?;
182
183 let written_hash = hash_bytes(&written_content);
184 let expected_hash = hash_bytes(content);
185
186 if written_hash != expected_hash {
187 drop(file); let _ = std::fs::remove_file(&temp_path); return Err(ExecutorError::IntegrityViolation);
191 }
192 } std::fs::rename(&temp_path, &safe_target_path)?;
198
199 Ok(ExecutionResult::Success)
200 }
201
202 fn execute_read(&self, action: &Action) -> Result<ExecutionResult> {
203 let safe_path = self.sanitize_path(&action.target)?;
205
206 if !safe_path.exists() {
207 return Ok(ExecutionResult::Denied {
208 reason: "File not found".into(),
209 });
210 }
211 let _content = std::fs::read(safe_path)?;
212 Ok(ExecutionResult::Success)
213 }
214
215 fn execute_delete(&self, action: &Action) -> Result<ExecutionResult> {
216 let safe_path = self.sanitize_path(&action.target)?;
217
218 if !safe_path.exists() {
219 return Ok(ExecutionResult::Denied {
220 reason: "File not found".into(),
221 });
222 }
223 remove_file(safe_path)?;
224 Ok(ExecutionResult::Success)
225 }
226}