199 lines
9.3 KiB
Rust
199 lines
9.3 KiB
Rust
use std::collections::HashMap;
|
||
|
||
use sdk_client::api::{
|
||
create_device_from_sp_wallet, create_transaction, dump_device, get_address, get_outputs, parse_cipher, reset_device, reset_shared_secrets, restore_device, set_shared_secrets, setup
|
||
};
|
||
use sdk_common::log::debug;
|
||
use sdk_common::secrets::SecretsStore;
|
||
use sdk_common::sp_client::bitcoin::OutPoint;
|
||
use sdk_common::sp_client::OwnedOutput;
|
||
|
||
use tsify::JsValueSerdeExt;
|
||
use wasm_bindgen_test::*;
|
||
|
||
mod utils;
|
||
|
||
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.
|
||
#[wasm_bindgen_test]
|
||
fn test_connect() {
|
||
setup();
|
||
let mut alice_secrets_store = SecretsStore::new();
|
||
let mut bob_secrets_store = SecretsStore::new();
|
||
|
||
debug!("==============================================\nStarting test_connect\n==============================================");
|
||
|
||
// ========================= Alice
|
||
reset_device().unwrap();
|
||
create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap();
|
||
|
||
// we get our own address
|
||
let alice_address = get_address().unwrap();
|
||
|
||
// we scan the qr code or get the address by any other means
|
||
let bob_address = helper_get_bob_address();
|
||
|
||
debug!("Alice establishes a shared secret with Bob");
|
||
// We just send a transaction to Bob device to allow them to share encrypted message
|
||
// Since we're not paired we can just put bob's address in a disposable member
|
||
// create_connect_transaction needs to take members though because most of the time we'd rather create secrets with all the devices of a member
|
||
// Dans l'API actuelle, on crée directement une transaction vers les adresses SP
|
||
let alice_connect_return = create_transaction(vec![bob_address.clone()], 1).unwrap();
|
||
|
||
debug!("alice_connect_return: {:#?}", alice_connect_return);
|
||
|
||
let connect_tx_msg = alice_connect_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<OutPoint, OwnedOutput> = get_outputs_result.into_serde().unwrap();
|
||
|
||
let alice_pairing_tweak_data =
|
||
helper_get_tweak_data(&connect_tx_msg.transaction, alice_outputs);
|
||
|
||
// End of the test only part
|
||
|
||
// Alice parses her own transaction
|
||
helper_parse_transaction(&connect_tx_msg.transaction, &alice_pairing_tweak_data);
|
||
|
||
let alice_connect_transaction = connect_tx_msg.transaction;
|
||
|
||
let alice_device = dump_device().unwrap();
|
||
|
||
// 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();
|
||
reset_shared_secrets().unwrap();
|
||
create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap();
|
||
|
||
debug!("Bob parses Alice connect transaction");
|
||
let bob_parsed_transaction_return = helper_parse_transaction(&alice_connect_transaction, &alice_pairing_tweak_data);
|
||
|
||
let bob_to_alice_cipher = &bob_parsed_transaction_return.ciphers_to_send[0];
|
||
|
||
let bob_device = dump_device().unwrap();
|
||
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();
|
||
restore_device(serde_wasm_bindgen::to_value(&alice_device).unwrap()).unwrap();
|
||
set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap();
|
||
|
||
debug!("Alice receives the connect Prd");
|
||
let alice_parsed_connect = parse_cipher(bob_to_alice_cipher.clone(), sdk_common::serialization::OutPointMemberMap(std::collections::HashMap::new())).unwrap();
|
||
|
||
// debug!("alice_parsed_confirm: {:#?}", alice_parsed_confirm);
|
||
|
||
let alice_to_bob_cipher = alice_parsed_connect.ciphers_to_send.get(0).unwrap();
|
||
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();
|
||
restore_device(serde_wasm_bindgen::to_value(&bob_device).unwrap()).unwrap();
|
||
set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap();
|
||
|
||
debug!("Bob parses alice prd connect");
|
||
let bob_parsed_connect = parse_cipher(alice_to_bob_cipher.clone(), sdk_common::serialization::OutPointMemberMap(std::collections::HashMap::new())).unwrap();
|
||
|
||
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()));
|
||
}
|