Skip to content
This repository has been archived by the owner on Feb 3, 2025. It is now read-only.

Commit

Permalink
Merge pull request #638 from MutinyWallet/change-password
Browse files Browse the repository at this point in the history
Change password
  • Loading branch information
AnthonyRonning authored Jul 8, 2023
2 parents 0f506ad + 09bf7ad commit b3938c1
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 112 deletions.
1 change: 1 addition & 0 deletions mutiny-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ nostr-sdk = { version = "0.22.0-bitcoin-v0.29", default-features = false }
cbc = { version = "0.1", features = ["alloc"] }
aes = { version = "0.8" }
jwt-compact = { version = "0.8.0-beta.1", features = ["es256k"] }
argon2 = { version = "0.5.0", features = ["password-hash", "alloc"] }

base64 = "0.13.0"
pbkdf2 = "0.11"
Expand Down
138 changes: 85 additions & 53 deletions mutiny-core/src/encrypt.rs
Original file line number Diff line number Diff line change
@@ -1,73 +1,105 @@
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};
use crate::error::MutinyError;
use aes_gcm::Aes256Gcm;
use aes_gcm::{
aead::{generic_array::GenericArray, Aead},
KeyInit,
};
use argon2::Argon2;
use base64;
use getrandom::getrandom;
use std::sync::Arc;

pub fn encrypt(content: &str, password: &str) -> String {
#[derive(Clone)]
pub struct Cipher {
key: Arc<Aes256Gcm>,
salt: [u8; 16],
}

pub fn encryption_key_from_pass(password: &str) -> Result<Cipher, MutinyError> {
let mut salt = [0u8; 16];
getrandom::getrandom(&mut salt).unwrap();
let derive_key = derive_key(password, &salt);
let key = derive_key.as_bytes();

let mut iv = [0u8; 12];
getrandom::getrandom(&mut iv).unwrap();

let cipher = Aes256Gcm::new_from_slice(key).unwrap();
let nonce = Nonce::from_slice(&iv);
let mut bytes = cipher.encrypt(nonce, content.as_bytes()).unwrap();

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())
getrandom(&mut salt).unwrap();

let key = get_encryption_key(password, &salt)?;

// convert key to proper format for aes_gcm
let key = GenericArray::clone_from_slice(&key);
Ok(Cipher {
key: Arc::new(Aes256Gcm::new(&key)),
salt,
})
}

pub fn decrypt(encrypted: &str, password: &str) -> String {
let buffer = base64::decode(encrypted)
.unwrap_or_else(|_| panic!("Error reading ciphertext: {encrypted}"));
let buffer_slice = buffer.as_slice();
let salt = &buffer_slice[0..16];
let iv = &buffer_slice[16..28];
let data = &buffer_slice[28..];

let derive_key = derive_key(password, salt);
let key = derive_key.as_bytes();

let cipher = Aes256Gcm::new_from_slice(key).unwrap();
let nonce = Nonce::from_slice(iv);
let decrypted = cipher.decrypt(nonce, data).unwrap();
String::from_utf8(decrypted).unwrap()
pub fn encrypt(content: &str, c: Cipher) -> Result<String, MutinyError> {
// convert key and nonce to proper format for aes_gcm
let mut nonce = [0u8; 12];
getrandom(&mut nonce).unwrap();

// convert nonce to proper format for aes_gcm
let nonce = GenericArray::from_slice(&nonce);

let encrypted_data = c.key.encrypt(nonce, content.as_bytes().to_vec().as_ref())?;

let mut result: Vec<u8> = Vec::new();
result.extend(&c.salt);
result.extend(nonce);
result.extend(encrypted_data);

Ok(base64::encode(&result))
}

fn derive_key(password: &str, salt: &[u8]) -> Output {
let params = Params {
rounds: 2048,
output_length: 32,
};

let salt_string = SaltString::b64_encode(salt).unwrap();
let salt = Salt::from(&salt_string);
let password = password.as_bytes();
let key = Pbkdf2
.hash_password_customized(password, None, None, params, salt)
.unwrap();
key.hash.unwrap()
pub fn decrypt(encrypted: &str, password: &str) -> Result<String, MutinyError> {
let encrypted = base64::decode(encrypted).map_err(|_| MutinyError::IncorrectPassword)?;
if encrypted.len() < 12 + 16 {
return Err(MutinyError::IncorrectPassword);
}

let (rest, encrypted_bytes) = encrypted.split_at(16 + 12);
let (salt, nonce_bytes) = rest.split_at(16);

let key = get_encryption_key(password, salt)?;

// convert key and nonce to proper format for aes_gcm
let key = GenericArray::clone_from_slice(&key);
let nonce = GenericArray::from_slice(nonce_bytes);

let cipher = Aes256Gcm::new(&key);

let decrypted_data = cipher.decrypt(nonce, encrypted_bytes)?;

let decrypted_string =
String::from_utf8(decrypted_data).map_err(|_| MutinyError::IncorrectPassword)?;

Ok(decrypted_string)
}

pub fn get_encryption_key(password: &str, salt: &[u8]) -> Result<[u8; 32], MutinyError> {
let mut key = [0u8; 32];
argon2()
.hash_password_into(password.as_bytes(), salt, &mut key)
.map_err(|_| MutinyError::IncorrectPassword)?;
Ok(key)
}

fn argon2() -> Argon2<'static> {
let mut binding = argon2::ParamsBuilder::new();
let params = binding.m_cost(7 * 1024).t_cost(1).p_cost(1);
Argon2::from(params.build().expect("valid params"))
}

#[cfg(test)]
mod tests {
use crate::encrypt::{decrypt, encrypt};
use crate::encrypt::{decrypt, encrypt, encryption_key_from_pass};

#[test]
fn test_encryption() {
let password = "password";
let content = "hello world";
let encrypted = encrypt(content, password);
let cipher = encryption_key_from_pass(password).unwrap();

let encrypted = encrypt(content, cipher).unwrap();
println!("{encrypted}");

let decrypted = decrypt(&encrypted, password);
let decrypted = decrypt(&encrypted, password).unwrap();
println!("{decrypted}");
assert_eq!(content, decrypted);
}
Expand Down
15 changes: 15 additions & 0 deletions mutiny-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -152,6 +155,18 @@ impl MutinyError {
}
}

impl From<aes_gcm::Error> for MutinyError {
fn from(_: aes_gcm::Error) -> Self {
Self::IncorrectPassword
}
}

impl From<aes_gcm::aes::cipher::InvalidLength> for MutinyError {
fn from(_: aes_gcm::aes::cipher::InvalidLength) -> Self {
Self::IncorrectPassword
}
}

impl From<bdk::Error> for MutinyError {
fn from(e: bdk::Error) -> Self {
match e {
Expand Down
2 changes: 1 addition & 1 deletion mutiny-core/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ mod test {

#[cfg(not(target_arch = "wasm32"))]
async fn create_fee_estimator() -> MutinyFeeEstimator<MemoryStorage> {
let storage = MemoryStorage::new(None);
let storage = MemoryStorage::new(None, None);
let esplora = Arc::new(
Builder::new("https://mutinynet.com/api")
.build_async()
Expand Down
8 changes: 6 additions & 2 deletions mutiny-core/src/keymanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ mod tests {

wasm_bindgen_test_configure!(run_in_browser);

use crate::{keymanager::pubkey_from_keys_manager, test_utils::*};
use crate::{
encrypt::encryption_key_from_pass, keymanager::pubkey_from_keys_manager, test_utils::*,
};

use super::create_keys_manager;
use crate::fees::MutinyFeeEstimator;
Expand All @@ -265,7 +267,9 @@ mod tests {
.build_async()
.unwrap(),
);
let db = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let db = MemoryStorage::new(Some(pass), Some(cipher));
let logger = Arc::new(MutinyLogger::default());
let fees = Arc::new(MutinyFeeEstimator::new(
db.clone(),
Expand Down
63 changes: 54 additions & 9 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY
pub use crate::keymanager::generate_seed;
pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY};

use crate::nostr::NostrManager;
use crate::storage::MutinyStorage;
use crate::{error::MutinyError, nostr::ReservedProfile};
use crate::{nodemanager::NodeManager, nostr::ProfileType};
use crate::{nostr::NostrManager, utils::sleep};
use ::nostr::Kind;
use bip39::Mnemonic;
use bitcoin::secp256k1::PublicKey;
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;
Expand Down Expand Up @@ -308,6 +308,36 @@ impl<S: MutinyStorage> MutinyWallet<S> {
self.node_manager.stop().await
}

pub async fn change_password(
&mut self,
old: Option<String>,
new: Option<String>,
) -> 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()),
)?;

// There's not a good way to check that all the indexeddb
// data is saved in the background. This should get better
// once we have async saving, but for now just make sure
// the user has saved their seed already.
sleep(5_000).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.
Expand Down Expand Up @@ -341,7 +371,10 @@ impl<S: MutinyStorage> MutinyWallet<S> {

#[cfg(test)]
mod tests {
use crate::{nodemanager::NodeManager, MutinyWallet, MutinyWalletConfig};
use crate::{
encrypt::encryption_key_from_pass, nodemanager::NodeManager, MutinyWallet,
MutinyWalletConfig,
};
use bitcoin::Network;

use crate::test_utils::*;
Expand All @@ -356,7 +389,9 @@ mod tests {
let test_name = "create_mutiny_wallet";
log!("{}", test_name);

let storage = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage = MemoryStorage::new(Some(pass), Some(cipher));
assert!(!NodeManager::has_node_manager(storage.clone()));
let config = MutinyWalletConfig::new(
None,
Expand All @@ -380,7 +415,9 @@ mod tests {
let test_name = "restart_mutiny_wallet";
log!("{}", test_name);

let storage = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage = MemoryStorage::new(Some(pass), Some(cipher));
assert!(!NodeManager::has_node_manager(storage.clone()));
let config = MutinyWalletConfig::new(
None,
Expand Down Expand Up @@ -410,7 +447,9 @@ mod tests {
let test_name = "restart_mutiny_wallet_with_nodes";
log!("{}", test_name);

let storage = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage = MemoryStorage::new(Some(pass), Some(cipher));

assert!(!NodeManager::has_node_manager(storage.clone()));
let config = MutinyWalletConfig::new(
Expand Down Expand Up @@ -443,7 +482,9 @@ mod tests {
let test_name = "restore_mutiny_mnemonic";
log!("{}", test_name);

let storage = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage = MemoryStorage::new(Some(pass), Some(cipher));
assert!(!NodeManager::has_node_manager(storage.clone()));
let config = MutinyWalletConfig::new(
None,
Expand All @@ -463,7 +504,9 @@ mod tests {
assert_ne!(seed.to_string(), "");

// create a second mw and make sure it has a different seed
let storage2 = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage2 = MemoryStorage::new(Some(pass), Some(cipher));
assert!(!NodeManager::has_node_manager(storage2.clone()));
let config2 = MutinyWalletConfig::new(
None,
Expand All @@ -486,7 +529,9 @@ mod tests {
mw2.stop().await.expect("should stop");
drop(mw2);

let storage3 = MemoryStorage::new(Some(uuid::Uuid::new_v4().to_string()));
let pass = uuid::Uuid::new_v4().to_string();
let cipher = encryption_key_from_pass(&pass).unwrap();
let storage3 = MemoryStorage::new(Some(pass), Some(cipher));
MutinyWallet::restore_mnemonic(storage3.clone(), seed.clone())
.await
.expect("mutiny wallet should restore");
Expand Down
Loading

0 comments on commit b3938c1

Please sign in to comment.