diff --git a/src/api.rs b/src/api.rs index 0043cc8..0f246d6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -24,9 +24,10 @@ use sdk_common::crypto::{ KeyInit, Purpose, AAD, }; use sdk_common::process::Process; +use sdk_common::signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; 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::{sha256, Hash}; +use sdk_common::sp_client::bitcoin::hashes::{sha256, sha256t, Hash}; use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, @@ -63,7 +64,9 @@ use sdk_common::network::{ self, AnkFlag, CachedMessage, CachedMessageStatus, CommitMessage, Envelope, FaucetMessage, NewTxMessage, }; -use sdk_common::pcd::{compare_maps, AnkPcdHash, Member, Pcd, RoleDefinition, ValidationRule}; +use sdk_common::pcd::{ + compare_maps, AnkPcdHash, AnkPcdTag, Member, Pcd, RoleDefinition, ValidationRule, +}; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; use sdk_common::sp_client::spclient::{ @@ -602,6 +605,56 @@ fn try_decrypt_with_processes( None } +#[wasm_bindgen] +pub fn response_prd( + root_commitment: String, + prd: String, // The Prd we respond to + approval: bool, +) -> ApiResult { + let local_device = lock_local_device()?; + let member = local_device + .to_member() + .ok_or(ApiError::new("Unpaired device".to_owned()))?; + + let prd_to_respond = serde_json::from_str::(&prd)?; + + // Probably we should answer differently depending on the type of prd + let message_hash = match prd_to_respond.prd_type { + PrdType::Update => { + let pcd_hash: AnkPcdHash = + AnkPcdHash::from_value(&Value::from_str(&prd_to_respond.payload)?); + + let message_hash = if approval { + AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(pcd_hash)) + } else { + AnkHash::ValidationNo(AnkValidationNoHash::from_commitment(pcd_hash)) + }; + + let proof = Proof::new( + message_hash, + local_device + .get_wallet() + .get_client() + .get_spend_key() + .try_into()?, + ); + + let prd = Prd::new_response( + OutPoint::from_str(&root_commitment)?, + serde_json::to_string(&member)?, + proof, + pcd_hash, + ); + + return Ok(ApiReturn { + ciphers_to_send: vec![prd.to_network_msg(local_device.get_wallet())?.to_string()], + ..Default::default() + }); + } + _ => unimplemented!(), + }; +} + fn confirm_prd(prd: Prd, shared_secret: &str) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { @@ -631,20 +684,18 @@ fn confirm_prd(prd: Prd, shared_secret: &str) -> AnyhowResult { } }; - let payload = Value::from_str(&prd.payload)?; + let pcd_commitment = AnkPcdHash::from_str(&prd.payload)?; - let prd_confirm = Prd::new_confirm(outpoint, member, payload.tagged_hash()); + let prd_confirm = Prd::new_confirm(outpoint, member, pcd_commitment); let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?; - debug!("encrypting with key {}", shared_secret); Ok(encrypt_with_key(prd_msg, shared_secret.to_owned()).unwrap()) } fn send_data(prd: &Prd, shared_secret: &str) -> AnyhowResult { let pcd = &prd.payload; - debug!("encrypting with key: {:#?}", shared_secret); let cipher = encrypt_with_key(pcd.clone(), shared_secret.to_owned()).unwrap(); Ok(ApiReturn { @@ -661,7 +712,6 @@ fn decrypt_with_cached_messages( let nonce = Nonce::from_slice(&cipher[..12]); for message in messages.iter_mut() { - debug!("Attempting decryption with cached message {:#?}", message); for shared_secret in message.shared_secrets.iter() { let aes_key = match AnkSharedSecretHash::from_str(shared_secret) { Ok(key) => key, @@ -837,6 +887,24 @@ fn handle_prd( ..Default::default() }); } + PrdType::Response => { + // We must know of a prd update that the response answers to + let original_request = relevant_process + .impending_requests + .iter() + .find(|r| { + if r.prd_type != PrdType::Update { + return false; + } + let hash = Value::from_str(&r.payload).unwrap().tagged_hash(); + hash.to_string() == prd.payload + }) + .ok_or(anyhow::Error::msg("Original request not found"))?; + + return Ok(ApiReturn { + ..Default::default() + }); + } _ => unimplemented!(), } } @@ -844,7 +912,6 @@ fn handle_prd( fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult { let pcd = Value::from_str(&String::from_utf8(plain)?)?; - // debug!("Found pcd: {:#?}", pcd); let pcd_commitment = pcd.tagged_hash(); let mut processes = lock_processes()?; @@ -854,7 +921,7 @@ fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult Result>, Error> .lock_anyhow() } +// TODO move to sdk-common #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 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 + pub validation_token: Vec, // This signs the encrypted pcd } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -51,6 +53,10 @@ impl RelevantProcess { pub fn get_latest_state(&self) -> Option { self.states.last().cloned() } + + pub fn get_impending_requests(&self) -> Vec { + self.impending_requests.clone() + } } pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); diff --git a/tests/pairing.rs b/tests/pairing.rs index 335ff34..affc254 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -4,10 +4,10 @@ use std::str::FromStr; use sdk_client::api::{ create_device_from_sp_wallet, create_update_transaction, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, - restore_device, set_process_cache, setup, ApiReturn, + response_prd, restore_device, set_process_cache, setup, ApiReturn, }; use sdk_common::log::debug; -use sdk_common::pcd::{Member, RoleDefinition}; +use sdk_common::pcd::{Member, Pcd, RoleDefinition}; use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; use serde_json::{json, Value}; @@ -31,15 +31,15 @@ fn test_pairing() { create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap(); // we get our own address - let device_address = get_address().unwrap(); + let alice_address = get_address().unwrap(); // we scan the qr code or get the address by any other means - let paired_device = helper_get_bob_address(); + let bob_address = helper_get_bob_address(); // Alice creates the new member with Bob address let new_member = Member::new(vec![ - device_address.as_str().try_into().unwrap(), - paired_device.as_str().try_into().unwrap(), + alice_address.as_str().try_into().unwrap(), + bob_address.as_str().try_into().unwrap(), ]) .unwrap(); @@ -47,6 +47,7 @@ fn test_pairing() { "html": "", "style": "", "script": "", + "description": "AliceBob", "roles": { "owner": { "members": @@ -77,26 +78,19 @@ fn test_pairing() { let alice_pairing_return = create_update_transaction(None, pairing_init_state.to_string(), 1).unwrap(); - // debug!("{:?}", alice_pairing_return); - - // todo must take all the necessary validation before commiting - - // debug!("Alice prepares the commit message for the relay"); - // let (outpoint, process) = alice_pairing_return.updated_process.unwrap(); - // let to_commit = process.get_status_at(0).unwrap().encrypted_pcd; - // let init_return = create_commit_message(serde_json::to_string(&to_commit).unwrap(), RELAY_ADDRESS.to_owned(), None, 1).unwrap(); - - // todo send the commit message to the relay + let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); + let alice_pcd_commitment = Value::from_str( + &alice_init_process + .get_impending_requests() + .get(0) + .unwrap() + .payload, + ) + .unwrap() + .tagged_hash(); let pairing_tx = alice_pairing_return.new_tx_to_send.unwrap(); - // We can update the local device with the actual pairing outpoint - pair_device( - OutPoint::new(pairing_tx.txid(), 0).to_string(), - vec![helper_get_bob_address()], - ) - .unwrap(); - // This is only for testing, the relay takes care of that in prod let get_outputs_result = get_outputs().unwrap(); @@ -110,10 +104,35 @@ fn test_pairing() { helper_parse_transaction(&pairing_tx, &alice_pairing_tweak_data); // Notify user that we're waiting for confirmation from the other device + // We can update the local device with the actual pairing outpoint + pair_device( + OutPoint::new(pairing_tx.txid(), 0).to_string(), + vec![helper_get_bob_address()], + ) + .unwrap(); + // TODO unpair device + + // We can produce the prd response now even if we can't use it yet + // TODO pass the process as argument + let alice_prd_response = response_prd( + root_outpoint, + alice_init_process + .get_impending_requests() + .get(0) + .unwrap() + .to_string(), + true, + ) + .unwrap() + .ciphers_to_send + .get(0) + .unwrap() + .clone(); + + // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); let alice_processes = dump_process_cache().unwrap(); - debug!("Alice processes: {:#?}", alice_processes); // ======================= Bob reset_device().unwrap(); @@ -136,12 +155,23 @@ fn test_pairing() { } } - assert!(bob_retrieved_prd != ApiReturn::default()); + assert!(bob_retrieved_prd.ciphers_to_send.len() == 1); + assert!(bob_retrieved_prd.updated_process.is_some()); debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); + let (root_commitment, relevant_process) = bob_retrieved_prd.updated_process.unwrap(); + let pcd_commitment = relevant_process + .get_impending_requests() + .get(0) + .unwrap() + .payload + .clone(); + let prd_confirm_cipher = bob_retrieved_prd.ciphers_to_send.iter().next().unwrap(); + debug!("Bob sends a Confirm Prd to Alice"); + // this is only for testing, as we're playing both parts let bob_device = dump_device().unwrap(); let bob_processes = dump_process_cache().unwrap(); @@ -151,7 +181,7 @@ fn test_pairing() { set_process_cache(alice_processes).unwrap(); debug!("Alice receives the Confirm Prd"); - let alice_parsed_prd = parse_cipher(bob_retrieved_prd.ciphers_to_send[0].clone()).unwrap(); + let alice_parsed_prd = parse_cipher(prd_confirm_cipher.clone()).unwrap(); debug!("Alice parsed Bob's Confirm Prd: {:#?}", alice_parsed_prd); @@ -160,6 +190,7 @@ fn test_pairing() { restore_device(bob_device).unwrap(); set_process_cache(bob_processes).unwrap(); + debug!("Bob parses Alice's pcd"); let bob_parsed_pcd_return = parse_cipher(alice_parsed_prd.ciphers_to_send[0].clone()).unwrap(); debug!("bob_parsed_pcd: {:#?}", bob_parsed_pcd_return); @@ -173,47 +204,65 @@ fn test_pairing() { // get the pairing tx from the proposal let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap(); - let pairing_tx = proposal.get("pairing_tx").unwrap().as_str().unwrap(); - let roles: RoleDefinition = - serde_json::from_str(proposal.get("roles").unwrap().as_str().unwrap()).unwrap(); + debug!("proposal: {:#?}", proposal); + let pairing_tx = proposal + .get("pairing_tx") + .unwrap() + .as_str() + .unwrap() + .trim_matches('"'); + + let roles = proposal + .get("roles") + .and_then(|v| Value::from_str(v.as_str().unwrap()).ok()) + .unwrap() + .as_object() + .unwrap() + .iter() + .map(|(role_name, role_value)| { + let role_def: RoleDefinition = serde_json::from_value(role_value.clone())?; + Ok((role_name.clone(), role_def)) + }) + .collect::, anyhow::Error>>(); + + let roles = roles.unwrap(); // we check that the proposal contains only one member - assert!(roles.members.len() == 1); + assert!(roles.len() == 1); + assert!(roles["owner"].members.len() == 1); // we get all the addresses of the members of the proposal let proposal_members = roles - .members .iter() - .flat_map(|member| member.get_addresses()) + .flat_map(|(_, members)| members.members.iter().flat_map(|m| m.get_addresses())) .collect::>(); // we can automatically check that a pairing member contains local device address + the one that sent the proposal - assert!(proposal_members.contains(&device_address)); - assert!(proposal_members.contains(&paired_device)); + assert!(proposal_members.contains(&alice_address)); + assert!(proposal_members.contains(&bob_address)); assert!(proposal_members.len() == 2); // no free riders + // We remove the local address, but maybe that's the responsibility of the Member type + let proposal_members = proposal_members + .into_iter() + .filter(|m| m != &bob_address) + .collect::>(); + + debug!("proposal_members: {:?}", proposal_members); + // we can now show all the addresses + pairing tx to the user on device to prompt confirmation + debug!("Bob pairs device with Alice"); pair_device(pairing_tx.to_owned(), proposal_members).unwrap(); - // Bob signs the proposal and send a prd response to Alice + // Bob signs the proposal and sends a prd response too + let bob_prd_response = response_prd(root_commitment, pcd_commitment, true) + .unwrap() + .ciphers_to_send + .get(0) + .unwrap(); - // debug!("Bob pairs device with Alice"); - // 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 - // let pairing_tx: Txid; - // 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(); - // debug!("decrypted pcd: {:?}", pcd); - // pairing_tx = Txid::from_str(pcd.get("pairing_tx").unwrap().as_str().unwrap()).unwrap(); - // pair_device(relevant_process.get_process().uuid, vec![device_address]).unwrap(); - // } - - // To make the pairing effective, alice and bob must now spend their respective output into a new transaction + // To make the pairing effective, alice and bob must now creates a new transaction where they both control one output // login(); // Once we know this tx id, we can commit to the relay