Skip to main content

aura_effects/
secure.rs

1//! Layer 3: Secure Storage Effect Handlers - Production Only
2//!
3//! Stateless single-party implementation of SecureStorageEffects from aura-core (Layer 1).
4//! This handler implements pure secure storage effect operations, delegating to platform APIs.
5//!
6//! **Layer Constraint**: No mock handlers - those belong in aura-testkit (Layer 8).
7//! This module contains only production stateless handlers.
8
9#[cfg(target_arch = "wasm32")]
10use crate::storage::FilesystemStorageHandler;
11use async_trait::async_trait;
12use aura_core::effects::{
13    SecureStorageCapability, SecureStorageEffects, SecureStorageError, SecureStorageLocation,
14};
15#[cfg(target_arch = "wasm32")]
16use aura_core::effects::{StorageCoreEffects, StorageExtendedEffects};
17use cfg_if::cfg_if;
18#[cfg(not(target_arch = "wasm32"))]
19use std::fs;
20#[cfg(not(target_arch = "wasm32"))]
21use std::io::Write;
22use std::path::PathBuf;
23
24cfg_if! {
25    if #[cfg(target_arch = "wasm32")] {
26        use js_sys::Date;
27    } else {
28        use std::time::{SystemTime, UNIX_EPOCH};
29    }
30}
31
32/// Real secure storage handler for production use
33#[derive(Debug)]
34pub struct RealSecureStorageHandler {
35    platform_config: String,
36    base_path: PathBuf,
37}
38
39impl RealSecureStorageHandler {
40    /// Create a secure storage handler with a custom base path.
41    ///
42    /// The secure storage files will be placed in `base_path/secure_store/`.
43    pub fn with_base_path(base_path: PathBuf) -> Self {
44        Self {
45            platform_config: "filesystem-fallback".to_string(),
46            base_path: base_path.join("secure_store"),
47        }
48    }
49
50    /// Create a handler for testing with an ephemeral temp directory.
51    #[cfg(test)]
52    pub fn for_testing() -> Self {
53        let suffix = fastrand::u64(..);
54        let temp_dir = std::env::temp_dir().join(format!("aura-secure-test-{suffix}"));
55        Self::with_base_path(temp_dir)
56    }
57
58    fn require_capability(
59        &self,
60        caps: &[SecureStorageCapability],
61        required: SecureStorageCapability,
62    ) -> Result<(), SecureStorageError> {
63        if caps.contains(&required) {
64            Ok(())
65        } else {
66            Err(SecureStorageError::permission_denied(format!(
67                "missing capability: {required:?}"
68            )))
69        }
70    }
71
72    #[cfg(not(target_arch = "wasm32"))]
73    fn path_for(&self, location: &SecureStorageLocation) -> PathBuf {
74        let mut path = self.base_path.join(&location.namespace).join(&location.key);
75        if let Some(sub) = &location.sub_key {
76            path = path.join(sub);
77        }
78        path
79    }
80
81    #[allow(clippy::disallowed_methods)] // Effect implementation reads wall clock directly
82    fn current_time_ms(&self) -> Result<u64, SecureStorageError> {
83        #[cfg(target_arch = "wasm32")]
84        {
85            Ok(Date::now() as u64)
86        }
87        #[cfg(not(target_arch = "wasm32"))]
88        {
89            Ok(SystemTime::now()
90                .duration_since(UNIX_EPOCH)
91                .map_err(|e| SecureStorageError::storage(e.to_string()))?
92                .as_millis() as u64)
93        }
94    }
95
96    #[cfg(target_arch = "wasm32")]
97    fn wasm_storage(&self) -> FilesystemStorageHandler {
98        FilesystemStorageHandler::new(self.base_path.clone())
99    }
100}
101
102#[async_trait]
103impl SecureStorageEffects for RealSecureStorageHandler {
104    async fn secure_store(
105        &self,
106        location: &SecureStorageLocation,
107        key: &[u8],
108        caps: &[aura_core::effects::SecureStorageCapability],
109    ) -> Result<(), SecureStorageError> {
110        self.require_capability(caps, SecureStorageCapability::Write)?;
111        #[cfg(target_arch = "wasm32")]
112        {
113            return self
114                .wasm_storage()
115                .store(&location.full_path(), key.to_vec())
116                .await
117                .map_err(|e| SecureStorageError::storage(e.to_string()));
118        }
119        #[cfg(not(target_arch = "wasm32"))]
120        {
121            let path = self.path_for(location);
122            if let Some(dir) = path.parent() {
123                fs::create_dir_all(dir).map_err(|e| SecureStorageError::storage(e.to_string()))?;
124            }
125            let mut file = fs::OpenOptions::new()
126                .create(true)
127                .write(true)
128                .truncate(true)
129                .open(&path)
130                .map_err(|e| SecureStorageError::storage(e.to_string()))?;
131            file.write_all(key)
132                .map_err(|e| SecureStorageError::storage(e.to_string()))?;
133            Ok(())
134        }
135    }
136
137    async fn secure_retrieve(
138        &self,
139        location: &SecureStorageLocation,
140        caps: &[aura_core::effects::SecureStorageCapability],
141    ) -> Result<Vec<u8>, SecureStorageError> {
142        self.require_capability(caps, SecureStorageCapability::Read)?;
143        #[cfg(target_arch = "wasm32")]
144        {
145            return self
146                .wasm_storage()
147                .retrieve(&location.full_path())
148                .await
149                .map_err(|e| SecureStorageError::storage(e.to_string()))?
150                .ok_or_else(|| SecureStorageError::storage("secure key not found"));
151        }
152        #[cfg(not(target_arch = "wasm32"))]
153        {
154            let path = self.path_for(location);
155            fs::read(&path).map_err(|e| SecureStorageError::storage(e.to_string()))
156        }
157    }
158
159    async fn secure_delete(
160        &self,
161        location: &SecureStorageLocation,
162        caps: &[aura_core::effects::SecureStorageCapability],
163    ) -> Result<(), SecureStorageError> {
164        self.require_capability(caps, SecureStorageCapability::Delete)?;
165        #[cfg(target_arch = "wasm32")]
166        {
167            let _ = self
168                .wasm_storage()
169                .remove(&location.full_path())
170                .await
171                .map_err(|e| SecureStorageError::storage(e.to_string()))?;
172            return Ok(());
173        }
174        #[cfg(not(target_arch = "wasm32"))]
175        {
176            let path = self.path_for(location);
177            if path.exists() {
178                fs::remove_file(&path).map_err(|e| SecureStorageError::storage(e.to_string()))?;
179            }
180            Ok(())
181        }
182    }
183
184    async fn secure_exists(
185        &self,
186        location: &SecureStorageLocation,
187    ) -> Result<bool, SecureStorageError> {
188        #[cfg(target_arch = "wasm32")]
189        {
190            return self
191                .wasm_storage()
192                .exists(&location.full_path())
193                .await
194                .map_err(|e| SecureStorageError::storage(e.to_string()));
195        }
196        #[cfg(not(target_arch = "wasm32"))]
197        {
198            let path = self.path_for(location);
199            Ok(path.exists())
200        }
201    }
202
203    async fn secure_list_keys(
204        &self,
205        namespace: &str,
206        caps: &[aura_core::effects::SecureStorageCapability],
207    ) -> Result<Vec<String>, SecureStorageError> {
208        self.require_capability(caps, SecureStorageCapability::List)?;
209        #[cfg(target_arch = "wasm32")]
210        {
211            return self
212                .wasm_storage()
213                .list_keys(Some(&format!("{namespace}/")))
214                .await
215                .map_err(|e| SecureStorageError::storage(e.to_string()));
216        }
217        #[cfg(not(target_arch = "wasm32"))]
218        {
219            let ns_path = self.base_path.join(namespace);
220            if !ns_path.exists() {
221                return Ok(Vec::new());
222            }
223            let mut keys = Vec::new();
224            for entry in
225                fs::read_dir(&ns_path).map_err(|e| SecureStorageError::storage(e.to_string()))?
226            {
227                let entry = entry.map_err(|e| SecureStorageError::storage(e.to_string()))?;
228                if let Some(name) = entry.file_name().to_str() {
229                    keys.push(name.to_string());
230                }
231            }
232            Ok(keys)
233        }
234    }
235
236    async fn secure_generate_key(
237        &self,
238        location: &SecureStorageLocation,
239        context: &str,
240        caps: &[aura_core::effects::SecureStorageCapability],
241    ) -> Result<Option<Vec<u8>>, SecureStorageError> {
242        self.require_capability(caps, SecureStorageCapability::Write)?;
243        let mut key = [0u8; 32];
244        getrandom::getrandom(&mut key).map_err(|e| SecureStorageError::storage(e.to_string()))?;
245        let mut material = key.to_vec();
246        material.extend_from_slice(context.as_bytes());
247        self.secure_store(location, &material, caps).await?;
248        Ok(Some(material))
249    }
250
251    async fn secure_create_time_bound_token(
252        &self,
253        location: &SecureStorageLocation,
254        caps: &[aura_core::effects::SecureStorageCapability],
255        expires_at: &aura_core::time::PhysicalTime,
256    ) -> Result<Vec<u8>, SecureStorageError> {
257        self.require_capability(caps, SecureStorageCapability::Read)?;
258        let token = format!(
259            "{}:{}:{}",
260            location.full_path(),
261            expires_at.ts_ms,
262            self.platform_config
263        );
264        Ok(token.into_bytes())
265    }
266
267    async fn secure_access_with_token(
268        &self,
269        token: &[u8],
270        _location: &SecureStorageLocation,
271    ) -> Result<Vec<u8>, SecureStorageError> {
272        let token_str = std::str::from_utf8(token)
273            .map_err(|e| SecureStorageError::serialization(e.to_string()))?;
274        let parts: Vec<&str> = token_str.splitn(3, ':').collect();
275        if parts.len() != 3 {
276            return Err(SecureStorageError::invalid("invalid secure access token"));
277        }
278        let expires_at_ms: u64 = parts[1].parse().map_err(|e: std::num::ParseIntError| {
279            SecureStorageError::serialization(e.to_string())
280        })?;
281
282        let now_ms = self.current_time_ms()?;
283        if now_ms > expires_at_ms {
284            return Err(SecureStorageError::permission_denied(
285                "secure access token expired",
286            ));
287        }
288
289        Ok(parts[0].as_bytes().to_vec())
290    }
291
292    async fn get_device_attestation(&self) -> Result<Vec<u8>, SecureStorageError> {
293        #[derive(serde::Serialize)]
294        struct Attestation<'a> {
295            platform: &'a str,
296            issued_at_ms: u64,
297            capabilities: Vec<String>,
298        }
299
300        let issued_at_ms = self.current_time_ms()?;
301
302        let attestation = Attestation {
303            platform: &self.platform_config,
304            issued_at_ms,
305            capabilities: self.get_secure_storage_capabilities(),
306        };
307
308        serde_json::to_vec(&attestation)
309            .map_err(|e| SecureStorageError::serialization(e.to_string()))
310    }
311
312    async fn is_secure_storage_available(&self) -> bool {
313        true
314    }
315
316    fn get_secure_storage_capabilities(&self) -> Vec<String> {
317        vec![
318            "filesystem-fallback".to_string(),
319            "time-bound-token".to_string(),
320        ]
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use tempfile::tempdir;
328
329    #[tokio::test]
330    async fn test_real_secure_storage_store_and_retrieve() {
331        let temp = match tempdir() {
332            Ok(dir) => dir,
333            Err(err) => panic!("create tempdir: {err}"),
334        };
335        let handler = RealSecureStorageHandler::with_base_path(temp.path().to_path_buf());
336        let location = SecureStorageLocation::new("test_namespace", "test_key");
337        let capabilities = vec![
338            SecureStorageCapability::Read,
339            SecureStorageCapability::Write,
340            SecureStorageCapability::Delete,
341            SecureStorageCapability::List,
342        ];
343
344        handler
345            .secure_store(&location, b"data", &capabilities)
346            .await
347            .unwrap();
348        let data = handler
349            .secure_retrieve(&location, &capabilities)
350            .await
351            .unwrap();
352        assert_eq!(data, b"data");
353        assert!(handler.secure_exists(&location).await.unwrap());
354        handler
355            .secure_delete(&location, &capabilities)
356            .await
357            .unwrap();
358    }
359}