1use crate::cipher_export::StoredExportedStoreCipher;
5use crate::error::StorageError;
6use indexed_db_futures::transaction::TransactionMode;
7use nym_store_cipher::{
8 Aes256Gcm, Algorithm, EncryptedData, KdfInfo, KeySizeUser, Params, StoreCipher, Unsigned,
9 Version,
10};
11use nym_wasm_utils::console_log;
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14use std::future::IntoFuture;
15use wasm_bindgen::JsValue;
16
17pub use indexed_db_futures::database::{Database, VersionChangeEvent};
18pub use indexed_db_futures::prelude::*;
19pub use indexed_db_futures::primitive::{TryFromJs, TryToJs};
20pub use indexed_db_futures::Result as RawDbResult;
21
22mod cipher_export;
23pub mod error;
24pub mod traits;
25
26pub const CIPHER_INFO_STORE: &str = "_cipher_store";
27pub const CIPHER_STORE_EXPORT: &str = "cipher_store_export_info";
28
29const MEMORY_COST: u32 = 19 * 1024;
30const ITERATIONS: u32 = 2;
31const PARALLELISM: u32 = 1;
32const OUTPUT_LENGTH: usize = <Aes256Gcm as KeySizeUser>::KeySize::USIZE;
33
34pub fn new_default_kdf() -> Result<KdfInfo, StorageError> {
36 let kdf_salt = KdfInfo::random_salt()?;
37 let kdf_info = KdfInfo::Argon2 {
38 params: Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap(),
39 algorithm: Algorithm::Argon2id,
40 version: Version::V0x13,
41 kdf_salt,
42 };
43 Ok(kdf_info)
44}
45
46pub struct WasmStorage {
48 inner: IdbWrapper,
49 store_cipher: Option<StoreCipher>,
51}
52
53impl WasmStorage {
54 pub async fn new<F>(
55 db_name: &str,
56 version: u32,
57 migrate_fn: Option<F>,
58 passphrase: Option<&[u8]>,
59 ) -> Result<Self, StorageError>
60 where
61 F: Fn(VersionChangeEvent, Database) -> RawDbResult<()> + 'static,
62 {
63 let db = Database::open(db_name)
65 .with_version(version)
66 .with_on_upgrade_needed(move |event, db| {
67 let old_version = event.old_version() as u32;
71
72 if old_version < 1 {
73 db.create_object_store(CIPHER_INFO_STORE).build()?;
74 }
75
76 if let Some(migrate) = migrate_fn.as_ref() {
77 migrate(event, db)
78 } else {
79 Ok(())
80 }
81 })
82 .await?;
83
84 let inner = IdbWrapper(db);
85 let store_cipher = inner.setup_store_cipher(passphrase).await?;
86
87 Ok(WasmStorage {
88 inner,
89 store_cipher,
90 })
91 }
92
93 pub async fn delete(self) -> Result<(), StorageError> {
94 self.inner.0.delete()?.into_future().await?;
95 Ok(())
96 }
97
98 pub async fn remove(db_name: &str) -> Result<(), StorageError> {
99 Database::delete_by_name(db_name)?.into_future().await?;
100 Ok(())
101 }
102
103 pub async fn exists(db_name: &str) -> Result<bool, StorageError> {
104 let db = Database::open(db_name).await?;
105
106 let some_stores_exist = db.object_store_names().next().is_some();
109
110 if !some_stores_exist {
113 db.delete()?.into_future().await?
114 }
115
116 Ok(some_stores_exist)
117 }
118
119 pub fn serialize_value<T: Serialize>(&self, value: &T) -> Result<JsValue, StorageError> {
120 if let Some(cipher) = &self.store_cipher {
121 let encrypted = cipher.encrypt_json_value(value)?;
122 Ok(serde_wasm_bindgen::to_value(&encrypted)?)
123 } else {
124 Ok(serde_wasm_bindgen::to_value(&value)?)
125 }
126 }
127
128 pub fn deserialize_value<T: DeserializeOwned>(
129 &self,
130 value: JsValue,
131 ) -> Result<T, StorageError> {
132 if let Some(cipher) = &self.store_cipher {
133 let encrypted: EncryptedData = serde_wasm_bindgen::from_value(value)?;
134 Ok(cipher.decrypt_json_value(encrypted)?)
135 } else {
136 Ok(serde_wasm_bindgen::from_value(value)?)
137 }
138 }
139
140 pub async fn read_value<T, K>(&self, store: &str, key: K) -> Result<Option<T>, StorageError>
141 where
142 T: DeserializeOwned,
143 K: TryToJs,
144 {
145 self.inner
146 .read_value_raw(store, key)
147 .await?
148 .map(|raw| self.deserialize_value(raw))
149 .transpose()
150 }
151
152 pub async fn store_value<T, K>(
153 &self,
154 store: &str,
155 key: K,
156 value: &T,
157 ) -> Result<(), StorageError>
158 where
159 T: Serialize,
160 K: TryToJs + TryFromJs,
161 {
162 self.inner
163 .store_value_raw(store, key, &self.serialize_value(&value)?)
164 .await
165 }
166
167 pub async fn remove_value<K>(&self, store: &str, key: K) -> Result<(), StorageError>
168 where
169 K: TryToJs,
170 {
171 self.inner.remove_value_raw(store, key).await
172 }
173
174 pub async fn has_value<K>(&self, store: &str, key: K) -> Result<bool, StorageError>
175 where
176 K: TryToJs,
177 {
178 match self.key_count(store, key).await? {
179 0 => Ok(false),
180 1 => Ok(true),
181 n => Err(StorageError::DuplicateKey { count: n }),
182 }
183 }
184
185 pub async fn key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
186 where
187 K: TryToJs,
188 {
189 self.inner.get_key_count(store, key).await
190 }
191
192 pub async fn get_all_keys(&self, store: &str) -> Result<Vec<JsValue>, StorageError> {
193 self.inner.get_all_keys(store).await
194 }
195}
196
197struct IdbWrapper(Database);
198
199impl IdbWrapper {
200 async fn read_value_raw<K>(&self, store: &str, key: K) -> Result<Option<JsValue>, StorageError>
201 where
202 K: TryToJs,
203 {
204 self.0
205 .transaction(store)
206 .with_mode(TransactionMode::Readonly)
207 .build()?
208 .object_store(store)?
209 .get(&key)
210 .primitive()?
211 .await
212 .map_err(Into::into)
213 }
214
215 async fn store_value_raw<K, T>(
216 &self,
217 store: &str,
218 key: K,
219 value: &T,
220 ) -> Result<(), StorageError>
221 where
222 K: TryToJs + TryFromJs,
223 T: TryToJs,
224 {
225 let tx = self
226 .0
227 .transaction(store)
228 .with_mode(TransactionMode::Readwrite)
229 .build()?;
230
231 let store = tx.object_store(store)?;
232 store.put(value).with_key(key).primitive()?.await?;
233
234 tx.commit().await.map_err(Into::into)
235 }
236
237 async fn remove_value_raw<K>(&self, store: &str, key: K) -> Result<(), StorageError>
238 where
239 K: TryToJs,
240 {
241 let tx = self
242 .0
243 .transaction(store)
244 .with_mode(TransactionMode::Readwrite)
245 .build()?;
246
247 let store = tx.object_store(store)?;
248 store.delete(key).primitive()?.await?;
249
250 tx.commit().await.map_err(Into::into)
251 }
252
253 async fn get_key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
254 where
255 K: TryToJs,
256 {
257 self.0
258 .transaction(store)
259 .with_mode(TransactionMode::Readonly)
260 .build()?
261 .object_store(store)?
262 .count()
263 .with_query(key)
264 .primitive()?
265 .await
266 .map_err(Into::into)
267 }
268
269 async fn get_all_keys(&self, store: &str) -> Result<Vec<JsValue>, StorageError> {
270 self.0
271 .transaction(store)
272 .with_mode(TransactionMode::Readonly)
273 .build()?
274 .object_store(store)?
275 .get_all_keys()
276 .primitive()?
277 .await?
278 .collect::<Result<Vec<_>, _>>()
279 .map_err(Into::into)
280 }
281
282 async fn read_exported_cipher_store(
283 &self,
284 ) -> Result<Option<StoredExportedStoreCipher>, StorageError> {
285 self.read_value_raw(CIPHER_INFO_STORE, JsValue::from_str(CIPHER_STORE_EXPORT))
286 .await?
287 .map(serde_wasm_bindgen::from_value)
288 .transpose()
289 .map_err(Into::into)
290 }
291
292 async fn store_exported_cipher_store(
293 &self,
294 exported_store_cipher: StoredExportedStoreCipher,
295 ) -> Result<(), StorageError> {
296 self.store_value_raw(
297 CIPHER_INFO_STORE,
298 JsValue::from_str(CIPHER_STORE_EXPORT),
299 &serde_wasm_bindgen::to_value(&exported_store_cipher)?,
300 )
301 .await
302 }
303
304 async fn setup_new_store_cipher(
305 &self,
306 passphrase: Option<&[u8]>,
307 ) -> Result<Option<StoreCipher>, StorageError> {
308 if let Some(passphrase) = passphrase {
309 console_log!("attempting to derive new encryption key");
310 let kdf_info = new_default_kdf()?;
311 let store_cipher = StoreCipher::<Aes256Gcm>::new(passphrase, kdf_info)?;
312 let exported = store_cipher.export_aes256gcm()?;
313 self.store_exported_cipher_store(Some(exported).into())
314 .await?;
315
316 Ok(Some(store_cipher))
317 } else {
318 console_log!("this new storage will not use any encryption");
319 self.store_exported_cipher_store(StoredExportedStoreCipher::NoEncryption)
320 .await?;
321 Ok(None)
322 }
323 }
324
325 async fn restore_existing_cipher(
326 &self,
327 existing: StoredExportedStoreCipher,
328 passphrase: Option<&[u8]>,
329 ) -> Result<Option<StoreCipher>, StorageError> {
330 if let Some(passphrase) = passphrase {
331 console_log!("attempting to use previously derived encryption key");
332 if let StoredExportedStoreCipher::Cipher(exported_cipher) = existing {
333 Ok(Some(StoreCipher::import_aes256gcm(
334 passphrase,
335 exported_cipher,
336 )?))
337 } else {
338 Err(StorageError::UnexpectedPassphraseProvided)
339 }
340 } else {
341 console_log!("attempting to restore old unencrypted data");
342 if existing.uses_encryption() {
343 Err(StorageError::NoPassphraseProvided)
344 } else {
345 Ok(None)
346 }
347 }
348 }
349
350 async fn setup_store_cipher(
351 &self,
352 passphrase: Option<&[u8]>,
353 ) -> Result<Option<StoreCipher>, StorageError> {
354 if let Some(existing_cipher_info) = self.read_exported_cipher_store().await? {
361 self.restore_existing_cipher(existing_cipher_info, passphrase)
362 .await
363 } else {
364 self.setup_new_store_cipher(passphrase).await
365 }
366 }
367}