diff --git a/src/api.rs b/src/api.rs index bebdf80..5ce65de 100644 --- a/src/api.rs +++ b/src/api.rs @@ -23,7 +23,7 @@ use sdk_common::crypto::{ encrypt_with_key, AeadCore, Aes256Gcm, AnkSharedSecretHash, KeyInit, AAD, }; use sdk_common::process::{lock_processes, Process, ProcessState}; -use sdk_common::signature::{AnkHash, AnkValidationNoHash, AnkValidationYesHash, Proof}; +use sdk_common::signature::{AnkHash, AnkMessageHash, 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, sha256t, Hash}; @@ -31,7 +31,7 @@ use sdk_common::sp_client::bitcoin::hashes::{FromSliceError, HashEngine}; use sdk_common::sp_client::bitcoin::hex::{ self, parse, DisplayHex, FromHex, HexToArrayError, HexToBytesError, }; -use sdk_common::sp_client::bitcoin::key::{Parity, Secp256k1}; +use sdk_common::sp_client::bitcoin::key::{Keypair, Parity, Secp256k1}; use sdk_common::sp_client::bitcoin::network::ParseNetworkError; use sdk_common::sp_client::bitcoin::p2p::message::NetworkMessage; use sdk_common::sp_client::bitcoin::psbt::raw; @@ -52,7 +52,7 @@ use sdk_common::sp_client::silentpayments::{ use sdk_common::{signature, MutexExt, MAX_PRD_PAYLOAD_SIZE}; use serde_json::{Error as SerdeJsonError, Map, Value}; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize}; use tsify::{JsValueSerdeExt, Tsify}; use wasm_bindgen::convert::{FromWasmAbi, VectorFromWasmAbi}; use wasm_bindgen::prelude::*; @@ -63,7 +63,7 @@ use sdk_common::network::{ NewTxMessage, }; use sdk_common::pcd::{ - compare_maps, AnkPcdHash, AnkPcdTag, Member, Pcd, RoleDefinition, ValidationRule, + AnkPcdHash, AnkPcdTag, Member, Pcd, RoleDefinition, ValidationRule, }; use sdk_common::prd::{AnkPrdHash, Prd, PrdType}; use sdk_common::silentpayments::{create_transaction, map_outputs_to_sp_address}; @@ -77,11 +77,49 @@ use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE}; use crate::wallet::{generate_sp_wallet, lock_freezed_utxos}; #[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum DiffStatus { + #[default] + None, + Rejected, + Validated, +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct UserDiff { + pub process_id: String, + pub new_state_merkle_root: String, // TODO add a merkle proof that the new_value belongs to that state + pub value_commitment: String, + pub field: String, + pub description: Option, + pub previous_value: Value, + pub new_value: Value, + pub notify_user: bool, + pub need_validation: bool, + pub validation_status: DiffStatus, +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct UpdatedProcess { + pub commitment_tx: OutPoint, + pub current_process: Process, + pub up_to_date_roles: HashMap, + pub new_diffs: Vec, // All diffs should have the same new_state_merkle_root + pub modified_state: Option, // basically when we add/receive validation proofs for a state + // I think we should never have both new_state and modified_state +} + +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] #[allow(non_camel_case_types)] pub struct ApiReturn { - pub secrets: SecretsStore, - pub updated_process: Option<(String, Process)>, + pub secrets: Option, + pub updated_process: Option, pub new_tx_to_send: Option, pub ciphers_to_send: Vec, pub commit_to_send: Option, @@ -89,6 +127,15 @@ pub struct ApiReturn { pub type ApiResult = Result; +#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)] +#[tsify(into_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct NewKey { + pub private_key: String, + pub x_only_public_key: String, + pub key_parity: bool +} + const IS_TESTNET: bool = true; const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000); @@ -210,6 +257,22 @@ pub fn get_address() -> ApiResult { .get_receiving_address()) } +#[wasm_bindgen] +pub fn get_new_keypair() -> NewKey { + let secp = Secp256k1::new(); + let mut rng = thread_rng(); + let keypair = Keypair::new(&secp, &mut rng); + + let secret_hex = keypair.secret_bytes().to_lower_hex_string(); + let (xonly, parity) = keypair.x_only_public_key(); + + NewKey { + private_key: secret_hex, + x_only_public_key: xonly.to_string(), + key_parity: if parity == Parity::Even { true } else { false } + } +} + #[wasm_bindgen] pub fn restore_device(device_str: String) -> ApiResult<()> { let device: Device = serde_json::from_str(&device_str)?; @@ -240,33 +303,31 @@ pub fn create_new_device(birthday: u32, network_str: String) -> ApiResult ApiResult { +pub fn is_paired() -> ApiResult { let local_device = lock_local_device()?; - if local_device.is_linked() || local_device.is_linking() { - Ok(true) - } else { - Ok(false) - } + Ok(local_device.get_pairing_commitment().is_some()) } #[wasm_bindgen] pub fn pair_device(commitment_tx: String, mut sp_addresses: Vec) -> ApiResult<()> { let mut local_device = lock_local_device()?; - if local_device.is_linked() { + if local_device.get_pairing_commitment().is_some() { return Err(ApiError::new("Already paired".to_owned())); } - sp_addresses.push( - local_device - .get_wallet() - .get_client() - .get_receiving_address(), - ); + let local_address = local_device + .get_wallet() + .get_client() + .get_receiving_address(); + + if !sp_addresses.iter().any(|a| *a == local_address) { + sp_addresses.push(local_address); + } local_device.pair( - OutPoint::from_str(&commitment_tx)?.txid, + OutPoint::from_str(&commitment_tx)?, Member::new( sp_addresses .into_iter() @@ -298,93 +359,93 @@ impl outputs_list { } } -#[wasm_bindgen] -pub fn login(previous_login_tx: String, fee_rate: u32) -> ApiResult { - // We first create a transaction that spends both pairing tx outputs - let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?; +// #[wasm_bindgen] +// pub fn login(previous_login_tx: String, fee_rate: u32) -> ApiResult { +// // We first create a transaction that spends both pairing tx outputs +// let previous_tx: Txid = deserialize(&Vec::from_hex(&previous_login_tx)?)?; - let device = lock_local_device()?; - if !device.is_linked() { - return Err(ApiError::new("Device is not linked".to_owned())); - } +// let device = lock_local_device()?; +// if !device.is_linked() { +// return Err(ApiError::new("Device is not linked".to_owned())); +// } - let member = device.to_member().unwrap(); - let nb_outputs = member.get_addresses().len(); +// let member = device.to_member().unwrap(); +// let nb_outputs = member.get_addresses().len(); - let other_addresses = device.get_other_addresses(); +// let other_addresses = device.get_other_addresses(); - // We get the pairing process out of cache - let commitment_txid = device.get_process_commitment().unwrap(); - let commitment_outpoint = OutPoint::new(commitment_txid, 0); +// // We get the pairing process out of cache +// let commitment_txid = device.get_process_commitment().unwrap(); +// let commitment_outpoint = OutPoint::new(commitment_txid, 0); - let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone(); - let state = process.get_latest_state().unwrap().clone(); +// let process = lock_processes()?.get(&commitment_outpoint).unwrap().clone(); +// let state = process.get_latest_state().unwrap().clone(); - let mut shared_secrets = Vec::new(); - for address in other_addresses { - let shared_secret = - process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?); - if let Some(shared_secret) = shared_secret { - shared_secrets.push(shared_secret); - } - } +// let mut shared_secrets = Vec::new(); +// for address in other_addresses { +// let shared_secret = +// process.get_shared_secret_for_address(&SilentPaymentAddress::try_from(address)?); +// if let Some(shared_secret) = shared_secret { +// shared_secrets.push(shared_secret); +// } +// } - let mut decrypted_pcd = Map::new(); - state - .encrypted_pcd - .decrypt_fields(&state.keys, &mut decrypted_pcd)?; +// let mut decrypted_pcd = Map::new(); +// state +// .encrypted_pcd +// .decrypt_fields(&state.keys, &mut decrypted_pcd)?; - let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap(); +// let pairing_tx = decrypted_pcd.get("pairing_tx").unwrap().as_str().unwrap(); - let wallet = device.get_wallet(); +// let wallet = device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; +// let freezed_utxos = lock_freezed_utxos()?; - let recipients: Vec = device - .to_member() - .unwrap() - .get_addresses() - .iter() - .map(|a| Recipient { - address: a.clone(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1, - }) - .collect(); +// let recipients: Vec = device +// .to_member() +// .unwrap() +// .get_addresses() +// .iter() +// .map(|a| Recipient { +// address: a.clone(), +// amount: DEFAULT_AMOUNT, +// nb_outputs: 1, +// }) +// .collect(); - let mut mandatory_inputs = Vec::new(); - for i in 0u32..nb_outputs.try_into().unwrap() { - mandatory_inputs.push(OutPoint::new(previous_tx, i)); - } +// let mut mandatory_inputs = Vec::new(); +// for i in 0u32..nb_outputs.try_into().unwrap() { +// mandatory_inputs.push(OutPoint::new(previous_tx, i)); +// } - let signed_psbt = create_transaction( - mandatory_inputs, - &freezed_utxos, - wallet, - recipients, - None, - Amount::from_sat(fee_rate.into()), - None, - )?; +// let signed_psbt = create_transaction( +// mandatory_inputs, +// &freezed_utxos, +// wallet, +// recipients, +// None, +// Amount::from_sat(fee_rate.into()), +// None, +// )?; - // We send it in a TxProposal prd - let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt); +// // We send it in a TxProposal prd +// let tx_proposal = Prd::new_tx_proposal(commitment_outpoint, member, signed_psbt); - debug!("tx_proposal: {:?}", tx_proposal); - // We encrypt the prd with the shared_secret for pairing process - let prd_msg = tx_proposal.to_network_msg(wallet)?; - debug!("prd_msg: {:?}", prd_msg); - let mut ciphers = Vec::new(); - for shared_secret in shared_secrets { - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; - ciphers.push(cipher.to_lower_hex_string()); - } - // We return the cipher - Ok(ApiReturn { - ciphers_to_send: ciphers, - ..Default::default() - }) -} +// debug!("tx_proposal: {:?}", tx_proposal); +// // We encrypt the prd with the shared_secret for pairing process +// let prd_msg = tx_proposal.to_network_msg(wallet)?; +// debug!("prd_msg: {:?}", prd_msg); +// let mut ciphers = Vec::new(); +// for shared_secret in shared_secrets { +// let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; +// ciphers.push(cipher.to_lower_hex_string()); +// } +// // We return the cipher +// Ok(ApiReturn { +// ciphers_to_send: ciphers, +// ..Default::default() +// }) +// } #[wasm_bindgen] pub fn logout() -> ApiResult<()> { @@ -537,6 +598,10 @@ fn handle_transaction( // We keep the shared_secret as unconfirmed shared_secrets.add_unconfirmed_secret(shared_secret); + // We also return it + let mut new_secret = SecretsStore::new(); + new_secret.add_unconfirmed_secret(shared_secret); + // We hash the shared secret to commit into the prd connect let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array()); @@ -549,7 +614,7 @@ fn handle_transaction( let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?; return Ok(ApiReturn { - secrets: shared_secrets.to_owned(), + secrets: Some(new_secret), ciphers_to_send: vec![cipher.to_lower_hex_string()], ..Default::default() }) @@ -587,7 +652,7 @@ fn process_transaction( } #[wasm_bindgen] -pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> ApiResult { +pub fn parse_new_tx(new_tx_msg: String, block_height: u32) -> ApiResult { let new_tx: NewTxMessage = serde_json::from_str(&new_tx_msg)?; if let Some(error) = new_tx.error { @@ -608,125 +673,7 @@ pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> Api )?) } -#[wasm_bindgen] -/// Produce a proof and append it to a prd -pub fn add_validation_token_to_prd( - root_commitment: String, - prd_commitment: String, - approval: bool, -) -> ApiResult { - let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; - let outpoint = OutPoint::from_str(&root_commitment)?; - - // find the prd in the registered process - let mut processes = lock_processes()?; - let process = processes - .get_mut(&outpoint) - .ok_or(ApiError::new("Unknown process".to_owned()))?; - - let prd_ref = process - .get_impending_requests_mut() - .into_iter() - .find(|r| r.create_commitment() == prd_hash) - .ok_or(ApiError::new( - "Failed to find the prd in registered processes".to_owned(), - ))?; - - let local_device = lock_local_device()?; - - let wallet = local_device.get_wallet(); - - let spend_key: SecretKey = wallet.get_client().get_spend_key().try_into()?; - - match prd_ref.prd_type { - PrdType::Update => { - let new_state = Value::from_str(&prd_ref.payload)?; - - let new_state_commitment = new_state.tagged_hash(); - - let message_hash = if approval { - AnkHash::ValidationYes(AnkValidationYesHash::from_commitment(new_state_commitment)) - } else { - AnkHash::ValidationNo(AnkValidationNoHash::from_commitment(new_state_commitment)) - }; - - let proof = Proof::new(message_hash, spend_key); - - prd_ref.validation_tokens.push(proof); - - Ok(ApiReturn { - updated_process: Some((root_commitment, process.clone())), - ..Default::default() - }) - } - _ => return Err(ApiError::new("Can't validate that prd".to_owned())), - } -} - -#[wasm_bindgen] -pub fn response_prd( - root_commitment: String, - prd_commitment: String, // The commitment to the Prd we respond to - approval: bool, -) -> ApiResult { - let prd_hash = AnkPrdHash::from_str(&prd_commitment)?; - let outpoint = OutPoint::from_str(&root_commitment)?; - let local_device = lock_local_device()?; - let member = local_device - .to_member() - .ok_or(ApiError::new("Unpaired device".to_owned()))?; - - // find the prd in the registered process - let mut processes = lock_processes()?; - let process = processes - .get_mut(&outpoint) - .ok_or(ApiError::new("Unknown process".to_owned()))?; - - let prd_ref = process - .get_impending_requests_mut() - .into_iter() - .find(|r| r.create_commitment() == prd_hash) - .ok_or(ApiError::new( - "Failed to find the prd in registered processes".to_owned(), - ))?; - - match prd_ref.prd_type { - PrdType::Update => { - let pcd_hash: AnkPcdHash = AnkPcdHash::from_value(&Value::from_str(&prd_ref.payload)?); - - let prd_response = Prd::new_response( - OutPoint::from_str(&root_commitment)?, - serde_json::to_string(&member)?, - prd_ref.validation_tokens.clone(), - pcd_hash, - ); - - let prd_msg = prd_response.to_network_msg(local_device.get_wallet())?; - - let mut ciphers = vec![]; - for (sp_address, shared_secret) in process.get_all_secrets() { - if sp_address.to_string() - == local_device - .get_wallet() - .get_client() - .get_receiving_address() - { - continue; - } - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; - ciphers.push(cipher.to_lower_hex_string()); - } - - return Ok(ApiReturn { - ciphers_to_send: ciphers, - ..Default::default() - }); - } - _ => unimplemented!(), - }; -} - -fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { +fn confirm_prd(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult { match prd.prd_type { PrdType::Confirm | PrdType::Response | PrdType::List => { return Err(AnyhowError::msg("Invalid prd type")); @@ -737,90 +684,160 @@ fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult member, - None => { - // This might be because we're pairing, let's see if our address is part of sender of the initial prd - let remote_member: Member = serde_json::from_str(&prd.sender)?; - let addresses = remote_member.get_addresses(); - let this_device_address = local_device - .get_wallet() - .get_client() - .get_receiving_address(); - if let Some(_) = addresses.into_iter().find(|a| *a == this_device_address) { - remote_member - } else { - return Err(AnyhowError::msg("Must pair device first")); - } - } - }; + let sender = local_device.to_member(); - let pcd_commitment = AnkPcdHash::from_str(&prd.payload)?; + let prd_confirm = Prd::new_confirm(outpoint, sender, prd.pcd_commitments.clone()); - let prd_confirm = Prd::new_confirm(outpoint, member, pcd_commitment); - - debug!("Sending confirm prd: {:?}", prd_confirm); + // debug!("Sending confirm prd: {:?}", prd_confirm); let prd_msg = prd_confirm.to_network_msg(local_device.get_wallet())?; Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string()) } +fn create_diffs(process: &Process, new_state: &ProcessState) -> AnyhowResult> { + let new_state_commitments = new_state.pcd_commitment.as_object().ok_or(AnyhowError::msg("new_state commitments is not an object"))?; + + let device = lock_local_device()?; + let our_id = device.to_member(); + let is_pairing = device.get_pairing_commitment().is_none(); + + let fields_to_validate = if new_state.encrypted_pcd != Value::Null { + new_state.get_fields_to_validate_for_member(&our_id)? + } else { + vec![] + }; + + let new_state_root = &new_state.merkle_root; + let new_state_decrypted = match new_state.decrypt_pcd() { + Ok(val) => val, + Err(_) => Map::new() + }; + + let new_state_descriptions = &new_state.descriptions; + + let process_id = process.get_process_id()?.to_string(); + let mut diffs = vec![]; + if let Some(prev_state) = process.get_latest_commited_state() { + // We first decrypt as much as we can of the prev_state + let clear_prev_state = prev_state.decrypt_pcd()?; + // We just make a diff for values that are different from previous state + for (field, prev_hash) in prev_state.pcd_commitment.as_object().unwrap() { + let description = new_state_descriptions.get(field).map(|d| d.to_string()); + let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; + let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; + if let Some(new_hash) = new_state_commitments.get(field.as_str()) { + if new_hash.as_str() == prev_hash.as_str() { + continue; + } else { + // There's a diff + let previous_value = clear_prev_state.get(field.as_str()).unwrap().clone(); + diffs.push(UserDiff { + process_id: process_id.clone(), + new_state_merkle_root: new_state_root.to_owned(), + value_commitment: new_hash.as_str().unwrap().to_string(), + field: field.to_owned(), + description, + previous_value, + new_value, + notify_user: false, + need_validation, + validation_status: DiffStatus::None, + }); + } + } else { + // We're missing a hash + return Err(AnyhowError::msg(format!("No commitment for field {} in new state", field))); + } + } + } else { + // All fields need a diff + for (field, hash) in new_state_commitments { + let description = new_state_descriptions.get(field).map(|d| d.to_string()); + let new_value = if let Some(val) = new_state_decrypted.get(field.as_str()) { val.clone() } else { Value::Null }; + let need_validation = if (is_pairing && field.as_str() == "roles" && new_value != Value::Null) || fields_to_validate.contains(field) { true } else { false }; + diffs.push(UserDiff { + process_id: process_id.clone(), + new_state_merkle_root: new_state_root.to_owned(), + value_commitment: hash.as_str().unwrap().to_string(), + field: field.to_owned(), + description, + previous_value: Value::Null, + new_value, + notify_user: false, + need_validation, + validation_status: DiffStatus::None, + }); + } + } + + Ok(diffs) +} + +fn handle_prd_connect(prd: Prd, secret: AnkSharedSecretHash) -> AnyhowResult { + let local_device = lock_local_device()?; + let local_member = local_device.to_member(); + let sp_wallet = local_device.get_wallet(); + let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); + let mut shared_secrets = lock_shared_secrets()?; + if let Some(prev_proof) = prd.validation_tokens.get(0) { + // check that the proof is valid + prev_proof.verify()?; + // Check it's signed with our key + let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; + if prev_proof.get_key() != local_address.get_spend_key() { + return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); + } + // Check it signs a prd connect that contains the commitment to the shared secret + let empty_prd = Prd::new_connect(local_member, secret_hash, None); + let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); + if *msg.as_byte_array() != prev_proof.get_message() { + return Err(anyhow::Error::msg("Previous proof signs another message")); + } + // Now we can confirm the secret and link it to an address + let sender = serde_json::from_str::(&prd.sender)?; + let proof = prd.proof.unwrap(); + let actual_sender = sender.get_address_for_key(&proof.get_key()) + .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; + shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); + let mut secrets_return = SecretsStore::new(); + secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); + return Ok(ApiReturn { + secrets: Some(secrets_return), + ..Default::default() + }) + } else { + let proof = prd.proof.unwrap(); + let sender = serde_json::from_str::(&prd.sender)?; + let actual_sender = sender.get_address_for_key(&proof.get_key()) + .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; + + shared_secrets.confirm_secret_for_address(secret, actual_sender.clone().try_into()?); + + let mut secrets_return = SecretsStore::new(); + secrets_return.confirm_secret_for_address(secret, actual_sender.try_into()?); + + let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); + let msg = prd_connect.to_network_msg(sp_wallet)?; + let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; + + return Ok(ApiReturn { + ciphers_to_send: vec![cipher.to_lower_hex_string()], + secrets: Some(secrets_return), + ..Default::default() + }) + } +} + fn handle_prd( prd: Prd, secret: AnkSharedSecretHash ) -> AnyhowResult { + debug!("handle_prd: {:#?}", prd); // Connect is a bit different here because there's no associated process // Let's handle that case separately if prd.prd_type == PrdType::Connect { - let local_device = lock_local_device()?; - let local_member = local_device.to_member(); - let sp_wallet = local_device.get_wallet(); - let secret_hash = AnkMessageHash::from_message(secret.as_byte_array()); - let mut shared_secrets = lock_shared_secrets()?; - if let Some(prev_proof) = prd.validation_tokens.get(0) { - // check that the proof is valid - prev_proof.verify()?; - // Check it's signed with our key - let local_address = SilentPaymentAddress::try_from(sp_wallet.get_client().get_receiving_address())?; - if prev_proof.get_key() != local_address.get_spend_key() { - return Err(anyhow::Error::msg("Previous proof of a prd connect isn't signed by us")); - } - // Check it signs a prd connect that contains the commitment to the shared secret - let empty_prd = Prd::new_connect(local_member, secret_hash, None); - let msg = AnkMessageHash::from_message(empty_prd.to_string().as_bytes()); - if *msg.as_byte_array() != prev_proof.get_message() { - return Err(anyhow::Error::msg("Previous proof signs another message")); - } - // Now we can confirm the secret and link it to an address - let sender = serde_json::from_str::(&prd.sender)?; - let proof = prd.proof.unwrap(); - let actual_sender = sender.get_address_for_key(&proof.get_key()) - .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; - shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); - debug!("updated secrets"); - return Ok(ApiReturn { - secrets: shared_secrets.to_owned(), - ..Default::default() - }) - } else { - let proof = prd.proof.unwrap(); - let sender = serde_json::from_str::(&prd.sender)?; - let actual_sender = sender.get_address_for_key(&proof.get_key()) - .ok_or(anyhow::Error::msg("Signer of the proof is not part of sender"))?; - - shared_secrets.confirm_secret_for_address(secret, actual_sender.try_into()?); - - let prd_connect = Prd::new_connect(local_member, secret_hash, prd.proof); - let msg = prd_connect.to_network_msg(sp_wallet)?; - let cipher = encrypt_with_key(secret.as_byte_array(), msg.as_bytes())?; - - return Ok(ApiReturn { - ciphers_to_send: vec![cipher.to_lower_hex_string()], - secrets: shared_secrets.to_owned(), - ..Default::default() - }) - } + return handle_prd_connect(prd, secret); } let outpoint = OutPoint::from_str(&prd.root_commitment)?; @@ -830,131 +847,106 @@ fn handle_prd( std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(), std::collections::hash_map::Entry::Vacant(entry) => { debug!("Creating new process for outpoint: {}", outpoint); - entry.insert(Process::new(vec![], vec![])) + entry.insert(Process::new(outpoint)) } }; match prd.prd_type { - PrdType::Confirm => { - // It must match a prd we sent previously - // We send the whole data in a pcd - debug!("Received confirm prd {:#?}", prd); - let original_request = relevant_process - .get_impending_requests() - .into_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"))?; - let member: Member = serde_json::from_str(&prd.sender)?; - - // We send the data to all addresses of the member we know a secret for - let mut ciphers = vec![]; - for address in member.get_addresses() { - if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { - let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd.payload.as_bytes()); - } else { - // For now we don't fail if we're missing an address for a member but maybe we should - warn!("Failed to find secret for address {}", address); - } + PrdType::Update => { + // Compute the merkle tree root for the proposed new state to see if we already know about it + let update_merkle_root = prd.pcd_commitments.create_merkle_tree()?.root().ok_or(AnyhowError::msg("Invalid merkle tree"))?.to_lower_hex_string(); + if relevant_process.get_state_for_commitments_root(&update_merkle_root).is_ok() { + // We already know about that state + return Err(AnyhowError::msg("Received update for a state we already know")); } - // This should never happen since we sent a message to get a confirmation back - if ciphers.is_empty() { - return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); - } + let commited_in = OutPoint::from_str(&prd.root_commitment)?; - return Ok(ApiReturn { - ciphers_to_send: ciphers, + // Extract the roles from the payload + let proposal_roles: HashMap = serde_json::from_str(&prd.payload)?; + + // TODO: check that the role in the prd has the right commitment + + let new_state = ProcessState { + commited_in, + pcd_commitment: prd.pcd_commitments, + merkle_root: update_merkle_root.clone(), + keys: prd.keys, ..Default::default() - }) - } - PrdType::Update | PrdType::TxProposal | PrdType::Message => { - // Those all have some new data we don't know about yet - // We send a Confirm to get the pcd - // Add the prd to our list of actions for this process - relevant_process.insert_impending_request(prd.clone()); - let member: Member = serde_json::from_str(&prd.sender)?; - let mut ciphers = vec![]; - for address in member.get_addresses() { - if let Some(shared_secret) = lock_shared_secrets()?.get_secret_for_address(address.as_str().try_into()?) { - let cipher = confirm_prd(&prd, &shared_secret)?; - } else { - // For now we don't fail if we're missing an address for a member but maybe we should - warn!("Failed to find secret for address {}", address); - } + }; + + // Compute the diffs + // At this point we don't have the encrypted values + let diffs = create_diffs(&relevant_process, &new_state)?; + + // Take the roles from the last validated state + let mut roles = HashMap::new(); + if let Some(last_state) = relevant_process.get_latest_commited_state() { + let decrypted_last_state = last_state.decrypt_pcd()?; + roles = Value::Object(decrypted_last_state).extract_roles()?; + } else { + // We don't have commited state yet, let's take the current roles + roles = proposal_roles; } - // This should never happen since we sent a message to get a confirmation back - if ciphers.is_empty() { - return Err(anyhow::Error::msg(format!("No available secrets for member {:?}", member))); - } + relevant_process.insert_concurrent_state(new_state); - return Ok(ApiReturn { - ciphers_to_send: ciphers, - updated_process: Some((outpoint.to_string(), relevant_process.clone())), + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: relevant_process.clone(), + new_diffs: diffs, + up_to_date_roles: roles, ..Default::default() - }); - } - PrdType::Response => { - // We must know of a prd update that the response answers to - let original_request = relevant_process - .get_impending_requests_mut() - .into_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"))?; - - // Once we found the prd update, we can add the received proofs as validation tokens - original_request - .validation_tokens - .extend(prd.validation_tokens); - - // We must return an update of the process - let updated_process = (prd.root_commitment, relevant_process.clone()); + }; return Ok(ApiReturn { updated_process: Some(updated_process), ..Default::default() }); } + PrdType::Response => { + let mut to_update = relevant_process + .get_latest_concurrent_states_mut()? + .into_iter() + .find(|r| { + r.pcd_commitment == prd.pcd_commitments + }) + .ok_or(anyhow::Error::msg("Original request not found"))?; + + to_update + .validation_tokens + .extend(prd.validation_tokens); + + let updated_state = to_update.clone(); + + let clear_state = to_update.decrypt_pcd()?; + + let roles = Value::Object(clear_state).extract_roles()?; + + // We must return an update of the process + let updated_process = UpdatedProcess { + commitment_tx: OutPoint::from_str(&prd.root_commitment)?, + current_process: relevant_process.clone(), + modified_state: Some(updated_state.merkle_root), + ..Default::default() + }; + + let commit_msg = CommitMessage::new_update_commitment( + OutPoint::from_str(&prd.root_commitment)?, + updated_state.pcd_commitment, + roles + ); + + return Ok(ApiReturn { + updated_process: Some(updated_process), + commit_to_send: Some(commit_msg), + ..Default::default() + }); + } _ => unimplemented!(), } } -fn handle_pcd(plain: Vec, root_commitment: OutPoint) -> AnyhowResult { - let pcd = Value::from_str(&String::from_utf8(plain)?)?; - - let pcd_commitment = pcd.tagged_hash(); - - let mut processes = lock_processes()?; - let relevant_process = processes.get_mut(&root_commitment).unwrap(); - - // We match the pcd with a prd and act accordingly - let prd = relevant_process - .get_impending_requests_mut() - .into_iter() - .find(|r| *r.payload == pcd_commitment.to_string()) - .ok_or(AnyhowError::msg("Failed to retrieve the matching prd"))?; - - // We update the process and return it - prd.payload = pcd.to_string(); - - return Ok(ApiReturn { - updated_process: Some((root_commitment.to_string(), relevant_process.clone())), - ..Default::default() - }); -} - fn handle_decrypted_message( secret: AnkSharedSecretHash, plain: Vec, @@ -962,13 +954,99 @@ fn handle_decrypted_message( let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?; if let Ok(prd) = Prd::extract_from_message(&plain, local_address) { handle_prd(prd, secret) - } else if let Ok(pcd) = Value::from_str(&String::from_utf8(plain)?) { - handle_pcd(pcd) } else { Err(anyhow::Error::msg("Failed to handle decrypted message")) } } +#[wasm_bindgen] +/// Use the provided Map to update a state +/// The map uses hash commitment as keys, as in storage +pub fn update_process_state(init_commitment: String, state_id: String, hash2values: String) -> ApiResult { + let hash2values_map = serde_json::from_str::(&hash2values)?.to_value_object()?; + + // Get the process + let outpoint = OutPoint::from_str(&init_commitment)?; + + let mut processes = lock_processes()?; + { + // First a mutable borrow of the process + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + // Get the state + let state = process.get_latest_concurrent_states_mut()? + .into_iter() + .find(|state| state.merkle_root == state_id) + .ok_or(ApiError::new("Unknown state".to_owned()))?; + + // Update each value + // Check if there's already something + // If we have the key, decrypt and compare to the commitment + if state.encrypted_pcd.as_object().is_some() && !state.encrypted_pcd.as_object().unwrap().is_empty() { + return Err(ApiError::new("State already existing".to_owned())); + } + let state_commitments = state.pcd_commitment.to_value_object()?; + let mut new_encrypted_pcd: Map = Map::with_capacity(hash2values_map.len()); + + for (hash, value) in hash2values_map { + // Check the hash in pcd_commitment, get the corresponding field name + let (field, _) = state_commitments.iter().find(|(field, commitment)| *hash == **commitment) + .ok_or(ApiError::new(format!("Failed to find the commitment {}", hash)))?; + + new_encrypted_pcd.insert(field.clone(), value); + } + + // decrypt all we can and check it matches the commitment + state.encrypted_pcd = Value::Object(new_encrypted_pcd); + let commited_in = serialize(&state.commited_in); + + let clear_pcd = state.decrypt_pcd()?; + + for (i, (key, value)) in clear_pcd.iter().enumerate() { + // hash each value, and check the result against commitments + if let Some(expected) = state_commitments.get(key.as_str()) { + // value can already be the commitment, if we don't have the encryption key + if value.is_hex_string(Some(32)).is_ok() { + // check if the clear value is the commitment + if expected.as_str().unwrap() == value.as_str().unwrap() { continue; } + } + // Otherwise we hash the value whatever it is, it must match the commitment + let mut value_bin = value.to_string().into_bytes(); + value_bin.push(i.try_into().unwrap()); + let tagged_hash = AnkPcdHash::from_value_with_outpoint(&value_bin, &commited_in); + if tagged_hash.as_byte_array().to_lower_hex_string() != expected.as_str().unwrap() { + // We set the encrypted pcd back to empty + state.encrypted_pcd = Value::Object(Map::new()); + return Err(ApiError::new(format!("Retrieved value for {} doesn't match the commitment", key))); + } + } else { + // This shouldn't be possible + state.encrypted_pcd = Value::Object(Map::new()); + return Err(ApiError::new(format!("Missing commitment for key {}", key))); + } + } + } + + // If every value we can decrypt is valid, then we return the new state and diffs + // We borrow it again immutably + let process = processes.get(&outpoint).unwrap(); + let state = process.get_latest_concurrent_states()?.into_iter().find(|s| s.merkle_root == state_id).unwrap(); + let diffs = create_diffs(&process, &state)?; + + let udpated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process.clone(), + new_diffs: diffs, + ..Default::default() + }; + + Ok(ApiReturn { + updated_process: Some(udpated_process), + ..Default::default() + }) +} + #[wasm_bindgen] pub fn parse_cipher(cipher_msg: String) -> ApiResult { // Check that the cipher is not empty or too long @@ -1003,86 +1081,67 @@ pub fn get_available_amount() -> ApiResult { Ok(device.get_wallet().get_outputs().get_balance().to_sat()) } -#[wasm_bindgen] -/// This takes a reference to a process and creates a commit msg for the latest state -pub fn create_commit_message( - init_commitment_outpoint: String, - relay_address: String, - fee_rate: u32, -) -> ApiResult { - let outpoint = OutPoint::from_str(&init_commitment_outpoint)?; +fn get_shared_secrets_in_transaction( + psbt: &Psbt, + addresses: Vec +) -> anyhow::Result> { + let local_device = lock_local_device()?; - if let Some(process) = lock_processes()?.get(&outpoint) { - match process.get_number_of_states() { - 0 => Err(ApiError::new("Process has no states".to_owned())), - 1 => { - // This is a creation - let state = process.get_latest_state().unwrap(); - if state.commited_in.vout != u32::MAX { - return Err(ApiError::new("Latest state is already commited".to_owned())); - } - let encrypted_pcd = state.encrypted_pcd.clone(); - let keys = state.keys.clone(); + let sp_wallet = local_device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; + let partial_secret = sp_wallet + .get_client() + .get_partial_secret_from_psbt(&psbt)?; - let local_device = lock_local_device()?; + let mut new_secrets = HashMap::new(); + for address in addresses { + let sp_address = SilentPaymentAddress::try_from(address.as_str())?; + let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( + &sp_address.get_scan_key(), + &partial_secret, + ); - let sp_wallet = local_device.get_wallet(); + let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - vec![Recipient { - address: relay_address, - amount: Amount::from_sat(1000), - nb_outputs: 1, - }], - None, - Amount::from_sat(fee_rate.into()), - None, - )?; - - let tx = signed_psbt.extract_tx()?; - - Ok(ApiReturn { - commit_to_send: Some(CommitMessage::new_first_commitment( - tx, - encrypted_pcd.as_object().unwrap().clone(), - keys, - )), - ..Default::default() - }) - } - _ => { - // We're updating an existing process - // Check that initial outpoint is not a placeholder and that latest state has a commited_in of null - if outpoint.vout != u32::MAX { - return Err(ApiError::new( - "Initial outpoint is a placeholder".to_owned(), - )); - } - let state = process.get_latest_state().unwrap(); - if state.commited_in != OutPoint::null() { - return Err(ApiError::new("Latest state is already commited".to_owned())); - } - let encrypted_pcd = state.encrypted_pcd.clone(); - let keys = state.keys.clone(); - // We just send the message with the outpoint - return Ok(ApiReturn { - commit_to_send: Some(CommitMessage::new_update_commitment( - outpoint, - encrypted_pcd.as_object().unwrap().clone(), - keys, - )), - ..Default::default() - }); - } - } - } else { - return Err(ApiError::new("Process not found".to_owned())); + new_secrets.insert(sp_address, shared_secret); } + + Ok(new_secrets) +} + +fn create_transaction_for_addresses(addresses: Vec, fee_rate: u32) -> anyhow::Result { + let mut sp_addresses: Vec = Vec::with_capacity(addresses.len()); + for address in &addresses { + let sp_address = SilentPaymentAddress::try_from(address.as_str())?; + sp_addresses.push(sp_address); + } + + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + let freezed_utxos = lock_freezed_utxos()?; + + let mut recipients = Vec::with_capacity(addresses.len()); + for address in addresses { + let recipient = Recipient { + address: address, + amount: DEFAULT_AMOUNT, + nb_outputs: 1, + }; + recipients.push(recipient); + } + + let signed_psbt = create_transaction( + vec![], + &freezed_utxos, + sp_wallet, + recipients, + None, + Amount::from_sat(fee_rate.into()), + None, + )?; + + Ok(signed_psbt) } #[wasm_bindgen] @@ -1095,132 +1154,199 @@ pub fn create_connect_transaction(members_str: Vec, fee_rate: u32) -> Ap members.push(serde_json::from_str(&member)?) } - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - let freezed_utxos = lock_freezed_utxos()?; - - let recipients = members.iter() - .flat_map(|member| { - member.get_addresses() - }) - .map(|address| { - Recipient { - address: address.clone(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1 - } - }) - .collect(); - - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - recipients, - None, - Amount::from_sat(fee_rate.into()), - None, - )?; - - let partial_secret = sp_wallet - .get_client() - .get_partial_secret_from_psbt(&signed_psbt)?; - - // We now generate the shared secret for each address - let mut shared_secrets = lock_shared_secrets()?; + let mut addresses = vec![]; for member in members { - let addresses = member.get_addresses(); - - for address in addresses { - let sp_address = SilentPaymentAddress::try_from(address.as_str())?; - let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &sp_address.get_scan_key(), - &partial_secret, - ); - - let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); - - shared_secrets.confirm_secret_for_address(shared_secret, sp_address); - } + addresses.extend(member.get_addresses().into_iter()); } - let transaction = signed_psbt.extract_tx()?; + if addresses.is_empty() { + return Err(ApiError::new("No addresses to connect to".to_owned())); + } + + let psbt = create_transaction_for_addresses(addresses.clone(), fee_rate)?; + + let new_secrets = get_shared_secrets_in_transaction(&psbt, addresses)?; + + let transaction = psbt.extract_tx()?; + + let mut shared_secrets = lock_shared_secrets()?; + let mut secrets_return = SecretsStore::new(); + for (address, secret) in new_secrets { + shared_secrets.confirm_secret_for_address(secret, address); + secrets_return.confirm_secret_for_address(secret, address); + } Ok(ApiReturn { new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)), - secrets: shared_secrets.to_owned(), + secrets: Some(secrets_return), ..Default::default() }) } #[wasm_bindgen] -/// We assume that the provided tx outpoint exist -pub fn create_update_transaction( - init_commitment: Option, - new_state: String, +pub fn create_new_process( + init_state_str: String, + descriptions_str: Option, + relay_address: String, fee_rate: u32, ) -> ApiResult { - let pcd = Value::from_str(&new_state)?; - let pcd_map = pcd - .as_object() - .ok_or(ApiError::new("new_state must be an object".to_owned()))?; + let init_state = ::new_from_string(&init_state_str)?; + let descriptions = if let Some(d) = descriptions_str { ::new_from_string(&d)? } else { Value::Object(Map::new()) }; - let mut processes = lock_processes()?; + // check that we have a proper roles map + let roles = init_state.extract_roles()?; - let commitment_outpoint: OutPoint; - let relevant_process: &mut Process; - if let Some(s) = init_commitment { - // We're updating an existing contract - let outpoint = OutPoint::from_str(&s)?; + // We create a transaction that spends to the relay address + let psbt = create_transaction_for_addresses(vec![relay_address.clone()], fee_rate)?; - if let Some(p) = processes.get_mut(&outpoint) { - // compare the provided new_state with the process defined template - let previous_state = &p.get_state_at(0).unwrap().encrypted_pcd; - if !compare_maps(previous_state.as_object().unwrap(), pcd_map) { - return Err(ApiError::new( - "Provided updated state is not consistent with the process template".to_owned(), - )); - } - relevant_process = p; - commitment_outpoint = outpoint; - } else { - // This is a process we don't know about, so we insert a new entry - processes.insert(outpoint, Process::default()); - relevant_process = processes.get_mut(&outpoint).unwrap(); - commitment_outpoint = outpoint; - } - } else { - // This is a creation with an init state, the commitment will come later - // We need a placeholder to keep track of the process before it's commited on chain - // We can take the hash of the init_state as a txid, and set the vout to the max as it is very unlikely to ever have a real commitment that will look like this - let dummy = pcd.tagged_hash(); + // We take the secret out + let new_secrets = get_shared_secrets_in_transaction(&psbt, vec![relay_address])?; - let dummy_outpoint = OutPoint::new(Txid::from_slice(dummy.as_byte_array())?, u32::MAX); - - processes.insert(dummy_outpoint, Process::default()); - - relevant_process = processes.get_mut(&dummy_outpoint).unwrap(); - commitment_outpoint = dummy_outpoint; + let mut shared_secrets = lock_shared_secrets()?; + let mut secrets_return = SecretsStore::new(); + for (address, secret) in new_secrets { + shared_secrets.confirm_secret_for_address(secret, address); + secrets_return.confirm_secret_for_address(secret, address); } - // We assume that all processes must have a roles key - let roles = pcd - .get("roles") - .ok_or(ApiError::new("No roles in new_state".to_owned()))?; - let roles_map = roles - .as_object() - .ok_or(ApiError::new("roles is not an object".to_owned()))? - .clone(); + let transaction = psbt.extract_tx()?; + + // We now have the outpoint that will serve as id for the whole process + let outpoint = OutPoint::new(transaction.txid(), 0); + + let new_state = ProcessState::new(outpoint, init_state.to_value_object()?, descriptions.to_value_object()?)?; + + let mut process = Process::new(outpoint); + + let diffs = create_diffs(&process, &new_state)?; + + process.insert_concurrent_state(new_state.clone())?; + + { + let mut processes = lock_processes()?; + // If we already have an entry with this outpoint, something's wrong + if processes.contains_key(&outpoint) { + return Err(ApiError::new("There's already a process for this outpoint".to_owned())); + } + processes.insert(outpoint.clone(), process.clone()); + } + + let commit_msg = CommitMessage::new_first_commitment(transaction, new_state.pcd_commitment, roles.clone()); + + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process, + up_to_date_roles: roles, + new_diffs: diffs, + ..Default::default() + }; + + Ok(ApiReturn { + secrets: Some(secrets_return), + commit_to_send: Some(commit_msg), + updated_process: Some(updated_process), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn update_process( + init_commitment: String, + new_state_str: String, +) -> ApiResult { + let outpoint = OutPoint::from_str(&init_commitment)?; + + let mut processes = lock_processes()?; + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + let prev_state = process.get_latest_commited_state() + .ok_or(ApiError::new("Process must have at least one state already commited".to_owned()))?; + + let last_state_commitments = &prev_state.pcd_commitment; + let last_state_descriptions = &prev_state.descriptions; + + let clear_new_state = Value::from_str(&new_state_str)?; + + let roles = clear_new_state.extract_roles()?; + + let new_state = ProcessState::new(prev_state.commited_in, clear_new_state.to_value_object()?, last_state_descriptions.clone())?; + + // We compare the new state with the previous one + let last_state_merkle_root = &prev_state.merkle_root; + + if *last_state_merkle_root == new_state.merkle_root { + return Err(ApiError::new("new proposed state is identical to the previous commited state".to_owned())); + } + + // We check that we don't have already a similar concurrent state + let concurrent_processes = process.get_latest_concurrent_states()?; + if concurrent_processes.iter().any(|p| p.merkle_root == new_state.merkle_root) { + return Err(ApiError::new("New state already known".to_owned())); + } + + let diffs = create_diffs(&process, &new_state)?; + + // Add the new state to the process + process.insert_concurrent_state(new_state.clone())?; + + let updated_process = UpdatedProcess { + commitment_tx: outpoint, + current_process: process.clone(), + up_to_date_roles: roles, + new_diffs: diffs, + ..Default::default() + }; + + Ok(ApiReturn { + updated_process: Some(updated_process), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn create_update_message( + init_commitment: String, + merkle_root_hex: String, +) -> ApiResult { + let mut processes = lock_processes()?; + + let outpoint = OutPoint::from_str(&init_commitment)?; + + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + let update_state = process.get_state_for_commitments_root(&merkle_root_hex)?; + + // We must have at least the key for the roles field, otherwise we don't know who to send the message to + let clear_state = update_state.decrypt_pcd()?; + + let roles = Value::Object(clear_state).extract_roles()?; + + let local_device = lock_local_device()?; + + let sp_wallet = local_device.get_wallet(); + let local_address = sp_wallet.get_client().get_receiving_address(); + let mut all_members: HashMap> = HashMap::new(); - for (name, role_def) in roles_map { - let role: RoleDefinition = serde_json::from_str(&role_def.to_string())?; + let shared_secrets = lock_shared_secrets()?; + for (name, role) in &roles { let fields: Vec = role .validation_rules .iter() .flat_map(|rule| rule.fields.clone()) .collect(); - for member in role.members { + for member in &role.members { + // Check that we have a shared_secret with all members + if let Some(no_secret_address) = member.get_addresses().iter() + .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) + { + // We ignore it if we don't have a secret with ourselves + if *no_secret_address != local_address { + // for now we return an error to keep it simple + return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); + } + } if !all_members.contains_key(&member) { all_members.insert(member.clone(), HashSet::new()); } @@ -1228,86 +1354,21 @@ pub fn create_update_transaction( } } - let nb_recipients = all_members.len(); - if nb_recipients == 0 { - return Err(ApiError::new( - "Can't create a process with 0 member".to_owned(), - )); - } - - let mut recipients: Vec = Vec::with_capacity(nb_recipients * 2); // We suppose that will work most of the time - // we actually have multiple "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.keys() { - let addresses = member.get_addresses(); - for sp_address in addresses.into_iter() { - recipients.push(Recipient { - address: sp_address.into(), - amount: DEFAULT_AMOUNT, - nb_outputs: 1, - }); - } - } - - let mut fields2keys = Map::new(); - let mut fields2cipher = Map::new(); - let encrypted_pcd = pcd.clone(); - let fields_to_encrypt: Vec = encrypted_pcd - .as_object() - .unwrap() - .keys() - .map(|k| k.clone()) - .collect(); - encrypted_pcd.encrypt_fields(&fields_to_encrypt, &mut fields2keys, &mut fields2cipher); - - let local_device = lock_local_device()?; - - let sp_wallet = local_device.get_wallet(); - let local_address = sp_wallet.get_client().get_receiving_address(); - let sender: Member = local_device - .to_member() - .ok_or(ApiError::new("unpaired device".to_owned()))?; + .to_member(); - // We first generate the prd with all the keys that we will keep to ourselves let full_prd = Prd::new_update( - commitment_outpoint, + outpoint, serde_json::to_string(&sender)?, - fields2cipher.clone(), - fields2keys.clone(), + roles, + update_state.keys.clone(), + update_state.pcd_commitment.clone(), ); - let prd_commitment = full_prd.create_commitment(); - - let freezed_utxos = lock_freezed_utxos()?; - - let signed_psbt = create_transaction( - vec![], - &freezed_utxos, - sp_wallet, - 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)?; - - let final_tx = signed_psbt.extract_tx()?; - let mut ciphers = vec![]; for (member, visible_fields) in all_members { let mut prd = full_prd.clone(); prd.filter_keys(visible_fields); - // we hash the payload - prd.payload = Value::from_str(&prd.payload) - .unwrap() - .tagged_hash() - .to_string(); let prd_msg = prd.to_network_msg(sp_wallet)?; let addresses = member.get_addresses(); @@ -1316,33 +1377,179 @@ pub fn create_update_transaction( if sp_address == local_address { continue; } - let shared_point = sp_utils::sending::calculate_ecdh_shared_secret( - &::try_from(sp_address.as_str())?.get_scan_key(), - &partial_secret, - ); - let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point); + // We shouldn't ever have error here since we already checked above + let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; ciphers.push(cipher.to_lower_hex_string()); - relevant_process - .insert_shared_secret(SilentPaymentAddress::try_from(sp_address)?, shared_secret); } } - relevant_process.insert_impending_request(full_prd); - relevant_process.insert_state(ProcessState { - commited_in: OutPoint::null(), - encrypted_pcd: Value::Object(fields2cipher), - keys: fields2keys, - validation_tokens: vec![], - }); - // Create the new_tx message - let new_tx_msg = NewTxMessage::new(serialize(&final_tx).to_lower_hex_string(), None); + if ciphers.is_empty() { + return Err(ApiError::new("Empty ciphers list".to_owned())); + } + + Ok(ApiReturn { + ciphers_to_send: ciphers, + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn validate_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { + add_validation_token(init_commitment, merkle_root_hex, true) +} + +#[wasm_bindgen] +pub fn refuse_state(init_commitment: String, merkle_root_hex: String) -> ApiResult { + add_validation_token(init_commitment, merkle_root_hex, false) +} + +#[wasm_bindgen] +pub fn evaluate_state(init_commitment: String, previous_state: Option, state: String) -> ApiResult { + let prev_state: Option = if let Some(s) = previous_state { Some(serde_json::from_str(&s)?) } else { None }; + let process_state: ProcessState = serde_json::from_str(&state)?; + + process_state.is_valid(prev_state.as_ref())?; + + let clear_pcd = process_state.decrypt_pcd()?; + let roles = Value::Object(clear_pcd).extract_roles()?; + + // We create a commit msg with the valid state + let outpoint: OutPoint = OutPoint::from_str(&init_commitment)?; + let commit_msg = CommitMessage::new_update_commitment(outpoint, process_state.pcd_commitment, roles); + + Ok(ApiReturn { + commit_to_send: Some(commit_msg), + ..Default::default() + }) +} + +fn add_validation_token(init_commitment: String, merkle_root_hex: String, approval: bool) -> ApiResult { + let mut processes = lock_processes()?; + + let outpoint = OutPoint::from_str(&init_commitment)?; + + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + { + let update_state: &mut ProcessState = process.get_state_for_commitments_root_mut(&merkle_root_hex)?; + + let mut merkle_root = [0u8; 32]; + + merkle_root.copy_from_slice(&Vec::from_hex(&merkle_root_hex)?); + + let message_hash = if approval { + AnkHash::ValidationYes(AnkValidationYesHash::from_merkle_root(merkle_root)) + } else { + AnkHash::ValidationNo(AnkValidationNoHash::from_merkle_root(merkle_root)) + }; + + let local_device = lock_local_device()?; + let sp_wallet = local_device.get_wallet(); + let proof = Proof::new(message_hash, sp_wallet.get_client().get_spend_key().try_into()?); + + update_state.validation_tokens.push(proof); + } + + let updated_process = UpdatedProcess { + commitment_tx: OutPoint::from_str(&init_commitment)?, + current_process: process.clone(), + modified_state: Some(merkle_root_hex), + ..Default::default() + }; + + Ok(ApiReturn { + updated_process: Some(updated_process), + ..Default::default() + }) +} + +#[wasm_bindgen] +pub fn create_response_prd(init_commitment: String, merkle_root_hex: String) -> ApiResult { + let mut processes = lock_processes()?; + + let outpoint = OutPoint::from_str(&init_commitment)?; + + let process = processes.get_mut(&outpoint) + .ok_or(ApiError::new("Unknown process".to_owned()))?; + + let update_state: &mut ProcessState = process.get_state_for_commitments_root_mut(&merkle_root_hex)?; + + // We must have at least the key for the roles field, otherwise we don't know who to send the message to + let clear_state = update_state.decrypt_pcd()?; + + let roles = Value::Object(clear_state).extract_roles()?; + + let local_device = lock_local_device()?; + let sp_wallet = local_device.get_wallet(); + let local_address = sp_wallet.get_client().get_receiving_address(); + + let mut all_members: HashMap> = HashMap::new(); + let shared_secrets = lock_shared_secrets()?; + for (name, role) in roles { + let fields: Vec = role + .validation_rules + .iter() + .flat_map(|rule| rule.fields.clone()) + .collect(); + for member in role.members { + // Check that we have a shared_secret with all members + if let Some(no_secret_address) = member.get_addresses().iter() + .find(|a| shared_secrets.get_secret_for_address(a.as_str().try_into().unwrap()).is_none()) + { + // We ignore it if we don't have a secret with ourselves + if *no_secret_address != local_address { + // for now we return an error to keep it simple + return Err(ApiError::new(format!("No shared secret for all addresses of {:?}\nPlease first connect", member))); + } + } + if !all_members.contains_key(&member) { + all_members.insert(member.clone(), HashSet::new()); + } + all_members.get_mut(&member).unwrap().extend(fields.clone()); + } + } + + let our_key = SilentPaymentAddress::try_from(local_address.as_str())?.get_spend_key(); + let proof = update_state.validation_tokens.iter().find(|t| t.get_key() == our_key) + .ok_or(ApiError::new("We haven't added our validation token yet".to_owned()))?; + + let sender: Member = local_device + .to_member(); + + let response_prd = Prd::new_response( + outpoint, + serde_json::to_string(&sender)?, + vec![*proof], + update_state.pcd_commitment.clone(), + ); + let prd_msg = response_prd.to_network_msg(sp_wallet)?; + + let mut ciphers = vec![]; + for (member, visible_fields) in all_members { + let addresses = member.get_addresses(); + for sp_address in addresses.into_iter() { + // We skip our own device address, no point sending ourself a cipher + if sp_address == local_address { + continue; + } + + // We shouldn't ever have error here since we already checked above + let shared_secret = shared_secrets.get_secret_for_address(sp_address.as_str().try_into()?).unwrap(); + + let cipher = encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?; + ciphers.push(cipher.to_lower_hex_string()); + } + } + + if ciphers.is_empty() { + return Err(ApiError::new("Empty ciphers list".to_owned())); + } Ok(ApiReturn { - new_tx_to_send: Some(new_tx_msg), - updated_process: Some((commitment_outpoint.to_string(), relevant_process.clone())), ciphers_to_send: ciphers, ..Default::default() }) @@ -1368,81 +1575,9 @@ pub fn create_faucet_msg() -> ApiResult { Ok(faucet_msg.to_string()) } -/// Get active update proposals for a given process outpoint -/// Returns a vector with the latest commited state first, if any, and all active proposals #[wasm_bindgen] -pub fn get_update_proposals(process_outpoint: String) -> ApiResult> { +pub fn get_storages(process_outpoint: String) -> ApiResult> { let outpoint = OutPoint::from_str(&process_outpoint)?; - - let mut processes = lock_processes()?; - - // TODO: We clone the process to prevent double borrowing issue, this can certainly be improved - let relevant_process = processes - .get(&outpoint) - .ok_or(ApiError::new("process not found".to_owned()))?; - - let mut updated_process = relevant_process.clone(); - - let update_proposals: Vec<&Prd> = relevant_process - .get_impending_requests() - .into_iter() - .filter(|r| r.prd_type == PrdType::Update) - .collect(); - - if update_proposals.is_empty() { - return Err(ApiError::new(format!( - "No active update proposals for process {}", - process_outpoint - ))); - } - - let mut res = vec![]; - - // We first push the last commited state, if any - match relevant_process.get_latest_commited_state() { - Some(state) => res.push(serde_json::to_string(state)?), - None => () - } - - // Maybe that's the right place for adding a new state with what we've got from the prd update - // We should probably iterate on every update proposals and see which one don't have a state yet - let mut update_states = false; - for proposal in update_proposals { - // Is there a state that matches this proposal? If not, let's add it - debug!("Trying proposal {:#?}", proposal); - let pcd = match Value::from_str(&proposal.payload) { - Ok(value) => value, - Err(e) => continue - }; - debug!("found pcd {:#?}", pcd); - let pcd_hash = AnkPcdHash::from_value(&pcd); - // We look for a pending state for the exact same state as the one in the proposal - if let None = relevant_process.get_latest_concurrent_states() - .into_iter() - .find(|state| { - AnkPcdHash::from_value(&state.encrypted_pcd) == pcd_hash - }) - { - // If not, we first add a new state - updated_process.insert_state(ProcessState { - commited_in: OutPoint::new(Txid::from_str(&pcd_hash.to_string())?, u32::MAX), - encrypted_pcd: pcd.clone(), - keys: proposal.keys.clone(), - validation_tokens: proposal.validation_tokens.clone() - }); - update_states = true; - } - // We add the decrypted state to our return variable - let mut decrypted_pcd = Map::new(); - pcd.decrypt_fields(&proposal.keys, &mut decrypted_pcd)?; - res.push(serde_json::to_string(&decrypted_pcd)?); - } - - if update_states { - // We replace the process - processes.insert(outpoint, updated_process); - } - // else we do nothing - - Ok(res) + + Ok(vec![]) } diff --git a/tests/connect.rs b/tests/connect.rs index 1e4b87a..c560fdc 100644 --- a/tests/connect.rs +++ b/tests/connect.rs @@ -1,18 +1,14 @@ use std::collections::HashMap; -use std::str::FromStr; use sdk_client::api::{ - add_validation_token_to_prd, create_commit_message, create_connect_transaction, create_device_from_sp_wallet, create_update_message, dump_device, dump_process_cache, get_address, get_outputs, get_update_proposals, pair_device, parse_cipher, reset_device, reset_shared_secrets, response_prd, restore_device, set_process_cache, set_shared_secrets, setup, ApiReturn + create_connect_transaction, create_device_from_sp_wallet, dump_device, get_address, get_outputs, parse_cipher, reset_device, reset_shared_secrets, restore_device, set_shared_secrets, setup, ApiReturn }; -use sdk_common::log::{debug, info}; -use sdk_common::pcd::{Member, RoleDefinition}; +use sdk_common::log::debug; +use sdk_common::pcd::Member; use sdk_common::secrets::SecretsStore; -use sdk_common::sp_client::bitcoin::consensus::deserialize; -use sdk_common::sp_client::bitcoin::hex::FromHex; -use sdk_common::sp_client::bitcoin::{OutPoint, Transaction}; +use sdk_common::sp_client::bitcoin::OutPoint; use sdk_common::sp_client::spclient::OwnedOutput; use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress; -use serde_json::{json, Value}; use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; @@ -24,10 +20,50 @@ use utils::*; wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] +/// Tests the connection process between two devices, Alice and Bob, by executing a secure +/// transaction to establish a shared secret for encrypted communication. +/// +/// The basics are that one device will initiate the process by sending a transaction that pays another device. +/// The recipient of the transaction as soon as it finds it, can extract a shared secret and send an encrypted +/// message back. Upon receiving this message, the initiator answers with a similar message similarly encrypted. +/// Upon receiving this message, the recipient can be assured that the communication is safe, and start using +/// the secret to communicate. +/// +/// The security of the shared secret rest on the soundness of the silent payment protocol for Bitcoin. +/// In its encrypted response, the initiator adds a signature that is proof that it indeed controls the +/// private key for the silent payment address it announced, so recipient knows there's no mitm or impostor. +/// +/// # Detailed Process +/// +/// ## Alice sends a transaction that pays Bob: +/// - Alice initializes her device from an `sp_wallet` object and sets it as the local device. +/// - She retrieves her own address and obtains Bob’s address. +/// - Alice creates a new member using Bob’s device address (this is mainly for testing purpose, +/// because `create_connection_transaction` would take members as argument). +/// - She generates a connection transaction (`connect_tx`) targeting Bob's device. +/// - Alice processes her own transaction and stores the derived shared secrets in `alice_secrets_store`, +/// associating the shared secret with Bob's addresses. +/// +/// ## Bob parses the transaction: +/// - Bob initializes his device from his own `sp_wallet`. +/// - He parses Alice’s connection transaction to retrieve the shared secret Alice created for him. +/// - Bob saves these derived shared secrets in `bob_secrets_store` but can't index it with Alice's address yet. +/// +/// ## Prd Connect exchange +/// - Bob then responds by sending a prd connect back to Alice encrypted with the shared secret. +/// This prd is very simple and basically contains the following: +/// * All Bob's devices addresses +/// * a commitment to the shared secret +/// * a proof signed with Bob's device spend key +/// - Alice receives and decrypts the message from Bob. +/// - She replies to Bob by encrypting another prd connect which is basically the same, but keeping Bob's proof and adding her own. +/// - **Bob’s Confirmation**: Bob receives Alice’s confirmation message, decrypts it, and updates his secret in `bob_secrets_store`. +/// +/// ## Verification: +/// - Finally, the function asserts that Alice and Bob now share the same secrets, confirming successful +/// connection and mutual authentication between the devices. fn test_connect() { setup(); - // let mut alice_process_cache = HashMap::new(); - // let mut bob_process_cache = HashMap::new(); let mut alice_secrets_store = SecretsStore::new(); let mut bob_secrets_store = SecretsStore::new(); @@ -70,7 +106,21 @@ fn test_connect() { let alice_connect_transaction = connect_tx_msg.transaction; let alice_device = dump_device().unwrap(); - alice_secrets_store = alice_connect_return.secrets; + + // Below is how to update our secrets store when secrets is Some + let secrets_update = alice_connect_return.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Bob reset_device().unwrap(); @@ -83,7 +133,19 @@ fn test_connect() { let bob_to_alice_cipher = &bob_parsed_transaction_return.ciphers_to_send[0]; let bob_device = dump_device().unwrap(); - bob_secrets_store = bob_parsed_transaction_return.secrets; + let updated_secrets = bob_parsed_transaction_return.secrets.unwrap(); + let updated_unconfirmed_secrets = updated_secrets.get_all_unconfirmed_secrets(); + if !updated_unconfirmed_secrets.is_empty() { + for secret in updated_unconfirmed_secrets { + bob_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = updated_secrets.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + bob_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Alice reset_device().unwrap(); @@ -96,7 +158,19 @@ fn test_connect() { // debug!("alice_parsed_confirm: {:#?}", alice_parsed_confirm); let alice_to_bob_cipher = alice_parsed_connect.ciphers_to_send.get(0).unwrap(); - alice_secrets_store = alice_parsed_connect.secrets; + let secrets_update = alice_parsed_connect.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } // ======================= Bob reset_device().unwrap(); @@ -106,7 +180,19 @@ fn test_connect() { debug!("Bob parses alice prd connect"); let bob_parsed_connect = parse_cipher(alice_to_bob_cipher.clone()).unwrap(); - bob_secrets_store = bob_parsed_connect.secrets; + let updated_secrets = bob_parsed_connect.secrets.unwrap(); + let updated_unconfirmed_secrets = updated_secrets.get_all_unconfirmed_secrets(); + if !updated_unconfirmed_secrets.is_empty() { + for secret in updated_unconfirmed_secrets { + bob_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = updated_secrets.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + bob_secrets_store.confirm_secret_for_address(secret, address); + } + } // Assert that Alice and Bob now has the same secret assert!(alice_secrets_store.get_secret_for_address(bob_address.try_into().unwrap()) == bob_secrets_store.get_secret_for_address(alice_address.try_into().unwrap())); diff --git a/tests/pairing.rs b/tests/pairing.rs index 15f1dcb..e06a5c3 100644 --- a/tests/pairing.rs +++ b/tests/pairing.rs @@ -2,17 +2,16 @@ use std::collections::HashMap; use std::str::FromStr; use sdk_client::api::{ - add_validation_token_to_prd, create_commit_message, 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, response_prd, restore_device, set_process_cache, setup, ApiReturn + create_device_from_sp_wallet, create_new_process, create_response_prd, create_update_message, dump_device, get_address, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, update_process_state, validate_state }; -use sdk_common::log::{debug, info}; -use sdk_common::pcd::{Member, RoleDefinition}; -use sdk_common::sp_client::bitcoin::consensus::deserialize; +use sdk_common::crypto::AnkSharedSecretHash; +use sdk_common::log::debug; +use sdk_common::pcd::{Member, Pcd, RoleDefinition}; +use sdk_common::secrets::SecretsStore; use sdk_common::sp_client::bitcoin::hex::FromHex; -use sdk_common::sp_client::bitcoin::{OutPoint, Transaction}; -use sdk_common::sp_client::spclient::OwnedOutput; -use serde_json::{json, Value}; +use sdk_common::sp_client::bitcoin::OutPoint; +use serde_json::{json, Map, Value}; -use tsify::JsValueSerdeExt; use wasm_bindgen_test::*; mod utils; @@ -21,11 +20,85 @@ use utils::*; wasm_bindgen_test_configure!(run_in_browser); +/// # Pairing Process Documentation between Alice and Bob +/// +/// This test describes the secure pairing process between two devices, Alice and Bob. +/// +/// ## What's pairing? +/// Pairing is a process, and abide by the same rules than any other process. The goal of pairing +/// is to define an identity on the network as a set of devices (defined by their sp_address). +/// Being a process it is public and can be audited by anyone, and be used as one's proof of identity. +/// It also contains a session keypair that is updated as necessary. Since all devices are needed to +/// update the key in the process it can then be used to sign a proof that someone was indeed in control +/// of all the devices for some amount of time in a MFA setup. +/// It contains the following mandatory fields: +/// * `roles`: multiple devices represented as sp adresses linked together in the same member. It is recommended +/// to have one `owner` role with one member which is the actual identity and whose signatures are all +/// needed to modify anything in the process. +/// * `session_privkey`: a private key visible by all devices of the member defined in the process, but +/// not by other members. It *must* be changed at every update of the process. This key will be used +/// to sign documents and validate actions for other processes. It's valid as soon as the commitment +/// transaction for the process udpate is seen and it stays valid for _n_ blocks after the update being mined. +/// * `session_pubkey`: the x-only public key derived from the session private key. It's visible by everyone and +/// used for validation by any third party. Obviously it changes with the private key at any update. +/// * `parity`: the parity of the session_pubkey. We could use 33 bytes compressed public key format +/// but using 32 bytes publick key + parity allows for more standard serialization. +/// +/// ## Detailed protocol +/// (Here Alice and Bob are used as a convention, but keep in mind they're not 2 different users, but +/// 2 devices belonging to the same user) +/// ## Step 0 - Preliminary step +/// 1. **Establishing a Shared Secret**: A shared secret is established to secure +/// communication between Alice and Bob (see `connect.rs`). +/// ## Step 1 - Pairing Preparation by Alice +/// 1. **Pairing Status Check**: Alice verifies that it's not already paired. +/// 2. **Adding Bob's Address**: Alice adds Bob’s address to her own, setting the base for creating +/// a new `Member` object. +/// 3. **Creation of the pairing process**: Alice initializes pairing by creating a prd update that contains +/// both its address and Bob's, and send it to Bob. +/// +/// ## Step 2 - Receiving and Confirming the `prd` by Bob +/// 1. **Receiving and Verifying**: Bob receives and decrypts the update `prd` message sent by Alice. +/// 2. **Updating Process State**: Bob identifies the new process and store it, but it doesn't have access +/// to the actual data for now. +/// 3. **Creating and Sending `Prd Confirm`**: Bob creates a confirmation `prd`, which he then +/// sends to Alice to get the pcd containing the state for this new process. +/// +/// ## Step 3 - Alice gets confirmation and answers with a pcd +/// 1. **Receiving and Verifying**: Alice receives the `Prd Confirm` sent by Bob. +/// 2. **Sending PCD**: Alice having confirmation that Bob got the update proposal, +/// it now sends the actual data in a pcd. +/// 3. **User confirmation**: At this step we must get the approval of the user. If user confirms +/// the pairing we create a prd response with a valid signature from Alice spend key and send +/// it to Bob. +/// +/// ## Step 4 - Finalizing Pairing by Bob +/// 1. **Receiving and Verifying `pcd`**: Bob received the `pcd` and only now can tell what's the +/// process was about. +/// 2. **Validating Pairing State**: Bob retrieves the latest process state and the state change +/// request, in this case, the pairing. User is prompted for validation, and if confirmed a prd response +/// is created and sent(see the **User confirmation** step for Alice). +/// +/// ## Commiting the process state +/// 1. **Creating the `commit_msg`**: The first device that got both validations creates the commit_msg that +/// contains a transaction paying a relay to generate the first outpoint to commit the state of the process, +/// the hash of the encrypted state of the process (relay must have access to roles though, either it is clear +/// all along or it was provided with the encryption keys) and the proofs that all devices validated this state. +/// 2. **Actual commitment**: As soon as the relay validated the proofs it spends the outpoint and puts the hash of +/// the whole prd response (including pcd hash and all the proofs) in an OP_RETURN output. The process is now +/// public and can be used to prove identity for other processes. + #[wasm_bindgen_test] fn test_pairing() { + const RELAY_ADDRESS: &str = "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4"; + setup(); let mut alice_process_cache = HashMap::new(); let mut bob_process_cache = HashMap::new(); + let mut alice_secrets_store = SecretsStore::new(); + let mut bob_secrets_store = SecretsStore::new(); + let mut alice_diff_cache = Vec::new(); + let mut bob_diff_cache = Vec::new(); debug!("==============================================\nStarting test_pairing\n=============================================="); @@ -35,9 +108,18 @@ fn test_pairing() { // we get our own address let alice_address = get_address().unwrap(); + debug!("alice address: {}", alice_address); // we scan the qr code or get the address by any other means let bob_address = helper_get_bob_address(); + debug!("bob_address: {}", bob_address); + + // we add some shared_secret in both secrets_store + let shared_secret = AnkSharedSecretHash::from_str("c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45").unwrap(); + alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.as_str().try_into().unwrap()); + bob_secrets_store.confirm_secret_for_address(shared_secret, alice_address.as_str().try_into().unwrap()); + + set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap(); // Alice creates the new member with Bob address let new_member = Member::new(vec![ @@ -46,10 +128,14 @@ fn test_pairing() { ]) .unwrap(); - let initial_session_privkey = [0u8; 32]; + let initial_session_privkey = [0u8; 32]; // In reality we would generate a random new key here let initial_session_pubkey = [0u8; 32]; let pairing_init_state = json!({ + "html": "", + "js": "", + "style": "", + "zones": [], "description": "AliceBob", "roles": { "owner": { @@ -70,7 +156,8 @@ fn test_pairing() { ], "min_sig_member": 1.0 } - ] + ], + "storages": [] } }, "session_privkey": initial_session_privkey, @@ -78,223 +165,155 @@ fn test_pairing() { "key_parity": true, // This allows us to use a 32 bytes array in serialization }); + debug!("Alice creates the pairing process"); + let create_process_return = create_new_process(pairing_init_state.to_string(), None, RELAY_ADDRESS.to_owned(), 1).unwrap(); + + let commit_msg = create_process_return.commit_to_send.unwrap(); + + let secrets_update = create_process_return.secrets.unwrap(); + let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets(); + if !unconfirmed_secrets.is_empty() { + for secret in unconfirmed_secrets { + alice_secrets_store.add_unconfirmed_secret(secret); + } + } + let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets(); + if !updated_confirmed_secrets.is_empty() { + for (address, secret) in updated_confirmed_secrets { + alice_secrets_store.confirm_secret_for_address(secret, address); + } + } + + let updated_process = create_process_return.updated_process.unwrap(); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); + + // Alice keeps track of the change she needs to validate + let create_process_diffs = updated_process.new_diffs; + + let new_state_id = &create_process_diffs.get(0).unwrap().new_state_merkle_root; + + alice_diff_cache.extend(create_process_diffs.iter()); + + // We send the commit_msg to the relay we got the address from + + // now we create prd update for this new process + debug!("Alice creates an update prd to Bob"); + let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); + + let updated_process = create_update_return.updated_process.unwrap(); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); + debug!("Alice pairs her device"); - // we can update our local device now, first with an empty txid - pair_device(OutPoint::null().to_string(), vec![helper_get_bob_address()]).unwrap(); + pair_device(updated_process.commitment_tx.to_string(), vec![helper_get_bob_address()]).unwrap(); - debug!("Alice sends a transaction commiting to an update prd to Bob"); - let alice_pairing_return = - create_update_transaction(None, pairing_init_state.to_string(), 1).unwrap(); - - let (root_outpoint, alice_init_process) = alice_pairing_return.updated_process.unwrap(); - alice_process_cache.insert(root_outpoint.clone(), alice_init_process.clone()); - - let pairing_tx_msg = alice_pairing_return.new_tx_to_send.unwrap(); - - // This is only for testing, the relay takes care of that in prod - 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(&pairing_tx_msg.transaction, alice_outputs); - - // End of the test only part - - // Alice parses her own transaction - helper_parse_transaction(&pairing_tx_msg.transaction, &alice_pairing_tweak_data); + let alice_to_bob_cipher = &create_update_return.ciphers_to_send[0]; // this is only for testing, as we're playing both parts let alice_device = dump_device().unwrap(); - let alice_processes = dump_process_cache().unwrap(); // ======================= Bob reset_device().unwrap(); create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap(); + set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap(); - // Bob receives Alice pairing transaction - debug!("Bob parses Alice pairing transaction"); - helper_parse_transaction(&pairing_tx_msg.transaction, &alice_pairing_tweak_data); + debug!("Bob receives the update prd"); + let bob_parsed_return = parse_cipher(alice_to_bob_cipher.to_owned()).unwrap(); - debug!("Bob receives the prd"); - let mut bob_retrieved_prd: ApiReturn = ApiReturn::default(); - for cipher in alice_pairing_return.ciphers_to_send.iter() { - // debug!("Parsing cipher: {:#?}", cipher); - match parse_cipher(cipher.clone()) { - Ok(res) => bob_retrieved_prd = res, - Err(e) => { - debug!("Error parsing cipher: {:#?}", e); - continue; - } - } - } + let updated_process = bob_parsed_return.updated_process.unwrap(); - assert!(bob_retrieved_prd.ciphers_to_send.len() == 1); - assert!(bob_retrieved_prd.updated_process.is_some()); + let parsed_prd_diffs = updated_process.new_diffs; - debug!("Bob retrieved prd: {:#?}", bob_retrieved_prd); + // debug!("Bob creates process {} with state {}", updated_process.commitment_tx, new_state_id); + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let (root_commitment, relevant_process) = bob_retrieved_prd.updated_process.unwrap(); + // Bob also keeps track of changes - bob_process_cache.insert(root_commitment.clone(), relevant_process); + bob_diff_cache.extend(parsed_prd_diffs.into_iter()); - let prd_confirm_cipher = bob_retrieved_prd.ciphers_to_send.iter().next().unwrap(); + debug!("Bob can now fetch the data from storage using the hashes"); + // We have to cheat here and let Bob access Alice process cache + let process = alice_process_cache.get(&updated_process.commitment_tx).unwrap(); - debug!("Bob sends a Confirm Prd to Alice"); + let state = process.get_state_for_commitments_root(&new_state_id).unwrap(); + + let hash2values: Map = bob_diff_cache.iter() + .filter(|diff| diff.new_state_merkle_root == *new_state_id) + .map(|diff| { + let encrypted_value = state.encrypted_pcd.as_object().unwrap().get(&diff.field).unwrap(); + (diff.value_commitment.clone(), encrypted_value.clone()) + }) + .collect(); + let update_process_res = update_process_state(updated_process.commitment_tx.to_string(), new_state_id.clone(), serde_json::to_string(&Value::Object(hash2values)).unwrap()).unwrap(); + + let updated_process = update_process_res.updated_process.unwrap(); + + let parsed_prd_diffs = updated_process.new_diffs; + + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); + + bob_diff_cache.extend(parsed_prd_diffs); + + // We can also prune the old diffs from the cache + bob_diff_cache.retain(|diff| diff.new_value != Value::Null); // this is only for testing, as we're playing both parts let bob_device = dump_device().unwrap(); - let bob_processes = dump_process_cache().unwrap(); // ======================= Alice reset_device().unwrap(); restore_device(alice_device).unwrap(); - set_process_cache(alice_processes).unwrap(); + set_process_cache(serde_json::to_string(&alice_process_cache).unwrap()).unwrap(); + set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap(); - debug!("Alice receives the Confirm Prd"); - let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap(); + let commitment_outpoint = alice_process_cache.keys().next().unwrap(); - debug!( - "Alice parsed Bob's Confirm Prd: {:#?}", - alice_parsed_confirm - ); + debug!("Alice can validate the new state of the process"); + let relevant_process = alice_process_cache.get(&commitment_outpoint).unwrap(); - // Alice simply shoots back the return value in the ws - let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone(); + for diff in alice_diff_cache { + debug!("User validate diff: {:#?}", diff); + } - // Now that we're sure that bob got the prd udpate we also produce the prd response and shoot it - let alice_prd_update_commitment = alice_init_process - .get_impending_requests() - .get(0) - .unwrap() - .create_commitment(); - let (_, alice_validated_prd) = add_validation_token_to_prd( - root_outpoint.clone(), - alice_prd_update_commitment.to_string(), - true, - ) - .unwrap() - .updated_process - .unwrap(); + // Alice can also sign her response and send it to Bob + let validate_state_return = validate_state(commitment_outpoint.to_string(), new_state_id.clone()).unwrap(); - alice_process_cache.insert(root_outpoint.clone(), alice_validated_prd); + let updated_process = validate_state_return.updated_process.unwrap(); - let alice_prd_response = - response_prd(root_outpoint, alice_prd_update_commitment.to_string(), true).unwrap(); + alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let bob_received_response = alice_prd_response.ciphers_to_send.get(0).unwrap().clone(); + let alice_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); // ======================= Bob reset_device().unwrap(); restore_device(bob_device).unwrap(); - set_process_cache(bob_processes).unwrap(); + set_process_cache(serde_json::to_string(&bob_process_cache).unwrap()).unwrap(); + set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap(); - debug!("Bob parses Alice's pcd"); - let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap(); - - debug!("bob_parsed_pcd: {:#?}", bob_parsed_pcd_return); - - // Here we would update our database - bob_process_cache.insert( - root_commitment.clone(), - bob_parsed_pcd_return.updated_process.unwrap().1, - ); - - // We now need Alice prd response, and update our process with it - debug!("Bob also parses alice prd response"); - let bob_parsed_response = parse_cipher(bob_received_response).unwrap(); - - debug!("bob_parsed_response: {:#?}", bob_parsed_response); - - bob_process_cache.insert( - root_commitment.clone(), - bob_parsed_response.updated_process.unwrap().1, - ); - - debug!("{:#?}", bob_process_cache.get(&root_commitment).unwrap()); - - // At this point, user must validate the pairing proposal received from Alice - // We decrypt the content of the pcd so that we can display to user what matters - let alice_proposal = get_update_proposals(root_commitment.clone()).unwrap(); - - debug!("Alice proposal: {:#?}", alice_proposal); - - let proposal = Value::from_str(&alice_proposal.get(0).unwrap()).unwrap(); - debug!("proposal: {:#?}", proposal); - - // get the roles from the proposal - 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.len() == 1); - assert!(roles["owner"].members.len() == 1); - - // we get all the addresses of the members of the proposal - let proposal_members = roles - .iter() - .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(&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 to the user on device to prompt confirmation - info!("Pop-up: User confirmation"); + for diff in &bob_diff_cache { + if diff.need_validation { + debug!("Pop-up: User confirmation"); + debug!("{:#?}", diff); + } + } // If user is ok, we can add our own validation token - let prd_to_respond = bob_process_cache - .get(&root_commitment) - .unwrap() - .get_impending_requests() - .get(0) - .unwrap() - .to_owned(); - let bob_added_validation = - add_validation_token_to_prd(root_commitment.clone(), prd_to_respond.create_commitment().to_string(), true).unwrap(); + // Get the whole commitment from the process + let bob_validated_process = validate_state(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); - bob_process_cache.insert( - root_commitment.clone(), - bob_added_validation.updated_process.unwrap().1, - ); + let updated_process = bob_validated_process.updated_process.unwrap(); - // We create the commit msg for the relay that includes Alice and Bob's proofs - let commit_msg = create_commit_message(root_commitment.clone(), "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4".to_owned(), 1).unwrap().commit_to_send.unwrap(); + bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process); - let tx: Transaction = deserialize(&Vec::from_hex(&commit_msg.init_tx).unwrap()).unwrap(); + let bob_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap(); - // We send the commit_msg to the relay we got the address from - // We also send - - // We can just take the txid of the transaction we created for the commitment - let commitment_outpoint = OutPoint::new(tx.txid(), 0); + let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree debug!("Bob pairs device with Alice"); - pair_device(commitment_outpoint.to_string(), proposal_members).unwrap(); + let roles: HashMap = serde_json::from_value(bob_diff_cache.iter().find(|diff| diff.field == "roles").unwrap().new_value.clone()).unwrap(); + let owner = roles.get("owner").unwrap(); + let members_to_pair: Vec = owner.members.iter().flat_map(|m| m.get_addresses()).collect(); + pair_device(updated_process.commitment_tx.to_string(), members_to_pair).unwrap(); - // To make the pairing effective, alice and bob must now creates a new transaction where they both control one output - - // login(); + // We can also check alice response + let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap(); } diff --git a/tests/utils.rs b/tests/utils.rs index e04736c..53cd531 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -69,7 +69,7 @@ pub fn helper_parse_transaction(transaction: &str, tweak_data: &str) -> ApiRetur )) .unwrap(); // debug!("new_tx_msg: {:?}", new_tx_msg); - let result = parse_new_tx(new_tx_msg, 0, 1); + let result = parse_new_tx(new_tx_msg, 0); match result { Ok(m) => m, Err(e) => panic!("Unexpected error: {}", e.message),