From 287f74136e7ec10252d2f291617f7b7a3cf93d90 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 28 Aug 2024 09:51:40 +0200 Subject: [PATCH] Implement minimal prd/pcd, remove challenges --- Cargo.toml | 6 +- src/api.rs | 918 ++++++++++++--------------------------------- src/lib.rs | 45 +++ src/process.rs | 405 -------------------- src/user.rs | 1 + src/wallet.rs | 2 +- tests/challenge.rs | 153 -------- tests/pairing.rs | 303 +++++---------- tests/prd.rs | 81 ---- tests/utils.rs | 17 +- 10 files changed, 391 insertions(+), 1540 deletions(-) delete mode 100644 src/process.rs delete mode 100644 tests/challenge.rs delete mode 100644 tests/prd.rs diff --git a/Cargo.toml b/Cargo.toml index 4324f32..23ae408 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,9 @@ wasm-bindgen = "0.2.91" getrandom = { version="0.2.12", features = ["js"] } wasm-logger = "0.2.0" rand = "0.8.5" -log = "0.4.6" tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } -# sdk_common = { path = "../sdk_common" } -sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" } -shamir = { git = "https://github.com/Sosthene00/shamir", branch = "master" } +sdk_common = { path = "../sdk_common" } +# sdk_common = { git = "https://git.4nkweb.com/4nk/sdk_common.git", branch = "dev" } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/src/api.rs b/src/api.rs index 0ecfd7a..6ee8900 100644 --- a/src/api.rs +++ b/src/api.rs @@ -2,20 +2,21 @@ use std::any::Any; use std::borrow::Borrow; use std::collections::HashMap; use std::io::Write; +use std::ops::Index; use std::str::FromStr; use std::string::FromUtf8Error; use std::sync::{Mutex, OnceLock, PoisonError}; use std::time::{Duration, Instant}; -use log::{debug, warn}; +use sdk_common::log::{debug, warn}; use rand::{thread_rng, Fill, Rng, RngCore}; use anyhow::Error as AnyhowError; use sdk_common::crypto::{ AeadCore, Aes256Decryption, Aes256Encryption, Aes256Gcm, AnkSharedSecret, KeyInit, Purpose, }; -use sdk_common::device::{REVOKATION_INDEX, SESSION_INDEX}; -use sdk_common::process::{Member, PairingPcd, Process, Role, ValidationRules}; +use sdk_common::process::{Process, ValidationRules}; +use sdk_common::signature; use sdk_common::sp_client::bitcoin::blockdata::fee_rate; use sdk_common::sp_client::bitcoin::consensus::{deserialize, serialize}; use sdk_common::sp_client::bitcoin::hashes::HashEngine; @@ -41,16 +42,18 @@ use sdk_common::sp_client::silentpayments::{ utils::{Network as SpNetwork, SilentPaymentAddress}, Error as SpError, }; -use serde_json::{Error as SerdeJsonError, Value}; +use sdk_common::uuid::Uuid; +use serde_json::{Error as SerdeJsonError, Map, Value}; use serde::{Deserialize, Serialize}; use tsify::{JsValueSerdeExt, Tsify}; -use wasm_bindgen::convert::FromWasmAbi; +use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi}; use wasm_bindgen::prelude::*; use sdk_common::network::{ - self, AnkFlag, CachedMessage, CachedMessageStatus, Envelope, FaucetMessage, NewTxMessage, Pcd, Prd, PrdType -}; + self, AnkFlag, CachedMessage, CachedMessageStatus, Envelope, FaucetMessage, NewTxMessage}; +use sdk_common::pcd::{AnkPcdHash, Pcd, Member}; +use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient, @@ -59,12 +62,13 @@ use sdk_common::sp_client::spclient::{SpWallet, SpendKey}; use sdk_common::device::Device; use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; -use crate::{lock_messages, CACHEDMESSAGES}; +use crate::{lock_messages, lock_processes, ProcessState, ProcessStatus, RelevantProcess, CACHEDMESSAGES, CACHEDPROCESSES}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; pub type ApiResult = Result; const IS_TESTNET: bool = true; +const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); #[derive(Debug, PartialEq, Eq)] pub struct ApiError { @@ -188,15 +192,6 @@ pub fn get_address() -> ApiResult { .get_receiving_address()) } -#[wasm_bindgen] -pub fn restore_device_from_sp_wallet(sp_wallet: String) -> ApiResult { - let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?; - - let our_address = set_new_device(sp_wallet)?; - - Ok(our_address) -} - #[wasm_bindgen] pub fn restore_device(device_str: String) -> ApiResult<()> { let device: Device = serde_json::from_str(&device_str)?; @@ -208,6 +203,15 @@ pub fn restore_device(device_str: String) -> ApiResult<()> { Ok(()) } +#[wasm_bindgen] +pub fn create_device_from_sp_wallet(sp_wallet: String) -> ApiResult { + let sp_wallet: SpWallet = serde_json::from_str(&sp_wallet)?; + + let our_address = set_new_device(sp_wallet)?; + + Ok(our_address) +} + #[wasm_bindgen] pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult { let sp_wallet = generate_sp_wallet(None, Network::from_core_arg(&network_str)?)?; @@ -218,77 +222,20 @@ pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult ApiResult { +pub fn pair_device(uuid: String, new_device_address: String, state: String) -> ApiResult<()> { let mut local_device = lock_local_device()?; - // check that we're still in pairing phase - if !local_device.is_pairing() { + if local_device.is_linked() { return Err(ApiError { message: "Already paired".to_owned(), }); } - let mut messages = lock_messages()?; + local_device.set_process_uuid(Uuid::parse_str(&uuid).map_err(|e| ApiError { message: e.to_string() })?); + local_device.push_paired_device(new_device_address.try_into().map_err(|_| ApiError { message: "Invalid address".to_owned() })?); + local_device.update_latest_state(serde_json::from_str(&state)?); - 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; - - let pairing_pcd = PairingPcd::new( - nym, - local_device.get_wallet().get_client().get_receiving_address().try_into().unwrap(), - local_device.get_paired_device_info().unwrap().address.try_into().unwrap(), - SESSION_INDEX, - REVOKATION_INDEX, - Txid::from_str(&incoming_pairing_txid)?, - pairing_tx.txid - ); - - let pairing_process = Process::new_pairing_process(pairing_pcd, pairing_tx.txid); - - Ok(pairing_process) - } else { - return Err(ApiError { - message: format!("Can't find message with id {}", message_id), - }); - } + Ok(()) } #[derive(Debug, Tsify, Serialize, Deserialize)] @@ -302,57 +249,6 @@ impl outputs_list { } } -#[wasm_bindgen] -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(¬ification_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<()> { unimplemented!(); @@ -427,14 +323,15 @@ pub fn reset_device() -> ApiResult<()> { fn handle_transaction( updated: HashMap, tx: &Transaction, - sp_wallet: &mut SpWallet, tweak_data: PublicKey, ) -> anyhow::Result { + let device = lock_local_device()?; + let sp_wallet = device.get_wallet(); + let op_return = tx.output.iter().find(|o| o.script_pubkey.is_op_return()); - let commitment = if op_return.is_none() { - vec![] - } else { - op_return.unwrap().script_pubkey.as_bytes()[2..].to_vec() + let mut commitment = [0u8; 32]; + if op_return.is_some() { + commitment.copy_from_slice(&op_return.unwrap().script_pubkey.as_bytes()[2..]); }; let commitment_str = commitment.to_lower_hex_string(); @@ -453,58 +350,12 @@ fn handle_transaction( // empty utxo_destroyed means we received this transaction if utxo_destroyed.is_empty() { - // We first check for faucet transactions - if let Some(message) = messages.iter_mut().find(|m| { - if m.status == CachedMessageStatus::FaucetWaiting { - m.commitment.as_ref() == Some(&commitment_str) - } else { - false - } - }) { - message.status = CachedMessageStatus::Closed; - message.commited_in = utxo_created - .into_iter() - .next() - .map(|(outpoint, _)| *outpoint); - return Ok(message.clone()); - } - - // we inspect inputs looking for links with previous tx - for input in tx.input.iter() { - if let Some(message) = messages - .iter_mut() - .find(|m| m.confirmed_by == Some(input.previous_output)) - { - // 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) - && m.status == CachedMessageStatus::SentWaitingConfirmation - }) { - // sender needs to spent it back again to receiver - let (outpoint, output) = utxo_created.into_iter().next().unwrap(); - - message.confirmed_by = Some(outpoint.clone()); - message.status = CachedMessageStatus::MustSpendConfirmation; - - // Caller must interpret this message as "do spend confirmed_by outpoint to receiver" - return Ok(message.clone()); - } - } - - // if we've found nothing we are being notified let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret( &tweak_data, &sp_wallet.get_client().get_scan_key(), ); let shared_secret = AnkSharedSecret::new(shared_point); - // debug!( - // "Shared secret: {}", - // shared_secret.to_byte_array().to_lower_hex_string() - // ); - let mut plaintext: Vec = vec![]; if let Some(message) = messages.iter_mut().find(|m| { if m.status != CachedMessageStatus::CipherWaitingTx { @@ -520,17 +371,13 @@ fn handle_transaction( }) { let (outpoint, output) = utxo_created.into_iter().next().unwrap(); - let cipher_msg: Prd = 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); + let prd = Prd::extract_from_message(&plaintext, commitment)?; - message.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); + message.prd = Some(prd.to_string()); + message.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); message.commitment = Some(commitment_str); - message.sender = Some(cipher_msg.sender); - message.recipient = Some(sp_wallet.get_client().get_receiving_address()); - message.status = CachedMessageStatus::ReceivedMustConfirm; + message.sender = Some(serde_json::from_str(&prd.sender)?); + message.status = CachedMessageStatus::Opened; return Ok(message.clone()); } else { @@ -540,71 +387,15 @@ fn handle_transaction( .into_iter() .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()); + new_msg.shared_secrets.push(shared_secret.to_byte_array().to_lower_hex_string()); new_msg.status = CachedMessageStatus::TxWaitingPrd; messages.push(new_msg.clone()); return Ok(new_msg.clone()); } } else { - // We are sender of a transaction - // We only need to return the message - // eiter this is notification, a challenge, or response to a challenge - // if notification, commitment is the same than in the message - if let Some(message) = messages.iter().find(|m| { - if commitment.is_empty() || m.commitment.is_none() { - return false; - } - match m.status { - CachedMessageStatus::SentWaitingConfirmation - | CachedMessageStatus::Pairing - | CachedMessageStatus::Login => { - // commitment we're looking for is simply what's in the message - m.commitment - .as_ref() - .map(|c| Vec::from_hex(&c).unwrap()) - .unwrap() - == commitment - } - CachedMessageStatus::ReceivedMustConfirm => { - // we look for a message that has commited_in as input of the transaction - if let Some(_) = tx - .input - .iter() - .find(|i| Some(i.previous_output) == m.commited_in) - { - return true; - } else { - return false; - } - } - CachedMessageStatus::MustSpendConfirmation | CachedMessageStatus::Trusted => { - // we look for a message that has confirm_by as input - if let Some(_) = tx - .input - .iter() - .find(|i| Some(i.previous_output) == m.confirmed_by) - { - return true; - } else { - return false; - } - } - _ => return false, - } - }) { - return Ok(message.clone()); - } else { - return Err(anyhow::Error::msg( - "Failed to find the message for one of our transactions", - )); - } + // We're sender of the transaction, do nothing + return Ok(CachedMessage::new()); } } @@ -619,12 +410,15 @@ fn process_transaction( let tweak_data = PublicKey::from_str(&tweak_data_hex)?; - let mut device = lock_local_device()?; - let wallet = device.get_mut_wallet(); - let updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + let updated: HashMap; + { + let mut device = lock_local_device()?; + let wallet = device.get_mut_wallet(); + updated = wallet.update_wallet_with_transaction(&tx, blockheight, tweak_data)?; + } if updated.len() > 0 { - let updated_msg = handle_transaction(updated, &tx, wallet, tweak_data)?; + let updated_msg = handle_transaction(updated, &tx, tweak_data)?; return Ok(Some(updated_msg)); } @@ -658,64 +452,74 @@ pub fn parse_cipher(cipher_msg: String, fee_rate: u32) -> ApiResult { - m.try_decrypt_prd(cipher.clone()).is_ok() - } - CachedMessageStatus::GotPrdWaitingPcd => { - m.try_decrypt_pcd(cipher.clone()).is_ok() + CachedMessageStatus::TxWaitingPrd | CachedMessageStatus::Opened => { + if let Ok(m) = m.try_decrypt_message(cipher.clone()) { + plain = m; + return true; + } else { return false } } _ => return false, }) { if message.status == CachedMessageStatus::TxWaitingPrd { - let plain = message.try_decrypt_prd(cipher).unwrap(); - debug!("Found message {}", String::from_utf8(plain.clone())?); - let prd: Prd = serde_json::from_slice(&plain)?; - // does the retrieved message match with the commited hash? - let hash = create_commitment(serde_json::to_string(&prd)?); - if Some(hash) != message.commitment { + // debug!("Found message {}", String::from_utf8(plain.clone())?); + let mut commitment = [0u8; 32]; + commitment.copy_from_slice(&Vec::from_hex(message.commitment.as_ref().unwrap())?); + let prd = Prd::extract_from_message(&plain, commitment)?; + + message.prd = Some(prd.to_string()); + message.sender = Some(serde_json::from_str(&prd.sender)?); + message.status = CachedMessageStatus::Opened; + } else { + // debug!("Found message {}", String::from_utf8(plain.clone())?); + // we're receiving a pcd for a prd we already have + let mut pcd = Value::from_str(&String::from_utf8(plain)?)?; + // check that the hash of the pcd is the same than commited in the prd + let pcd_commitment = AnkPcdHash::from_value(&pcd); + let prd: Prd = serde_json::from_str(message.prd.as_ref().unwrap()).unwrap(); + if pcd_commitment.to_string() != prd.pcd_commitment { return Err(ApiError { - message: "Prd doesn't match commitment".to_owned(), + message: format!("Pcd doesn't match commitment: expected {:?}\ngot {}", prd.pcd_commitment, pcd_commitment), }); } - message.prd_type = prd.prd_type.clone(); - message.sender = Some(prd.sender.clone()); - message.pcd_commitment = Some(prd.pcd_commitment.to_string()); - message.prd_cipher = None; - message.prd = Some(prd); - message.status = CachedMessageStatus::GotPrdWaitingPcd; - } else { - let plain = message.try_decrypt_pcd(cipher).unwrap(); - debug!("Found message {}", String::from_utf8(plain.clone())?); - // we're receiving a pcd for a prd we already have - let pcd: Pcd = serde_json::from_slice(&plain)?; - // check that the hash of the pcd is the same than commited in the prd - let pcd_commitment = create_commitment(pcd.to_string()); - if Some(&pcd_commitment) != message.pcd_commitment.as_ref() { - return Err(ApiError { - message: format!("Pcd doesn't match commitment: expected {:?}\ngot {}", message.pcd_commitment.as_ref(), pcd_commitment), - }); + message.pcd = Some(pcd.clone()); + + // Now we decrypt all we can from the pcd + pcd.decrypt_fields(&prd.keys)?; + + // we take a few mandatory informations + let process = pcd["process"].take(); + let uuid = Uuid::parse_str(&prd.process_uuid).expect("prd can't have an invalid uuid"); + // todo check that the uuid in the process we took from pcd is consistent + + // we complete the process we keep in storage + let mut processes = lock_processes()?; + + match prd.prd_type { + PrdType::Init => { + let relevant_process = RelevantProcess { + process: serde_json::from_value(process)?, + states: vec![ProcessState { + commited_in: OutPoint::null(), // At this point process is not commited yet + encrypted_pcd: message.pcd.clone().unwrap(), + keys: prd.keys.clone(), + validation_token: vec![], + }], + current_status: ProcessStatus::Active(message.shared_secrets.clone()) + }; + processes.insert(uuid, relevant_process); + }, + _ => unimplemented!() } - if pcd.message.starts_with("PAIRING") { - // we don't follow the classic confirmation pattern - // 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 pcd.message.starts_with("LOGIN") { - message.status = CachedMessageStatus::Login; - } else { - message.status = CachedMessageStatus::ReceivedMustConfirm; - } - message.pcd = Some(pcd); - message.pcd_cipher = None; } return Ok(message.clone()); } else { // let's keep it in case we receive the transaction later let mut new_msg = CachedMessage::new(); new_msg.status = CachedMessageStatus::CipherWaitingTx; - new_msg.prd_cipher = Some(cipher_msg); + new_msg.cipher.push(cipher_msg); messages.push(new_msg.clone()); return Ok(new_msg); } @@ -741,344 +545,102 @@ pub fn get_available_amount() -> ApiResult { pub struct createTransactionReturn { pub txid: String, pub transaction: String, - pub new_network_msg: CachedMessage, + pub new_messages: Vec +} + +#[derive(Debug, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct CreateProcessInitTransactionArguments{ + pub member2fields: HashMap>, + pub process: Process, + pub stringified_pcd: String, // must be valid json } -/// This is what we call to answer a confirmation as a sender #[wasm_bindgen] -pub fn answer_confirmation_transaction( - message_id: u32, +pub fn create_process_init_transaction( + args: CreateProcessInitTransactionArguments, fee_rate: u32, ) -> ApiResult { - let mut messages = lock_messages()?; - let message: &mut CachedMessage; - if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { - if m.sender.is_none() || m.commited_in.is_none() { - return Err(ApiError { - message: "Invalid network message".to_owned(), + if args.member2fields.len() == 0 { + return Err(ApiError { + message: "Must have at least one recipient".to_owned(), + }); + } + + let pcd: Map; + if let Some(object) = Value::from_str(&args.stringified_pcd)?.as_object() { + pcd = object.to_owned(); + } else { + return Err(ApiError { + message: "provided pcd is not a valid json".to_owned(), + }); + } + + let mut process = args.process; + + // process doesn't have an initial state at this stage + if process.init_state != Value::Null { + return Err(ApiError { + message: "new process must have a null initial state".to_owned(), + }); + } + + // maybe the process script can embed some basic checks for the pcd, for example `pcd.members.len() == 1` + + let all_members: Vec<&Member> = args.member2fields.keys().collect(); + let nb_recipients = all_members.len(); + + let mut recipients: Vec = Vec::with_capacity(nb_recipients*2); + // we actually have 2 "recipients" in a technical sense for each social recipient + // that's necessary because we don't want to miss a notification because we don't have a device atm + for member in all_members { + let (address_a, address_b) = member.get_addresses(); + for sp_address in [address_a, address_b].into_iter() { + recipients.push(Recipient { + address: sp_address.into(), + amount: DEFAULT_AMOUNT, + nb_outputs: 1, }); } + } - message = m; - } else { + let mut fields2keys = Map::new(); + let mut fields2cipher = Map::new(); + Value::from_str(&args.stringified_pcd).unwrap().encrypt_fields(&mut fields2keys, &mut fields2cipher); + + process.init_state = Value::Object(fields2cipher.clone()); + + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + + let latest_state = local_device.get_latest_state(); + if latest_state == Value::Null { return Err(ApiError { - message: format!("Can't find message for id {}", message_id), + message: "Device not paired, nor pairing".to_owned(), }); } - let sp_address: SilentPaymentAddress = - message.recipient.as_ref().unwrap().as_str().try_into()?; - - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - - let recipient = Recipient { - address: sp_address.into(), - 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], - &freezed_utxos, - sp_wallet, - vec![recipient], - None, - Amount::from_sat(fee_rate.into()), - message.recipient.clone(), + let sender: Member = serde_json::from_value( + latest_state.get("members") + .unwrap() + .as_array() + .ok_or(ApiError { message: "members must be an array".to_owned() })? + .get(0) + .ok_or(ApiError { message: "empty array".to_owned() })? + .clone() + )?; + + // We first generate the prd with all the keys that we will keep to ourselves + let full_prd = Prd::new( + PrdType::Init, + Uuid::from_str(&process.uuid).expect("We can trust process to have a valid uuid"), + serde_json::to_string(&sender)?, + fields2cipher.clone(), + fields2keys.clone() )?; - let final_tx = signed_psbt.extract_tx()?; - - message.status = CachedMessageStatus::Trusted; - - Ok(createTransactionReturn { - txid: final_tx.txid().to_string(), - transaction: serialize(&final_tx).to_lower_hex_string(), - new_network_msg: message.clone(), - }) -} - -/// This is what we call to confirm as a receiver -#[wasm_bindgen] -pub fn create_confirmation_transaction( - message_id: u32, - fee_rate: u32, -) -> ApiResult { - let mut messages = lock_messages()?; - let message: &mut CachedMessage; - if let Some(m) = messages.iter_mut().find(|m| m.id == message_id) { - if m.sender.is_none() || m.commited_in.is_none() || m.prd.is_none() { - return Err(ApiError { - message: "Invalid network message".to_owned(), - }); - } - - message = m; - } else { - return Err(ApiError { - message: format!("Can't find message for id {}", message_id), - }); - } - - let sp_address: SilentPaymentAddress = message.sender.as_ref().unwrap().as_str().try_into()?; - - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - - let recipient = Recipient { - address: sp_address.into(), - 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, - vec![recipient], - None, - Amount::from_sat(fee_rate.into()), - message.sender.clone(), - )?; - - // what's the vout of the output sent to sender? - let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; - let recipients_vouts = sp_address2vouts - .get::(&sp_address.into()) - .expect("recipients didn't change") - .as_slice(); - - let final_tx = signed_psbt.extract_tx()?; - - message.confirmed_by = Some(OutPoint { - txid: final_tx.txid(), - vout: recipients_vouts[0] as u32, - }); - - Ok(createTransactionReturn { - txid: final_tx.txid().to_string(), - transaction: serialize(&final_tx).to_lower_hex_string(), - new_network_msg: message.clone(), - }) -} - -#[wasm_bindgen] -pub fn create_login_transaction(fee_rate: u32) -> ApiResult { - // 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, - ); - } - - let pcd = Pcd::new("LOGIN".to_owned()); - let pcd_commitment = sha256sum(pcd.to_string().as_bytes()); - - let key: [u8; 32] = Aes256Gcm::generate_key(thread_rng()).into(); - - let process = Process::new( - "empty".to_owned(), - vec![], - ValidationRules::new(0.0, Role::User), - Txid::all_zeros(), - String::default(), - String::default(), - String::default(), - Value::Null - ); - - let prd = Prd::new( - PrdType::Message, - process, - local_device - .get_wallet() - .get_client() - .get_receiving_address() - .try_into() - .unwrap(), - key, - pcd_commitment, - )?; - - let pcd_cipher = prd.encrypt_pcd(&pcd)?; - - let commitment = sha256sum(prd.to_string().as_bytes()); - - 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(commitment.as_byte_array().to_vec()), - 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( - prd.to_string(), - 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::(&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.sender = Some(prd.sender.clone()); - new_msg.pcd = Some(pcd); - new_msg.pcd_cipher = Some(pcd_cipher.to_lower_hex_string()); - new_msg.prd = Some(prd); - new_msg.prd_cipher = Some(cipher); - new_msg.commitment = Some(commitment.to_string()); - 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.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 { - let my_address = lock_local_device()? - .get_wallet() - .get_client() - .get_receiving_address(); - - let pcd = Pcd::new("PAIRING".to_owned()); - - let process = Process::new( - "empty".to_owned(), - vec![], - ValidationRules::new(0.0, Role::User), - Txid::all_zeros(), - String::default(), - String::default(), - String::default(), - Value::Null - ); - - let mut res = create_notification_transaction(address, process, pcd, fee_rate)?; - - let mut messages = lock_messages()?; - - 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"); - } - - Ok(res) -} - -#[wasm_bindgen] -pub fn create_notification_transaction( - address: String, - process: Process, - pcd: Pcd, - fee_rate: u32, -) -> ApiResult { - let sp_address: SilentPaymentAddress = address.as_str().try_into()?; - - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - - let recipient = Recipient { - address: sp_address.into(), - amount: Amount::from_sat(1200), - nb_outputs: 1, - }; - - let pcd_commitment = sha256sum(pcd.to_string().as_bytes()); - - let key: [u8; 32] = Aes256Gcm::generate_key(thread_rng()).into(); - - let prd = Prd::new( - PrdType::Message, - process, - sp_wallet.get_client().get_receiving_address().try_into().unwrap(), - key, - pcd_commitment - )?; - - let prd_commitment = sha256sum(prd.to_string().as_bytes()); - - let pcd_cipher = prd.encrypt_pcd(&pcd)?; + let prd_commitment = full_prd.create_commitment(); let freezed_utxos = lock_freezed_utxos()?; @@ -1086,58 +648,76 @@ pub fn create_notification_transaction( &vec![], &freezed_utxos, sp_wallet, - vec![recipient], + recipients, Some(prd_commitment.as_byte_array().to_vec()), Amount::from_sat(fee_rate.into()), None, )?; + let sp_address2vouts = map_outputs_to_sp_address(&signed_psbt.to_string())?; + let partial_secret = sp_wallet .get_client() - .get_partial_secret_from_psbt(&signed_psbt)?; + .get_partial_secret_from_psbt(&signed_psbt)?; - let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &sp_address.get_scan_key(), - &partial_secret, - ); - - let shared_secret = AnkSharedSecret::new(shared_point); - - let cipher = encrypt_with_key( - serde_json::to_string(&prd)?, - 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::(&address) - .expect("recipients didn't change") - .as_slice(); - // for now let's just take the smallest vout that belongs to the recipient let final_tx = signed_psbt.extract_tx()?; - let mut new_msg = CachedMessage::new(); - new_msg.sender = Some(prd.sender.clone()); - new_msg.prd = Some(prd); - new_msg.prd_cipher = Some(cipher); - new_msg.pcd = Some(pcd); - new_msg.pcd_cipher = Some(pcd_cipher.to_lower_hex_string()); - new_msg.commitment = Some(prd_commitment.as_byte_array().to_lower_hex_string()); - new_msg.commited_in = Some(OutPoint { - txid: final_tx.txid(), - vout: recipients_vouts[0] as u32, + let mut new_messages = vec![]; + let mut shared_secrets = vec![]; // This is a bit ugly, but this way we can update the process status + for (member, visible_fields) in args.member2fields { + let mut prd = full_prd.clone(); + prd.filter_keys(visible_fields); + let prd_msg = prd.to_network_msg(sp_wallet)?; + let mut res = CachedMessage::new(); + res.recipient = Some(member.clone()); + res.prd = Some(prd_msg.clone()); + res.sender = Some(sender.clone()); + res.pcd = Some(Value::Object(pcd.clone())); + res.commitment = Some(prd_commitment.to_string()); + res.status = CachedMessageStatus::Opened; + + let (addresses1, addresses2) = member.get_addresses(); + for sp_address in [addresses1, addresses2].into_iter() { + let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( + &::try_from(sp_address).unwrap().get_scan_key(), + &partial_secret, + ); + + let shared_secret = AnkSharedSecret::new(shared_point).to_byte_array().to_lower_hex_string(); + + let cipher = encrypt_with_key( + prd_msg.clone(), + shared_secret.clone(), + )?; + res.cipher.push(cipher); + res.shared_secrets.push(shared_secret.clone()); + shared_secrets.push(shared_secret); + } + + new_messages.push(res); + } + + lock_messages()?.extend(new_messages.clone()); + + let mut processes = lock_processes()?; + let init_state = ProcessState { + commited_in: OutPoint::null(), + encrypted_pcd: Value::Object(fields2cipher), + keys: fields2keys, + validation_token: vec![] + }; + + // We are initializing a process, so we shouldn't have it in our cache yet + processes.insert(Uuid::parse_str(&process.uuid).unwrap(), RelevantProcess { + process, + states: vec![init_state], + current_status: ProcessStatus::Active(shared_secrets) }); - new_msg.shared_secret = Some(shared_secret.to_byte_array().to_lower_hex_string()); - new_msg.recipient = Some(address); - 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()); - Ok(createTransactionReturn { - txid: final_tx.txid().to_string(), - transaction: serialize(&final_tx).to_lower_hex_string(), - new_network_msg: new_msg, + Ok(createTransactionReturn { + txid: final_tx.txid().to_string(), + transaction: serialize(&final_tx).to_lower_hex_string(), + new_messages }) } @@ -1218,25 +798,7 @@ pub fn create_faucet_msg() -> ApiResult { let faucet_msg = FaucetMessage::new(sp_address.clone()); - let mut cached_msg = CachedMessage::new(); - cached_msg.recipient = Some(sp_address); - cached_msg.commitment = Some(faucet_msg.commitment.clone()); - cached_msg.status = CachedMessageStatus::FaucetWaiting; - lock_messages()?.push(cached_msg.clone()); - let network_msg = Envelope::new(AnkFlag::Faucet, &faucet_msg.to_string()); Ok(network_msg.to_string()) } - -fn sha256sum(input: &[u8]) -> sha256::Hash { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&input); - sha256::Hash::from_engine(engine) -} - -#[wasm_bindgen] -pub fn create_commitment(payload_to_hash: String) -> String { - let hash = sha256sum(payload_to_hash.as_bytes()); - hash.to_byte_array().to_lower_hex_string() -} diff --git a/src/lib.rs b/src/lib.rs index 4661ad5..6e38363 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,15 @@ use anyhow::Error; use sdk_common::crypto::AnkSharedSecret; use sdk_common::network::CachedMessage; +use sdk_common::process::Process; +use sdk_common::sp_client::bitcoin::OutPoint; +use sdk_common::uuid::Uuid; +use sdk_common::prd::ValidationToken; use serde::{Deserialize, Serialize}; +use serde_json::{Value, Map}; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +use std::ops::Index; use std::sync::{Mutex, MutexGuard, OnceLock}; use tsify::Tsify; @@ -21,6 +27,45 @@ pub fn lock_messages() -> Result>, Error> .lock_anyhow() } +#[derive(Debug)] +pub enum ProcessStatus { + Sealed, + Active(Vec), // shared_secrets used to communicate on current session +} + +#[derive(Debug, Clone)] +pub struct ProcessState { + pub commited_in: OutPoint, + pub encrypted_pcd: Value, + pub keys: Map, // We may not always have all the keys + pub validation_token: Vec // This signs the encrypted pcd +} + +#[derive(Debug)] +pub struct RelevantProcess { + process: Process, + states: Vec, + current_status: ProcessStatus, +} + +impl RelevantProcess { + pub fn get_process(&self) -> Process { + self.process.clone() + } + + pub fn get_status_at(&self, index: usize) -> Option { + self.states.get(index).cloned() + } +} + +pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); + +pub fn lock_processes() -> Result>, Error> { + CACHEDPROCESSES + .get_or_init(|| Mutex::new(HashMap::new())) + .lock_anyhow() +} + pub(crate) trait MutexExt { fn lock_anyhow(&self) -> Result, Error>; } diff --git a/src/process.rs b/src/process.rs deleted file mode 100644 index f9d7692..0000000 --- a/src/process.rs +++ /dev/null @@ -1,405 +0,0 @@ -use std::fmt::DebugStruct; - -use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use tsify::Tsify; -use wasm_bindgen::prelude::*; - -pub const HTML_KOTPART: &str = " -
-
-

Send encrypted messages

- -
-
- - -
- - -
- -
-
- "; - -pub const HTML_STORAGE: &str = " -
-
-

Send encrypted messages

- -
-
- - -
- - -
- -
-
- "; - -pub const HTML_MESSAGING: &str = " -
-
-

Send encrypted messages

- -
-
-
- - -
- - -
- -
-
- "; - -pub const CSS: &str = " - body { - margin: 0; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - background-color: #f4f4f4; - font-family: 'Arial', sans-serif; - } - .container { - text-align: center; - } - .card { - max-width: 400px; - width: 100%; - padding: 20px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - background-color: #ffffff; - border-radius: 8px; - text-align: left; - overflow: hidden; - } - form { - display: flex; - flex-direction: column; - /* flex-wrap: wrap; */ - } - label { - font-weight: bold; - margin-bottom: 8px; - } - hr { - border: 0; - height: 1px; - background-color: #ddd; - margin: 10px 0; - } - input, select { - width: 100%; - padding: 10px; - margin: 8px 0; - box-sizing: border-box; - } - select { - padding: 10px; - background-color: #f9f9f9; - border: 1px solid #ddd; - border-radius: 4px; - cursor: pointer; - } - button { - display: inline-block; - background-color: #4caf50; - color: #fff; - border: none; - padding: 12px 17px; - border-radius: 4px; - cursor: pointer; - } - button:hover { - background-color: #45a049; - } - .side-by-side { - display: flex; - align-items: center; - justify-content: space-between; - } - .side-by-side>* { - display: inline-block; - } - button.recover { - display: inline-block; - text-align: center; - text-decoration: none; - display: inline-block; - background-color: #4caf50; - color: #fff; - border: none; - padding: 12px 17px; - border-radius: 4px; - cursor: pointer; - } - button.recover:hover { - background-color: #45a049; - } - a.btn { - display: inline-block; - text-align: center; - text-decoration: none; - display: inline-block; - background-color: #4caf50; - color: #fff; - border: none; - padding: 12px 17px; - border-radius: 4px; - cursor: pointer; - } - - a.btn:hover { - background-color: #45a049; - } - - a { - text-decoration: none; - color: #78a6de; - } - .bg-secondary { - background-color: #2b81ed; - } - .bg-primary { - background-color: #1A61ED; - } - .bg-primary:hover { - background-color: #457be8; - } - .card-revoke { - display: flex; - flex-direction: column; - max-width: 400px; - width: 100%; - padding: 20px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - background-color: #ffffff; - border-radius: 8px; - text-align: center; - align-items: center; - overflow: hidden; - } - .card-revoke a { - max-width: 50px; - width: 100%; - background: none; - border: none; - cursor: pointer; - } - .card-revoke button { - max-width: 200px; - width: 100%; - background: none; - border: none; - cursor: pointer; - color: #78a6de; - } - .card-revoke svg { - width: 100%; - height: auto; - fill: #333; - } - .image-label { - display: block; - color: #fff; - padding: 5px; - margin-top: 10px; - } - .image-container { - width: 400px; - height: 300px; - overflow: hidden; - } - .image-container img { - text-align: center; - width: 100%; - height: 100%; - object-fit: cover; - object-position: center center; - } - .passwordalert { - color: #FF0000; - } - "; - -pub const CSSUPDATE: &str = " - - "; - -pub const JSUPDATE: &str = " - var addSpAddressBtn = document.getElementById('add-sp-address-btn'); - var removeSpAddressBtn = document.querySelectorAll('.minus-sp-address-btn'); - - addSpAddressBtn.addEventListener('click', function (event) { - addDynamicField(this); - }); - - function addDynamicField(element) { - var addSpAddressBlock = document.getElementById('sp-address-block'); - var spAddress = addSpAddressBlock.querySelector('#sp-address').value; - addSpAddressBlock.querySelector('#sp-address').value = ''; - spAddress = spAddress.trim(); - if (spAddress != '') { - var sideBySideDiv = document.createElement('div'); - sideBySideDiv.className = 'side-by-side'; - - var inputElement = document.createElement('input'); - inputElement.type = 'text'; - inputElement.name = 'spAddresses[]'; - inputElement.setAttribute('form', 'no-form'); - inputElement.value = spAddress; - inputElement.disabled = true; - - var buttonElement = document.createElement('button'); - buttonElement.type = 'button'; - buttonElement.className = - 'circle-btn bg-secondary minus-sp-address-btn'; - buttonElement.innerHTML = '-'; - - buttonElement.addEventListener('click', function (event) { - removeDynamicField(this.parentElement); - }); - - sideBySideDiv.appendChild(inputElement); - sideBySideDiv.appendChild(buttonElement); - - addSpAddressBlock.appendChild(sideBySideDiv); - } - function removeDynamicField(element) { - element.remove(); - } - } - "; - -#[derive(Debug, Serialize, Deserialize, Default, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Process { - pub id: u32, - pub name: String, - pub version: String, - pub members: Vec, - pub html: String, - pub style: String, - pub script: String, - // Add conditions : process, member, peer, artefact -} diff --git a/src/user.rs b/src/user.rs index 2f8b11d..360f02f 100644 --- a/src/user.rs +++ b/src/user.rs @@ -9,6 +9,7 @@ use sdk_common::sp_client::bitcoin::{ Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey, }; use sdk_common::sp_client::spclient::SpClient; +use sdk_common::uuid::Uuid; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tsify::Tsify; diff --git a/src/wallet.rs b/src/wallet.rs index 6114274..c82f079 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -33,7 +33,7 @@ pub fn generate_sp_wallet(label: Option, network: Network) -> anyhow::Re network, )?; let our_address: SilentPaymentAddress = sp_client.get_receiving_address().try_into()?; - log::info!( + sdk_common::log::info!( "Created client for sp with address: {}", our_address.to_string() ); diff --git a/tests/challenge.rs b/tests/challenge.rs deleted file mode 100644 index 5750aa2..0000000 --- a/tests/challenge.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::collections::HashMap; - -use log::debug; -use sdk_client::api::{ - create_confirmation_transaction, create_notification_transaction, dump_device, dump_message_cache, get_outputs, reset_device, restore_device, set_message_cache, setup -}; -use sdk_common::network::{ - CachedMessage, CachedMessageStatus, Pcd -}; -use sdk_common::sp_client::bitcoin::OutPoint; -use sdk_common::sp_client::spclient::OwnedOutput; - -use tsify::JsValueSerdeExt; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -mod utils; - -use utils::*; - -#[wasm_bindgen_test] -fn test_bob_challenges_alice() { - reset_device().unwrap(); - debug!("==============================================\nStarting test_bob_challenges_alice\n=============================================="); - - // =============================== Bob - setup(); - restore_device(BOB_CHALLENGE_DEVICE.to_owned()).unwrap(); - set_message_cache( - serde_json::from_str::>(BOB_CHALLENGE_CACHE) - .unwrap() - .into_iter() - .map(|v| v.to_string()) - .collect(), - ) - .unwrap(); - - let notification_msg: CachedMessage = serde_json::from_str(dump_message_cache().unwrap().get(0).unwrap()).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let bob_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - debug!("Bob sends a challenge transaction back to Alice"); - debug!("outpoints: {:?}", bob_outputs); - let confirmation_tx = create_confirmation_transaction(notification_msg.id, 1).unwrap(); - - let confirmation_tweak_data = helper_get_tweak_data( - &confirmation_tx.transaction, - bob_outputs - ); - - debug!("Bob parsing its own confirmation tx"); - helper_parse_transaction( - &confirmation_tx.transaction, - &confirmation_tweak_data, - ); - - // ========================== Alice - reset_device().unwrap(); - restore_device(ALICE_CHALLENGE_DEVICE.to_owned()).unwrap(); - set_message_cache( - serde_json::from_str::>(ALICE_CHALLENGE_CACHE) - .unwrap() - .into_iter() - .map(|v| v.to_string()) - .collect(), - ) - .unwrap(); - - debug!("Alice parsing confirmation tx"); - helper_parse_transaction(&confirmation_tx.transaction, &confirmation_tweak_data); -} - -// #[wasm_bindgen_test] -// fn test_alice_answers_bob() { -// reset_device().unwrap(); -// setup(); -// debug!("==============================================\nStarting test_alice_answers_bob\n=============================================="); -// restore_device(ALICE_ANSWER_DEVICE.to_owned()).unwrap(); -// set_message_cache( -// serde_json::from_str::>(ALICE_ANSWER_CACHE) -// .unwrap() -// .into_iter() -// .map(|v| v.to_string()) -// .collect(), -// ) -// .unwrap(); - -// let challenge_msg: CachedMessage = serde_json::from_str(dump_message_cache().unwrap().get(0).unwrap()).unwrap(); - -// debug!("Alice answers bob's challenge"); -// let answer_tx = answer_confirmation_transaction(challenge_msg.id, 1).unwrap(); - -// let get_outputs_result = get_outputs().unwrap(); - -// let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - -// let answer_tweak_data = helper_get_tweak_data( -// &answer_tx.transaction, -// alice_outputs, -// ); - -// debug!("Alice parses her own transaction"); -// helper_parse_transaction(&answer_tx.transaction, &answer_tweak_data); - -// reset_device().unwrap(); -// restore_device(BOB_ANSWER_DEVICE.to_owned()).unwrap(); - -// set_message_cache( -// serde_json::from_str::>(BOB_ANSWER_CACHE) -// .unwrap() -// .into_iter() -// .map(|v| v.to_string()) -// .collect(), -// ) -// .unwrap(); - -// debug!("Bob parses answer transaction {}", answer_tx.txid); -// helper_parse_transaction(&answer_tx.transaction, &answer_tweak_data); -// } - -// #[wasm_bindgen_test] -// fn test_alice_sends_message_through_trusted_channel() { -// reset_device().unwrap(); -// setup(); -// debug!("==============================================\nStarting test_alice_sends_message_through_trusted_channel\n=============================================="); - -// restore_device(ALICE_FINAL_DEVICE.to_owned()).unwrap(); - -// set_message_cache( -// serde_json::from_str::>(ALICE_FINAL_CACHE) -// .unwrap() -// .into_iter() -// .map(|v| v.to_string()) -// .collect(), -// ) -// .unwrap(); - -// let answered_msg: CachedMessage = serde_json::from_str(dump_message_cache().unwrap().get(0).unwrap()).unwrap(); - -// // Bob sends a message to Alice -// debug!("Bob Sends a message to Alice"); -// let secret_msg = "Hello Alice"; -// let cipher = -// encrypt_with_key(secret_msg.to_owned(), answered_msg.shared_secret.unwrap()).unwrap(); -// let bob_msg = Envelope::new(sdk_common::network::AnkFlag::Cipher, &cipher); - -// // Alice can find the message -// let mut result = helper_parse_cipher(bob_msg.to_string()); -// assert!(result.plaintext.pop() == Some(secret_msg.to_owned())); -// } \ No newline at end of file diff --git a/tests/pairing.rs b/tests/pairing.rs index d3686d0..ca94b08 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -1,15 +1,18 @@ use std::collections::HashMap; -use log::debug; use sdk_client::api::{ - create_login_transaction, create_pairing_transaction, dump_device, dump_message_cache, dump_wallet, get_outputs, login, pair_device, reset_device, restore_device, set_message_cache, setup -}; -use sdk_common::network::{ - CachedMessage, CachedMessageStatus, + create_device_from_sp_wallet, create_process_init_transaction, get_outputs, pair_device, reset_device, setup, CreateProcessInitTransactionArguments }; +use sdk_client::lock_processes; +use sdk_common::network::CachedMessage; +use sdk_common::pcd::{Member, Pcd, Role}; +use sdk_common::prd::Prd; +use sdk_common::process::{Process, ValidationRules}; use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; -use serde_json; +use sdk_common::uuid::Uuid; +use sdk_common::log::debug; +use serde_json::{self, json, Value}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; @@ -26,229 +29,119 @@ fn test_pairing() { debug!("==============================================\nStarting test_pairing\n=============================================="); // ========================= Alice - helper_switch_device(ALICE_LOGIN_WALLET.to_owned()); + reset_device().unwrap(); + create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap(); - debug!("Alice sends a pairing transaction to Bob"); - let alice_pairing_tx = create_pairing_transaction(helper_get_bob_address(), 1).unwrap(); + // Alice creates the new member with Bob address + let new_member = Member::new( + DEFAULT_NYM.to_owned(), + helper_get_alice_address().try_into().unwrap(), + helper_get_bob_address().try_into().unwrap(), + Role::User + ); + + let initial_state = json!({ + "nym": DEFAULT_NYM, + "members": [ + new_member, + ], + "current_session_tx": null, + }); + + let validation_rules = ValidationRules::new( + 1.0, + Role::Admin, + 1.0 + ); + + let mut member2fields: HashMap> = HashMap::new(); + member2fields.insert(new_member, initial_state.as_object().unwrap().keys().map(|k| k.to_owned()).collect()); + + // We define the process for pairing + let pairing_process = Process::new( + "pairing".to_owned(), + validation_rules, + String::default(), + String::default(), + String::default(), + Value::Null, + OutPoint::null() + ); + + // we can update our local device now + pair_device(pairing_process.uuid.clone(), helper_get_bob_address(), initial_state.to_string()).unwrap(); + + debug!("Alice sends a transaction commiting to an init prd to Bob"); + let args = CreateProcessInitTransactionArguments { + member2fields, + process: pairing_process, + stringified_pcd: initial_state.to_string() + }; + let alice_pairing_return = create_process_init_transaction(args, 1).unwrap(); let get_outputs_result = get_outputs().unwrap(); let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); let alice_pairing_tweak_data = - helper_get_tweak_data(&alice_pairing_tx.transaction, alice_outputs); + helper_get_tweak_data(&alice_pairing_return.transaction, alice_outputs); // Alice parse her own transaction - let alice_msg_id = - helper_parse_transaction(&alice_pairing_tx.transaction, &alice_pairing_tweak_data).id; - - let alice_wallet = dump_wallet().unwrap(); - let alice_cache = dump_message_cache().unwrap(); + helper_parse_transaction(&alice_pairing_return.transaction, &alice_pairing_tweak_data).id; // ======================= Bob reset_device().unwrap(); - helper_switch_device(BOB_LOGIN_WALLET.to_owned()); + create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); // Bob receives Alice pairing transaction - // if he agrees, he must send another pairing transaction to Alice - // he can also spend the output that notified him that will become Alice first session key debug!("Bob parses Alice pairing transaction"); - helper_parse_transaction(&alice_pairing_tx.transaction, &alice_pairing_tweak_data); + helper_parse_transaction(&alice_pairing_return.transaction, &alice_pairing_tweak_data); debug!("Bob receives the prd"); - helper_parse_cipher(alice_pairing_tx.new_network_msg.prd_cipher.unwrap()); + let mut bob_retrieved_prd = CachedMessage::default(); + for message in alice_pairing_return.new_messages.iter() { + for cipher in message.cipher.iter() { + match helper_parse_cipher(cipher.clone()) { + Ok(res) => bob_retrieved_prd = res, + Err(_) => continue + } + } + } + + if bob_retrieved_prd == CachedMessage::default() { + panic!("Bob failed to retrieve Alice message"); + } debug!("Bob receives the pcd"); - let alice_pairing_res = helper_parse_cipher(alice_pairing_tx.new_network_msg.pcd_cipher.unwrap()); + let mut bob_retrieved_pcd = CachedMessage::default(); + for message in alice_pairing_return.new_messages { + for cipher in message.cipher { + match helper_parse_cipher(cipher) { + Ok(res) => bob_retrieved_pcd = res, + Err(_) => continue + } + } + } - assert!(alice_pairing_res.status == CachedMessageStatus::Pairing); - // Bob takes the txid of the incoming transaction from Alice, he will need it for pairing - let incoming_txid = alice_pairing_res.commited_in.unwrap().txid; + if bob_retrieved_pcd == CachedMessage::default() { + panic!("Bob failed to retrieve Alice message"); + } // At this point, user must validate the pairing proposal received from Alice - debug!("Bob sends a pairing transaction back"); - let bob_pairing_tx = create_pairing_transaction(alice_pairing_res.sender.unwrap(), 1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let bob_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let bob_pairing_tweak_data = helper_get_tweak_data(&bob_pairing_tx.transaction, bob_outputs); - - helper_parse_transaction(&bob_pairing_tx.transaction, &bob_pairing_tweak_data); - debug!("Bob pairs device with Alice"); - let pairing_process = pair_device(DEFAULT_NYM.to_owned(), bob_pairing_tx.new_network_msg.id, incoming_txid.to_string()).unwrap(); + let process = lock_processes().unwrap(); + let prd: Prd = serde_json::from_str(&bob_retrieved_prd.prd.unwrap()).unwrap(); + let relevant_process = process.get(&Uuid::parse_str(&prd.process_uuid).unwrap()).unwrap(); + // decrypt the pcd and update bob device + if let Some(initial_state) = relevant_process.get_status_at(0) { + let keys = initial_state.keys; + let mut pcd = initial_state.encrypted_pcd; + pcd.decrypt_fields(&keys).unwrap(); + pair_device(relevant_process.get_process().uuid, helper_get_alice_address(), pcd.to_string()).unwrap(); + } - // sign the pairing process + // To make the pairing effective, alice and bob must now spend their respective output into a new transaction - // send it to Alice so that she can sign it too - - // commit it to a transaction to make it public - - // ======================== Alice - reset_device().unwrap(); - helper_switch_device(alice_wallet); - set_message_cache(alice_cache).unwrap(); - - // parse Bob's pairing transaction - helper_parse_transaction(&bob_pairing_tx.transaction, &bob_pairing_tweak_data); - helper_parse_cipher(bob_pairing_tx.new_network_msg.prd_cipher.unwrap()); - - let bob_pairing_msg = helper_parse_cipher(bob_pairing_tx.new_network_msg.pcd_cipher.unwrap()); - - assert!(bob_pairing_msg.status == CachedMessageStatus::Pairing); - - debug!("Alice pairs device"); - pair_device(DEFAULT_NYM.to_owned(), alice_msg_id, bob_pairing_tx.txid).unwrap(); -} - -#[wasm_bindgen_test] -fn test_first_login() { - reset_device().unwrap(); - setup(); - debug!("==============================================\nStarting test_first_login\n=============================================="); - - restore_device(BOB_PAIRED_DEVICE.to_owned()).unwrap(); - set_message_cache( - serde_json::from_str::>(BOB_PAIRING_CACHE) - .unwrap() - .into_iter() - .map(|v| v.to_string()) - .collect(), - ) - .unwrap(); - - // Bob can now spend the notification output, that will become Alice's first session key - debug!("Bob first login"); - let bob_first_login_tx = create_login_transaction(1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let bob_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let bob_login_tweak_data = helper_get_tweak_data(&bob_first_login_tx.transaction, bob_outputs); - - debug!("Bob parses his own login transaction"); - helper_parse_transaction(&bob_first_login_tx.transaction, &bob_login_tweak_data); - - let bob_device = dump_device().unwrap(); - let bob_cache = dump_message_cache().unwrap(); - - // ======================== Alice - reset_device().unwrap(); - restore_device(ALICE_PAIRED_DEVICE.to_owned()).unwrap(); - set_message_cache( - serde_json::from_str::>(ALICE_PAIRING_CACHE) - .unwrap() - .into_iter() - .map(|v| v.to_string()) - .collect(), - ) - .unwrap(); - - debug!("Alice finds out the login demand from Bob"); - helper_parse_transaction(&bob_first_login_tx.transaction, &bob_login_tweak_data); - helper_parse_cipher(bob_first_login_tx.new_network_msg.prd_cipher.unwrap()); - let bob_login_msg = helper_parse_cipher(bob_first_login_tx.new_network_msg.pcd_cipher.unwrap()); - - // At this point Alice can fire up the revokation output if the login demand is illegitimate - // OR she must answer with a login transaction to Bob - debug!("Alice first login"); - let alice_first_login_tx = create_login_transaction(1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let alice_login_tweak_data = - helper_get_tweak_data(&alice_first_login_tx.transaction, alice_outputs); - - helper_parse_transaction(&alice_first_login_tx.transaction, &alice_login_tweak_data); - - login(bob_login_msg.id, alice_first_login_tx.transaction.clone()).unwrap(); - - // ======================= Bob - reset_device().unwrap(); - restore_device(bob_device).unwrap(); - set_message_cache(bob_cache).unwrap(); - - helper_parse_transaction(&alice_first_login_tx.transaction, &alice_login_tweak_data); - helper_parse_cipher(alice_first_login_tx.new_network_msg.prd_cipher.unwrap()); - let alice_login_msg = helper_parse_cipher(alice_first_login_tx.new_network_msg.pcd_cipher.unwrap()); - - assert!(alice_login_msg.status == CachedMessageStatus::Login); - - login(alice_login_msg.id, bob_first_login_tx.transaction).unwrap(); -} - -#[wasm_bindgen_test] -fn test_login() { - reset_device().unwrap(); - setup(); - debug!("==============================================\nStarting test_login\n=============================================="); - - // ======================= Alice - restore_device(ALICE_LOGGED_DEVICE.to_owned()).unwrap(); - - debug!("Alice sends a login transaction to Bob, which creates a new key for him"); - let alice_login_tx = create_login_transaction(1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let alice_login_tweak_data = helper_get_tweak_data( - &alice_login_tx.transaction, - alice_outputs - ); - - debug!("Parsing Alice login transaction"); - helper_parse_transaction(&alice_login_tx.transaction, &alice_login_tweak_data); - - let alice_device = dump_device().unwrap(); - let alice_cache = dump_message_cache().unwrap(); - - // ============================== Bob - reset_device().unwrap(); - restore_device(BOB_LOGGED_DEVICE.to_owned()).unwrap(); - - debug!("Bob finds out the login demand from Alice"); - helper_parse_transaction(&alice_login_tx.transaction, &alice_login_tweak_data); - helper_parse_cipher(alice_login_tx.new_network_msg.prd_cipher.unwrap()); - let alice_login_msg = helper_parse_cipher(alice_login_tx.new_network_msg.pcd_cipher.unwrap()); - - // Bob must confirm that he agrees to log in - - // Boutons "login" ou "Refuser" - // At this point Bob can fire up the revokation output if the login demand is illegitimate - // revoke_paired_device(1).unwrap(); - // OR it must answer with a login transaction to Alice - let bob_login_tx = create_login_transaction(1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let bob_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - - let bob_login_tweak_data = helper_get_tweak_data(&bob_login_tx.transaction, bob_outputs); - - debug!("Bob parses his own login transaction"); - helper_parse_transaction(&bob_login_tx.transaction, &bob_login_tweak_data); - - login(alice_login_msg.id, alice_login_tx.transaction.clone()).unwrap(); - - // =================== Alice - - reset_device().unwrap(); - restore_device(alice_device).unwrap(); - set_message_cache(alice_cache).unwrap(); - - helper_parse_transaction(&bob_login_tx.transaction, &bob_login_tweak_data); - helper_parse_cipher(bob_login_tx.new_network_msg.prd_cipher.unwrap()); - let bob_login_msg = helper_parse_cipher(bob_login_tx.new_network_msg.pcd_cipher.unwrap()); - - assert!(bob_login_msg.status == CachedMessageStatus::Login); - - login(bob_login_msg.id, alice_login_tx.transaction).unwrap(); + // Once we know this tx id, we can commit to the relay } diff --git a/tests/prd.rs b/tests/prd.rs deleted file mode 100644 index 736d487..0000000 --- a/tests/prd.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::collections::HashMap; - -use log::debug; -use sdk_client::api::{ - create_confirmation_transaction, create_notification_transaction, dump_device, dump_message_cache, get_outputs, reset_device, restore_device, set_message_cache, setup -}; -use sdk_common::network::{ - CachedMessage, CachedMessageStatus, Pcd -}; -use sdk_common::process::{Process, Role, ValidationRules}; -use sdk_common::sp_client::bitcoin::hashes::Hash; -use sdk_common::sp_client::bitcoin::{OutPoint, Txid}; -use sdk_common::sp_client::spclient::OwnedOutput; - -use serde_json::Value; -use tsify::JsValueSerdeExt; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -mod utils; - -use utils::*; - -#[wasm_bindgen_test] -fn test_alice_sends_prd_message_to_bob() { - reset_device().unwrap(); - setup(); - debug!("==============================================\nStarting test_alice_sends_prd_message_to_bob\n=============================================="); - - // ============================ Alice - restore_device(ALICE_LOGGED_DEVICE.to_owned()).unwrap(); - - // Alice first puts her message in a pcd - let pcd = Pcd::new("TEST".to_owned()); - debug!("Alice notified Bob about a message it sent"); - - let empty_process = Process::new( - "empty".to_owned(), - vec![], - ValidationRules::new(0.0, Role::User), - Txid::all_zeros(), - String::default(), - String::default(), - String::default(), - Value::Null - ); - - let notification_tx = - create_notification_transaction(helper_get_bob_address(), empty_process, pcd, 1).unwrap(); - - let get_outputs_result = get_outputs().unwrap(); - - let alice_outputs: HashMap = get_outputs_result.into_serde().unwrap(); - let notification_tweak_data = helper_get_tweak_data( - ¬ification_tx.transaction, - alice_outputs - ); - - debug!("Alice parses her own notification transaction"); - // Alice parse her own transaction to update her utxos - helper_parse_transaction( - ¬ification_tx.transaction, - ¬ification_tweak_data, - ); - - reset_device().unwrap(); - restore_device(BOB_LOGGED_DEVICE.to_owned()).unwrap(); - - debug!("bob parses the transaction and the message"); - helper_parse_transaction(¬ification_tx.transaction, ¬ification_tweak_data); - helper_parse_cipher(notification_tx.new_network_msg.prd_cipher.unwrap()); - let bob_notification_msg = helper_parse_cipher(notification_tx.new_network_msg.pcd_cipher.unwrap()); - - let msg_dump = dump_message_cache().unwrap(); - debug!("bob_wallet: {:?}", dump_device()); - debug!("bob_notification_msg: {:?}", msg_dump); - debug!("commited_in: {:?}", msg_dump.get(0).unwrap().find("0x71b37cede4655932a5ce97bb8c4a7845adce96d4f85b64bc699bf74942c19f89")); - - assert!(bob_notification_msg.status == CachedMessageStatus::ReceivedMustConfirm); -} diff --git a/tests/utils.rs b/tests/utils.rs index ed8e4d1..262c209 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use log::debug; -use sdk_client::api::{parse_cipher, parse_new_tx, reset_device, restore_device_from_sp_wallet}; +use sdk_client::api::{parse_cipher, parse_new_tx, reset_device, ApiResult}; use sdk_common::network::{ CachedMessage, NewTxMessage, }; @@ -13,6 +12,7 @@ use sdk_common::sp_client::silentpayments::utils::receiving::{ calculate_tweak_data, get_pubkey_from_input, }; use sdk_common::sp_client::spclient::{OwnedOutput, SpWallet}; +use sdk_common::log::debug; use serde_json; // We're using alice and bob for clarity, but it's important to remember that for pairing and login Alice and Bob are the same person @@ -39,11 +39,6 @@ pub const ALICE_FINAL_DEVICE: &str = "{\"sp_wallet\":{\"client\":{\"label\":\"de pub const DEFAULT_NYM: &str = "AliceBob"; -pub fn helper_switch_device(wallet: String) { - reset_device().unwrap(); - restore_device_from_sp_wallet(wallet.clone()).unwrap(); -} - pub fn helper_get_alice_address() -> String { let wallet: SpWallet = serde_json::from_str(ALICE_START_WALLET).unwrap(); wallet.get_client().get_receiving_address() @@ -100,10 +95,6 @@ pub fn helper_parse_transaction(transaction: &str, tweak_data: &str) -> CachedMe } } -pub fn helper_parse_cipher(cipher_msg: String) -> CachedMessage { - let result = parse_cipher(cipher_msg, 1); - match result { - Ok(r) => return r, - Err(e) => panic!("Unexpected error: {}", e.message), - }; +pub fn helper_parse_cipher(cipher_msg: String) -> ApiResult { + parse_cipher(cipher_msg, 1) }