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:
parent
6f74996f39
commit
d737242bb5
337
src/api.rs
337
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<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);
|
||||
|
501
src/user.rs
501
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<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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user