working login

This commit is contained in:
Sosthene 2024-08-01 12:26:26 +02:00
parent 30759027b0
commit d82e145f50
4 changed files with 867 additions and 750 deletions

View File

@ -21,12 +21,18 @@ use sdk_common::sp_client::bitcoin::hashes::{sha256, Hash};
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::key::{Parity, Secp256k1};
use sdk_common::sp_client::bitcoin::network::ParseNetworkError;
use sdk_common::sp_client::bitcoin::psbt::raw;
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::secp256k1::{PublicKey, Scalar, 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::bitcoin::{
Amount, Network, OutPoint, Psbt, Transaction, Txid, XOnlyPublicKey,
};
use sdk_common::sp_client::constants::{
DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE,
};
use sdk_common::sp_client::silentpayments::utils as sp_utils;
use sdk_common::sp_client::silentpayments::{
utils::{Network as SpNetwork, SilentPaymentAddress},
@ -45,15 +51,13 @@ use sdk_common::network::{
};
use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address};
use crate::wallet::generate_sp_wallet;
use crate::wallet::{generate_sp_wallet, lock_freezed_utxos};
use sdk_common::sp_client::spclient::{
derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient,
};
use sdk_common::sp_client::spclient::{SpWallet, SpendKey};
use crate::user::{
lock_local_device, lock_spending_client, Device, RevokeOutput, LOCAL_DEVICE, SPENDING_CLIENT,
};
use crate::user::{lock_local_device, set_new_device, Device, LOCAL_DEVICE};
use crate::{images, lock_messages, CACHEDMESSAGES};
use crate::process::Process;
@ -179,116 +183,98 @@ pub fn get_address() -> ApiResult<String> {
let local_device = lock_local_device()?;
Ok(local_device
.get_watch_only()
.get_wallet()
.get_client()
.get_receiving_address())
}
#[wasm_bindgen]
pub fn restore_device_from_sp_wallet(sp_wallet: String) -> ApiResult<String> {
let wallet: SpWallet = serde_json::from_str(&sp_wallet)?;
let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?;
let mut device = Device::new(
wallet.get_client().get_scan_key(),
wallet.get_client().get_spend_key().into(),
wallet.get_client().get_network(),
);
let birthday = wallet.get_outputs().get_birthday();
let outputs = wallet.get_outputs().to_outpoints_list();
device
.get_watch_only_mut()
.get_mut_outputs()
.set_birthday(birthday);
device
.get_watch_only_mut()
.get_mut_outputs()
.extend_from(outputs);
let json_return = serde_json::to_string(&device)?;
// Set the LOCAL_DEVICE const with the new value
let mut local_device = lock_local_device()?;
if *local_device.get_watch_only().get_client() == SpClient::default() {
*local_device = device;
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
// Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking
let mut spending_client = lock_spending_client()?;
if *spending_client == SpClient::default() {
*spending_client = wallet.get_client().clone();
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
Ok(json_return)
}
#[wasm_bindgen]
pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult<String> {
let network = Network::from_core_arg(&network_str)?;
let wallet = generate_sp_wallet(None, Network::Regtest)?;
// 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,
);
device
.get_watch_only_mut()
.get_mut_outputs()
.set_birthday(birthday);
let our_address = device.get_watch_only().get_client().get_receiving_address();
// Set the LOCAL_DEVICE const with the new value
let mut local_device = lock_local_device()?;
if *local_device.get_watch_only().get_client() == SpClient::default() {
*local_device = device;
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
// Set the LOGGED_WALLET with the new wallet to keep it in memory while we wait for the linking
let mut spending_client = lock_spending_client()?;
if *spending_client == SpClient::default() {
*spending_client = wallet.get_client().clone();
} else {
return Err(ApiError {
message: "device is already initialized".to_owned(),
});
}
let our_address = set_new_device(sp_wallet)?;
Ok(our_address)
}
#[wasm_bindgen]
pub fn pair_device(
spend_sk_cipher: Vec<u8>,
linked_with: String,
revokation_output: String,
) -> ApiResult<()> {
let mut device = lock_local_device()?;
pub fn restore_device(device_str: String) -> ApiResult<()> {
let device: Device = serde_json::from_str(&device_str)?;
device.new_link(
spend_sk_cipher,
linked_with.try_into()?,
OutPoint::from_str(&revokation_output)?,
);
let mut local_device = lock_local_device()?;
*local_device = device;
Ok(())
}
#[wasm_bindgen]
pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult<String> {
let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?;
let our_address = set_new_device(sp_wallet)?;
Ok(our_address)
}
#[wasm_bindgen]
pub fn pair_device(message_id: u32, incoming_pairing_txid: String) -> ApiResult<()> {
let mut local_device = lock_local_device()?;
// check that we're still in pairing phase
if !local_device.is_pairing() {
return Err(ApiError {
message: "Already paired".to_owned(),
});
}
let mut messages = lock_messages()?;
let my_address = local_device
.get_wallet()
.get_client()
.get_receiving_address();
if let Some(message) = messages.iter_mut().find(|m| m.id == message_id) {
if message.status != CachedMessageStatus::Pairing {
return Err(ApiError {
message: "Message is not pairing message".to_owned(),
});
}
let link_with = if Some(my_address.as_str()) == message.sender.as_ref().map(|s| s.as_str())
{
message.recipient.as_ref().ok_or(ApiError {
message: "Missing recipient".to_owned(),
})?
} else if Some(my_address.as_str()) == message.recipient.as_ref().map(|s| s.as_str()) {
message.sender.as_ref().ok_or(ApiError {
message: "Missing sender".to_owned(),
})?
} else {
// This should never happen
return Err(ApiError {
message: "We're not part of that message".to_owned(),
});
};
let revokation_index = message.tied_by.ok_or(ApiError {
message: "Missing tied_by".to_owned(),
})?;
let pairing_tx = message.commited_in.ok_or(ApiError {
message: "Missing commited_in".to_owned(),
})?;
local_device.new_link(
SilentPaymentAddress::try_from(link_with.as_str()).unwrap(),
pairing_tx.txid,
revokation_index,
Txid::from_str(&incoming_pairing_txid)?,
)?;
message.status = CachedMessageStatus::Closed;
} else {
return Err(ApiError {
message: format!("Can't find message with id {}", message_id),
});
}
Ok(())
}
@ -378,26 +364,66 @@ impl outputs_list {
}
#[wasm_bindgen]
pub fn login_user(fee_rate: u32) -> ApiResult<()> {
create_login_transaction(fee_rate)?;
pub fn login(message_id: u32, outgoing_pairing_transaction: String) -> ApiResult<()> {
let mut local_device = lock_local_device()?;
let pairing_transaction: Transaction =
deserialize(&Vec::from_hex(&outgoing_pairing_transaction)?)?;
let pairing_transaction_txid = pairing_transaction.txid();
let messages = lock_messages()?;
let login_output = pairing_transaction
.output
.first()
.expect("Transaction has at least one output");
let new_remote_key = XOnlyPublicKey::from_slice(&login_output.script_pubkey.as_bytes()[2..])?;
let new_session_key: SecretKey;
let notification_outpoint: OutPoint;
if let Some(message) = messages.iter().find(|m| m.id == message_id) {
notification_outpoint = message
.commited_in
.expect("message without commitment outpoint");
let sp_wallet = local_device.get_wallet();
let spendable_outputs = sp_wallet.get_outputs().to_spendable_list();
let output = spendable_outputs
.get(&notification_outpoint)
.ok_or(ApiError {
message: "Unknown outpoint".to_owned(),
})?;
let spend_key = sp_wallet.get_client().try_get_secret_spend_key()?;
let tweak = SecretKey::from_slice(&Vec::from_hex(&output.tweak)?)?;
new_session_key = spend_key.add_tweak(&tweak.into())?;
} else {
return Err(ApiError {
message: "Unknown message".to_owned(),
});
}
local_device.update_session(
new_session_key,
notification_outpoint,
OutPoint::new(pairing_transaction_txid, 1),
new_remote_key,
OutPoint::new(pairing_transaction_txid, 0),
OutPoint::new(notification_outpoint.txid, 1),
);
Ok(())
}
#[wasm_bindgen]
pub fn logout() -> ApiResult<()> {
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
Ok(())
unimplemented!();
}
#[wasm_bindgen]
pub fn dump_watch_only_wallet() -> ApiResult<String> {
pub fn dump_wallet() -> ApiResult<String> {
let device = lock_local_device()?;
Ok(serde_json::to_string(device.get_watch_only()).unwrap())
Ok(serde_json::to_string(device.get_wallet()).unwrap())
}
#[wasm_bindgen]
@ -441,12 +467,15 @@ pub fn dump_message_cache() -> ApiResult<Vec<String>> {
Ok(res)
}
#[wasm_bindgen]
pub fn dump_device() -> ApiResult<String> {
let local_device = lock_local_device()?;
Ok(serde_json::to_string(&local_device.clone())?)
}
#[wasm_bindgen]
pub fn reset_device() -> ApiResult<()> {
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
let mut device = lock_local_device()?;
*device = Device::default();
@ -456,32 +485,6 @@ pub fn reset_device() -> ApiResult<()> {
Ok(())
}
#[wasm_bindgen]
pub fn encrypt_remote_key(
remote_key: String,
aes_key: Option<String>,
nonce: Option<String>,
) -> ApiResult<String> {
let mut device = lock_local_device()?;
let encrypted_key: Vec<u8>;
if let (Some(key_hex), Some(nonce_hex)) = (aes_key, nonce) {
// check that the hex we got for key and nonce is valid
let mut key = [0u8; 32];
let mut nonce = [0u8; 12];
key.copy_from_slice(&Vec::from_hex(&key_hex)?);
nonce.copy_from_slice(&Vec::from_hex(&nonce_hex)?);
encrypted_key = device.encrypt_for_remote_device(
SecretKey::from_str(&remote_key)?,
Some(key),
Some(nonce),
)?;
} else {
encrypted_key =
device.encrypt_for_remote_device(SecretKey::from_str(&remote_key)?, None, None)?;
}
Ok(encrypted_key.to_lower_hex_string())
}
fn handle_recover_transaction(
updated: HashMap<OutPoint, OwnedOutput>,
tx: &Transaction,
@ -536,10 +539,10 @@ fn handle_recover_transaction(
// If we are receiver, that's pretty much it
message.status = CachedMessageStatus::Trusted;
return Ok(message.clone());
} else if let Some(message) = messages
.iter_mut()
.find(|m| m.commited_in == Some(input.previous_output))
{
} else if let Some(message) = messages.iter_mut().find(|m| {
m.commited_in == Some(input.previous_output)
&& m.status == CachedMessageStatus::SentWaitingConfirmation
}) {
// sender needs to spent it back again to receiver
let (outpoint, output) = utxo_created.into_iter().next().unwrap();
@ -558,10 +561,10 @@ fn handle_recover_transaction(
);
let shared_secret = AnkSharedSecret::new(shared_point);
debug!(
"Shared secret: {}",
shared_secret.to_byte_array().to_lower_hex_string()
);
// debug!(
// "Shared secret: {}",
// shared_secret.to_byte_array().to_lower_hex_string()
// );
let mut plaintext: Vec<u8> = vec![];
if let Some(message) = messages.iter_mut().find(|m| {
@ -580,6 +583,10 @@ fn handle_recover_transaction(
let cipher_msg: CipherMessage = serde_json::from_slice(&plaintext)?;
message.commited_in = Some(outpoint.clone());
// freeze the commitment utxo
let mut freezed_utxos = lock_freezed_utxos()?;
freezed_utxos.insert(*outpoint);
message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
message.commitment = Some(commitment_str);
message.plaintext.push(cipher_msg.message);
@ -597,6 +604,10 @@ fn handle_recover_transaction(
.next()
.expect("utxo_created shouldn't be empty");
new_msg.commited_in = Some(outpoint.clone());
// freeze the commitment utxo
let mut freezed_utxos = lock_freezed_utxos()?;
freezed_utxos.insert(*outpoint);
new_msg.commitment = Some(commitment.to_lower_hex_string());
new_msg.recipient = Some(sp_wallet.get_client().get_receiving_address());
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
@ -614,7 +625,9 @@ fn handle_recover_transaction(
return false;
}
match m.status {
CachedMessageStatus::SentWaitingConfirmation => {
CachedMessageStatus::SentWaitingConfirmation
| CachedMessageStatus::Pairing
| CachedMessageStatus::Login => {
// commitment we're looking for is simply what's in the message
m.commitment
.as_ref()
@ -670,7 +683,7 @@ fn process_transaction(
let tweak_data = PublicKey::from_str(&tweak_data_hex)?;
let mut device = lock_local_device()?;
let wallet = device.get_watch_only_mut();
let wallet = device.get_mut_wallet();
let updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?;
if updated.len() > 0 {
@ -721,18 +734,16 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
// let's try to decrypt with keys we found in transactions but haven't used yet
let mut messages = lock_messages()?;
let cipher = Vec::from_hex(&ank_msg.content.trim_matches('\"'))?;
if let Some(message) = messages.iter_mut().find(|m| {
// debug!("Trying message: {:?}", m);
match m.status {
CachedMessageStatus::TxWaitingCipher | CachedMessageStatus::Trusted => {
m.try_decrypt_cipher(cipher.clone()).is_ok()
}
_ => return false,
if let Some(message) = messages.iter_mut().find(|m| match m.status {
CachedMessageStatus::TxWaitingCipher | CachedMessageStatus::Trusted => {
m.try_decrypt_cipher(cipher.clone()).is_ok()
}
_ => return false,
}) {
let plain = message.try_decrypt_cipher(cipher).unwrap();
let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?;
debug!("Found message {}", String::from_utf8(plain.clone())?);
if message.status == CachedMessageStatus::TxWaitingCipher {
let cipher_msg: CipherMessage = serde_json::from_slice(&plain)?;
// does the retrieved message match with the commited hash?
let hash = create_commitment(serde_json::to_string(&cipher_msg)?);
if Some(hash) != message.commitment {
@ -740,15 +751,24 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
message: "Message doesn't match commitment".to_owned(),
});
}
message.plaintext.push(cipher_msg.message);
message.sender = Some(cipher_msg.sender);
message.ciphertext = None;
message.status = CachedMessageStatus::ReceivedMustConfirm;
if cipher_msg.message.starts_with("PAIRING") {
// we don't follow the classic confirmation pattern
// set the status to sth else
// if we agree, we must send another notification to remote
// the notification output here will be spent as our first session
message.status = CachedMessageStatus::Pairing;
} else if cipher_msg.message.starts_with("LOGIN") {
message.status = CachedMessageStatus::Login;
} else {
message.status = CachedMessageStatus::ReceivedMustConfirm;
}
message.plaintext.push(cipher_msg.message);
} else {
// We're receiving a message for some action already engaged
// We could check the sender, but do we really care since we alredy trust it?
// Let's update the message by pushing what we just found out
message.plaintext.push(cipher_msg.message);
message.plaintext.push(String::from_utf8(plain)?);
}
return Ok(message.clone());
} else {
@ -772,7 +792,7 @@ pub fn parse_network_msg(raw: String, fee_rate: u32) -> ApiResult<CachedMessage>
#[wasm_bindgen]
pub fn get_outputs() -> ApiResult<JsValue> {
let device = lock_local_device()?;
let outputs = device.get_watch_only().get_outputs().clone();
let outputs = device.get_wallet().get_outputs().clone();
Ok(JsValue::from_serde(&outputs.to_outpoints_list())?)
}
@ -780,7 +800,7 @@ pub fn get_outputs() -> ApiResult<JsValue> {
pub fn get_available_amount() -> ApiResult<u64> {
let device = lock_local_device()?;
Ok(device.get_watch_only().get_outputs().get_balance().to_sat())
Ok(device.get_wallet().get_outputs().get_balance().to_sat())
}
#[derive(Debug, Tsify, Serialize, Deserialize, Default)]
@ -819,24 +839,26 @@ pub fn answer_confirmation_transaction(
let local_device = lock_local_device()?;
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), vec![])?;
let sp_wallet = local_device.get_wallet();
let recipient = Recipient {
address: sp_address.into(),
amount: Amount::from_sat(0), // we'll set amount to what's available in the confirmed_by output we don't want change
amount: DUST_THRESHOLD,
nb_outputs: 1,
};
let confirmed_by = message.confirmed_by.as_ref().unwrap();
let mut freezed_utxos = lock_freezed_utxos()?;
// we remove the outpoint we want to spend from our freezed utxos list
freezed_utxos.remove(&confirmed_by);
let signed_psbt = create_transaction(
&vec![confirmed_by],
&sp_wallet,
recipient,
&freezed_utxos,
sp_wallet,
vec![recipient],
None,
Amount::from_sat(fee_rate.into()),
message.recipient.clone(),
@ -877,24 +899,28 @@ pub fn create_confirmation_transaction(
let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?;
let current_outputs = lock_local_device()?.get_watch_only().get_outputs().clone();
let local_device = lock_local_device()?;
let spending_client = lock_spending_client()?.clone();
let sp_wallet = SpWallet::new(spending_client, Some(current_outputs), vec![])?;
let sp_wallet = local_device.get_wallet();
let recipient = Recipient {
address: sp_address.into(),
amount: Amount::from_sat(0),
amount: DUST_THRESHOLD,
nb_outputs: 1,
};
let commited_in = message.commited_in.as_ref().unwrap();
let mut freezed_utxos = lock_freezed_utxos()?;
// we remove the outpoint we want to spend from our freezed utxos list
freezed_utxos.remove(&commited_in);
let signed_psbt = create_transaction(
&vec![commited_in],
&freezed_utxos,
&sp_wallet,
recipient,
vec![recipient],
None,
Amount::from_sat(fee_rate.into()),
message.sender.clone(),
@ -921,64 +947,138 @@ pub fn create_confirmation_transaction(
})
}
#[wasm_bindgen]
pub fn create_login_transaction(fee_rate: u32) -> ApiResult<createTransactionReturn> {
// First thing is to check that all the revokation outpoints are still unspent
// We assume it has been checked before calling this function
let local_device = lock_local_device()?;
if !local_device.is_linked() {
return Err(ApiError {
message: "No linked device".to_owned(),
});
}
let sp_wallet = local_device.get_wallet();
let paired_device = local_device.get_paired_device_info().unwrap();
let mut outpoint_to_spend = local_device.get_next_output_to_spend();
if outpoint_to_spend == OutPoint::default() {
// First login
outpoint_to_spend = OutPoint::new(
Txid::from_byte_array(paired_device.incoming_pairing_transaction),
0,
);
}
debug!("outpoint_to_spend: {}", outpoint_to_spend);
debug!("outputs: {:?}", sp_wallet.get_outputs().to_outpoints_list());
let cipher_msg = CipherMessage::new(
local_device
.get_wallet()
.get_client()
.get_receiving_address(),
"LOGIN".to_owned(),
);
let commitment = create_commitment(serde_json::to_string(&cipher_msg)?);
let recipient_address = SilentPaymentAddress::try_from(paired_device.address.as_str()).unwrap();
let recipient = Recipient {
address: recipient_address.into(),
amount: Amount::from_sat(1200),
nb_outputs: 1,
};
let mut freezed_utxos = lock_freezed_utxos()?;
// we remove the outpoint we want to spend from our freezed utxos list
freezed_utxos.remove(&outpoint_to_spend);
let signed_psbt = create_transaction(
&vec![&outpoint_to_spend],
&freezed_utxos,
sp_wallet,
vec![recipient],
Some(Vec::from_hex(&commitment)?),
Amount::from_sat(fee_rate.into()),
None,
)?;
let partial_secret = sp_wallet
.get_client()
.get_partial_secret_from_psbt(&signed_psbt)?;
let shared_point = sp_utils::sending::calculate_ecdh_shared_secret(
&recipient_address.get_scan_key(),
&partial_secret,
);
let shared_secret = AnkSharedSecret::new(shared_point);
let cipher = encrypt_with_key(
serde_json::to_string(&cipher_msg)?,
shared_secret.to_byte_array().to_lower_hex_string(),
)?;
// update our cache
let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?;
let recipients_vouts = sp_address2vouts
.get::<String>(&recipient_address.into())
.expect("recipients didn't change")
.as_slice();
let final_tx = signed_psbt.extract_tx()?;
let mut new_msg = CachedMessage::new();
new_msg.plaintext.push(cipher_msg.message);
new_msg.ciphertext = Some(cipher);
new_msg.commitment = Some(commitment);
new_msg.commited_in = Some(OutPoint {
txid: final_tx.txid(),
vout: recipients_vouts[0] as u32,
});
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
new_msg.recipient = Some(recipient_address.into());
new_msg.sender = Some(cipher_msg.sender);
new_msg.tied_by = Some(1); // for now we just assume that's the second utxo, first being the notification itself
new_msg.status = CachedMessageStatus::Login;
lock_messages()?.push(new_msg.clone());
return Ok(createTransactionReturn {
txid: final_tx.txid().to_string(),
transaction: serialize(&final_tx).to_lower_hex_string(),
new_network_msg: new_msg,
});
}
#[wasm_bindgen]
pub fn create_pairing_transaction(
address: String,
fee_rate: u32,
) -> ApiResult<createTransactionReturn> {
let message: CipherMessage;
{
let mut spending_client = lock_spending_client()?;
let my_address = lock_local_device()?
.get_wallet()
.get_client()
.get_receiving_address();
let our_address = spending_client.get_receiving_address();
let spend_sk: SecretKey = spending_client.get_spend_key().try_into()?;
let cipher_message = CipherMessage::new(my_address, "PAIRING".to_owned());
message = CipherMessage::new(our_address, format!("{}", spend_sk.display_secret()));
}
let mut res = create_notification_transaction(address, cipher_message, fee_rate)?;
let mut res = create_notification_transaction(address, message, fee_rate);
// we slightly alter the message
if let Ok(ref mut new_msg) = res {
new_msg.new_network_msg.plaintext = vec![]; // we don't want to let that information available
new_msg.new_network_msg.status = CachedMessageStatus::Trusted;
} else {
return res;
}
// Update the msg in cache
let mut messages = lock_messages()?;
let mut updated = res.as_ref().unwrap().new_network_msg.clone();
let pos = messages.iter().position(|m| m.id == updated.id).unwrap();
// clone the cached message and erase the ciphertext
updated.ciphertext = None;
messages[pos] = updated;
// we forget our own wallet
let mut spending_client = lock_spending_client()?;
*spending_client = SpClient::default();
res
}
#[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);
if let Some(m) = messages.iter_mut().find(|m| m.id == res.new_network_msg.id) {
m.status = CachedMessageStatus::Pairing;
res.new_network_msg = m.clone();
} else {
unreachable!("We don't have in cache the message we just created");
}
create_notification_transaction(address, message, fee_rate)
Ok(res)
}
#[wasm_bindgen]
@ -991,11 +1091,7 @@ pub fn create_notification_transaction(
let local_device = lock_local_device()?;
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), vec![])?;
let sp_wallet = local_device.get_wallet();
let recipient = Recipient {
address: sp_address.into(),
@ -1005,10 +1101,13 @@ pub fn create_notification_transaction(
let commitment = create_commitment(serde_json::to_string(&cipher_message)?);
let freezed_utxos = lock_freezed_utxos()?;
let signed_psbt = create_transaction(
&vec![],
&sp_wallet,
recipient,
&freezed_utxos,
sp_wallet,
vec![recipient],
Some(Vec::from_hex(&commitment)?),
Amount::from_sat(fee_rate.into()),
None,
@ -1025,11 +1124,6 @@ pub fn create_notification_transaction(
let shared_secret = AnkSharedSecret::new(shared_point);
// debug!(
// "Created transaction with secret {}",
// shared_secret.to_byte_array().to_lower_hex_string()
// );
let cipher = encrypt_with_key(
serde_json::to_string(&cipher_message)?,
shared_secret.to_byte_array().to_lower_hex_string(),
@ -1054,6 +1148,7 @@ pub fn create_notification_transaction(
new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string());
new_msg.recipient = Some(address);
new_msg.sender = Some(cipher_message.sender);
new_msg.tied_by = Some(1); // for now we just assume that's the second utxo, first being the notification itself
new_msg.status = CachedMessageStatus::SentWaitingConfirmation;
lock_messages()?.push(new_msg.clone());
@ -1135,7 +1230,7 @@ pub fn try_decrypt_with_key(cipher: String, key: String) -> ApiResult<String> {
#[wasm_bindgen]
pub fn create_faucet_msg() -> ApiResult<CachedMessage> {
let sp_address = lock_local_device()?
.get_watch_only()
.get_wallet()
.get_client()
.get_receiving_address();

View File

@ -1,13 +1,18 @@
use anyhow::{Error, Result};
use rand::{self, thread_rng, Rng, RngCore};
use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize};
use sdk_common::sp_client::bitcoin::hashes::{Hash, HashEngine};
use sdk_common::sp_client::bitcoin::hex::{DisplayHex, FromHex};
use sdk_common::sp_client::bitcoin::key::{Parity, Secp256k1};
use sdk_common::sp_client::bitcoin::secp256k1::{PublicKey, SecretKey, ThirtyTwoByteHash};
use sdk_common::sp_client::bitcoin::{Network, OutPoint, ScriptBuf};
use sdk_common::sp_client::bitcoin::{
Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey,
};
use sdk_common::sp_client::spclient::SpClient;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tsify::Tsify;
use wasm_bindgen::convert::VectorFromWasmAbi;
use wasm_bindgen::prelude::*;
use std::collections::HashMap;
@ -30,172 +35,165 @@ use sdk_common::crypto::{
pub static LOCAL_DEVICE: OnceLock<Mutex<Device>> = OnceLock::new();
pub fn set_new_device(sp_wallet: SpWallet) -> Result<String> {
let mut device = Device::new(sp_wallet);
let mut local_device = lock_local_device()?;
if *local_device.get_wallet().get_client() != SpClient::default() {
return Err(Error::msg("Device already initialized".to_owned()));
} else {
*local_device = device;
}
let our_address = local_device
.get_wallet()
.get_client()
.get_receiving_address();
Ok(our_address)
}
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, Serialize, Deserialize, Clone, Default, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct PairedDevice {
pub address: String,
pub outgoing_pairing_transaction: [u8; 32],
pub revokation_index: u32,
pub incoming_pairing_transaction: [u8; 32],
pub current_remote_key: [u8; 32],
pub current_session_outpoint: OutPoint, // This will be spend by remote device to notify us of next login
pub current_session_revokation_outpoint: OutPoint, // remote device can revoke current session by spending this
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RevokeOutput {
key: [u8; 32],
spk: ScriptBuf,
outpoint: OutPoint,
}
impl PairedDevice {
pub fn new(address: SilentPaymentAddress, pairing_txid: Txid, revokation_index: u32) -> Self {
let mut pairing_transaction_buf = [0u8; 32];
pairing_transaction_buf.copy_from_slice(&serialize(&pairing_txid));
impl RevokeOutput {
pub fn new(key: [u8; 32], spk: ScriptBuf, outpoint: OutPoint) -> Self {
Self { key, spk, outpoint }
Self {
address: address.into(),
outgoing_pairing_transaction: pairing_transaction_buf,
revokation_index,
incoming_pairing_transaction: [0u8; 32],
current_session_revokation_outpoint: OutPoint::default(),
current_session_outpoint: OutPoint::default(),
current_remote_key: [0u8; 32],
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
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>,
sp_wallet: SpWallet,
current_session_outpoint: OutPoint, // This is the notification output of incoming login tx
current_session_key: [u8; 32],
current_session_revokation_outpoint: OutPoint, // This is the revokation outpoint of outgoing login tx
paired_device: Option<PairedDevice>,
}
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, vec![]).expect("watch_only_wallet creation failed");
let spend_sk_cipher = vec![];
let remote_device_key = [0; 32];
pub fn new(sp_wallet: SpWallet) -> Self {
Self {
watch_only_wallet,
spend_sk_cipher,
remote_device_key,
remote_address: None,
revokation_output: None,
sp_wallet,
current_session_outpoint: OutPoint::default(),
current_session_key: [0u8; 32],
current_session_revokation_outpoint: OutPoint::default(),
paired_device: None,
}
}
pub fn get_watch_only(&self) -> &SpWallet {
&self.watch_only_wallet
pub fn get_wallet(&self) -> &SpWallet {
&self.sp_wallet
}
pub fn get_watch_only_mut(&mut self) -> &mut SpWallet {
&mut self.watch_only_wallet
pub fn get_mut_wallet(&mut self) -> &mut SpWallet {
&mut self.sp_wallet
}
pub fn is_linked(&self) -> bool {
self.remote_address.is_some() && self.spend_sk_cipher.len() != 0
self.paired_device.is_some()
}
pub fn get_remote_address(&self) -> Option<String> {
self.remote_address.clone()
pub fn is_pairing(&self) -> bool {
self.current_session_key == [0u8; 32]
}
pub fn get_encrypted_key(&self) -> Vec<u8> {
self.spend_sk_cipher.clone()
pub fn get_paired_device_info(&self) -> Option<PairedDevice> {
self.paired_device.clone()
}
pub fn encrypt_for_remote_device(
&mut self,
remote_spend_sk: SecretKey,
aes_key: Option<[u8; 32]>,
nonce: Option<[u8; 12]>,
) -> Result<Vec<u8>> {
let encryption: Aes256Encryption;
match (aes_key, nonce) {
(Some(key), Some(nonce)) => {
encryption = Aes256Encryption::import_key(
Purpose::ThirtyTwoBytes,
remote_spend_sk.secret_bytes().to_vec(),
key,
nonce,
)?;
}
(None, None) => {
encryption = Aes256Encryption::new(
Purpose::ThirtyTwoBytes,
remote_spend_sk.secret_bytes().to_vec(),
)?;
}
_ => {
return Err(Error::msg(
"aes_key and nonce must either both be set or empty",
))
}
}
let remote_spend_sk_cipher = encryption.encrypt_with_aes_key();
self.remote_device_key = encryption.export_key();
remote_spend_sk_cipher
pub fn get_next_output_to_spend(&self) -> OutPoint {
self.current_session_outpoint
}
pub fn get_session_revokation_outpoint(&self) -> OutPoint {
self.current_session_revokation_outpoint
}
pub fn sign_with_current_session_key(&self) -> Result<()> {
unimplemented!();
}
pub fn encrypt_with_current_session_key(&self) -> Result<()> {
unimplemented!();
}
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"));
link_with: SilentPaymentAddress,
outgoing_pairing_tx: Txid,
revokation_output: u32,
incoming_pairing_tx: Txid,
) -> Result<()> {
let address_looked_for: String = link_with.into();
if let Some(paired_device) = self.paired_device.as_ref() {
return Err(Error::msg(format!(
"Found an already paired device with address {} and revokable by {}:{}",
paired_device.address,
Txid::from_byte_array(paired_device.outgoing_pairing_transaction),
paired_device.revokation_index
)));
} else {
let mut new_device =
PairedDevice::new(link_with, outgoing_pairing_tx, revokation_output);
new_device.incoming_pairing_transaction = incoming_pairing_tx.to_byte_array();
self.paired_device = Some(new_device);
}
let device = lock_local_device()?;
let watch_only = device.get_watch_only().get_client();
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,
};
let spending_client =
SpClient::new(label, scan_sk, SpendKey::Secret(spend_sk), None, network)?;
// 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"));
}
*locked_client = spending_client;
Ok(())
}
pub fn logout() -> Result<()> {
if let Ok(mut client) = lock_spending_client() {
*client = SpClient::default();
Ok(())
} else {
Err(Error::msg("Failed to lock CONNECTED_USER"))
// We call that when we spent to the remote device and it similarly spent to us
pub fn update_session(
&mut self,
new_session_key: SecretKey,
new_session_outpoint: OutPoint,
new_revokation_outpoint: OutPoint,
new_remote_key: XOnlyPublicKey,
new_remote_session_outpoint: OutPoint,
new_remote_revokation_outpoint: OutPoint,
) -> Result<()> {
if !self.is_linked() {
return Err(Error::msg("Can't update an unpaired device"));
}
self.paired_device
.as_mut()
.map(|d| {
d.current_remote_key = new_remote_key.serialize();
d.current_session_outpoint = new_remote_session_outpoint;
d.current_session_revokation_outpoint = new_remote_revokation_outpoint;
});
self.current_session_key = new_session_key.secret_bytes();
self.current_session_outpoint = new_session_outpoint;
self.current_session_revokation_outpoint = new_revokation_outpoint;
Ok(())
}
}

View File

@ -1,10 +1,26 @@
use std::{
collections::HashSet,
sync::{Mutex, MutexGuard, OnceLock},
};
use anyhow::Error;
use rand::Rng;
use sdk_common::sp_client::{
bitcoin::Network,
bitcoin::{Network, OutPoint},
silentpayments::utils::SilentPaymentAddress,
spclient::{derive_keys_from_seed, SpClient, SpWallet, SpendKey},
};
use crate::MutexExt;
pub static FREEZED_UTXOS: OnceLock<Mutex<HashSet<OutPoint>>> = OnceLock::new();
pub fn lock_freezed_utxos() -> Result<MutexGuard<'static, HashSet<OutPoint>>, Error> {
FREEZED_UTXOS
.get_or_init(|| Mutex::new(HashSet::new()))
.lock_anyhow()
}
pub fn generate_sp_wallet(label: Option<String>, network: Network) -> anyhow::Result<SpWallet> {
let mut seed = [0u8; 64];
rand::thread_rng().fill(&mut seed);

File diff suppressed because one or more lines are too long