Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce secrets passing #371

Draft
wants to merge 3 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ server = ["tokio/full", "tower-http/cors"]
anyhow = "1.0.71"
amplify = "4.0.0"
argon2 = "0.5.0"
automerge = "0.5.1"
autosurgeon = "0.8"
base64 = { package = "base64-compat", version = "1.0.0" }
base85 = "2.0.0"
bech32 = "0.9.1"
bip39 = { version = "2.0.0", features = ["rand"] }
bitcoin_30 = { package = "bitcoin", version = "0.30", features = ["base64"] }
bitcoin = { version = "0.29.2", features = ["base64"] }
bitcoin_hashes = "0.12.0"
bitcoin_scripts = "0.10.0-alpha.2"
bitcoin_blockchain = "0.10.0-alpha.2"
blake3 = "1.4.1"
bp-core = { version = "0.10.6", features = ["stl"] }
bp-seals = "0.10.6"
commit_verify = { version = "0.10.5", features = ["stl"] }
Expand Down Expand Up @@ -85,10 +89,6 @@ strict_types = "~1.6.0"
thiserror = "1.0"
tokio = { version = "1.28.2", features = ["macros", "sync"] }
zeroize = "1.6.0"
blake3 = "1.4.1"
base85 = "2.0.0"
automerge = "0.5.1"
autosurgeon = "0.8"

[target.'cfg(target_arch = "wasm32")'.dependencies]
bdk = { version = "0.28.0", features = [
Expand Down
106 changes: 56 additions & 50 deletions src/bitcoin.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use std::str::FromStr;
use std::{pin::Pin, str::FromStr};

use ::bitcoin::util::address::Address;
use ::psbt::Psbt;
use argon2::Argon2;
use bdk::{wallet::AddressIndex, FeeRate, LocalUtxo, SignOptions, TransactionDetails};
use bitcoin::psbt::PartiallySignedTransaction;
use once_cell::sync::OnceCell;
use rand::{rngs::StdRng, Rng, SeedableRng};
use serde_encrypt::{
serialize::impls::BincodeSerializer, shared_key::SharedKey, traits::SerdeEncryptSharedKey,
AsSharedKey, EncryptedMessage,
};
use thiserror::Error;
use zeroize::Zeroize;
use zeroize::{Zeroize, Zeroizing};

mod assets;
mod keys;
Expand Down Expand Up @@ -77,6 +78,9 @@ pub enum BitcoinError {
/// Drain wallet was unable to find tx details
#[error("No wallet transaction details were found when draining wallet")]
DrainWalletNoTxDetails,
/// No hash available. Has wallet been unlocked?
#[error("No hash available. Has wallet been unlocked?")]
NoHashAvailable,
/// BitMask Core Bitcoin Keys error
#[error(transparent)]
BitcoinKeysError(#[from] BitcoinKeysError),
Expand Down Expand Up @@ -112,10 +116,13 @@ pub enum BitcoinError {
/// Bitcoin Wallet Operations
const BITMASK_ARGON2_SALT: &[u8] = b"DIBA BitMask Password Hash"; // Never change this

pub fn hash_password(password: &SecretString) -> SecretString {
static PASSWORD_HASH: OnceCell<Pin<Zeroizing<[u8; 32]>>> = OnceCell::new();

pub fn hash_password(password: &SecretString) {
use argon2::{Algorithm, Params, Version};

let mut output_key_material = [0u8; 32];
let mut hash = Zeroizing::new([0u8; 32]);
Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default())
.hash_password_into(
password.0.as_bytes(),
Expand All @@ -124,18 +131,15 @@ pub fn hash_password(password: &SecretString) -> SecretString {
)
.expect("Password hashed with Argon2id");

let hash = SecretString(hex::encode(output_key_material));
hash.copy_from_slice(&output_key_material);
output_key_material.zeroize();
hash

let _ = PASSWORD_HASH.set(Pin::new(hash));
}

pub fn decrypt_wallet(
hash: &SecretString,
encrypted_descriptors: &SecretString,
) -> Result<DecryptedWalletData, BitcoinError> {
let mut shared_key: [u8; 32] = hex::decode(&hash.0)?
.try_into()
.expect("hash is of fixed size");
let encrypted_descriptors: Vec<u8> = hex::decode(&encrypted_descriptors.0)?;
let (version_prefix, encrypted_descriptors) = encrypted_descriptors.split_at(5);

Expand All @@ -153,45 +157,65 @@ pub fn decrypt_wallet(

let encrypted_message = EncryptedMessage::deserialize(encrypted_descriptors.to_owned())?;

let decrypted_wallet_data =
DecryptedWalletData::decrypt_owned(&encrypted_message, &SharedKey::from_array(shared_key))?;
let shared_key: Pin<&[u8; 32]> = PASSWORD_HASH
.get()
.ok_or(BitcoinError::NoHashAvailable)?
.as_ref();

shared_key.zeroize();
let decrypted_wallet_data = DecryptedWalletData::decrypt_owned(
&encrypted_message,
&SharedKey::from_array(*shared_key),
)?;

// shared_key.zeroize();

Ok(decrypted_wallet_data)
}

pub async fn encrypt_wallet(
mnemonic_phrase: &SecretString,
seed_password: &SecretString,
) -> Result<SecretString, BitcoinError> {
let shared_key: Pin<&[u8; 32]> = PASSWORD_HASH
.get()
.ok_or(BitcoinError::NoHashAvailable)?
.as_ref();

let wallet_data = save_mnemonic(mnemonic_phrase, seed_password).await?;
let encrypted_message = wallet_data.encrypt(&SharedKey::from_array(*shared_key))?;
let encrypted_descriptors = versioned_descriptor(encrypted_message);
Ok(encrypted_descriptors)
}

pub async fn upgrade_wallet(
hash: &SecretString,
encrypted_descriptors: &SecretString,
seed_password: &SecretString,
) -> Result<SecretString, BitcoinError> {
// read hash digest and consume hasher
let shared_key: [u8; 32] = hex::decode(&hash.0)?
.try_into()
.expect("hash is of fixed size");
let shared_key: Pin<&[u8; 32]> = PASSWORD_HASH
.get()
.ok_or(BitcoinError::NoHashAvailable)?
.as_ref();
let encrypted_descriptors: Vec<u8> = hex::decode(&encrypted_descriptors.0)?;
let encrypted_message = EncryptedMessage::deserialize(encrypted_descriptors)?;

match DecryptedWalletData::decrypt_owned(&encrypted_message, &SharedKey::from_array(shared_key))
{
match DecryptedWalletData::decrypt_owned(
&encrypted_message,
&SharedKey::from_array(*shared_key),
) {
Ok(_data) => Err(BitcoinError::UpgradeUnnecessary),
Err(_err) => {
// If there's a deserialization error, attempt to recover just the mnemnonic.
let recovered_wallet_data = EncryptedWalletDataV04::decrypt_owned(
&encrypted_message,
&SharedKey::from_array(shared_key),
&SharedKey::from_array(*shared_key),
)?;

// println!("Recovered wallet data: {recovered_wallet_data:?}"); // Keep commented out for security
// todo!("Add later version migrations here");

let upgraded_descriptor = encrypt_wallet(
&SecretString(recovered_wallet_data.mnemonic),
hash,
seed_password,
)
.await?;
let upgraded_descriptor =
encrypt_wallet(&SecretString(recovered_wallet_data.mnemonic), seed_password)
.await?;

Ok(upgraded_descriptor)
}
Expand All @@ -210,33 +234,15 @@ pub fn versioned_descriptor(encrypted_message: EncryptedMessage) -> SecretString
encrypted
}

pub async fn new_wallet(
hash: &SecretString,
seed_password: &SecretString,
) -> Result<SecretString, BitcoinError> {
let mut shared_key: [u8; 32] = hex::decode(&hash.0)?
.try_into()
.expect("hash is of fixed size");
pub async fn new_wallet(seed_password: &SecretString) -> Result<SecretString, BitcoinError> {
let shared_key: Pin<&[u8; 32]> = PASSWORD_HASH
.get()
.ok_or(BitcoinError::NoHashAvailable)?
.as_ref();
let wallet_data = new_mnemonic(seed_password).await?;
let encrypted_message = wallet_data.encrypt(&SharedKey::from_array(shared_key))?;
let encrypted_message = wallet_data.encrypt(&SharedKey::from_array(*shared_key))?;
let encrypted_descriptors = versioned_descriptor(encrypted_message);

shared_key.zeroize();
Ok(encrypted_descriptors)
}

pub async fn encrypt_wallet(
mnemonic_phrase: &SecretString,
hash: &SecretString,
seed_password: &SecretString,
) -> Result<SecretString, BitcoinError> {
let shared_key: [u8; 32] = hex::decode(&hash.0)?
.try_into()
.expect("hash is of fixed size");

let wallet_data = save_mnemonic(mnemonic_phrase, seed_password).await?;
let encrypted_message = wallet_data.encrypt(&SharedKey::from_array(shared_key))?;
let encrypted_descriptors = versioned_descriptor(encrypted_message);
Ok(encrypted_descriptors)
}

Expand Down
27 changes: 7 additions & 20 deletions src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,10 @@ pub mod bitcoin {
use super::*;

#[wasm_bindgen]
pub fn hash_password(password: String) -> String {
pub fn hash_password(password: String) {
set_panic_hook();

crate::bitcoin::hash_password(&SecretString(password))
.0
.to_owned()
}

#[wasm_bindgen]
Expand All @@ -134,14 +132,11 @@ pub mod bitcoin {
}

#[wasm_bindgen]
pub fn decrypt_wallet(hash: String, encrypted_descriptors: String) -> Promise {
pub fn decrypt_wallet(encrypted_descriptors: String) -> Promise {
set_panic_hook();

future_to_promise(async move {
match crate::bitcoin::decrypt_wallet(
&SecretString(hash),
&SecretString(encrypted_descriptors),
) {
match crate::bitcoin::decrypt_wallet(&SecretString(encrypted_descriptors)) {
Ok(result) => Ok(JsValue::from_string(
serde_json::to_string(&result).unwrap(),
)),
Expand All @@ -151,16 +146,11 @@ pub mod bitcoin {
}

#[wasm_bindgen]
pub fn upgrade_wallet(
hash: String,
encrypted_descriptors: String,
seed_password: String,
) -> Promise {
pub fn upgrade_wallet(encrypted_descriptors: String, seed_password: String) -> Promise {
set_panic_hook();

future_to_promise(async move {
match crate::bitcoin::upgrade_wallet(
&SecretString(hash),
&SecretString(encrypted_descriptors),
&SecretString(seed_password),
)
Expand All @@ -175,13 +165,11 @@ pub mod bitcoin {
}

#[wasm_bindgen]
pub fn new_wallet(hash: String, seed_password: String) -> Promise {
pub fn new_wallet(seed_password: String) -> Promise {
set_panic_hook();

future_to_promise(async move {
match crate::bitcoin::new_wallet(&SecretString(hash), &SecretString(seed_password))
.await
{
match crate::bitcoin::new_wallet(&SecretString(seed_password)).await {
Ok(result) => Ok(JsValue::from_string(
serde_json::to_string(&result).unwrap(),
)),
Expand All @@ -191,13 +179,12 @@ pub mod bitcoin {
}

#[wasm_bindgen]
pub fn encrypt_wallet(mnemonic: String, hash: String, seed_password: String) -> Promise {
pub fn encrypt_wallet(mnemonic: String, seed_password: String) -> Promise {
set_panic_hook();

future_to_promise(async move {
match crate::bitcoin::encrypt_wallet(
&SecretString(mnemonic),
&SecretString(hash),
&SecretString(seed_password),
)
.await
Expand Down
29 changes: 10 additions & 19 deletions tests/migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use anyhow::Result;
use bitmask_core::{
bitcoin::{decrypt_wallet, upgrade_wallet},
bitcoin::{decrypt_wallet, hash_password, upgrade_wallet},
constants::switch_network,
structs::SecretString,
util::init_logging,
Expand All @@ -22,15 +22,14 @@ async fn migration_v4() -> Result<()> {
switch_network("testnet").await?;

info!("Import bitmask-core 0.4 encrypted descriptor");
let wallet = decrypt_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&SecretString(ENCRYPTED_DESCRIPTOR_04.to_owned()),
);

hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned()));

let wallet = decrypt_wallet(&SecretString(ENCRYPTED_DESCRIPTOR_04.to_owned()));

assert!(wallet.is_err(), "Importing an old descriptor should error");

let upgraded_descriptor = upgrade_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&SecretString(ENCRYPTED_DESCRIPTOR_04.to_owned()),
&SecretString(SEED_PASSWORD.to_owned()),
)
Expand All @@ -41,10 +40,7 @@ async fn migration_v4() -> Result<()> {
serde_json::to_string_pretty(&upgraded_descriptor)?
);

let wallet = decrypt_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&upgraded_descriptor,
)?;
let wallet = decrypt_wallet(&upgraded_descriptor)?;

assert_eq!(
wallet.public.xpub, "tpubD6NzVbkrYhZ4Xxrh54Ew5kjkagEfUhS3aCNqRJmUuNfnTXhK4LGXyUzZ5kxgn8f2txjnFtypnoYfRQ9Y8P2nhSNXffxVKutJgxNPxgmwpUR",
Expand All @@ -61,16 +57,14 @@ async fn migration_v5() -> Result<()> {

switch_network("testnet").await?;

hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned()));

info!("Import bitmask-core 0.5 encrypted descriptor");
let wallet = decrypt_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&SecretString(ENCRYPTED_DESCRIPTOR_05.to_owned()),
);
let wallet = decrypt_wallet(&SecretString(ENCRYPTED_DESCRIPTOR_05.to_owned()));

assert!(wallet.is_err(), "Importing an old descriptor should error");

let upgraded_descriptor = upgrade_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&SecretString(ENCRYPTED_DESCRIPTOR_05.to_owned()),
&SecretString(SEED_PASSWORD.to_owned()),
)
Expand All @@ -81,10 +75,7 @@ async fn migration_v5() -> Result<()> {
serde_json::to_string_pretty(&upgraded_descriptor)?
);

let wallet = decrypt_wallet(
&SecretString(ENCRYPTION_PASSWORD.to_owned()),
&upgraded_descriptor,
)?;
let wallet = decrypt_wallet(&upgraded_descriptor)?;

assert_eq!(
wallet.public.xpub, "tpubD6NzVbkrYhZ4XJmEMNjxuARFrP5kME8ndqpk9M2QeqtuTv2kTrm87a93Td47bHRRCrSSVvVEu3trvwthVswtPNwK2Kyc9PpudxC1MZrPuNL",
Expand Down
5 changes: 2 additions & 3 deletions tests/payjoin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ async fn payjoin() -> Result<()> {

info!("Import wallets");
let mnemonic = env::var("TEST_WALLET_SEED")?;
let hash = hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned()));
hash_password(&SecretString(ENCRYPTION_PASSWORD.to_owned()));
let encrypted_descriptors = encrypt_wallet(
&SecretString(mnemonic),
&hash,
&SecretString(SEED_PASSWORD.to_owned()),
)
.await?;

let vault = decrypt_wallet(&hash, &encrypted_descriptors)?;
let vault = decrypt_wallet(&encrypted_descriptors)?;

let wallet = get_wallet_data(
&SecretString(vault.private.btc_descriptor_xprv.clone()),
Expand Down
Loading