From d737242bb586d4fe75dceaadb1217a19f4ea4346 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 11 Jul 2024 12:41:23 +0200 Subject: [PATCH] New User creation * Device is an unlogged user that is just able to scan incoming transactions * There's always a Device defined either we load one from indexedDB or create one * Logged users have a defined SPENDING_CLIENT that can sign transactions * The pairing phase is when a new Device is created, we need another Device to send it it's own spending key via normal 4nk messaging * The new Device then encrypt it and send it back * In another transaction, the first Device does the exact same thing (probably possible to optimize) * The logging phase is sending a 4nk message with the encrypted spending key and getting the clear key back, basically logging with one device of a pair is the same operation than pairing but in reverse * Revokation output is an unique output for both devices, the validity of the pair depends of it being unspent * Revokation output is locked with some random key that both devices will need to keep * Todo: 1. implement the revokation scheme 2. optimize the pairing/logging flow --- src/api.rs | 337 +++++++++++++++++------------------ src/user.rs | 501 +++++++++++++--------------------------------------- 2 files changed, 290 insertions(+), 548 deletions(-) diff --git a/src/api.rs b/src/api.rs index 83e44cc..28824ae 100644 --- a/src/api.rs +++ b/src/api.rs @@ -22,8 +22,10 @@ use sdk_common::sp_client::bitcoin::hex::{ parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, }; use sdk_common::sp_client::bitcoin::key::Secp256k1; +use sdk_common::sp_client::bitcoin::network::ParseNetworkError; use sdk_common::sp_client::bitcoin::secp256k1::ecdh::shared_secret_point; use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, SecretKey}; +use sdk_common::sp_client::bitcoin::transaction::ParseOutPointError; use sdk_common::sp_client::bitcoin::{Amount, Network, OutPoint, Psbt, Transaction, Txid}; use sdk_common::sp_client::silentpayments::utils as sp_utils; use sdk_common::sp_client::silentpayments::{ @@ -52,7 +54,9 @@ use sdk_common::sp_client::spclient::{ use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use crate::wallet::generate_sp_wallet; -use crate::user::{lock_connected_user, User, UserWallets, CONNECTED_USER}; +use crate::user::{ + lock_local_device, lock_spending_client, Device, RevokeOutput, LOCAL_DEVICE, SPENDING_CLIENT, +}; use crate::{images, lock_messages, CACHEDMESSAGES}; use crate::process::Process; @@ -146,88 +150,90 @@ impl From for ApiError { } } +impl From for ApiError { + fn from(value: ParseNetworkError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + +impl From for ApiError { + fn from(value: ParseOutPointError) -> Self { + ApiError { + message: value.to_string(), + } + } +} + impl Into for ApiError { fn into(self) -> JsValue { JsValue::from_str(&self.message) } } -#[derive(Tsify, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct createUserReturn { - pub user: User, - pub output_list_vec: Vec, -} - #[wasm_bindgen] pub fn setup() { wasm_logger::init(wasm_logger::Config::default()); } #[wasm_bindgen] -pub fn get_recover_address() -> ApiResult { - if let Ok(my_wallets) = lock_connected_user() { - Ok(my_wallets - .try_get_recover()? - .get_client() - .get_receiving_address()) - } else { - Err(ApiError { - message: "Unknown user pre_id".to_owned(), - }) - } +pub fn get_address() -> ApiResult { + let local_device = lock_local_device()?; + + Ok(local_device + .get_watch_only() + .get_client() + .get_receiving_address()) } #[wasm_bindgen] -pub fn get_main_address() -> ApiResult { - if let Ok(my_wallets) = lock_connected_user() { - Ok(my_wallets - .try_get_main()? - .get_client() - .get_receiving_address()) - } else { - Err(ApiError { - message: "Unknown user pre_id".to_owned(), - }) - } -} +pub fn create_new_wallet(birthday: u32, network_str: String) -> ApiResult { + let network = Network::from_core_arg(&network_str)?; + let wallet = generate_sp_wallet(None, Network::Regtest)?; -#[wasm_bindgen] -pub fn create_user( - password: String, // Attention à la conversion depuis le js - label: Option, - birthday_main: u32, - birthday_signet: u32, - process: String, -) -> ApiResult { - //recover - let sp_wallet_recover = generate_sp_wallet(label.clone(), birthday_signet, Network::Signet)?; - //revoke - let sp_wallet_revoke = generate_sp_wallet(label.clone(), birthday_signet, Network::Signet)?; - //mainet - let sp_wallet_main = generate_sp_wallet(label, birthday_main, Network::Bitcoin)?; - - let user_wallets = UserWallets::new( - Some(sp_wallet_main), - Some(sp_wallet_recover), - Some(sp_wallet_revoke), + // Let's create the new device + let mut device = Device::new( + wallet.get_client().get_scan_key(), + wallet.get_client().get_spend_key().into(), + network, ); - let user = User::new(user_wallets.clone(), password, process)?; + device + .get_watch_only_mut() + .get_mut_outputs() + .set_birthday(birthday); - let outputs = user_wallets.get_all_outputs(); + let our_address = device.get_watch_only().get_client().get_receiving_address(); - // Setting CONNECTED_USER to user - let mut connected_user = lock_connected_user()?; - *connected_user = user_wallets; + // Set the LOCAL_DEVICE const with the new value + LOCAL_DEVICE + .set(Mutex::new(device)) + .expect("We shouldn't already have initialized LOCAL_DEVICE now"); - let generate_user = createUserReturn { - user, - output_list_vec: outputs, - }; + // Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking + SPENDING_CLIENT + .set(Mutex::new(wallet.get_client().clone())) + .expect("We shouldn't already have initialized SPENDING_CLIENT now"); - Ok(generate_user) + Ok(our_address) +} + +#[wasm_bindgen] +pub fn update_linked_address( + spend_sk_cipher: Vec, + linked_with: String, + revokation_output: String, +) -> ApiResult<()> { + let mut device = lock_local_device()?; + + device.new_link( + spend_sk_cipher, + linked_with.try_into()?, + OutPoint::from_str(&revokation_output)?, + ); + + Ok(()) } #[wasm_bindgen] @@ -306,29 +312,19 @@ impl recover_data { #[derive(Debug, Tsify, Serialize, Deserialize)] #[tsify(from_wasm_abi, into_wasm_abi)] #[allow(non_camel_case_types)] -pub struct outputs_list(Vec); +pub struct outputs_list(OutputList); impl outputs_list { - fn as_inner(&self) -> &[OutputList] { + fn as_inner(&self) -> &OutputList { &self.0 } } #[wasm_bindgen] -pub fn login_user( - user_password: String, - pre_id: String, - recover: recover_data, - outputs: outputs_list, -) -> ApiResult<()> { - let res = User::login( - pre_id, - user_password, - recover.as_inner(), - outputs.as_inner(), - )?; +pub fn login_user(fee_rate: u32) -> ApiResult<()> { + create_login_transaction(fee_rate)?; - Ok(res) + Ok(()) } fn handle_recover_transaction( @@ -336,7 +332,6 @@ fn handle_recover_transaction( tx: &Transaction, sp_wallet: &mut SpWallet, tweak_data: PublicKey, - fee_rate: u32, ) -> anyhow::Result { let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); let commitment = if op_return.is_none() { @@ -521,35 +516,18 @@ fn process_transaction( tx_hex: String, blockheight: u32, tweak_data_hex: String, - fee_rate: u32, ) -> anyhow::Result { let tx = deserialize::(&Vec::from_hex(&tx_hex)?)?; let tweak_data = PublicKey::from_str(&tweak_data_hex)?; - let mut connected_user = lock_connected_user()?; - if let Ok(recover) = connected_user.try_get_mut_recover() { - let updated = recover.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + let mut device = lock_local_device()?; + let wallet = device.get_watch_only_mut(); + let updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; - if updated.len() > 0 { - let updated_msg = - handle_recover_transaction(updated, &tx, recover, tweak_data, fee_rate)?; - return Ok(updated_msg); - } - } - - if let Ok(main) = connected_user.try_get_mut_main() { - let updated = main.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; - if updated.len() > 0 { - unimplemented!(); - } - } - - if let Ok(revoke) = connected_user.try_get_mut_revoke() { - let updated = revoke.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; - if updated.len() > 0 { - unimplemented!(); - } + if updated.len() > 0 { + let updated_msg = handle_recover_transaction(updated, &tx, wallet, tweak_data)?; + return Ok(updated_msg); } Err(anyhow::Error::msg("No output found")) @@ -580,12 +558,8 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult message: "Missing tweak_data".to_owned(), }); } - let network_msg = process_transaction( - tx_message.transaction, - 0, - tx_message.tweak_data.unwrap(), - fee_rate, - )?; + let network_msg = + process_transaction(tx_message.transaction, 0, tx_message.tweak_data.unwrap())?; return Ok(network_msg); } AnkFlag::Faucet => { @@ -635,54 +609,19 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult #[wasm_bindgen] pub fn get_outpoints_for_user() -> ApiResult { - let connected_user = lock_connected_user()?; - if connected_user.is_not_empty() { - Ok(outputs_list(connected_user.get_all_outputs())) - } else { - Err(ApiError { - message: "No user logged in".to_owned(), - }) - } + let device = lock_local_device()?; + let outputs = device.get_watch_only().get_outputs().clone(); + Ok(outputs_list(outputs)) } #[wasm_bindgen] -pub fn get_available_amount_for_user(recover: bool) -> ApiResult { - let connected_user = lock_connected_user()?; - if recover { - if let Ok(recover_wallet) = connected_user.try_get_recover() { - Ok(recover_wallet.get_outputs().get_balance().to_sat()) - } else { - Err(ApiError { - message: "User doesn't have recover wallet available".to_owned(), - }) - } - } else { - Err(ApiError { - message: "No user logged in".to_owned(), - }) - } +pub fn get_available_amount_for_user() -> ApiResult { + let device = lock_local_device()?; + + Ok(device.get_watch_only().get_outputs().get_balance().to_sat()) } -#[wasm_bindgen] -pub fn is_tx_owned_by_user(pre_id: String, tx: String) -> ApiResult { - let transaction = deserialize::(&Vec::from_hex(&tx)?)?; - let txid = transaction.txid(); - let connected_user = lock_connected_user()?; - - if let Some(_) = connected_user - .try_get_recover()? - .get_outputs() - .to_outpoints_list() - .iter() - .find(|(outpoint, output)| outpoint.txid == txid) - { - Ok(true) - } else { - Ok(false) - } -} - -#[derive(Tsify, Serialize, Deserialize)] +#[derive(Tsify, Serialize, Deserialize, Default)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct createTransactionReturn { @@ -716,14 +655,13 @@ pub fn answer_confirmation_transaction( let sp_address: SilentPaymentAddress = message.recipient.as_ref().unwrap().as_str().try_into()?; - let connected_user = lock_connected_user()?; + let local_device = lock_local_device()?; - let sp_wallet: &SpWallet; - if sp_address.get_network() != SpNetwork::Mainnet { - sp_wallet = connected_user.try_get_recover()?; - } else { - sp_wallet = connected_user.try_get_main()?; - } + let current_outputs = local_device.get_watch_only().get_outputs().clone(); + + let spending_client = lock_spending_client()?.clone(); + + let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; let recipient = Recipient { address: sp_address.into(), @@ -736,9 +674,10 @@ pub fn answer_confirmation_transaction( let signed_psbt = create_transaction_spend_outpoint( &confirmed_by, - sp_wallet, + &sp_wallet, recipient, &commited_in.txid, + None, Amount::from_sat(fee_rate.into()), )?; @@ -776,15 +715,24 @@ pub fn create_confirmation_transaction( } let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; - let connected_user = lock_connected_user()?; - let sp_wallet: &SpWallet; - if sp_address.get_network() != SpNetwork::Mainnet { - sp_wallet = connected_user.try_get_recover()?; + let mut local_device = lock_local_device()?; + + // Are we waiting for pairing? + let remote_key_cipher: Option>; + if !local_device.is_linked() { + let remote_spend_sk = SecretKey::from_str(message.plaintext.as_deref().unwrap())?; + remote_key_cipher = Some(local_device.encrypt_for_remote_device(remote_spend_sk)?); } else { - sp_wallet = connected_user.try_get_main()?; + remote_key_cipher = None; } + let current_outputs = local_device.get_watch_only().get_outputs().clone(); + + let spending_client = lock_spending_client()?.clone(); + + let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; + let recipient = Recipient { address: sp_address.into(), amount: Amount::from_sat(0), @@ -795,9 +743,10 @@ pub fn create_confirmation_transaction( let signed_psbt = create_transaction_spend_outpoint( &commited_in, - sp_wallet, + &sp_wallet, recipient, &commited_in.txid, + remote_key_cipher, Amount::from_sat(fee_rate.into()), )?; @@ -822,6 +771,47 @@ pub fn create_confirmation_transaction( }) } +#[wasm_bindgen] +pub fn create_pairing_transaction( + address: String, + fee_rate: u32, +) -> ApiResult { + let message: CipherMessage; + { + let mut spending_wallet = lock_spending_client()?; + + let our_address = spending_wallet.get_receiving_address(); + let spend_sk: SecretKey = spending_wallet.get_spend_key().try_into()?; + + message = CipherMessage::new(our_address, format!("{}", spend_sk.display_secret())); + + // we forget our own wallet + *spending_wallet = SpClient::default(); + } + + create_notification_transaction(address, message, fee_rate) +} + +#[wasm_bindgen] +pub fn create_login_transaction(fee_rate: u32) -> ApiResult { + let address = lock_local_device()?.get_remote_address().ok_or(ApiError { + message: "Wallet is not linked".to_owned(), + })?; + + let message: CipherMessage; + { + let device = lock_local_device()?; + + let our_address = device.get_watch_only().get_client().get_receiving_address(); + + let encrypted_key = device.get_encrypted_key().to_lower_hex_string(); + + message = CipherMessage::new(our_address, encrypted_key); + } + + create_notification_transaction(address, message, fee_rate) +} + #[wasm_bindgen] pub fn create_notification_transaction( address: String, @@ -830,14 +820,13 @@ pub fn create_notification_transaction( ) -> ApiResult { let sp_address: SilentPaymentAddress = address.as_str().try_into()?; - let connected_user = lock_connected_user()?; + let local_device = lock_local_device()?; - let sp_wallet: &SpWallet; - if sp_address.get_network() != SpNetwork::Mainnet { - sp_wallet = connected_user.try_get_recover()?; - } else { - sp_wallet = connected_user.try_get_main()?; - } + let current_outputs = local_device.get_watch_only().get_outputs().clone(); + + let spending_client = lock_spending_client()?.clone(); + + let sp_wallet = SpWallet::new(spending_client, Some(current_outputs))?; let recipient = Recipient { address: sp_address.into(), @@ -849,7 +838,7 @@ pub fn create_notification_transaction( let signed_psbt = create_transaction_for_address_with_shared_secret( recipient, - sp_wallet, + &sp_wallet, Some(&commitment), Amount::from_sat(fee_rate.into()), )?; @@ -974,8 +963,10 @@ pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult { #[wasm_bindgen] pub fn create_faucet_msg() -> ApiResult { - let user = lock_connected_user()?; - let sp_address = user.try_get_recover()?.get_client().get_receiving_address(); + let sp_address = lock_local_device()? + .get_watch_only() + .get_client() + .get_receiving_address(); let mut commitment = [0u8; 64]; thread_rng().fill_bytes(&mut commitment); diff --git a/src/user.rs b/src/user.rs index 1236ee2..faa1616 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,11 +1,9 @@ use anyhow::{Error, Result}; use rand::{self, thread_rng, Rng, RngCore}; -use sdk_common::sp_client::bitcoin::hashes::Hash; -use sdk_common::sp_client::bitcoin::hashes::HashEngine; +use sdk_common::sp_client::bitcoin::hashes::{Hash, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{DisplayHex, FromHex}; -use sdk_common::sp_client::bitcoin::secp256k1::SecretKey; -use sdk_common::sp_client::bitcoin::secp256k1::ThirtyTwoByteHash; -use sdk_common::sp_client::bitcoin::Network; +use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, SecretKey, ThirtyTwoByteHash}; +use sdk_common::sp_client::bitcoin::{Network, OutPoint, ScriptBuf}; use sdk_common::sp_client::spclient::SpClient; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -20,411 +18,164 @@ use std::sync::{Mutex, MutexGuard, OnceLock}; use sdk_common::sp_client::bitcoin::secp256k1::constants::SECRET_KEY_SIZE; use sdk_common::sp_client::silentpayments::bitcoin_hashes::sha256; -use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; -use sdk_common::sp_client::spclient::SpendKey; -use sdk_common::sp_client::spclient::{OutputList, SpWallet}; +use sdk_common::sp_client::silentpayments::utils::{Network as SpNetwork, SilentPaymentAddress}; +use sdk_common::sp_client::spclient::{OutputList, SpWallet, SpendKey}; use crate::peers::Peer; -use crate::user; +use crate::wallet::generate_sp_wallet; use crate::MutexExt; use sdk_common::crypto::{ AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, HalfKey, KeyInit, Purpose, }; -type PreId = String; -pub static CONNECTED_USER: OnceLock> = OnceLock::new(); -pub fn lock_connected_user() -> Result> { - CONNECTED_USER - .get_or_init(|| Mutex::new(UserWallets::default())) +pub static LOCAL_DEVICE: OnceLock> = OnceLock::new(); + +pub fn lock_local_device() -> Result> { + LOCAL_DEVICE + .get_or_init(|| Mutex::new(Device::default())) + .lock_anyhow() +} + +pub static SPENDING_CLIENT: OnceLock> = OnceLock::new(); + +pub fn lock_spending_client() -> Result> { + SPENDING_CLIENT + .get_or_init(|| Mutex::new(SpClient::default())) .lock_anyhow() } #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct UserWallets { - main: Option, - recover: Option, - revoke: Option, +pub struct RevokeOutput { + key: [u8; 32], + spk: ScriptBuf, + outpoint: OutPoint, } -impl UserWallets { - pub fn new( - main: Option, - recover: Option, - revoke: Option, - ) -> Self { - Self { - main, - recover, - revoke, - } - } - - pub fn try_get_revoke(&self) -> Result<&SpWallet> { - if let Some(revoke) = &self.revoke { - Ok(revoke) - } else { - Err(Error::msg("No revoke wallet available")) - } - } - - pub fn try_get_recover(&self) -> Result<&SpWallet> { - if let Some(recover) = &self.recover { - Ok(recover) - } else { - Err(Error::msg("No recover wallet available")) - } - } - - pub fn try_get_main(&self) -> Result<&SpWallet> { - if let Some(main) = &self.main { - Ok(main) - } else { - Err(Error::msg("No main wallet available")) - } - } - - pub fn try_get_mut_revoke(&mut self) -> Result<&mut SpWallet> { - if let Some(revoke) = &mut self.revoke { - Ok(revoke) - } else { - Err(Error::msg("No revoke wallet available")) - } - } - - pub fn try_get_mut_recover(&mut self) -> Result<&mut SpWallet> { - if let Some(recover) = &mut self.recover { - Ok(recover) - } else { - Err(Error::msg("No recover wallet available")) - } - } - - pub fn try_get_mut_main(&mut self) -> Result<&mut SpWallet> { - if let Some(main) = &mut self.main { - Ok(main) - } else { - Err(Error::msg("No main wallet available")) - } - } - - pub(crate) fn is_not_empty(&self) -> bool { - self.get_all_outputs().len() > 0 - } - - pub(crate) fn get_all_outputs(&self) -> Vec { - let mut res = Vec::::new(); - if let Some(main) = &self.main { - res.push(main.get_outputs().clone()); - } - if let Some(revoke) = &self.revoke { - res.push(revoke.get_outputs().clone()); - } - if let Some(recover) = &self.recover { - res.push(recover.get_outputs().clone()); - } - - res +impl RevokeOutput { + pub fn new(key: [u8; 32], spk: ScriptBuf, outpoint: OutPoint) -> Self { + Self { key, spk, outpoint } } } -#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct User { - pub pre_id: PreId, - pub processes: Vec, - pub peers: Vec, - recover_data: Vec, - revoke_data: Option>, - outputs: Vec, +pub struct Device { + watch_only_wallet: SpWallet, + spend_sk_cipher: Vec, + // Key used to encrypt the remote device spend_sk in the 2FA scheme + remote_device_key: [u8; 32], + remote_address: Option, + revokation_output: Option, } -impl User { - pub fn new(user_wallets: UserWallets, user_password: String, process: String) -> Result { - // if we are already logged in, abort - if lock_connected_user()?.is_not_empty() { - return Err(Error::msg("User already logged in")); +impl Device { + pub fn new(scan_sk: SecretKey, spend_pk: PublicKey, network: Network) -> Self { + let watch_only_client = SpClient::new( + "default".to_owned(), + scan_sk, + SpendKey::Public(spend_pk), + None, + network, + ) + .expect("watch_only_client creation failed"); + + let mut watch_only_wallet = + SpWallet::new(watch_only_client, None).expect("watch_only_wallet creation failed"); + + let spend_sk_cipher = vec![]; + let remote_device_key = [0; 32]; + + Self { + watch_only_wallet, + spend_sk_cipher, + remote_device_key, + remote_address: None, + revokation_output: None, + } + } + + pub fn get_watch_only(&self) -> &SpWallet { + &self.watch_only_wallet + } + + pub fn get_watch_only_mut(&mut self) -> &mut SpWallet { + &mut self.watch_only_wallet + } + + pub fn is_linked(&self) -> bool { + self.remote_address.is_some() && self.spend_sk_cipher.len() != 0 + } + + pub fn get_remote_address(&self) -> Option { + self.remote_address.clone() + } + + pub fn get_encrypted_key(&self) -> Vec { + self.spend_sk_cipher.clone() + } + + pub fn encrypt_for_remote_device(&mut self, remote_spend_sk: SecretKey) -> Result> { + // encrypt with the remote_device_key + let encryption = Aes256Encryption::new( + Purpose::ThirtyTwoBytes, + remote_spend_sk.secret_bytes().to_vec(), + )?; + let remote_spend_sk_cipher = encryption.encrypt_with_aes_key(); + self.remote_device_key = encryption.export_key(); + remote_spend_sk_cipher + } + + pub fn new_link( + &mut self, + spend_sk_cipher: Vec, + linked_with: SilentPaymentAddress, + revokation_output: OutPoint, + ) { + self.spend_sk_cipher = spend_sk_cipher; + self.remote_address = Some(linked_with.into()); + self.revokation_output = Some(revokation_output); + } + + pub fn login(spend_sk: SecretKey) -> Result<()> { + let mut locked_client = lock_spending_client()?; + if *locked_client != SpClient::default() { + // We already have a key charged + return Err(Error::msg("We're already logged in")); } - let mut rng = thread_rng(); + let device = lock_local_device()?; - // image revoke - // We just take the 2 revoke keys - let mut revoke_data = Vec::with_capacity(64); - let revoke = user_wallets.try_get_revoke()?; - revoke_data.extend_from_slice(revoke.get_client().get_scan_key().as_ref()); - revoke_data.extend_from_slice(revoke.get_client().try_get_secret_spend_key()?.as_ref()); + let watch_only = device.get_watch_only().get_client(); - // Take the 2 recover keys - let recover_spend_key = user_wallets - .try_get_recover()? - .get_client() - .try_get_secret_spend_key()? - .clone(); - let mut recover_data = Vec::::with_capacity(180); // 32 * 3 + (12+16)*3 + let label = watch_only.label.clone(); + let scan_sk = watch_only.get_scan_key(); + let network = match watch_only.sp_receiver.network { + SpNetwork::Mainnet => Network::Bitcoin, + SpNetwork::Regtest => Network::Regtest, + SpNetwork::Testnet => Network::Testnet, + }; - // generate 3 tokens of 32B entropy - let mut entropy_1: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); - let mut entropy_2: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); + let spending_client = + SpClient::new(label, scan_sk, SpendKey::Secret(spend_sk), None, network)?; - recover_data.extend_from_slice(&entropy_1); - recover_data.extend_from_slice(&entropy_2); + // check that we loaded the right key + if spending_client.get_receiving_address() != watch_only.get_receiving_address() { + return Err(Error::msg("Provided the wrong spending key")); + } - // hash the concatenation - let mut engine = sha256::HashEngine::default(); - engine.write_all(&user_password.as_bytes()); - engine.write_all(&entropy_1); - let hash1 = sha256::Hash::from_engine(engine); + *locked_client = spending_client; - // take it as a AES key - let recover_key_encryption = Aes256Encryption::import_key( - Purpose::ThirtyTwoBytes, - recover_spend_key.secret_bytes().to_vec(), - hash1.to_byte_array(), - Aes256Gcm::generate_nonce(&mut rng).into(), - )?; - - // encrypt the part1 of the key - let cipher_recover = recover_key_encryption.encrypt_with_aes_key()?; - - recover_data.extend_from_slice(&cipher_recover); - - //Pre ID - let pre_id: PreId = Self::compute_pre_id(&user_password, &cipher_recover); - - //scan key: - let mut engine = sha256::HashEngine::default(); - engine.write_all(&user_password.as_bytes()); - engine.write_all(&entropy_2); - let hash2 = sha256::Hash::from_engine(engine); - - let scan_key_encryption = Aes256Encryption::import_key( - Purpose::ThirtyTwoBytes, - user_wallets - .try_get_recover()? - .get_client() - .get_scan_key() - .secret_bytes() - .to_vec(), - hash2.to_byte_array(), - Aes256Gcm::generate_nonce(&mut rng).into(), - )?; - - // encrypt the scan key - let cipher_scan_key = scan_key_encryption.encrypt_with_aes_key()?; - - recover_data.extend_from_slice(&cipher_scan_key); - - let all_outputs = user_wallets.get_all_outputs(); - - Ok(User { - pre_id: pre_id.to_string(), - processes: vec![process], - peers: vec![], - recover_data, - revoke_data: Some(revoke_data), - outputs: all_outputs, - }) + Ok(()) } pub fn logout() -> Result<()> { - if let Ok(mut user) = lock_connected_user() { - *user = UserWallets::default(); + if let Ok(mut client) = lock_spending_client() { + *client = SpClient::default(); Ok(()) } else { Err(Error::msg("Failed to lock CONNECTED_USER")) } } - - pub fn login( - pre_id: PreId, - user_password: String, - recover_data: &[u8], - outputs: &[OutputList], - ) -> Result<()> { - // if we are already logged in, abort - if lock_connected_user()?.is_not_empty() { - return Err(Error::msg("User already logged in")); - } - - let mut retrieved_spend_key = [0u8; 32]; - let mut retrieved_scan_key = [0u8; 32]; - let mut entropy1 = [0u8; 32]; - let mut entropy2 = [0u8; 32]; - let mut cipher_scan_key = [0u8; 60]; // cipher length == plain.len() + 16 + nonce.len() - let mut cipher_spend_key = [0u8; 60]; - - let mut reader = Cursor::new(recover_data); - reader.read_exact(&mut entropy1)?; - reader.read_exact(&mut entropy2)?; - reader.read_exact(&mut cipher_spend_key)?; - reader.read_exact(&mut cipher_scan_key)?; - - // We can retrieve the pre_id and check that it matches - let retrieved_pre_id = Self::compute_pre_id(&user_password, &cipher_spend_key); - - // If pre_id is not the same, password is probably false, or the client is feeding us garbage - if retrieved_pre_id != pre_id { - return Err(Error::msg("pre_id and recover_data don't match")); - } - - retrieved_spend_key.copy_from_slice(&Self::recover_key_slice( - &user_password, - &entropy1, - cipher_spend_key.to_vec(), - )?); - - retrieved_scan_key.copy_from_slice(&Self::recover_key_slice( - &user_password, - &entropy2, - cipher_scan_key.to_vec(), - )?); - - // we can create the recover sp_client - let recover_client = SpClient::new( - "".to_owned(), - SecretKey::from_slice(&retrieved_scan_key)?, - SpendKey::Secret(SecretKey::from_slice(&retrieved_spend_key)?), - None, - Network::Signet, - )?; - - let recover_outputs = outputs - .iter() - .find(|o| o.check_fingerprint(&recover_client)) - .cloned(); - - let recover_wallet = SpWallet::new(recover_client, recover_outputs)?; - - let user_wallets = UserWallets::new(None, Some(recover_wallet), None); - - if let Ok(mut user) = lock_connected_user() { - *user = user_wallets; - } else { - return Err(Error::msg("Failed to lock CONNECTED_USER")); - } - - Ok(()) - } - - fn recover_key_slice(password: &str, entropy: &[u8], ciphertext: Vec) -> Result> { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&password.as_bytes()); - engine.write_all(&entropy); - let hash = sha256::Hash::from_engine(engine); - - let aes_dec = - Aes256Decryption::new(Purpose::ThirtyTwoBytes, ciphertext, hash.to_byte_array())?; - - aes_dec.decrypt_with_key() - } - - fn compute_pre_id(user_password: &str, cipher_recover: &[u8]) -> PreId { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&user_password.as_bytes()); - engine.write_all(&cipher_recover); - let pre_id = sha256::Hash::from_engine(engine); - - pre_id.to_string() - } -} - -#[cfg(test)] -mod tests { - use super::*; // Import everything from the outer module - - const RECOVER_SPEND: &str = "394ef7757f5bc8cd692337c62abf6fa0ce9932fd4ec6676daddfbe3c1b3b9d11"; - const RECOVER_SCAN: &str = "3aa8cc570d17ec3a4dc4136e50151cc6de26052d968abfe02a5fea724ce38205"; - const REVOKE_SPEND: &str = "821c1a84fa9ee718c02005505fb8315bd479c7b9a878b1eff45929c48dfcaf28"; - const REVOKE_SCAN: &str = "a0f36cbc380624fa7eef022f39cab2716333451649dd8eb78e86d2e76bdb3f47"; - const MAIN_SPEND: &str = "b9098a6598ac55d8dd0e6b7aab0d1f63eb8792d06143f3c0fb6f5b80476a1c0d"; - const MAIN_SCAN: &str = "79dda4031663ac2cb250c46d896dc92b3c027a48a761b2342fabf1e441ea2857"; - const USER_PASSWORD: &str = "correct horse battery staple"; - const PROCESS: &str = "example"; - - fn helper_create_user_wallets() -> UserWallets { - let label = "default".to_owned(); - let sp_main = SpClient::new( - label.clone(), - SecretKey::from_str(MAIN_SCAN).unwrap(), - SpendKey::Secret(SecretKey::from_str(MAIN_SPEND).unwrap()), - None, - Network::Bitcoin, - ) - .unwrap(); - let sp_recover = SpClient::new( - label.clone(), - SecretKey::from_str(RECOVER_SCAN).unwrap(), - SpendKey::Secret(SecretKey::from_str(RECOVER_SPEND).unwrap()), - None, - Network::Signet, - ) - .unwrap(); - let sp_revoke = SpClient::new( - label.clone(), - SecretKey::from_str(REVOKE_SCAN).unwrap(), - SpendKey::Secret(SecretKey::from_str(REVOKE_SPEND).unwrap()), - None, - Network::Signet, - ) - .unwrap(); - let user_wallets = UserWallets::new( - Some(SpWallet::new(sp_main, None).unwrap()), - Some(SpWallet::new(sp_recover, None).unwrap()), - Some(SpWallet::new(sp_revoke, None).unwrap()), - ); - - user_wallets - } - - #[test] - fn test_successful_creation() { - let user_wallets = helper_create_user_wallets(); - let result = User::new(user_wallets, USER_PASSWORD.to_owned(), PROCESS.to_owned()); - - assert!(result.is_ok()); - } - - #[test] - fn test_logout() { - let res = User::logout(); - - assert!(res.is_ok()); - } - - #[test] - fn test_login() { - let user_wallets = helper_create_user_wallets(); - let user = User::new( - user_wallets.clone(), - USER_PASSWORD.to_owned(), - PROCESS.to_owned(), - ) - .unwrap(); - - let res = User::login( - user.pre_id.clone(), - USER_PASSWORD.to_owned(), - &user.recover_data, - &user_wallets.get_all_outputs(), - ); - - assert!(res.is_ok()); - - let connected = lock_connected_user().unwrap(); - - let recover = connected.try_get_recover().unwrap(); - - assert!( - format!( - "{}", - recover - .get_client() - .try_get_secret_spend_key() - .unwrap() - .display_secret() - ) == RECOVER_SPEND - ) - } }