b27fa51092
* Chore/browser extension bootstrap (#3257) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * Feature/nym browser extension login and send (#3373) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * add extension to mono-repo config * fix webpack build * util functions * add TX type * refactor routes * refactor pages + add send page * add page layout for app pages * set up app context * app components * set up connection config * fix lint errors * Chore/browser extension bootstrap (#3257) * init package * set up TS and Webpack * add eslint config * add prettier config * add react and mui theme * add CI * update mui theme version number * Chore/browser extension routes (#3327) * start routes * create layouts * add initial app routes * add initial app pages * add global types * create reuseable components * move password and mnemonic fields to shared react components package * refactor register routes * move client address component to shared package * move components to ui folder * create menu and appbar components * adjust layout components * add readme * use memory router * add extension to mono-repo config * util functions * add TX type * refactor routes * refactor pages + add send page * add page layout for app pages * set up app context * app components * set up connection config * use fee simulation when sending tokens * use object argument for simulate send api * login validation + fee refinements * use components from shared components lib * add receive modal (#3408) * account storage via wasm * method to get all storage keys * Feature/nym browser extension password encryption (single account) (#3442) * build wasm * reuse components and state for password pages * refactor registration pages * use login with password * import storage as local package * add yarn preinstall script to ts lint gh action * install wasm-pack for CI * use @nym scope for ext storage package * introduced a call to check if database was already initialised (#3465) * introduced a call to check if database was already initialised * use extension storage method to check for db existance --------- Co-authored-by: fmtabbara <fmtabbara@hotmail.co.uk> * introduced mnemonic key existence check (#3462) * Browser extension - Multi-accounts + view mnemonic action (#3488) * add UI for multi-accounts + add view mnemonic for accounts * refactor routes * set up import account * add account to existing wallet * check if account name exists before creating new one * handle password errors * add token to currency conversion * fixed ClientStorageError import path * fix CI * fix CI --------- Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
341 lines
11 KiB
Rust
341 lines
11 KiB
Rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
use crate::console_log;
|
|
use crate::storage::cipher_export::StoredExportedStoreCipher;
|
|
use crate::storage::error::StorageError;
|
|
use futures::TryFutureExt;
|
|
use indexed_db_futures::IdbDatabase;
|
|
use nym_store_cipher::{
|
|
Aes256Gcm, Algorithm, EncryptedData, KdfInfo, KeySizeUser, Params, StoreCipher, Unsigned,
|
|
Version,
|
|
};
|
|
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
use wasm_bindgen::JsValue;
|
|
|
|
pub use indexed_db_futures::prelude::*;
|
|
|
|
mod cipher_export;
|
|
pub mod error;
|
|
|
|
pub const CIPHER_INFO_STORE: &str = "_cipher_store";
|
|
pub const CIPHER_STORE_EXPORT: &str = "cipher_store_export_info";
|
|
|
|
const MEMORY_COST: u32 = 19 * 1024;
|
|
const ITERATIONS: u32 = 2;
|
|
const PARALLELISM: u32 = 1;
|
|
const OUTPUT_LENGTH: usize = <Aes256Gcm as KeySizeUser>::KeySize::USIZE;
|
|
|
|
// use hardcoded values in case any `Default` implementation changes in the future
|
|
pub fn new_default_kdf() -> Result<KdfInfo, StorageError> {
|
|
let kdf_salt = KdfInfo::random_salt()?;
|
|
let kdf_info = KdfInfo::Argon2 {
|
|
params: Params::new(MEMORY_COST, ITERATIONS, PARALLELISM, Some(OUTPUT_LENGTH)).unwrap(),
|
|
algorithm: Algorithm::Argon2id,
|
|
version: Version::V0x13,
|
|
kdf_salt,
|
|
};
|
|
Ok(kdf_info)
|
|
}
|
|
|
|
/// An indexeddb-backed in-browser storage with optional encryption.
|
|
pub struct WasmStorage {
|
|
inner: IdbWrapper,
|
|
// TODO: this might have to be put behind an Arc.
|
|
store_cipher: Option<StoreCipher>,
|
|
}
|
|
|
|
impl WasmStorage {
|
|
pub async fn new<F>(
|
|
db_name: &str,
|
|
version: u32,
|
|
migrate_fn: Option<F>,
|
|
passphrase: Option<&[u8]>,
|
|
) -> Result<Self, StorageError>
|
|
where
|
|
F: Fn(&IdbVersionChangeEvent) -> Result<(), JsValue> + 'static,
|
|
{
|
|
let mut db_req: OpenDbRequest = IdbDatabase::open_u32(db_name, version)?;
|
|
|
|
// we must always ensure the cipher table is present
|
|
db_req.set_on_upgrade_needed(Some(
|
|
move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
|
|
// Even if the web-sys bindings expose the version as a f64, the IndexedDB API
|
|
// works with an unsigned integer.
|
|
// See <https://github.com/rustwasm/wasm-bindgen/issues/1149>
|
|
let old_version = evt.old_version() as u32;
|
|
|
|
if old_version < 1 {
|
|
evt.db().create_object_store(CIPHER_INFO_STORE)?;
|
|
}
|
|
|
|
if let Some(migrate) = migrate_fn.as_ref() {
|
|
migrate(evt)
|
|
} else {
|
|
Ok(())
|
|
}
|
|
},
|
|
));
|
|
|
|
let db: IdbDatabase = db_req.into_future().await?;
|
|
let inner = IdbWrapper(db);
|
|
let store_cipher = inner.setup_store_cipher(passphrase).await?;
|
|
|
|
Ok(WasmStorage {
|
|
inner,
|
|
store_cipher,
|
|
})
|
|
}
|
|
|
|
pub async fn exists(db_name: &str) -> Result<bool, StorageError> {
|
|
let db_req: OpenDbRequest = IdbDatabase::open(db_name)?;
|
|
let db: IdbDatabase = db_req.into_future().await?;
|
|
|
|
// if the db was already created before, at the very least cipher info store should exist,
|
|
// thus the iterator should return at least one value
|
|
let some_stores_exist = db.object_store_names().next().is_some();
|
|
|
|
// that's super annoying - we have to do cleanup because opening db creates it
|
|
// (if it didn't exist before)
|
|
if !some_stores_exist {
|
|
db.delete()?.into_future().await?
|
|
}
|
|
|
|
Ok(some_stores_exist)
|
|
}
|
|
|
|
pub fn serialize_value<T: Serialize>(&self, value: &T) -> Result<JsValue, StorageError> {
|
|
if let Some(cipher) = &self.store_cipher {
|
|
let encrypted = cipher.encrypt_json_value(value)?;
|
|
Ok(serde_wasm_bindgen::to_value(&encrypted)?)
|
|
} else {
|
|
Ok(serde_wasm_bindgen::to_value(&value)?)
|
|
}
|
|
}
|
|
|
|
pub fn deserialize_value<T: DeserializeOwned>(
|
|
&self,
|
|
value: JsValue,
|
|
) -> Result<T, StorageError> {
|
|
if let Some(cipher) = &self.store_cipher {
|
|
let encrypted: EncryptedData = serde_wasm_bindgen::from_value(value)?;
|
|
Ok(cipher.decrypt_json_value(encrypted)?)
|
|
} else {
|
|
Ok(serde_wasm_bindgen::from_value(value)?)
|
|
}
|
|
}
|
|
|
|
pub async fn read_value<T, K>(&self, store: &str, key: K) -> Result<Option<T>, StorageError>
|
|
where
|
|
T: DeserializeOwned,
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.inner
|
|
.read_value_raw(store, key)
|
|
.await?
|
|
.map(|raw| self.deserialize_value(raw))
|
|
.transpose()
|
|
}
|
|
|
|
pub async fn store_value<T, K>(
|
|
&self,
|
|
store: &str,
|
|
key: K,
|
|
value: &T,
|
|
) -> Result<(), StorageError>
|
|
where
|
|
T: Serialize,
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.inner
|
|
.store_value_raw(store, key, &self.serialize_value(&value)?)
|
|
.await
|
|
}
|
|
|
|
pub async fn remove_value<K>(&self, store: &str, key: K) -> Result<(), StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.inner.remove_value_raw(store, key).await
|
|
}
|
|
|
|
pub async fn has_value<K>(&self, store: &str, key: K) -> Result<bool, StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
match self.key_count(store, key).await? {
|
|
n if n == 0 => Ok(false),
|
|
n if n == 1 => Ok(true),
|
|
n => Err(StorageError::DuplicateKey { count: n }),
|
|
}
|
|
}
|
|
|
|
pub async fn key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.inner.get_key_count(store, key).await
|
|
}
|
|
|
|
pub async fn get_all_keys(&self, store: &str) -> Result<js_sys::Array, StorageError> {
|
|
self.inner.get_all_keys(store).await
|
|
}
|
|
}
|
|
|
|
struct IdbWrapper(IdbDatabase);
|
|
|
|
impl IdbWrapper {
|
|
async fn read_value_raw<K>(&self, store: &str, key: K) -> Result<Option<JsValue>, StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.0
|
|
.transaction_on_one_with_mode(store, IdbTransactionMode::Readonly)?
|
|
.object_store(store)?
|
|
.get(&key)?
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn store_value_raw<K>(
|
|
&self,
|
|
store: &str,
|
|
key: K,
|
|
value: &JsValue,
|
|
) -> Result<(), StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.0
|
|
.transaction_on_one_with_mode(store, IdbTransactionMode::Readwrite)?
|
|
.object_store(store)?
|
|
.put_key_val_owned(key, value)?
|
|
.into_future()
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn remove_value_raw<K>(&self, store: &str, key: K) -> Result<(), StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.0
|
|
.transaction_on_one_with_mode(store, IdbTransactionMode::Readwrite)?
|
|
.object_store(store)?
|
|
.delete_owned(key)?
|
|
.into_future()
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn get_key_count<K>(&self, store: &str, key: K) -> Result<u32, StorageError>
|
|
where
|
|
K: wasm_bindgen::JsCast,
|
|
{
|
|
self.0
|
|
.transaction_on_one_with_mode(store, IdbTransactionMode::Readwrite)?
|
|
.object_store(store)?
|
|
.count_with_key_owned(key)?
|
|
.into_future()
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn get_all_keys(&self, store: &str) -> Result<js_sys::Array, StorageError> {
|
|
self.0
|
|
.transaction_on_one_with_mode(store, IdbTransactionMode::Readonly)?
|
|
.object_store(store)?
|
|
.get_all_keys()?
|
|
.into_future()
|
|
.await
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn read_exported_cipher_store(
|
|
&self,
|
|
) -> Result<Option<StoredExportedStoreCipher>, StorageError> {
|
|
self.read_value_raw(CIPHER_INFO_STORE, JsValue::from_str(CIPHER_STORE_EXPORT))
|
|
.await?
|
|
.map(serde_wasm_bindgen::from_value)
|
|
.transpose()
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn store_exported_cipher_store(
|
|
&self,
|
|
exported_store_cipher: StoredExportedStoreCipher,
|
|
) -> Result<(), StorageError> {
|
|
self.store_value_raw(
|
|
CIPHER_INFO_STORE,
|
|
JsValue::from_str(CIPHER_STORE_EXPORT),
|
|
&serde_wasm_bindgen::to_value(&exported_store_cipher)?,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn setup_new_store_cipher(
|
|
&self,
|
|
passphrase: Option<&[u8]>,
|
|
) -> Result<Option<StoreCipher>, StorageError> {
|
|
if let Some(passphrase) = passphrase {
|
|
console_log!("attempting to derive new encryption key");
|
|
let kdf_info = new_default_kdf()?;
|
|
let store_cipher = StoreCipher::<Aes256Gcm>::new(passphrase, kdf_info)?;
|
|
let exported = store_cipher.export_aes256gcm()?;
|
|
self.store_exported_cipher_store(Some(exported).into())
|
|
.await?;
|
|
|
|
Ok(Some(store_cipher))
|
|
} else {
|
|
console_log!("this new storage will not use any encryption");
|
|
self.store_exported_cipher_store(StoredExportedStoreCipher::NoEncryption)
|
|
.await?;
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
async fn restore_existing_cipher(
|
|
&self,
|
|
existing: StoredExportedStoreCipher,
|
|
passphrase: Option<&[u8]>,
|
|
) -> Result<Option<StoreCipher>, StorageError> {
|
|
if let Some(passphrase) = passphrase {
|
|
console_log!("attempting to use previously derived encryption key");
|
|
if let StoredExportedStoreCipher::Cipher(exported_cipher) = existing {
|
|
Ok(Some(StoreCipher::import_aes256gcm(
|
|
passphrase,
|
|
exported_cipher,
|
|
)?))
|
|
} else {
|
|
Err(StorageError::UnexpectedPassphraseProvided)
|
|
}
|
|
} else {
|
|
console_log!("attempting to restore old unencrypted data");
|
|
if existing.uses_encryption() {
|
|
Err(StorageError::NoPassphraseProvided)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn setup_store_cipher(
|
|
&self,
|
|
passphrase: Option<&[u8]>,
|
|
) -> Result<Option<StoreCipher>, StorageError> {
|
|
// we have few options of proceeding from here:
|
|
// no passphrase + no existing info => it's a fresh client that won't use encryption, so just store that info
|
|
// no passphrase + existing info => check if the existing info has kdf details, if so, reject
|
|
// passphrase + no existing info => it's a fresh client that will use encryption, so derive what's required and store it
|
|
// passphrase + existing info => check if the existing info has kdf details, if so, try to re-derive the key
|
|
|
|
if let Some(existing_cipher_info) = self.read_exported_cipher_store().await? {
|
|
self.restore_existing_cipher(existing_cipher_info, passphrase)
|
|
.await
|
|
} else {
|
|
self.setup_new_store_cipher(passphrase).await
|
|
}
|
|
}
|
|
}
|