Add connect logic
This commit is contained in:
parent
56cadb71f6
commit
2910125bd1
467
src/api.rs
467
src/api.rs
@ -49,7 +49,7 @@ use sdk_common::sp_client::silentpayments::{
|
||||
utils::{Network as SpNetwork, SilentPaymentAddress},
|
||||
Error as SpError,
|
||||
};
|
||||
use sdk_common::{signature, MAX_PRD_PAYLOAD_SIZE};
|
||||
use sdk_common::{signature, MutexExt, MAX_PRD_PAYLOAD_SIZE};
|
||||
use serde_json::{Error as SerdeJsonError, Map, Value};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -71,16 +71,16 @@ use sdk_common::sp_client::spclient::{
|
||||
derive_keys_from_seed, OutputList, OutputSpendStatus, OwnedOutput, Recipient, SpClient,
|
||||
};
|
||||
use sdk_common::sp_client::spclient::{SpWallet, SpendKey};
|
||||
use sdk_common::secrets::SecretsStore;
|
||||
|
||||
use crate::user::{lock_local_device, set_new_device, LOCAL_DEVICE};
|
||||
use crate::wallet::{generate_sp_wallet, lock_freezed_utxos};
|
||||
use crate::{lock_messages, CACHEDMESSAGES};
|
||||
|
||||
#[derive(Debug, PartialEq, Tsify, Serialize, Deserialize, Default)]
|
||||
#[tsify(into_wasm_abi, from_wasm_abi)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub struct ApiReturn {
|
||||
pub updated_cached_msg: Vec<CachedMessage>,
|
||||
pub secrets: SecretsStore,
|
||||
pub updated_process: Option<(String, Process)>,
|
||||
pub new_tx_to_send: Option<NewTxMessage>,
|
||||
pub ciphers_to_send: Vec<String>,
|
||||
@ -92,6 +92,14 @@ pub type ApiResult<T: FromWasmAbi> = Result<T, ApiError>;
|
||||
const IS_TESTNET: bool = true;
|
||||
const DEFAULT_AMOUNT: Amount = Amount::from_sat(1000);
|
||||
|
||||
pub static SHAREDSECRETS: OnceLock<Mutex<SecretsStore>> = OnceLock::new();
|
||||
|
||||
pub fn lock_shared_secrets() -> Result<MutexGuard<'static, SecretsStore>, anyhow::Error> {
|
||||
SHAREDSECRETS
|
||||
.get_or_init(|| Mutex::new(SecretsStore::new()))
|
||||
.lock_anyhow()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ApiError {
|
||||
pub message: String,
|
||||
@ -439,44 +447,23 @@ pub fn set_process_cache(processes: String) -> ApiResult<()> {
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn reset_message_cache() -> ApiResult<()> {
|
||||
let mut cached_msg = lock_messages()?;
|
||||
pub fn reset_shared_secrets() -> ApiResult<()> {
|
||||
let mut shared_secrets = lock_shared_secrets()?;
|
||||
|
||||
*cached_msg = vec![];
|
||||
|
||||
debug_assert!(cached_msg.is_empty());
|
||||
*shared_secrets = SecretsStore::new();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_message_cache(msg_cache: Vec<String>) -> ApiResult<()> {
|
||||
let mut cached_msg = lock_messages()?;
|
||||
pub fn set_shared_secrets(secrets: String) -> ApiResult<()>{
|
||||
let mut shared_secrets = lock_shared_secrets()?;
|
||||
|
||||
if !cached_msg.is_empty() {
|
||||
return Err(ApiError::new("Message cache not empty".to_owned()));
|
||||
}
|
||||
|
||||
let new_cache: Result<Vec<CachedMessage>, serde_json::Error> =
|
||||
msg_cache.iter().map(|m| serde_json::from_str(m)).collect();
|
||||
|
||||
*cached_msg = new_cache?;
|
||||
*shared_secrets = serde_json::from_str(&secrets)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn dump_message_cache() -> ApiResult<Vec<String>> {
|
||||
let cached_msg = lock_messages()?;
|
||||
|
||||
let res: Vec<String> = cached_msg
|
||||
.iter()
|
||||
.map(|m| serde_json::to_string(m).unwrap())
|
||||
.collect();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn dump_device() -> ApiResult<String> {
|
||||
let local_device = lock_local_device()?;
|
||||
@ -490,7 +477,7 @@ pub fn reset_device() -> ApiResult<()> {
|
||||
|
||||
*device = Device::default();
|
||||
|
||||
reset_message_cache()?;
|
||||
reset_shared_secrets()?;
|
||||
reset_process_cache()?;
|
||||
|
||||
Ok(())
|
||||
@ -506,14 +493,16 @@ pub fn get_txid(transaction: String) -> ApiResult<String> {
|
||||
fn handle_transaction(
|
||||
updated: HashMap<OutPoint, OwnedOutput>,
|
||||
tx: &Transaction,
|
||||
tweak_data: PublicKey,
|
||||
public_data: PublicKey,
|
||||
) -> AnyhowResult<ApiReturn> {
|
||||
let b_scan: SecretKey;
|
||||
let local_address: SilentPaymentAddress;
|
||||
let local_member: Member;
|
||||
let sp_wallet: SpWallet;
|
||||
{
|
||||
let local_device = lock_local_device()?;
|
||||
sp_wallet = local_device.get_wallet().clone();
|
||||
b_scan = local_device.get_wallet().get_client().get_scan_key();
|
||||
local_address = local_device.get_wallet().get_client().get_receiving_address().try_into()?;
|
||||
local_member = local_device.to_member();
|
||||
}
|
||||
|
||||
let op_return: Vec<&sdk_common::sp_client::bitcoin::TxOut> = tx
|
||||
@ -535,55 +524,35 @@ fn handle_transaction(
|
||||
.filter(|(outpoint, output)| output.spend_status != OutputSpendStatus::Unspent)
|
||||
.collect();
|
||||
|
||||
let mut messages = lock_messages()?;
|
||||
let mut shared_secrets = lock_shared_secrets()?;
|
||||
|
||||
// empty utxo_destroyed means we received this transaction
|
||||
if utxo_destroyed.is_empty() {
|
||||
let shared_point = sp_utils::receiving::calculate_ecdh_shared_secret(
|
||||
&tweak_data,
|
||||
&public_data,
|
||||
&b_scan,
|
||||
);
|
||||
let shared_secret = AnkSharedSecretHash::from_shared_point(shared_point);
|
||||
|
||||
let mut plaintext: Vec<u8> = vec![];
|
||||
if let Some(message) = messages.iter_mut().find(|m| {
|
||||
if m.status != CachedMessageStatus::CipherWaitingTx {
|
||||
return false;
|
||||
}
|
||||
let res = m.try_decrypt_with_shared_secret(shared_secret.to_byte_array());
|
||||
if res.is_ok() {
|
||||
plaintext = res.unwrap();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}) {
|
||||
// Calling this check that the prd we found check with the hashed commitment in transaction
|
||||
// We also check the signed proof that is included in the prd
|
||||
let prd = Prd::extract_from_message_with_commitment(&plaintext, local_address, &commitment)?;
|
||||
// We keep the shared_secret as unconfirmed
|
||||
shared_secrets.add_unconfirmed_secret(shared_secret);
|
||||
|
||||
// for now the previous method doesn't error if proof is missing,
|
||||
// We must define if there are cases where a valid prd doesn't have proof
|
||||
// We hash the shared secret to commit into the prd connect
|
||||
let secret_hash = AnkMessageHash::from_message(shared_secret.as_byte_array());
|
||||
|
||||
let outpoint = OutPoint::from_str(&prd.root_commitment)?;
|
||||
// We still don't know who sent it, so we reply with a `Connect` prd
|
||||
let prd_connect = Prd::new_connect(local_member, secret_hash, None);
|
||||
|
||||
handle_decrypted_message(plaintext, Some(shared_secret), Some(outpoint))
|
||||
} else {
|
||||
// store it and wait for the message
|
||||
let mut new_msg = CachedMessage::new();
|
||||
new_msg.commitment = Some(commitment.as_byte_array().to_lower_hex_string());
|
||||
new_msg
|
||||
.shared_secrets
|
||||
.push(shared_secret.to_byte_array().to_lower_hex_string());
|
||||
new_msg.status = CachedMessageStatus::TxWaitingPrd;
|
||||
let msg = prd_connect.to_network_msg(&sp_wallet)?;
|
||||
|
||||
messages.push(new_msg.clone());
|
||||
// We encrypt the prd connect with the same secret
|
||||
let cipher = encrypt_with_key(shared_secret.as_byte_array(), msg.as_bytes())?;
|
||||
|
||||
return Ok(ApiReturn {
|
||||
updated_cached_msg: vec![new_msg.clone()],
|
||||
secrets: shared_secrets.to_owned(),
|
||||
ciphers_to_send: vec![cipher.to_lower_hex_string()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// We're sender of the transaction, do nothing
|
||||
return Ok(ApiReturn {
|
||||
@ -639,29 +608,6 @@ pub fn parse_new_tx(new_tx_msg: String, block_height: u32, fee_rate: u32) -> Api
|
||||
)?)
|
||||
}
|
||||
|
||||
fn try_decrypt_with_processes(
|
||||
cipher: &[u8],
|
||||
processes: MutexGuard<HashMap<OutPoint, Process>>,
|
||||
) -> Option<(Vec<u8>, SilentPaymentAddress, OutPoint)> {
|
||||
let nonce = Nonce::from_slice(&cipher[..12]);
|
||||
|
||||
for (outpoint, process) in processes.iter() {
|
||||
for (address, secret) in process.get_all_secrets() {
|
||||
let engine = Aes256Gcm::new(&secret.to_byte_array().into());
|
||||
if let Ok(plain) = engine.decrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: &cipher[12..],
|
||||
aad: AAD,
|
||||
},
|
||||
) {
|
||||
return Some((plain, address, *outpoint));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
/// Produce a proof and append it to a prd
|
||||
pub fn add_validation_token_to_prd(
|
||||
@ -820,123 +766,62 @@ fn confirm_prd(prd: Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult<St
|
||||
Ok(encrypt_with_key(shared_secret.as_byte_array(), prd_msg.as_bytes())?.to_lower_hex_string())
|
||||
}
|
||||
|
||||
fn send_data(prd: &Prd, shared_secret: &AnkSharedSecretHash) -> AnyhowResult<ApiReturn> {
|
||||
let pcd = &prd.payload;
|
||||
|
||||
let cipher = encrypt_with_key(shared_secret.as_byte_array(), pcd.as_bytes())?;
|
||||
|
||||
Ok(ApiReturn {
|
||||
ciphers_to_send: vec![cipher.to_lower_hex_string()],
|
||||
fn handle_prd(
|
||||
prd: Prd,
|
||||
secret: AnkSharedSecretHash
|
||||
) -> AnyhowResult<ApiReturn> {
|
||||
// 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::<Member>(&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()
|
||||
})
|
||||
}
|
||||
|
||||
fn decrypt_with_cached_messages(
|
||||
cipher: &[u8],
|
||||
messages: &mut MutexGuard<Vec<CachedMessage>>
|
||||
) -> anyhow::Result<Option<(Vec<u8>, AnkSharedSecretHash)>> {
|
||||
let nonce = Nonce::from_slice(&cipher[..12]);
|
||||
let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?;
|
||||
|
||||
for message in messages.iter_mut() {
|
||||
for shared_secret in message.shared_secrets.iter() {
|
||||
let aes_key = match AnkSharedSecretHash::from_str(shared_secret) {
|
||||
Ok(key) => key,
|
||||
Err(_) => {
|
||||
debug!(
|
||||
"Invalid shared secret for message{}: {}",
|
||||
message.id, shared_secret
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let engine = Aes256Gcm::new(aes_key.as_byte_array().into());
|
||||
|
||||
let plain = match engine.decrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: &cipher[12..],
|
||||
aad: AAD,
|
||||
},
|
||||
) {
|
||||
Ok(plain) => plain,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let commitment = AnkPrdHash::from_str(
|
||||
message
|
||||
.commitment
|
||||
.as_ref()
|
||||
.ok_or(anyhow::Error::msg("Missing commitment".to_owned()))?,
|
||||
)?;
|
||||
// A message matched against a new transaction must be a prd
|
||||
// We just check the commitment while we're at it
|
||||
let _ = Prd::extract_from_message_with_commitment(&plain, local_address, &commitment)?;
|
||||
// Update the message status
|
||||
message.status = CachedMessageStatus::NoStatus;
|
||||
message.shared_secrets = vec![]; // this way we won't check it again
|
||||
|
||||
return Ok(Some((
|
||||
plain,
|
||||
aes_key,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn decrypt_with_known_processes(cipher: &[u8], processes: MutexGuard<HashMap<OutPoint, Process>>) -> anyhow::Result<Option<(Vec<u8>, OutPoint)>> {
|
||||
let nonce = Nonce::from_slice(&cipher[..12]);
|
||||
|
||||
for (outpoint, process) in processes.iter() {
|
||||
for (address, secret) in process.get_all_secrets() {
|
||||
debug!("Attempting decryption with key {} for {}", secret, address);
|
||||
let engine = Aes256Gcm::new(secret.as_byte_array().into());
|
||||
|
||||
if let Ok(plain) = engine.decrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: &cipher[12..],
|
||||
aad: AAD,
|
||||
},
|
||||
) {
|
||||
return Ok(Some((plain, *outpoint)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Prd can come with an attached transaction or without
|
||||
/// It's useful to commit it in a transaction in the case there are multiple recipients (guarantee that everyone gets the same payload)
|
||||
/// Or if for some reasons we don't want to use the same shared secret again
|
||||
fn handle_prd(
|
||||
plain: &[u8],
|
||||
new_shared_secret: Option<AnkSharedSecretHash>,
|
||||
) -> AnyhowResult<ApiReturn> {
|
||||
let local_address: SilentPaymentAddress = lock_local_device()?.get_wallet().get_client().get_receiving_address().try_into()?;
|
||||
// We already checked the commitment if any
|
||||
let prd = Prd::extract_from_message(plain, local_address)?;
|
||||
|
||||
debug!("found prd: {:#?}", prd);
|
||||
|
||||
let proof_key = prd.proof.unwrap().get_key();
|
||||
|
||||
let sp_address = serde_json::from_str::<Member>(&prd.sender)?
|
||||
.get_addresses()
|
||||
.into_iter()
|
||||
.find_map(|address| {
|
||||
let parsed_address = SilentPaymentAddress::try_from(address.as_str()).ok()?;
|
||||
let spend_key = parsed_address.get_spend_key().x_only_public_key().0;
|
||||
if spend_key == proof_key {
|
||||
Some(parsed_address)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let proof = prd.proof.unwrap();
|
||||
let sender = serde_json::from_str::<Member>(&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()
|
||||
})
|
||||
.ok_or_else(|| anyhow::Error::msg("No matching address found for the proof key"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let outpoint = OutPoint::from_str(&prd.root_commitment)?;
|
||||
|
||||
@ -945,11 +830,7 @@ 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);
|
||||
let shared_secret = new_shared_secret
|
||||
.ok_or_else(|| anyhow::Error::msg("Missing shared secret for new process"))?;
|
||||
let mut shared_secrets = HashMap::new();
|
||||
shared_secrets.insert(sp_address, shared_secret);
|
||||
entry.insert(Process::new(vec![], shared_secrets, vec![]))
|
||||
entry.insert(Process::new(vec![], vec![]))
|
||||
}
|
||||
};
|
||||
|
||||
@ -969,29 +850,52 @@ fn handle_prd(
|
||||
hash.to_string() == prd.payload
|
||||
})
|
||||
.ok_or(anyhow::Error::msg("Original request not found"))?;
|
||||
let shared_secret = relevant_process
|
||||
.get_shared_secret_for_address(&sp_address)
|
||||
.ok_or(anyhow::Error::msg(
|
||||
"Missing shared secret for address in original request",
|
||||
))?;
|
||||
let member: Member = serde_json::from_str(&prd.sender)?;
|
||||
|
||||
return send_data(original_request, &shared_secret);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)));
|
||||
}
|
||||
|
||||
return Ok(ApiReturn {
|
||||
ciphers_to_send: ciphers,
|
||||
..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 shared_secret = relevant_process
|
||||
.get_shared_secret_for_address(&sp_address)
|
||||
.ok_or(anyhow::Error::msg(
|
||||
"Missing shared secret for address in original request",
|
||||
))?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let cipher = confirm_prd(prd, &shared_secret)?;
|
||||
// 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)));
|
||||
}
|
||||
|
||||
return Ok(ApiReturn {
|
||||
ciphers_to_send: vec![cipher],
|
||||
ciphers_to_send: ciphers,
|
||||
updated_process: Some((outpoint.to_string(), relevant_process.clone())),
|
||||
..Default::default()
|
||||
});
|
||||
@ -1052,28 +956,21 @@ fn handle_pcd(plain: Vec<u8>, root_commitment: OutPoint) -> AnyhowResult<ApiRetu
|
||||
}
|
||||
|
||||
fn handle_decrypted_message(
|
||||
secret: AnkSharedSecretHash,
|
||||
plain: Vec<u8>,
|
||||
shared_secret: Option<AnkSharedSecretHash>,
|
||||
root_commitment: Option<OutPoint>,
|
||||
) -> anyhow::Result<ApiReturn> {
|
||||
// Try to handle as PRD first
|
||||
handle_prd(&plain, shared_secret)
|
||||
.or_else(|_| {
|
||||
// If PRD handling fails, try to handle as PCD
|
||||
handle_pcd(
|
||||
plain,
|
||||
root_commitment.ok_or(anyhow::Error::msg(
|
||||
"root_commitment must be known for a pcd",
|
||||
))?,
|
||||
)
|
||||
})
|
||||
.map_err(|e| anyhow::Error::msg(format!("Failed to handle decrypted message: {}", e)))
|
||||
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]
|
||||
pub fn parse_cipher(cipher_msg: String) -> ApiResult<ApiReturn> {
|
||||
let mut messages = lock_messages()?;
|
||||
|
||||
// Check that the cipher is not empty or too long
|
||||
if cipher_msg.is_empty() || cipher_msg.len() > MAX_PRD_PAYLOAD_SIZE {
|
||||
return Err(ApiError::new(
|
||||
@ -1083,30 +980,13 @@ pub fn parse_cipher(cipher_msg: String) -> ApiResult<ApiReturn> {
|
||||
|
||||
let cipher = Vec::from_hex(cipher_msg.trim_matches('"'))?;
|
||||
|
||||
// Try decrypting with cached messages first
|
||||
if let Ok(Some((plain, shared_secret))) = decrypt_with_cached_messages(&cipher, &mut messages) {
|
||||
return handle_decrypted_message(plain, Some(shared_secret), None)
|
||||
let decrypt_res = lock_shared_secrets()?.try_decrypt(&cipher);
|
||||
if let Ok((secret, plain)) = decrypt_res {
|
||||
return handle_decrypted_message(secret, plain)
|
||||
.map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e)));
|
||||
}
|
||||
|
||||
// If that fails, try decrypting with known processes
|
||||
let processes = lock_processes()?;
|
||||
if let Ok(Some((plain, root_commitment))) = decrypt_with_known_processes(&cipher, processes) {
|
||||
return handle_decrypted_message(plain, None, Some(root_commitment))
|
||||
.map_err(|e| ApiError::new(format!("Failed to handle decrypted message: {}", e)));
|
||||
}
|
||||
|
||||
// If both decryption attempts fail, we keep it just in case we receive the transaction later
|
||||
let mut return_msg = CachedMessage::new();
|
||||
return_msg.cipher = vec![cipher_msg];
|
||||
return_msg.status = CachedMessageStatus::CipherWaitingTx;
|
||||
|
||||
messages.push(return_msg.clone());
|
||||
|
||||
Ok(ApiReturn {
|
||||
updated_cached_msg: vec![return_msg],
|
||||
..Default::default()
|
||||
})
|
||||
Err(ApiError::new("Failed to decrypt cipher".to_owned()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@ -1205,6 +1085,75 @@ pub fn create_commit_message(
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
/// We send a transaction that pays at least one output to each address of each member
|
||||
/// The goal is to establish a shared_secret to be used as an encryption key for further communication
|
||||
pub fn create_connect_transaction(members_str: Vec<String>, fee_rate: u32) -> ApiResult<ApiReturn> {
|
||||
let mut members: Vec<Member> = vec![];
|
||||
|
||||
for member in members_str {
|
||||
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()?;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let transaction = signed_psbt.extract_tx()?;
|
||||
|
||||
Ok(ApiReturn {
|
||||
new_tx_to_send: Some(NewTxMessage::new(serialize(&transaction).to_lower_hex_string(), None)),
|
||||
secrets: shared_secrets.to_owned(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
/// We assume that the provided tx outpoint exist
|
||||
pub fn create_update_transaction(
|
||||
|
113
tests/connect.rs
Normal file
113
tests/connect.rs
Normal file
@ -0,0 +1,113 @@
|
||||
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
|
||||
};
|
||||
use sdk_common::log::{debug, info};
|
||||
use sdk_common::pcd::{Member, RoleDefinition};
|
||||
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::spclient::OwnedOutput;
|
||||
use sdk_common::sp_client::silentpayments::utils::SilentPaymentAddress;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use tsify::JsValueSerdeExt;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
mod utils;
|
||||
|
||||
use utils::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
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();
|
||||
|
||||
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
|
||||
let bob_member = Member::new(vec![SilentPaymentAddress::try_from(bob_address.as_str()).unwrap()]).unwrap();
|
||||
let alice_connect_return = create_connect_transaction(vec![serde_json::to_string(&bob_member).unwrap()], 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();
|
||||
alice_secrets_store = alice_connect_return.secrets;
|
||||
|
||||
// ======================= 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();
|
||||
bob_secrets_store = bob_parsed_transaction_return.secrets;
|
||||
|
||||
// ======================= Alice
|
||||
reset_device().unwrap();
|
||||
restore_device(alice_device).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()).unwrap();
|
||||
|
||||
// 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;
|
||||
|
||||
// ======================= Bob
|
||||
reset_device().unwrap();
|
||||
restore_device(bob_device).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()).unwrap();
|
||||
|
||||
bob_secrets_store = bob_parsed_connect.secrets;
|
||||
|
||||
// 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()));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user