diff --git a/mutiny-core/src/encrypt.rs b/mutiny-core/src/encrypt.rs index 998534721..f8806e51b 100644 --- a/mutiny-core/src/encrypt.rs +++ b/mutiny-core/src/encrypt.rs @@ -1,10 +1,11 @@ +use crate::error::MutinyError; use aes_gcm::aead::Aead; use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; use pbkdf2::password_hash::Output; use pbkdf2::password_hash::{PasswordHasher, Salt, SaltString}; use pbkdf2::{Params, Pbkdf2}; -pub fn encrypt(content: &str, password: &str) -> String { +pub fn encrypt(content: &str, password: &str) -> Result { let mut salt = [0u8; 16]; getrandom::getrandom(&mut salt).unwrap(); let derive_key = derive_key(password, &salt); @@ -13,20 +14,19 @@ pub fn encrypt(content: &str, password: &str) -> String { let mut iv = [0u8; 12]; getrandom::getrandom(&mut iv).unwrap(); - let cipher = Aes256Gcm::new_from_slice(key).unwrap(); + let cipher = Aes256Gcm::new_from_slice(key)?; let nonce = Nonce::from_slice(&iv); - let mut bytes = cipher.encrypt(nonce, content.as_bytes()).unwrap(); + let mut bytes = cipher.encrypt(nonce, content.as_bytes())?; let mut combined = vec![]; combined.append(&mut salt.to_vec()); combined.append(&mut iv.to_vec()); combined.append(&mut bytes); - base64::encode(combined.as_slice()) + Ok(base64::encode(combined.as_slice())) } -pub fn decrypt(encrypted: &str, password: &str) -> String { - let buffer = base64::decode(encrypted) - .unwrap_or_else(|_| panic!("Error reading ciphertext: {encrypted}")); +pub fn decrypt(encrypted: &str, password: &str) -> Result { + let buffer = base64::decode(encrypted).map_err(|_| MutinyError::IncorrectPassword)?; let buffer_slice = buffer.as_slice(); let salt = &buffer_slice[0..16]; let iv = &buffer_slice[16..28]; @@ -35,10 +35,10 @@ pub fn decrypt(encrypted: &str, password: &str) -> String { let derive_key = derive_key(password, salt); let key = derive_key.as_bytes(); - let cipher = Aes256Gcm::new_from_slice(key).unwrap(); + let cipher = Aes256Gcm::new_from_slice(key)?; let nonce = Nonce::from_slice(iv); - let decrypted = cipher.decrypt(nonce, data).unwrap(); - String::from_utf8(decrypted).unwrap() + let decrypted = cipher.decrypt(nonce, data)?; + Ok(String::from_utf8(decrypted).unwrap()) } fn derive_key(password: &str, salt: &[u8]) -> Output { @@ -64,10 +64,10 @@ mod tests { fn test_encryption() { let password = "password"; let content = "hello world"; - let encrypted = encrypt(content, password); + let encrypted = encrypt(content, password).unwrap(); println!("{encrypted}"); - let decrypted = decrypt(&encrypted, password); + let decrypted = decrypt(&encrypted, password).unwrap(); println!("{decrypted}"); assert_eq!(content, decrypted); } diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index aa8f80747..1c8716c9e 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -123,6 +123,9 @@ pub enum MutinyError { /// Error getting the bitcoin price #[error("Failed to get the bitcoin price.")] BitcoinPriceError, + /// Incorrect password entered. + #[error("Incorrect password entered.")] + IncorrectPassword, #[error(transparent)] Other(#[from] anyhow::Error), } @@ -152,6 +155,18 @@ impl MutinyError { } } +impl From for MutinyError { + fn from(_: aes_gcm::Error) -> Self { + Self::IncorrectPassword + } +} + +impl From for MutinyError { + fn from(_: aes_gcm::aes::cipher::InvalidLength) -> Self { + Self::IncorrectPassword + } +} + impl From for MutinyError { fn from(e: bdk::Error) -> Self { match e { diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 9471d1244..a13add1e5 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -54,7 +54,7 @@ use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; use futures::{pin_mut, select, FutureExt}; use lightning::util::logger::Logger; -use lightning::{log_error, log_warn}; +use lightning::{log_error, log_info, log_warn}; use lightning_invoice::Invoice; use nostr_sdk::{Client, RelayPoolNotification}; use std::sync::atomic::Ordering; @@ -308,6 +308,34 @@ impl MutinyWallet { self.node_manager.stop().await } + pub async fn change_password( + &mut self, + old: Option, + new: Option, + ) -> Result<(), MutinyError> { + // check if old password is correct + if old != self.storage.password().map(|s| s.to_owned()) { + return Err(MutinyError::IncorrectPassword); + } + + log_info!(self.node_manager.logger, "Changing password"); + + self.stop().await?; + + self.storage.start().await?; + + self.storage.change_password_and_rewrite_storage( + old.filter(|s| !s.is_empty()), + new.filter(|s| !s.is_empty()), + )?; + + self.storage.stop(); + + self.start().await?; + + Ok(()) + } + /// Resets BDK's keychain tracker. This will require a re-sync of the blockchain. /// /// This can be useful if you get stuck in a bad state. diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 04955e813..8f8184870 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -545,11 +545,15 @@ impl NodeManager { let mnemonic = match c.mnemonic { Some(seed) => storage.insert_mnemonic(seed)?, None => match storage.get_mnemonic() { - Ok(mnemonic) => mnemonic, - Err(_) => { + Ok(Some(mnemonic)) => mnemonic, + Ok(None) => { let seed = keymanager::generate_seed(12)?; storage.insert_mnemonic(seed)? } + Err(_) => { + // if we get an error, then we have the wrong password + return Err(MutinyError::IncorrectPassword); + } }, }; diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index dfe5c85d6..551cff379 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -32,7 +32,7 @@ pub fn encrypt_value( let res = match password { Some(pw) if needs_encryption(key.as_ref()) => { let str = serde_json::to_string(&value)?; - let ciphertext = encrypt(&str, pw); + let ciphertext = encrypt(&str, pw)?; Value::String(ciphertext) } _ => value, @@ -50,7 +50,7 @@ pub fn decrypt_value( let json: Value = match password { Some(pw) if needs_encryption(key.as_ref()) => { let str: String = serde_json::from_value(value)?; - let ciphertext = decrypt(&str, pw); + let ciphertext = decrypt(&str, pw)?; serde_json::from_str(&ciphertext)? } _ => value, @@ -143,12 +143,45 @@ pub trait MutinyStorage: Clone + Sized + 'static { } /// Get the mnemonic from the storage - fn get_mnemonic(&self) -> Result { - let mnemonic: Option = self.get_data(MNEMONIC_KEY)?; - match mnemonic { - Some(m) => Ok(m), - None => Err(MutinyError::NotFound), + fn get_mnemonic(&self) -> Result, MutinyError> { + self.get_data(MNEMONIC_KEY) + } + + fn change_password(&mut self, new: Option) -> Result<(), MutinyError>; + + fn change_password_and_rewrite_storage( + &mut self, + old: Option, + new: Option, + ) -> Result<(), MutinyError> { + // check if old password is correct + if old != self.password().map(|s| s.to_owned()) { + return Err(MutinyError::IncorrectPassword); + } + + // get all of our keys + let mut keys: Vec = self.scan_keys("", None)?; + // get the ones that need encryption + keys.retain(|k| needs_encryption(k)); + + // decrypt all of the values + let mut values: HashMap = HashMap::new(); + for key in keys.iter() { + let value = self.get_data(key)?; + if let Some(v) = value { + values.insert(key.to_owned(), v); + } + } + + // change the password + self.change_password(new)?; + + // encrypt all of the values + for (key, value) in values.iter() { + self.set_data(key, value)?; } + + Ok(()) } /// Override the storage with the new JSON object @@ -294,6 +327,11 @@ impl MutinyStorage for MemoryStorage { .collect()) } + fn change_password(&mut self, new: Option) -> Result<(), MutinyError> { + self.password = new; + Ok(()) + } + async fn import(_json: Value) -> Result<(), MutinyError> { Ok(()) } @@ -341,6 +379,10 @@ impl MutinyStorage for () { Ok(Vec::new()) } + fn change_password(&mut self, _new: Option) -> Result<(), MutinyError> { + Ok(()) + } + async fn import(_json: Value) -> Result<(), MutinyError> { Ok(()) } @@ -404,7 +446,7 @@ mod tests { let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); - assert_eq!(mnemonic, stored_mnemonic); + assert_eq!(Some(mnemonic), stored_mnemonic); } #[test] @@ -419,6 +461,6 @@ mod tests { let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); - assert_eq!(mnemonic, stored_mnemonic); + assert_eq!(Some(mnemonic), stored_mnemonic); } } diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index a656741b8..e677ef066 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -125,6 +125,9 @@ pub enum MutinyJsError { /// Invalid Arguments were given #[error("Invalid Arguments were given")] InvalidArgumentsError, + /// Incorrect password entered. + #[error("Incorrect password entered.")] + IncorrectPassword, /// Unknown error. #[error("Unknown Error")] UnknownError, @@ -168,6 +171,7 @@ impl From for MutinyJsError { MutinyError::IncorrectLnUrlFunction => MutinyJsError::IncorrectLnUrlFunction, MutinyError::BadAmountError => MutinyJsError::BadAmountError, MutinyError::BitcoinPriceError => MutinyJsError::BitcoinPriceError, + MutinyError::IncorrectPassword => MutinyJsError::IncorrectPassword, MutinyError::Other(_) => MutinyJsError::UnknownError, MutinyError::SubscriptionClientNotConfigured => { MutinyJsError::SubscriptionClientNotConfigured diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index 05e1b3096..483247269 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -394,6 +394,11 @@ impl MutinyStorage for IndexedDbStorage { .collect()) } + fn change_password(&mut self, new: Option) -> Result<(), MutinyError> { + self.password = new; + Ok(()) + } + async fn import(json: Value) -> Result<(), MutinyError> { Self::clear().await?; let indexed_db = Self::build_indexed_db_database().await?; @@ -594,7 +599,7 @@ mod tests { let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); - assert_eq!(mnemonic, stored_mnemonic); + assert_eq!(Some(mnemonic), stored_mnemonic); // clear the storage to clean up IndexedDbStorage::clear().await.unwrap(); @@ -615,7 +620,7 @@ mod tests { let mnemonic = storage.insert_mnemonic(seed).unwrap(); let stored_mnemonic = storage.get_mnemonic().unwrap(); - assert_eq!(mnemonic, stored_mnemonic); + assert_eq!(Some(mnemonic), stored_mnemonic); // clear the storage to clean up IndexedDbStorage::clear().await.unwrap(); diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index 79592e359..aca8fcf4a 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1136,6 +1136,18 @@ impl MutinyWallet { Ok(()) } + #[wasm_bindgen] + pub async fn change_password( + &mut self, + old_password: Option, + new_password: Option, + ) -> Result<(), MutinyJsError> { + self.inner + .change_password(old_password, new_password) + .await?; + Ok(()) + } + /// Converts a bitcoin amount in BTC to satoshis. #[wasm_bindgen] pub fn convert_btc_to_sats(btc: f64) -> Result {