localencrypt/storage/
local_storage.rs

1use std::collections::HashMap;
2
3use log::info;
4use serde_json;
5
6use crate::common::Credentials;
7use crate::crypto::{derive_crypto_key, hash_username};
8use crate::utils::generate_password_base64;
9use crate::{ItemMetaData, ObjectKey, SecureStorage, SecureStringError, SecureStringResult};
10
11use super::local_storage_user::LocalStorageUser;
12
13const PASSWORD_FIELD: &str = "__PASSWD__";
14
15#[derive(Clone, PartialEq, Debug)]
16pub struct LocalStorage {
17    credentials: Credentials,
18    storage: Option<SecureStorage>,
19}
20
21impl LocalStorage {
22    pub async fn new(credentials: Credentials) -> SecureStringResult<Self> {
23        let password = credentials.password();
24
25        let storage = if password.is_empty() {
26            None
27        } else {
28            let user = LocalStorageUser::create_or_validate(&credentials).await?;
29            Some(user.secure_storage().clone())
30        };
31        Ok(Self {
32            credentials,
33            storage,
34        })
35    }
36
37    pub fn get_username(&self) -> SecureStringResult<String> {
38        Ok(self.credentials.username().to_string())
39    }
40
41    pub async fn user_exists(environment: &str, username: &str) -> bool {
42        let hashed_username = hash_username(username);
43        let object_key = ObjectKey::new(environment, &hashed_username, "self").unwrap();
44        SecureStorage::exists(object_key).await
45    }
46
47    pub async fn soft_reset(&self) -> SecureStringResult<()> {
48        // TODO: soft reset should try to backup old master account
49        // for now we just hard reset
50        self.hard_reset().await?;
51        Ok(())
52    }
53
54    pub async fn hard_reset(&self) -> SecureStringResult<()> {
55        let environment = self.credentials.environment();
56        let username = self.credentials.username();
57        let hashed_username = hash_username(&username);
58        let object_key = ObjectKey::new(&environment, &hashed_username, "self")?;
59
60        let secure_storage = SecureStorage::for_deletion(object_key);
61        secure_storage.delete().await?;
62
63        Ok(())
64    }
65
66    pub async fn validate_password(&self) -> SecureStringResult<bool> {
67        // Check if storage is initialized
68        if self.storage.is_none() {
69            return Err(SecureStringError::StorageNotInitialized);
70        }
71
72        // Attempt to load the stored data using the current password
73        match self.storage.as_ref().unwrap().load().await {
74            // If data loading is successful, it implies the password is valid
75            Ok(_) => Ok(true),
76            // If a DecryptError occurs, the password is invalid
77            Err(SecureStringError::DecryptError(_)) => Ok(false),
78            // Any other error is propagated
79            Err(err) => Err(err),
80        }
81    }
82
83    pub async fn change_password(&mut self, new_password: &str) -> SecureStringResult<()> {
84        // copy existing items -- this will be gone after soft_reset
85        let items = self.get_items().await?;
86        let new_credentials = Credentials::new(
87            Some(&self.credentials.environment()),
88            &self.credentials.username(),
89            new_password,
90        );
91
92        // cleanup current user
93        self.soft_reset().await?;
94
95        // create user with same username, but new password
96        let user = LocalStorageUser::create_or_validate(&new_credentials).await?;
97        self.storage = Some(user.secure_storage().clone());
98
99        // put back original items with the new password
100        self.put_items(&items).await?;
101        Ok(())
102    }
103
104    pub async fn initiate_with_password(
105        environment: Option<&str>,
106        username: &str,
107        password: &str,
108    ) -> SecureStringResult<Self> {
109        let credentials = Credentials::new(environment, username, password);
110        Self::new(credentials).await
111    }
112
113    pub async fn initiate_with_no_password(
114        environment: Option<&str>,
115        username: &str,
116    ) -> SecureStringResult<Self> {
117        let password = "";
118        let credentials = Credentials::new(environment, username, &password);
119        Self::new(credentials).await
120    }
121
122    pub async fn list_items(&self) -> SecureStringResult<Vec<ItemMetaData>> {
123        let items = self.get_items().await?;
124
125        let items_as_meta = items
126            .into_iter()
127            .map(|(id, tags)| {
128                let mut new_tags = tags.clone();
129                new_tags.retain(|k, _| !k.starts_with("__") && !k.ends_with("__"));
130                ItemMetaData::new_with_tags(&id, new_tags)
131            })
132            .collect();
133
134        Ok(items_as_meta)
135    }
136
137    pub async fn add_item(&mut self, item_meta: ItemMetaData) -> SecureStringResult<()> {
138        let mut items = self.get_items().await?;
139
140        let item_id = item_meta.id();
141
142        if items.contains_key(&item_id) {
143            return Err(SecureStringError::FormAlreadyExists);
144        }
145
146        let item_meta_stored = self.populate_meta_stored(&item_meta, None);
147
148        items.insert(item_id, item_meta_stored);
149
150        self.put_items(&items).await
151    }
152
153    pub async fn delete_item(&mut self, item_id: &str) -> SecureStringResult<()> {
154        // remove form data first
155        let environment = self.credentials.environment();
156        let object_key = ObjectKey::new(&environment, "", &item_id)?;
157        let secure_storage = SecureStorage::for_deletion(object_key);
158        secure_storage.delete().await?;
159
160        // remove item meta
161        let mut items = self.get_items().await?;
162        if items.remove(item_id).is_none() {
163            return Err(SecureStringError::PasswordNotFound(format!(
164                "Configuration for {} not found",
165                item_id
166            )));
167        }
168        self.put_items(&items).await
169    }
170
171    pub async fn save_content(
172        &mut self,
173        item_meta: ItemMetaData,
174        content: &[u8],
175    ) -> SecureStringResult<()> {
176        let item_id = item_meta.id();
177        info!("Saving configuration for {}", item_id);
178
179        let password = generate_password_base64()?;
180        let derived_key = derive_crypto_key(&password, &item_id).await?;
181
182        let environment = self.credentials.environment();
183        let object_key = ObjectKey::new(&environment, "", &item_id)?;
184        let secure_storage_form = SecureStorage::new(object_key, derived_key);
185        secure_storage_form.save(content).await?;
186
187        let mut items = self.get_items().await?;
188        let item_meta_stored = self.populate_meta_stored(&item_meta, Some(password));
189
190        items.insert(item_id, item_meta_stored);
191
192        self.put_items(&items).await
193    }
194
195    pub async fn load_content(&self, item_id: &str) -> Result<Option<Vec<u8>>, SecureStringError> {
196        let items = self.get_items().await?;
197
198        if let Some(meta) = items.get(item_id) {
199            if let Some(password) = meta.get(PASSWORD_FIELD) {
200                let environment = self.credentials.environment();
201                let object_key = ObjectKey::new(&environment, "", &item_id)?;
202                let derived_key = derive_crypto_key(&password, &item_id).await?;
203                let secure_storage_form = SecureStorage::new(object_key, derived_key);
204
205                secure_storage_form.load().await.map(Some)
206            } else {
207                // item found, but no content saved yet
208                Ok(None)
209            }
210        } else {
211            // item not found
212            Err(SecureStringError::NoLocalStorageData)
213        }
214    }
215
216    async fn get_items(&self) -> SecureStringResult<HashMap<String, HashMap<String, String>>> {
217        if self.storage.is_none() {
218            return Err(SecureStringError::StorageNotInitialized);
219        }
220
221        match self.storage.as_ref().unwrap().load().await {
222            Ok(passwords_json) => serde_json::from_slice(&passwords_json).map_err(|err| {
223                SecureStringError::SerdeError(format!("Failed to parse forms_db: {:?}", err))
224            }),
225            Err(SecureStringError::NoLocalStorageData) => Ok(HashMap::new()),
226            Err(err) => Err(err),
227        }
228    }
229
230    async fn put_items(
231        &mut self,
232        items: &HashMap<String, HashMap<String, String>>,
233    ) -> SecureStringResult<()> {
234        if self.storage.is_none() {
235            return Err(SecureStringError::StorageNotInitialized);
236        }
237        let items_vec = serde_json::to_vec(items).map_err(|err| {
238            SecureStringError::SerdeError(format!("Failed to serialize forms_db: {:?}", err))
239        })?;
240        self.storage.as_ref().unwrap().save(&items_vec).await
241    }
242
243    fn populate_meta_stored(
244        &self,
245        item_meta: &ItemMetaData,
246        password: Option<String>,
247    ) -> HashMap<String, String> {
248        let mut item_meta_stored = HashMap::new();
249
250        if let Some(pwd) = password {
251            item_meta_stored.insert(PASSWORD_FIELD.to_string(), pwd);
252        }
253
254        if let Some(tags) = item_meta.tags() {
255            item_meta_stored.extend(tags);
256        }
257
258        item_meta_stored
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::collections::HashMap;
265
266    use wasm_bindgen_test::*;
267
268    use super::*;
269
270    wasm_bindgen_test_configure!(run_in_browser);
271
272    #[wasm_bindgen_test]
273    async fn test_list_items() {
274        let credentials = Credentials::new(None, "test_user_list_items", "password_for_list_items");
275
276        let local_storage = LocalStorage::new(credentials.clone()).await;
277        assert!(local_storage.is_ok());
278        let local_storage = local_storage.unwrap();
279
280        let result = local_storage.list_items().await;
281        assert!(
282            result.is_ok(),
283            "Failed to list documents: {:?}",
284            result.err().unwrap()
285        );
286
287        let document_map = result.unwrap();
288        assert_eq!(document_map.len(), 0);
289        local_storage.hard_reset().await.unwrap();
290    }
291
292    #[wasm_bindgen_test]
293    async fn test_save_and_load_content() {
294        let credentials = Credentials::new(None, "test_user_save_load", "password_for_save_load");
295
296        let local_storage = LocalStorage::new(credentials.clone()).await;
297        assert!(local_storage.is_ok());
298        let mut local_storage = local_storage.unwrap();
299
300        let mut config = HashMap::new();
301        config.insert("__NAME__".to_string(), "test_config".to_string());
302
303        // ensure unique id
304        let form_id = format!("{}-{}", credentials.username(), credentials.password());
305        let meta_data = ItemMetaData::new(&form_id);
306        let config_bytes = serde_json::to_vec(&config).unwrap();
307
308        let save_result = local_storage
309            .save_content(meta_data.clone(), &config_bytes)
310            .await;
311        assert!(
312            save_result.is_ok(),
313            "Failed to save document: {:?}",
314            save_result.err().unwrap()
315        );
316
317        let load_result = local_storage.load_content(&form_id).await;
318        assert!(
319            load_result.is_ok(),
320            "Failed to load document: {:?}",
321            load_result.err().unwrap()
322        );
323
324        let loaded_config_bytes_option = load_result.unwrap();
325        assert!(
326            loaded_config_bytes_option.is_some(),
327            "Loaded document did not exist"
328        );
329
330        let loaded_config_bytes = loaded_config_bytes_option.unwrap();
331        let loaded_config: HashMap<String, String> =
332            serde_json::from_slice(&loaded_config_bytes).unwrap();
333        assert_eq!(
334            loaded_config, config,
335            "Loaded document did not match saved document"
336        );
337        local_storage.hard_reset().await.unwrap();
338    }
339
340    #[wasm_bindgen_test]
341    async fn test_add_and_delete_items() {
342        let credentials = Credentials::new(
343            None,
344            "test_user_add_and_delete_items",
345            "password_for_add_and_delete",
346        );
347
348        let local_storage = LocalStorage::new(credentials.clone()).await;
349        assert!(local_storage.is_ok());
350        let mut local_storage = local_storage.unwrap();
351
352        // ensure unique id
353        let form_id = format!("{}-{}", credentials.username(), credentials.password());
354        let meta_data = ItemMetaData::new(&form_id);
355
356        // Test adding a document
357        let add_result = local_storage.add_item(meta_data.clone()).await;
358        assert!(
359            add_result.is_ok(),
360            "Failed to add document: {:?}",
361            add_result.err().unwrap()
362        );
363
364        let load_result = local_storage.load_content(&form_id).await;
365        assert!(load_result.is_ok(), "Load failed after add");
366
367        let loaded_data = load_result.unwrap();
368        assert!(loaded_data.is_none(), "Data present after add");
369
370        // Test deleting a document
371        let delete_result = local_storage.delete_item(&form_id).await;
372        assert!(
373            delete_result.is_ok(),
374            "Failed to delete document: {:?}",
375            delete_result.err().unwrap()
376        );
377
378        let load_result_after_delete = local_storage.load_content(&form_id).await;
379        assert!(
380            matches!(
381                load_result_after_delete,
382                Err(SecureStringError::NoLocalStorageData)
383            ),
384            "Successfully loaded document after delete"
385        );
386        local_storage.hard_reset().await.unwrap();
387    }
388
389    #[wasm_bindgen_test]
390    async fn test_delete_non_existent_item() {
391        let credentials = Credentials::new(
392            None,
393            "test_user_delete_non_existent_item",
394            "password_for_delete_non_existent",
395        );
396
397        let local_storage = LocalStorage::new(credentials.clone()).await;
398        assert!(local_storage.is_ok());
399        let mut local_storage = local_storage.unwrap();
400
401        // ensure unique id
402        let form_id = format!("{}-{}", credentials.username(), credentials.password());
403        let delete_result = local_storage.delete_item(&form_id).await;
404        assert!(
405            delete_result.is_err(),
406            "Successfully deleted a non-existent document"
407        );
408
409        local_storage.hard_reset().await.unwrap();
410    }
411
412    #[wasm_bindgen_test]
413    async fn test_load_non_existent_item() {
414        let credentials = Credentials::new(
415            None,
416            "test_user_load_non_existent_item",
417            "password_for_load_non_existent",
418        );
419
420        let local_storage = LocalStorage::new(credentials.clone()).await;
421        assert!(local_storage.is_ok());
422        let local_storage = local_storage.unwrap();
423
424        // ensure unique id
425        let form_id = format!("{}-{}", credentials.username(), credentials.password());
426        let load_result = local_storage.load_content(&form_id).await;
427        assert!(
428            load_result.is_err(),
429            "Successfully loaded a non-existent document"
430        );
431        local_storage.hard_reset().await.unwrap();
432    }
433
434    #[wasm_bindgen_test]
435    async fn test_populate_meta_stored() {
436        let credentials = Credentials::new(
437            None,
438            "test_user_populate_meta_stored",
439            "password_for_populate_meta_stored",
440        );
441
442        let local_storage = LocalStorage::new(credentials.clone()).await;
443        assert!(local_storage.is_ok());
444        let local_storage = local_storage.unwrap();
445
446        let mut tags = HashMap::new();
447        tags.insert("tag1".to_string(), "value1".to_string());
448        tags.insert("tag2".to_string(), "value2".to_string());
449
450        let item_meta = ItemMetaData::new_with_tags("test_populate_meta_stored", tags);
451
452        let item_meta_stored =
453            local_storage.populate_meta_stored(&item_meta, Some("password".to_string()));
454
455        assert_eq!(
456            item_meta_stored.get(PASSWORD_FIELD),
457            Some(&"password".to_string())
458        );
459        assert_eq!(item_meta_stored.get("tag1"), Some(&"value1".to_string()));
460        assert_eq!(item_meta_stored.get("tag2"), Some(&"value2".to_string()));
461        local_storage.hard_reset().await.unwrap();
462    }
463
464    #[wasm_bindgen_test]
465    async fn test_validate_password() {
466        let correct_password = "correct_password";
467        let incorrect_password = "incorrect_password";
468
469        // Initialization with correct password
470        let local_storage =
471            LocalStorage::initiate_with_password(None, "test_user", correct_password).await;
472        assert!(
473            local_storage.is_ok(),
474            "Failed to initialize with correct password"
475        );
476
477        // Attempting initialization with incorrect password
478        let result =
479            LocalStorage::initiate_with_password(None, "test_user", incorrect_password).await;
480        assert!(
481            matches!(result, Err(SecureStringError::DecryptError(_))),
482            "Unexpected error type returned"
483        );
484
485        // Clean up
486        local_storage.unwrap().hard_reset().await.unwrap();
487    }
488
489    #[wasm_bindgen_test]
490    async fn test_change_password() {
491        let old_password = "old_password";
492        let new_password = "new_password";
493        let username = "test_user_change_password_local_storage";
494
495        let item_id = "test_item_id";
496        let item_meta = ItemMetaData::new(item_id);
497        let test_content = b"test_content";
498
499        // Initialization with old password
500        let local_storage =
501            LocalStorage::initiate_with_password(None, username, old_password).await;
502        assert!(
503            local_storage.is_ok(),
504            "Failed to initialize with old password"
505        );
506        let mut local_storage = local_storage.unwrap();
507
508        // Add an item
509        let save_result = local_storage
510            .save_content(item_meta.clone(), test_content)
511            .await;
512        assert!(
513            save_result.is_ok(),
514            "Failed to save content before changing password"
515        );
516
517        // Change password to new password
518        let change_password_result = local_storage.change_password(new_password).await;
519        assert!(change_password_result.is_ok(), "Failed to change password");
520
521        // Ensure we can't load with old password
522        let result = LocalStorage::initiate_with_password(None, username, old_password).await;
523        assert!(
524            matches!(result, Err(SecureStringError::DecryptError(_))),
525            "Unexpectedly succeeded in loading with old password"
526        );
527
528        // Ensure we can load with new password
529        let result = LocalStorage::initiate_with_password(None, username, new_password).await;
530        assert!(result.is_ok(), "Failed to load with new password");
531        let new_local_storage = result.unwrap();
532
533        // Ensure our item still exists with new password
534        let load_result = new_local_storage.load_content(item_id).await;
535        assert!(
536            load_result.is_ok() && load_result.unwrap() == Some(test_content.to_vec()),
537            "Failed to load content or content was incorrect after changing password"
538        );
539
540        // Clean up
541        local_storage.hard_reset().await.unwrap();
542    }
543}