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

Commit

Permalink
WIP: Change password
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman authored and AnthonyRonning committed Jul 7, 2023
1 parent 0f506ad commit ce80f9b
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 26 deletions.
24 changes: 12 additions & 12 deletions mutiny-core/src/encrypt.rs
Original file line number Diff line number Diff line change
@@ -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<String, MutinyError> {
let mut salt = [0u8; 16];
getrandom::getrandom(&mut salt).unwrap();
let derive_key = derive_key(password, &salt);
Expand All @@ -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<String, MutinyError> {
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];
Expand All @@ -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 {
Expand All @@ -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);
}
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
30 changes: 29 additions & 1 deletion mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -308,6 +308,34 @@ 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()),
)?;

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.
Expand Down
8 changes: 6 additions & 2 deletions mutiny-core/src/nodemanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,11 +545,15 @@ impl<S: MutinyStorage> NodeManager<S> {
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);
}
},
};

Expand Down
60 changes: 51 additions & 9 deletions mutiny-core/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -143,12 +143,45 @@ pub trait MutinyStorage: Clone + Sized + 'static {
}

/// Get the mnemonic from the storage
fn get_mnemonic(&self) -> Result<Mnemonic, MutinyError> {
let mnemonic: Option<Mnemonic> = self.get_data(MNEMONIC_KEY)?;
match mnemonic {
Some(m) => Ok(m),
None => Err(MutinyError::NotFound),
fn get_mnemonic(&self) -> Result<Option<Mnemonic>, MutinyError> {
self.get_data(MNEMONIC_KEY)
}

fn change_password(&mut self, new: Option<String>) -> Result<(), MutinyError>;

fn change_password_and_rewrite_storage(
&mut self,
old: Option<String>,
new: Option<String>,
) -> 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<String> = 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<String, Value> = 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
Expand Down Expand Up @@ -294,6 +327,11 @@ impl MutinyStorage for MemoryStorage {
.collect())
}

fn change_password(&mut self, new: Option<String>) -> Result<(), MutinyError> {
self.password = new;
Ok(())
}

async fn import(_json: Value) -> Result<(), MutinyError> {
Ok(())
}
Expand Down Expand Up @@ -341,6 +379,10 @@ impl MutinyStorage for () {
Ok(Vec::new())
}

fn change_password(&mut self, _new: Option<String>) -> Result<(), MutinyError> {
Ok(())
}

async fn import(_json: Value) -> Result<(), MutinyError> {
Ok(())
}
Expand Down Expand Up @@ -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]
Expand All @@ -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);
}
}
4 changes: 4 additions & 0 deletions mutiny-wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -168,6 +171,7 @@ impl From<MutinyError> 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
Expand Down
9 changes: 7 additions & 2 deletions mutiny-wasm/src/indexed_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ impl MutinyStorage for IndexedDbStorage {
.collect())
}

fn change_password(&mut self, new: Option<String>) -> 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?;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
12 changes: 12 additions & 0 deletions mutiny-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,18 @@ impl MutinyWallet {
Ok(())
}

#[wasm_bindgen]
pub async fn change_password(
&mut self,
old_password: Option<String>,
new_password: Option<String>,
) -> 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<u64, MutinyJsError> {
Expand Down

0 comments on commit ce80f9b

Please sign in to comment.