1#[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#[derive(Debug)]
34pub struct RealSecureStorageHandler {
35 platform_config: String,
36 base_path: PathBuf,
37}
38
39impl RealSecureStorageHandler {
40 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 #[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)] 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}