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
This commit is contained in:
Sosthene 2024-07-11 12:41:23 +02:00
parent 6f74996f39
commit d737242bb5
2 changed files with 290 additions and 548 deletions

View File

@ -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<FromUtf8Error> for ApiError {
}
}
impl From<ParseNetworkError> for ApiError {
fn from(value: ParseNetworkError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl From<ParseOutPointError> for ApiError {
fn from(value: ParseOutPointError) -> Self {
ApiError {
message: value.to_string(),
}
}
}
impl Into<JsValue> 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<OutputList>,
}
#[wasm_bindgen]
pub fn setup() {
wasm_logger::init(wasm_logger::Config::default());
}
#[wasm_bindgen]
pub fn get_recover_address() -> ApiResult<String> {
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<String> {
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<String> {
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<String> {
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<String>,
birthday_main: u32,
birthday_signet: u32,
process: String,
) -> ApiResult<createUserReturn> {
//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<u8>,
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<OutputList>);
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<CachedMessage> {
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<CachedMessage> {
let tx = deserialize::<Transaction>(&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<CachedMessage>
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<CachedMessage>
#[wasm_bindgen]
pub fn get_outpoints_for_user() -> ApiResult<outputs_list> {
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<u64> {
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<u64> {
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<bool> {
let transaction = deserialize::<Transaction>(&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<Vec<u8>>;
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<createTransactionReturn> {
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<createTransactionReturn> {
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<createTransactionReturn> {
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<String> {
#[wasm_bindgen]
pub fn create_faucet_msg() -> ApiResult<CachedMessage> {
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);

View File

@ -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<Mutex<UserWallets>> = OnceLock::new();
pub fn lock_connected_user() -> Result<MutexGuard<'static, UserWallets>> {
CONNECTED_USER
.get_or_init(|| Mutex::new(UserWallets::default()))
pub static LOCAL_DEVICE: OnceLock<Mutex<Device>> = OnceLock::new();
pub fn lock_local_device() -> Result<MutexGuard<'static, Device>> {
LOCAL_DEVICE
.get_or_init(|| Mutex::new(Device::default()))
.lock_anyhow()
}
pub static SPENDING_CLIENT: OnceLock<Mutex<SpClient>> = OnceLock::new();
pub fn lock_spending_client() -> Result<MutexGuard<'static, SpClient>> {
SPENDING_CLIENT
.get_or_init(|| Mutex::new(SpClient::default()))
.lock_anyhow()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserWallets {
main: Option<SpWallet>,
recover: Option<SpWallet>,
revoke: Option<SpWallet>,
pub struct RevokeOutput {
key: [u8; 32],
spk: ScriptBuf,
outpoint: OutPoint,
}
impl UserWallets {
pub fn new(
main: Option<SpWallet>,
recover: Option<SpWallet>,
revoke: Option<SpWallet>,
) -> 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<OutputList> {
let mut res = Vec::<OutputList>::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<String>,
pub peers: Vec<Peer>,
recover_data: Vec<u8>,
revoke_data: Option<Vec<u8>>,
outputs: Vec<OutputList>,
pub struct Device {
watch_only_wallet: SpWallet,
spend_sk_cipher: Vec<u8>,
// Key used to encrypt the remote device spend_sk in the 2FA scheme
remote_device_key: [u8; 32],
remote_address: Option<String>,
revokation_output: Option<OutPoint>,
}
impl User {
pub fn new(user_wallets: UserWallets, user_password: String, process: String) -> Result<Self> {
// 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<String> {
self.remote_address.clone()
}
pub fn get_encrypted_key(&self) -> Vec<u8> {
self.spend_sk_cipher.clone()
}
pub fn encrypt_for_remote_device(&mut self, remote_spend_sk: SecretKey) -> Result<Vec<u8>> {
// 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<u8>,
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::<u8>::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<u8>) -> Result<Vec<u8>> {
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
)
}
}