From 41743ab3e75bd5aa1c11ba329bbc7687bb5db0cc Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 4 Jun 2024 11:56:32 +0200 Subject: [PATCH 01/13] Replace CipherMessage with Prd and put data in Pcd --- src/network.rs | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/network.rs b/src/network.rs index 586bdb1..3a4534a 100644 --- a/src/network.rs +++ b/src/network.rs @@ -109,18 +109,36 @@ impl NewTxMessage { } } -#[derive(Debug, Serialize, Deserialize, Tsify)] +#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct CipherMessage { +pub struct Prd { pub sender: String, + pub keys: Vec, + pub pcd_commitment: String, // hash of the pcd + pub error: Option, +} + +impl Prd { + pub fn new(sender: String, keys: Vec, pcd_commitment: String) -> Self { + Self { + sender, + keys, + pcd_commitment, + error: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Pcd { pub message: String, pub error: Option, } -impl CipherMessage { - pub fn new(sender: String, message: String) -> Self { +impl Pcd { + pub fn new(message: String) -> Self { Self { - sender, message, error: None, } @@ -199,6 +217,11 @@ pub struct CachedMessage { pub recipient: Option, // Never None when message sent pub shared_secret: Option, // Never None when message sent pub key: Option, // Never None when message sent + pub prd_cipher: Option, // When we receive message we can't decrypt we only have this and commited_in_tx + pub prd: Option, // Never None when message sent + pub pcd_cipher: Option, + pub pcd: Option, // Never None when message sent + pub pcd_commitment: Option, pub confirmed_by: Option, // If this None, Sender keeps sending pub timestamp: u64, pub error: Option, @@ -237,7 +260,7 @@ impl CachedMessage { } pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Result> { - if self.ciphertext.is_none() || self.shared_secret.is_some() { + if self.prd_cipher.is_none() || self.shared_secret.is_some() { return Err(Error::msg( "Can't try decrypt this message, ciphertext is none or shared_secret already found", )); From 95ed1e1f1c5038174ee99c51fbf625bd838bc362 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 4 Jun 2024 12:09:14 +0200 Subject: [PATCH 02/13] Rename AnkNetworkMsg to Envelope --- src/network.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network.rs b/src/network.rs index 3a4534a..a17d75d 100644 --- a/src/network.rs +++ b/src/network.rs @@ -151,12 +151,12 @@ impl Pcd { #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] -pub struct AnkNetworkMsg { +pub struct Envelope { pub flag: AnkFlag, pub content: String, } -impl AnkNetworkMsg { +impl Envelope { pub fn new(flag: AnkFlag, raw: &str) -> Self { Self { flag, From dd797bd981ad52b7099f303352b0fecebfab649d Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 15 Aug 2024 16:15:26 +0200 Subject: [PATCH 03/13] Update Pcd/Prd --- src/network.rs | 66 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/network.rs b/src/network.rs index a17d75d..9de12d4 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,13 +1,16 @@ -use std::default; +use std::{default, fmt}; use std::str::FromStr; -use aes_gcm::{AeadCore, Aes256Gcm}; +use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; use js_sys::Date; use rand::{thread_rng, RngCore}; -use serde::{Deserialize, Serialize}; +use serde::de::{MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use sp_client::bitcoin::consensus::serialize; -use sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::hashes::sha256::Hash; +// use sp_client::bitcoin::hashes::Hash; use sp_client::bitcoin::hex::{DisplayHex, FromHex}; use sp_client::bitcoin::secp256k1::PublicKey; use sp_client::bitcoin::{BlockHash, OutPoint, Transaction}; @@ -109,28 +112,42 @@ impl NewTxMessage { } } -#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] pub struct Prd { pub sender: String, - pub keys: Vec, - pub pcd_commitment: String, // hash of the pcd + pub key: [u8;32], + pub pcd_commitment: Hash, // hash of the pcd pub error: Option, } impl Prd { - pub fn new(sender: String, keys: Vec, pcd_commitment: String) -> Self { + pub fn new(sender: SilentPaymentAddress, key: [u8; 32], pcd_commitment: Hash) -> Self { Self { - sender, - keys, + sender: sender.into(), + key, pcd_commitment, error: None, } } + + pub fn encrypt_pcd(&self, pcd: &Pcd) -> Result> { + let plain = pcd.to_string().into_bytes(); + let nonce: [u8; 12] = Aes256Gcm::generate_nonce(thread_rng()).into(); + let aes_encrypt = Aes256Encryption::import_key(Purpose::Arbitrary, plain, self.key, nonce)?; + let cipher = aes_encrypt.encrypt_with_aes_key()?; + Ok(cipher) + } + + pub fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } } #[derive(Debug, Serialize, Deserialize, Clone, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] pub struct Pcd { pub message: String, pub error: Option, @@ -149,8 +166,7 @@ impl Pcd { } } -#[derive(Debug, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] +#[derive(Debug, Serialize, Deserialize)] pub struct Envelope { pub flag: AnkFlag, pub content: String, @@ -182,7 +198,8 @@ pub enum CachedMessageStatus { Pairing, Login, CipherWaitingTx, - TxWaitingCipher, + TxWaitingPrd, + GotPrdWaitingPcd, SentWaitingConfirmation, // we're sender and wait for commited_in to be spent ReceivedMustConfirm, // we're receiver and we spend commited_in to sender MustSpendConfirmation, // we're sender and we must spend confirmed_by @@ -208,15 +225,12 @@ pub enum CachedMessageStatus { pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, - pub ciphertext: Option, // Needed when we are waiting for a key in transaction, discarded after - pub plaintext: Vec, // Append new messages received while in Trusted state pub commited_in: Option, pub tied_by: Option, // index of the output that ties the proposal pub commitment: Option, // content of the op_return pub sender: Option, // Never None when message sent pub recipient: Option, // Never None when message sent pub shared_secret: Option, // Never None when message sent - pub key: Option, // Never None when message sent pub prd_cipher: Option, // When we receive message we can't decrypt we only have this and commited_in_tx pub prd: Option, // Never None when message sent pub pcd_cipher: Option, @@ -246,7 +260,7 @@ impl CachedMessage { serde_json::to_string(self).unwrap() } - pub fn try_decrypt_cipher(&self, cipher: Vec) -> Result> { + pub fn try_decrypt_prd(&self, cipher: Vec) -> Result> { if self.shared_secret.is_none() { return Err(Error::msg( "Can't try decrypt this message, there's no shared secret", @@ -259,15 +273,29 @@ impl CachedMessage { aes_decrypt.decrypt_with_key() } + pub fn try_decrypt_pcd(&self, cipher: Vec) -> Result> { + if self.prd.is_none() { + return Err(Error::msg( + "Can't try decrypt this message, there's no prd", + )); + } + let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; + + aes_decrypt.decrypt_with_key() + } + pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Result> { if self.prd_cipher.is_none() || self.shared_secret.is_some() { return Err(Error::msg( "Can't try decrypt this message, ciphertext is none or shared_secret already found", )); } - let cipher_bin = Vec::from_hex(self.ciphertext.as_ref().unwrap())?; - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher_bin, shared_secret)?; + let mut key = [0u8; 32]; + key.copy_from_slice(&shared_secret); + let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()).expect("Shouldn't keep an invalid hex as cipher"); + + let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, key)?; aes_decrypt.decrypt_with_key() } } From 8688b6ed79aa4836ada2ca928ad1bc51bb8c3778 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Thu, 15 Aug 2024 16:15:40 +0200 Subject: [PATCH 04/13] Update create_transaction test --- src/silentpayments.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/silentpayments.rs b/src/silentpayments.rs index b1088de..09d5dca 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -5,10 +5,8 @@ use anyhow::{Error, Result}; use rand::{thread_rng, Rng}; use sp_client::bitcoin::consensus::deserialize; -use sp_client::bitcoin::key::{Keypair, Secp256k1, TapTweak}; -use sp_client::bitcoin::psbt::{raw, Output}; -use sp_client::bitcoin::{Address, Psbt, ScriptBuf, Transaction, Txid}; -use sp_client::bitcoin::{Amount, OutPoint, TxOut}; +use sp_client::bitcoin::psbt::raw; +use sp_client::bitcoin::{Psbt, Amount, OutPoint}; use sp_client::constants::{ self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; @@ -52,7 +50,7 @@ pub fn create_transaction( for outpoint in mandatory_inputs { let (must_outpoint, must_output) = available_outpoints .remove_entry(&outpoint) - .ok_or_else(|| Error::msg("Mandatory outpoint unknown"))?; + .ok_or_else(|| Error::msg(format!("Mandatory outpoint unknown: {}", outpoint)))?; total_available += must_output.amount; inputs.insert(must_outpoint, must_output); } @@ -174,13 +172,13 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { let prevout = tx.input.get(0).unwrap().to_owned(); @@ -209,11 +207,11 @@ mod tests { tweak_data } - fn helper_create_commitment(payload_to_hash: String) -> String { + fn helper_create_commitment(payload_to_hash: String) -> sha256::Hash { let mut engine = sha256::HashEngine::default(); - engine.write_all(&payload_to_hash.as_bytes()); + engine.write_all(&payload_to_hash.as_bytes()).unwrap(); let hash = sha256::Hash::from_engine(engine); - hash.to_byte_array().to_lower_hex_string() + hash } #[test] @@ -225,17 +223,19 @@ mod tests { }; let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); - let message: CipherMessage = CipherMessage::new(ALICE_ADDRESS.to_owned(), "TEST".to_owned()); - let commitment = helper_create_commitment(serde_json::to_string(&message).unwrap()); - - assert!(commitment == "d12f3c5b37240bc3abf2976f41fdf9a594f0680aafd2781ac448f80440fbeb99"); + let pcd = Pcd::new("TEST".to_owned()); + let pcd_hash = helper_create_commitment(pcd.to_string()); + let mut key = [0u8; 32]; + key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); + let prd = Prd::new(ALICE_ADDRESS.try_into().unwrap(), key, pcd_hash); + let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap()); let psbt = create_transaction( &vec![], &HashSet::new(), &alice_wallet, vec![recipient], - Some(Vec::from_hex(COMMITMENT).unwrap()), + Some(commitment.as_byte_array().to_vec()), FEE_RATE, None, ) From b2c070642ee1b5a98909de7c89e018f86d1d4687 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 20 Aug 2024 12:21:15 +0200 Subject: [PATCH 05/13] Implement Process --- Cargo.toml | 1 + src/device.rs | 158 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + src/network.rs | 79 ++++++++++++++++---- src/process.rs | 170 ++++++++++++++++++++++++++++++++++++++++++ src/silentpayments.rs | 75 +++++++++++++++---- 6 files changed, 457 insertions(+), 28 deletions(-) create mode 100644 src/device.rs create mode 100644 src/process.rs diff --git a/Cargo.toml b/Cargo.toml index 991ac1a..271c705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ serde_json = "1.0.108" # sp_client = { path = "../sp-client" } sp_client = { git = "https://github.com/Sosthene00/sp-client.git", branch = "master" } tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } +uuid = { version = "1.10.0", features = ["v4"] } wasm-bindgen = "0.2.91" diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..ed06e05 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use anyhow::{Error, Result}; +use serde_json::{Map, Value}; +use sp_client::bitcoin::consensus::serialize; +use sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::{ + Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey, +}; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::prelude::*; + +use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::spclient::{OutputList, SpWallet, SpendKey}; + +pub const SESSION_INDEX: u32 = 0; +pub const REVOKATION_INDEX: u32 = 1; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct PairedDevice { + pub address: String, + pub outgoing_pairing_transaction: [u8; 32], + pub revokation_index: u32, + pub incoming_pairing_transaction: [u8; 32], + pub current_remote_key: [u8; 32], + pub current_session_outpoint: OutPoint, // This will be spend by remote device to notify us of next login + pub current_session_revokation_outpoint: OutPoint, // remote device can revoke current session by spending this +} + +impl PairedDevice { + pub fn new(address: SilentPaymentAddress, pairing_txid: Txid, revokation_index: u32) -> Self { + let mut pairing_transaction_buf = [0u8; 32]; + pairing_transaction_buf.copy_from_slice(&serialize(&pairing_txid)); + + Self { + address: address.into(), + outgoing_pairing_transaction: pairing_transaction_buf, + revokation_index, + incoming_pairing_transaction: [0u8; 32], + current_session_revokation_outpoint: OutPoint::default(), + current_session_outpoint: OutPoint::default(), + current_remote_key: [0u8; 32], + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Device { + sp_wallet: SpWallet, + current_session_outpoint: OutPoint, // This is the notification output of incoming login tx + current_session_key: [u8; 32], + current_session_revokation_outpoint: OutPoint, // This is the revokation outpoint of outgoing login tx + paired_device: Option, +} + +impl Device { + pub fn new(sp_wallet: SpWallet) -> Self { + Self { + sp_wallet, + current_session_outpoint: OutPoint::default(), + current_session_key: [0u8; 32], + current_session_revokation_outpoint: OutPoint::default(), + paired_device: None, + } + } + + pub fn get_wallet(&self) -> &SpWallet { + &self.sp_wallet + } + + pub fn get_mut_wallet(&mut self) -> &mut SpWallet { + &mut self.sp_wallet + } + + pub fn is_linked(&self) -> bool { + self.paired_device.is_some() + } + + pub fn is_pairing(&self) -> bool { + self.current_session_key == [0u8; 32] + } + + pub fn get_paired_device_info(&self) -> Option { + self.paired_device.clone() + } + + pub fn get_next_output_to_spend(&self) -> OutPoint { + self.current_session_outpoint + } + + pub fn get_session_revokation_outpoint(&self) -> OutPoint { + self.current_session_revokation_outpoint + } + + pub fn sign_with_current_session_key(&self) -> Result<()> { + unimplemented!(); + } + + pub fn encrypt_with_current_session_key(&self) -> Result<()> { + unimplemented!(); + } + + pub fn new_link( + &mut self, + link_with: SilentPaymentAddress, + outgoing_pairing_tx: Txid, + revokation_output: u32, + incoming_pairing_tx: Txid, + ) -> Result<()> { + // let address_looked_for: String = link_with.into(); + if let Some(paired_device) = self.paired_device.as_ref() { + return Err(Error::msg(format!( + "Found an already paired device with address {} and revokable by {}:{}", + paired_device.address, + Txid::from_byte_array(paired_device.outgoing_pairing_transaction), + paired_device.revokation_index + ))); + } else { + let mut new_device = + PairedDevice::new(link_with, outgoing_pairing_tx, revokation_output); + new_device.incoming_pairing_transaction = incoming_pairing_tx.to_byte_array(); + self.paired_device = Some(new_device); + } + + Ok(()) + } + + // We call that when we spent to the remote device and it similarly spent to us + pub fn update_session( + &mut self, + new_session_key: SecretKey, + new_session_outpoint: OutPoint, + new_revokation_outpoint: OutPoint, + new_remote_key: XOnlyPublicKey, + new_remote_session_outpoint: OutPoint, + new_remote_revokation_outpoint: OutPoint, + ) -> Result<()> { + if !self.is_linked() { + return Err(Error::msg("Can't update an unpaired device")); + } + self.paired_device + .as_mut() + .map(|d| { + d.current_remote_key = new_remote_key.serialize(); + d.current_session_outpoint = new_remote_session_outpoint; + d.current_session_revokation_outpoint = new_remote_revokation_outpoint; + }); + self.current_session_key = new_session_key.secret_bytes(); + self.current_session_outpoint = new_session_outpoint; + self.current_session_revokation_outpoint = new_revokation_outpoint; + + Ok(()) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 7d693bb..e4247f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ pub use sp_client; pub mod crypto; +pub mod device; pub mod error; pub mod network; +pub mod process; pub mod silentpayments; diff --git a/src/network.rs b/src/network.rs index 9de12d4..bd962b8 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,5 +1,5 @@ -use std::{default, fmt}; use std::str::FromStr; +use std::{default, fmt}; use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; @@ -12,6 +12,7 @@ use sp_client::bitcoin::consensus::serialize; use sp_client::bitcoin::hashes::sha256::Hash; // use sp_client::bitcoin::hashes::Hash; use sp_client::bitcoin::hex::{DisplayHex, FromHex}; +use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::secp256k1::PublicKey; use sp_client::bitcoin::{BlockHash, OutPoint, Transaction}; use sp_client::silentpayments::utils::SilentPaymentAddress; @@ -19,6 +20,7 @@ use tsify::Tsify; use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose}; use crate::error::AnkError; +use crate::process::{Member, Process}; #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -112,24 +114,67 @@ impl NewTxMessage { } } +#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum PrdType { + #[default] + None, + Message, + Update, + List, + Response, + Confirm, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +pub struct ValidationToken { + member: Member, + message: Hash, // Hash of Pcd | {"yes/no/blank"} + sig: Signature, + sig_alt: Signature, // User must sign with it's 2 paired devices +} + #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct Prd { + pub prd_type: PrdType, + pub process: Process, pub sender: String, - pub key: [u8;32], + pub key: [u8; 32], + pub validation_tokens: Vec, pub pcd_commitment: Hash, // hash of the pcd pub error: Option, } impl Prd { - pub fn new(sender: SilentPaymentAddress, key: [u8; 32], pcd_commitment: Hash) -> Self { - Self { + pub fn new( + prd_type: PrdType, + process: Process, + sender: SilentPaymentAddress, + key: [u8; 32], + pcd_commitment: Hash, + ) -> Result { + let mut res = Self { + prd_type, + process, sender: sender.into(), + validation_tokens: vec![], key, pcd_commitment, error: None, + }; + + Ok(res) + } + + pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> { + match self.prd_type { + PrdType::Confirm => self.validation_tokens.push(validation_token), + _ => return Err(Error::msg("This Prd type doesn't allow validation tokens")) } + Ok(()) } pub fn encrypt_pcd(&self, pcd: &Pcd) -> Result> { @@ -225,6 +270,7 @@ pub enum CachedMessageStatus { pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, + pub prd_type: PrdType, pub commited_in: Option, pub tied_by: Option, // index of the output that ties the proposal pub commitment: Option, // content of the op_return @@ -232,9 +278,9 @@ pub struct CachedMessage { pub recipient: Option, // Never None when message sent pub shared_secret: Option, // Never None when message sent pub prd_cipher: Option, // When we receive message we can't decrypt we only have this and commited_in_tx - pub prd: Option, // Never None when message sent - pub pcd_cipher: Option, - pub pcd: Option, // Never None when message sent + pub prd: Option, // Never None when message sent + pub pcd_cipher: Option, + pub pcd: Option, // Never None when message sent pub pcd_commitment: Option, pub confirmed_by: Option, // If this None, Sender keeps sending pub timestamp: u64, @@ -275,11 +321,10 @@ impl CachedMessage { pub fn try_decrypt_pcd(&self, cipher: Vec) -> Result> { if self.prd.is_none() { - return Err(Error::msg( - "Can't try decrypt this message, there's no prd", - )); + return Err(Error::msg("Can't try decrypt this message, there's no prd")); } - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; + let aes_decrypt = + Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; aes_decrypt.decrypt_with_key() } @@ -293,7 +338,8 @@ impl CachedMessage { let mut key = [0u8; 32]; key.copy_from_slice(&shared_secret); - let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()).expect("Shouldn't keep an invalid hex as cipher"); + let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()) + .expect("Shouldn't keep an invalid hex as cipher"); let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, key)?; aes_decrypt.decrypt_with_key() @@ -356,10 +402,15 @@ impl TrustedChannel { self.revoked_in_block != [0u8; 32] } - pub fn encrypt_msg_for(&self, msg: String) -> Result { + pub fn encrypt_msg_for(&self, msg: String) -> Result { let mut rng = thread_rng(); let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - let aes256_encryption = Aes256Encryption::import_key(Purpose::Arbitrary, msg.into_bytes(), self.shared_secret, nonce)?; + let aes256_encryption = Aes256Encryption::import_key( + Purpose::Arbitrary, + msg.into_bytes(), + self.shared_secret, + nonce, + )?; let cipher = aes256_encryption.encrypt_with_aes_key()?; Ok(cipher.to_lower_hex_string()) } diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 0000000..3fd021f --- /dev/null +++ b/src/process.rs @@ -0,0 +1,170 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sp_client::bitcoin::hashes::Hash; +use sp_client::bitcoin::secp256k1::schnorr::Signature; +use sp_client::bitcoin::{OutPoint, Txid}; +use sp_client::silentpayments::utils::SilentPaymentAddress; +use tsify::Tsify; +use uuid::Uuid; +use wasm_bindgen::prelude::*; + +use crate::device::Device; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, PartialOrd)] +pub enum Role { + User, + Manager, + Admin, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +pub struct Member { + nym: String, + sp_address: String, + sp_address_alt: String, + role: Role, +} + +impl Member { + pub fn new( + nym: String, + sp_address: SilentPaymentAddress, + sp_address_alt: SilentPaymentAddress, + role: Role, + ) -> Self { + Self { + nym, + sp_address: sp_address.into(), + sp_address_alt: sp_address_alt.into(), + role, + } + } + + pub fn get_nym(&self) -> String { + self.nym.clone() + } + + pub fn get_addresses(&self) -> (String, String) { + (self.sp_address.clone(), self.sp_address_alt.clone()) + } + + pub fn get_role(&self) -> Role { + self.role + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ValidationRules { + quorum: f32, // Must be > 0.0, <= 1.0 + min_permission: Role, // Only users with at least that Role can send a token +} + +impl ValidationRules { + pub fn new(quorum: f32, min_permission: Role) -> Self { + Self { + quorum, + min_permission, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Process { + pub uuid: String, + pub name: String, + pub version: u32, + pub roles: Vec, + pub validation_rules: ValidationRules, + pub initial_commit_tx: Txid, + pub latest_commit_tx: Txid, + pub html: String, + pub style: String, + pub script: String, + pub pcd_template: Value +} + +impl Process { + pub fn new( + name: String, + roles: Vec, + validation_rules: ValidationRules, + initial_commit_tx: Txid, + html: String, + style: String, + script: String, + pcd_template: Value, + ) -> Self { + Self { + uuid: Uuid::new_v4().to_string(), + name, + version: 1, + roles, + validation_rules, + initial_commit_tx, + latest_commit_tx: initial_commit_tx, + html, + style, + script, + pcd_template + } + } + + pub fn new_pairing_process( + pcd: PairingPcd, + initial_commit_tx: Txid, + ) -> Self { + let member = Member::new( + pcd.nym.clone(), + pcd.addresses[0].clone().try_into().unwrap(), + pcd.addresses[1].clone().try_into().unwrap(), + Role::Admin, + ); + let validation_rules = ValidationRules::new(1.0, Role::Admin); + Self::new( + "pairing".to_owned(), + vec![member], + validation_rules, + initial_commit_tx, + "".to_owned(), + "".to_owned(), + "".to_owned(), + Value::from_str(&serde_json::to_string(&pcd).unwrap()).unwrap() + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PairingPcd { + nym: String, + addresses: [String; 2], + session_index: u32, + revokation_index: u32, + pairing_txs: [Txid; 2], + current_session_txs: [Txid; 2] +} + +impl PairingPcd { + pub fn new( + nym: String, + local_address: SilentPaymentAddress, + remote_address: SilentPaymentAddress, + session_index: u32, + revokation_index: u32, + incoming_pairing_tx: Txid, + outgoing_pairing_tx: Txid, + ) -> Self { + let empty_txid = Txid::from_byte_array([0u8; Txid::LEN]); + Self { + nym, + addresses: [local_address.into(), remote_address.into()], + session_index, + revokation_index, + pairing_txs: [incoming_pairing_tx, outgoing_pairing_tx], + current_session_txs: [empty_txid, empty_txid] + } + } +} diff --git a/src/silentpayments.rs b/src/silentpayments.rs index 09d5dca..78dc7b5 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -6,7 +6,7 @@ use anyhow::{Error, Result}; use rand::{thread_rng, Rng}; use sp_client::bitcoin::consensus::deserialize; use sp_client::bitcoin::psbt::raw; -use sp_client::bitcoin::{Psbt, Amount, OutPoint}; +use sp_client::bitcoin::{Amount, OutPoint, Psbt}; use sp_client::constants::{ self, DUST_THRESHOLD, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, }; @@ -27,9 +27,7 @@ pub fn create_transaction( .to_spendable_list() // filter out freezed utxos .into_iter() - .filter(|(outpoint, _)| { - !freezed_utxos.contains(outpoint) - }) + .filter(|(outpoint, _)| !freezed_utxos.contains(outpoint)) .collect(); // if we have a payload, it means we are notifying, so let's add a revokation output @@ -37,13 +35,17 @@ pub fn create_transaction( recipients.push(Recipient { address: sp_wallet.get_client().get_receiving_address(), amount: DUST_THRESHOLD, - nb_outputs: 1 + nb_outputs: 1, }) } - let sum_outputs = recipients.iter().fold(Amount::from_sat(0), |acc, x| acc + x.amount); + let sum_outputs = recipients + .iter() + .fold(Amount::from_sat(0), |acc, x| acc + x.amount); - let zero_value_recipient = recipients.iter_mut().find(|r| r.amount == Amount::from_sat(0)); + let zero_value_recipient = recipients + .iter_mut() + .find(|r| r.amount == Amount::from_sat(0)); let mut inputs: HashMap = HashMap::new(); let mut total_available = Amount::from_sat(0); @@ -89,7 +91,8 @@ pub fn create_transaction( if let Some(address) = fee_payer { SpClient::set_fees(&mut new_psbt, fee_rate, address)?; } else { - let candidates: Vec> = new_psbt.outputs + let candidates: Vec> = new_psbt + .outputs .iter() .map(|o| { if let Some(value) = o.proprietary.get(&raw::ProprietaryKey { @@ -112,17 +115,17 @@ pub fn create_transaction( for candidate in candidates { if let Some(c) = candidate { if c == change_address { - SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?; + SpClient::set_fees(&mut new_psbt, fee_rate, change_address.clone())?; fee_set = true; break; } else if c == sender_address { - SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?; + SpClient::set_fees(&mut new_psbt, fee_rate, sender_address.clone())?; fee_set = true; break; } } } - + if !fee_set { return Err(Error::msg("Must specify payer for fee")); } @@ -172,13 +175,14 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { let prevout = tx.input.get(0).unwrap().to_owned(); @@ -227,7 +232,49 @@ mod tests { let pcd_hash = helper_create_commitment(pcd.to_string()); let mut key = [0u8; 32]; key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); - let prd = Prd::new(ALICE_ADDRESS.try_into().unwrap(), key, pcd_hash); + + let alice_member = Member::new( + "alice".to_owned(), + alice_wallet.get_client().get_receiving_address().try_into().unwrap(), + alice_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), + Role::Admin, + ); + let bob_member = Member::new( + "bob".to_owned(), + bob_wallet.get_client().get_receiving_address().try_into().unwrap(), + bob_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), + Role::User, + ); + + let validation_rules = ValidationRules::new(0.5, Role::User); + + let pcd_template = serde_json::json!({ + "int": 0, + "string": "exemple_data", + "array": [ + "element1", + "element2" + ] + }); + + let process = Process::new( + "default".to_owned(), + vec![alice_member, bob_member], + validation_rules, + Txid::from_str(INITIAL_COMMIT_TX).unwrap(), + "".to_owned(), + "".to_owned(), + "".to_owned(), + pcd_template, + ); + + let prd = Prd::new( + PrdType::Update, + process, + ALICE_ADDRESS.try_into().unwrap(), + key, + pcd_hash, + ).unwrap(); let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap()); let psbt = create_transaction( From 665d95554dbc0cb4788d951602ad3d84f9546c0b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 21 Aug 2024 13:17:34 +0200 Subject: [PATCH 06/13] Add basic signature --- src/lib.rs | 1 + src/signature.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/signature.rs diff --git a/src/lib.rs b/src/lib.rs index e4247f8..12aa3a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,4 @@ pub mod error; pub mod network; pub mod process; pub mod silentpayments; +pub mod signature; diff --git a/src/signature.rs b/src/signature.rs new file mode 100644 index 0000000..b965962 --- /dev/null +++ b/src/signature.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use rand::{thread_rng, RngCore}; +use sp_client::bitcoin::key::Secp256k1; +use sp_client::bitcoin::secp256k1::schnorr::Signature; +use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey}; +use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; + +sha256t_hash_newtype! { + pub struct AnkMessageTag = hash_str("4nk/Message"); + + #[hash_newtype(forward)] + pub struct AnkMessageHash(_); +} + +impl AnkMessageHash { + pub fn from_message(message: &[u8]) -> Self { + let mut eng = AnkMessageHash::engine(); + eng.input(&message); + AnkMessageHash::from_engine(eng) + } +} + +pub fn sign_message(message: &[u8], signing_key: SecretKey) -> Result { + let message_hash = AnkMessageHash::from_message(message); + + let secp = Secp256k1::signing_only(); + + let keypair = Keypair::from_secret_key(&secp, &signing_key); + + let mut aux_rand = [0u8; 32]; + + thread_rng().fill_bytes(&mut aux_rand); + + let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand); + + Ok(sig) +} + +pub fn verify(sig: Signature, message: &[u8], x_only_key: XOnlyPublicKey) -> Result<()> { + let secp = Secp256k1::verification_only(); + let message_hash = AnkMessageHash::from_message(message); + secp.verify_schnorr(&sig, &Message::from_digest(message_hash.to_byte_array()), &x_only_key)?; + + Ok(()) +} \ No newline at end of file From 0ea4c5f1180b01e4dd1da1f485caa9a00019f64e Mon Sep 17 00:00:00 2001 From: Sosthene Date: Wed, 28 Aug 2024 09:40:19 +0200 Subject: [PATCH 07/13] Implement prd/pcd logic --- Cargo.toml | 3 +- src/crypto.rs | 2 +- src/device.rs | 147 ++++----------- src/lib.rs | 4 + src/network.rs | 286 +++++----------------------- src/pcd.rs | 147 +++++++++++++++ src/prd.rs | 208 ++++++++++++++++++++ src/process.rs | 128 ++----------- src/signature.rs | 63 +++++-- src/silentpayments.rs | 428 +++++++++++++++++++++--------------------- 10 files changed, 712 insertions(+), 704 deletions(-) create mode 100644 src/pcd.rs create mode 100644 src/prd.rs diff --git a/Cargo.toml b/Cargo.toml index 271c705..0521f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ crate-type = ["cdylib", "rlib"] aes-gcm = "0.10.3" anyhow = "1.0" js-sys = "0.3.69" +log = "0.4.6" rand = "0.8.5" serde = { version = "1.0.193", features = ["derive"] } -serde_json = "1.0.108" +serde_json = { version = "1.0.108", features = ["preserve_order"]} # sp_client = { path = "../sp-client" } sp_client = { git = "https://github.com/Sosthene00/sp-client.git", branch = "master" } tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } diff --git a/src/crypto.rs b/src/crypto.rs index 6caefa5..0bbe5f1 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -20,7 +20,7 @@ use aes_gcm::aead::{Aead, Payload}; pub use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; use rand::thread_rng; -const AAD: &[u8] = "4nk".as_bytes(); +pub const AAD: &[u8] = "4nk".as_bytes(); const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2; diff --git a/src/device.rs b/src/device.rs index ed06e05..5e491b9 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,70 +1,29 @@ -use std::collections::HashMap; - -use anyhow::{Error, Result}; -use serde_json::{Map, Value}; -use sp_client::bitcoin::consensus::serialize; -use sp_client::bitcoin::hashes::Hash; -use sp_client::bitcoin::secp256k1::SecretKey; -use sp_client::bitcoin::{ - Network, OutPoint, ScriptBuf, Transaction, Txid, XOnlyPublicKey, -}; +use serde_json::Value; use serde::{Deserialize, Serialize}; use tsify::Tsify; +use uuid::Uuid; use wasm_bindgen::prelude::*; -use sp_client::silentpayments::utils::SilentPaymentAddress; -use sp_client::spclient::{OutputList, SpWallet, SpendKey}; +use sp_client::{silentpayments::utils::SilentPaymentAddress, spclient::SpWallet}; -pub const SESSION_INDEX: u32 = 0; -pub const REVOKATION_INDEX: u32 = 1; - -#[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct PairedDevice { - pub address: String, - pub outgoing_pairing_transaction: [u8; 32], - pub revokation_index: u32, - pub incoming_pairing_transaction: [u8; 32], - pub current_remote_key: [u8; 32], - pub current_session_outpoint: OutPoint, // This will be spend by remote device to notify us of next login - pub current_session_revokation_outpoint: OutPoint, // remote device can revoke current session by spending this -} - -impl PairedDevice { - pub fn new(address: SilentPaymentAddress, pairing_txid: Txid, revokation_index: u32) -> Self { - let mut pairing_transaction_buf = [0u8; 32]; - pairing_transaction_buf.copy_from_slice(&serialize(&pairing_txid)); - - Self { - address: address.into(), - outgoing_pairing_transaction: pairing_transaction_buf, - revokation_index, - incoming_pairing_transaction: [0u8; 32], - current_session_revokation_outpoint: OutPoint::default(), - current_session_outpoint: OutPoint::default(), - current_remote_key: [0u8; 32], - } - } -} +use crate::pcd::Member; #[derive(Debug, Serialize, Deserialize, Clone, Default, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Device { sp_wallet: SpWallet, - current_session_outpoint: OutPoint, // This is the notification output of incoming login tx - current_session_key: [u8; 32], - current_session_revokation_outpoint: OutPoint, // This is the revokation outpoint of outgoing login tx - paired_device: Option, + pairing_process_uuid: Option, + paired_devices: Vec, // map an address to the pairing process + latest_known_state: Value } impl Device { pub fn new(sp_wallet: SpWallet) -> Self { Self { sp_wallet, - current_session_outpoint: OutPoint::default(), - current_session_key: [0u8; 32], - current_session_revokation_outpoint: OutPoint::default(), - paired_device: None, + pairing_process_uuid: None, + paired_devices: vec![], + latest_known_state: Value::Null } } @@ -77,82 +36,40 @@ impl Device { } pub fn is_linked(&self) -> bool { - self.paired_device.is_some() + self.pairing_process_uuid.is_some() } - pub fn is_pairing(&self) -> bool { - self.current_session_key == [0u8; 32] + pub fn get_process_uuid(&self) -> Option { + self.pairing_process_uuid.clone() } - pub fn get_paired_device_info(&self) -> Option { - self.paired_device.clone() + pub fn set_process_uuid(&mut self, uuid: Uuid) { + self.pairing_process_uuid = Some(uuid.to_string()); } - pub fn get_next_output_to_spend(&self) -> OutPoint { - self.current_session_outpoint + pub fn get_paired_devices(&self) -> Vec { + self.paired_devices.clone() } - pub fn get_session_revokation_outpoint(&self) -> OutPoint { - self.current_session_revokation_outpoint + pub fn push_paired_device(&mut self, new_device_address: SilentPaymentAddress) { + self.paired_devices.push(new_device_address.into()) } - pub fn sign_with_current_session_key(&self) -> Result<()> { - unimplemented!(); - } - - pub fn encrypt_with_current_session_key(&self) -> Result<()> { - unimplemented!(); - } - - pub fn new_link( - &mut self, - link_with: SilentPaymentAddress, - outgoing_pairing_tx: Txid, - revokation_output: u32, - incoming_pairing_tx: Txid, - ) -> Result<()> { - // let address_looked_for: String = link_with.into(); - if let Some(paired_device) = self.paired_device.as_ref() { - return Err(Error::msg(format!( - "Found an already paired device with address {} and revokable by {}:{}", - paired_device.address, - Txid::from_byte_array(paired_device.outgoing_pairing_transaction), - paired_device.revokation_index - ))); - } else { - let mut new_device = - PairedDevice::new(link_with, outgoing_pairing_tx, revokation_output); - new_device.incoming_pairing_transaction = incoming_pairing_tx.to_byte_array(); - self.paired_device = Some(new_device); - } - - Ok(()) - } - - // We call that when we spent to the remote device and it similarly spent to us - pub fn update_session( - &mut self, - new_session_key: SecretKey, - new_session_outpoint: OutPoint, - new_revokation_outpoint: OutPoint, - new_remote_key: XOnlyPublicKey, - new_remote_session_outpoint: OutPoint, - new_remote_revokation_outpoint: OutPoint, - ) -> Result<()> { + pub fn to_member(&self) -> anyhow::Result { if !self.is_linked() { - return Err(Error::msg("Can't update an unpaired device")); + return Err(anyhow::Error::msg("device is not linked")); } - self.paired_device - .as_mut() - .map(|d| { - d.current_remote_key = new_remote_key.serialize(); - d.current_session_outpoint = new_remote_session_outpoint; - d.current_session_revokation_outpoint = new_remote_revokation_outpoint; - }); - self.current_session_key = new_session_key.secret_bytes(); - self.current_session_outpoint = new_session_outpoint; - self.current_session_revokation_outpoint = new_revokation_outpoint; - Ok(()) + let member = self.latest_known_state.as_object().unwrap().get("member").unwrap().clone(); + let res: Member = serde_json::from_value(member)?; + Ok(res) + } + + pub fn get_latest_state(&self) -> Value { + self.latest_known_state.clone() + } + + pub fn update_latest_state(&mut self, update: Value) { + self.latest_known_state = update; } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 12aa3a0..f1b0d88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,13 @@ pub use sp_client; +pub use uuid; +pub use log; pub mod crypto; pub mod device; pub mod error; pub mod network; +pub mod pcd; +pub mod prd; pub mod process; pub mod silentpayments; pub mod signature; diff --git a/src/network.rs b/src/network.rs index bd962b8..cc438dc 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,26 +1,17 @@ -use std::str::FromStr; -use std::{default, fmt}; - -use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; +use aes_gcm::aead::{Aead, Payload}; +use aes_gcm::{Aes256Gcm, KeyInit}; use anyhow::{Error, Result}; use js_sys::Date; use rand::{thread_rng, RngCore}; -use serde::de::{MapAccess, Visitor}; -use serde::ser::SerializeStruct; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use sp_client::bitcoin::consensus::serialize; -use sp_client::bitcoin::hashes::sha256::Hash; -// use sp_client::bitcoin::hashes::Hash; +use serde::{Deserialize, Serialize}; +use serde_json::Value; use sp_client::bitcoin::hex::{DisplayHex, FromHex}; -use sp_client::bitcoin::secp256k1::schnorr::Signature; -use sp_client::bitcoin::secp256k1::PublicKey; -use sp_client::bitcoin::{BlockHash, OutPoint, Transaction}; -use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::bitcoin::OutPoint; use tsify::Tsify; -use crate::crypto::{Aes256Decryption, Aes256Encryption, Purpose}; +use crate::crypto::AAD; use crate::error::AnkError; -use crate::process::{Member, Process}; +use crate::pcd::Member; #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] @@ -114,103 +105,6 @@ impl NewTxMessage { } } -#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub enum PrdType { - #[default] - None, - Message, - Update, - List, - Response, - Confirm, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -pub struct ValidationToken { - member: Member, - message: Hash, // Hash of Pcd | {"yes/no/blank"} - sig: Signature, - sig_alt: Signature, // User must sign with it's 2 paired devices -} - -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct Prd { - pub prd_type: PrdType, - pub process: Process, - pub sender: String, - pub key: [u8; 32], - pub validation_tokens: Vec, - pub pcd_commitment: Hash, // hash of the pcd - pub error: Option, -} - -impl Prd { - pub fn new( - prd_type: PrdType, - process: Process, - sender: SilentPaymentAddress, - key: [u8; 32], - pcd_commitment: Hash, - ) -> Result { - let mut res = Self { - prd_type, - process, - sender: sender.into(), - validation_tokens: vec![], - key, - pcd_commitment, - error: None, - }; - - Ok(res) - } - - pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> { - match self.prd_type { - PrdType::Confirm => self.validation_tokens.push(validation_token), - _ => return Err(Error::msg("This Prd type doesn't allow validation tokens")) - } - Ok(()) - } - - pub fn encrypt_pcd(&self, pcd: &Pcd) -> Result> { - let plain = pcd.to_string().into_bytes(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(thread_rng()).into(); - let aes_encrypt = Aes256Encryption::import_key(Purpose::Arbitrary, plain, self.key, nonce)?; - let cipher = aes_encrypt.encrypt_with_aes_key()?; - Ok(cipher) - } - - pub fn to_string(&self) -> String { - serde_json::to_string(self).unwrap() - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct Pcd { - pub message: String, - pub error: Option, -} - -impl Pcd { - pub fn new(message: String) -> Self { - Self { - message, - error: None, - } - } - - pub fn to_string(&self) -> String { - serde_json::to_string(self).unwrap() - } -} - #[derive(Debug, Serialize, Deserialize)] pub struct Envelope { pub flag: AnkFlag, @@ -239,52 +133,28 @@ impl Envelope { pub enum CachedMessageStatus { #[default] NoStatus, - FaucetWaiting, - Pairing, - Login, CipherWaitingTx, TxWaitingPrd, - GotPrdWaitingPcd, - SentWaitingConfirmation, // we're sender and wait for commited_in to be spent - ReceivedMustConfirm, // we're receiver and we spend commited_in to sender - MustSpendConfirmation, // we're sender and we must spend confirmed_by - Trusted, // Can receive more messages - Closed, // No more messages will be trusted from this handshake + Opened, // Can receive more messages } /// Unique struct for both 4nk messages and notification/key exchange, both rust and ts -/// 0. Faucet: commited_in with nothing else, status is NoStatus -/// 1. notification: -/// 0. sender: ciphertext, plaintext, commited_in, sender, recipient, shared_secret, key -/// 1. receiver (without tx): ciphertext -/// 2. receiver (tx without msg): commited_in, commitment, recipient, shared_secret -/// 3. receiver (receive tx after msg): plaintext, key, sender, commited_in, commitment, recipient, shared_secret -/// 4. receiver (msg after tx): ciphertext, key, plaintext, sender -/// 2. confirmation: -/// 0. receiver (spend the smallest vout that pays him in the first tx): confirmed_by -/// 1. sender (detect a transaction that pays him and spend commited_by): confirmed_by -/// 2. sender toggle status to complete when it spent confirmed_by, receiver when it detects the confirmed_by is spent -#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone)] +#[derive(Debug, Default, Serialize, Deserialize, Tsify, Clone, PartialEq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct CachedMessage { pub id: u32, pub status: CachedMessageStatus, - pub prd_type: PrdType, - pub commited_in: Option, - pub tied_by: Option, // index of the output that ties the proposal + pub transaction: Option, pub commitment: Option, // content of the op_return - pub sender: Option, // Never None when message sent - pub recipient: Option, // Never None when message sent - pub shared_secret: Option, // Never None when message sent - pub prd_cipher: Option, // When we receive message we can't decrypt we only have this and commited_in_tx - pub prd: Option, // Never None when message sent - pub pcd_cipher: Option, - pub pcd: Option, // Never None when message sent - pub pcd_commitment: Option, + pub sender: Option, // Never None when message sent + pub recipient: Option, // Never None when message sent + pub shared_secrets: Vec, // Max 2 secrets in case we send to both address of the recipient + pub cipher: Vec, // Max 2 ciphers in case we send to both address of the recipient + pub prd: Option, // Never None when message sent + pub pcd: Option, // Value is here an alias for impl Pcd pub confirmed_by: Option, // If this None, Sender keeps sending pub timestamp: u64, - pub error: Option, } impl CachedMessage { @@ -306,112 +176,54 @@ impl CachedMessage { serde_json::to_string(self).unwrap() } - pub fn try_decrypt_prd(&self, cipher: Vec) -> Result> { - if self.shared_secret.is_none() { + pub fn try_decrypt_message(&self, cipher: Vec) -> Result> { + if self.shared_secrets.is_empty() { return Err(Error::msg( "Can't try decrypt this message, there's no shared secret", )); } - let mut shared_secret = [0u8; 32]; - shared_secret.copy_from_slice(&Vec::from_hex(self.shared_secret.as_ref().unwrap())?); - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, shared_secret)?; + for shared_secret in &self.shared_secrets { + let mut key = [0u8; 32]; + let mut nonce = [0u8; 12]; + key.copy_from_slice(&Vec::from_hex(shared_secret)?); + nonce.copy_from_slice(&cipher[..12]); - aes_decrypt.decrypt_with_key() - } - - pub fn try_decrypt_pcd(&self, cipher: Vec) -> Result> { - if self.prd.is_none() { - return Err(Error::msg("Can't try decrypt this message, there's no prd")); + let engine = Aes256Gcm::new(&key.into()); + let payload = Payload { + msg: &cipher[12..], + aad: AAD, + }; + match engine.decrypt(&nonce.into(), payload) { + Ok(plain) => return Ok(plain), + Err(_) => continue + } } - let aes_decrypt = - Aes256Decryption::new(Purpose::Arbitrary, cipher, self.prd.as_ref().unwrap().key)?; - aes_decrypt.decrypt_with_key() + Err(Error::msg("Failed to decrypt message")) } pub fn try_decrypt_with_shared_secret(&self, shared_secret: [u8; 32]) -> Result> { - if self.prd_cipher.is_none() || self.shared_secret.is_some() { + if self.cipher.is_empty() || !self.shared_secrets.is_empty() { return Err(Error::msg( "Can't try decrypt this message, ciphertext is none or shared_secret already found", )); } + for prd_cipher in &self.cipher { + let cipher = Vec::from_hex(prd_cipher)?; + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&cipher[..12]); - let mut key = [0u8; 32]; - key.copy_from_slice(&shared_secret); - let cipher = Vec::from_hex(&self.prd_cipher.as_ref().unwrap()) - .expect("Shouldn't keep an invalid hex as cipher"); + let engine = Aes256Gcm::new(&shared_secret.into()); + let payload = Payload { + msg: &cipher[12..], + aad: AAD, + }; + match engine.decrypt(&nonce.into(), payload) { + Ok(plain) => return Ok(plain), + Err(_) => continue + } + } - let aes_decrypt = Aes256Decryption::new(Purpose::Arbitrary, cipher, key)?; - aes_decrypt.decrypt_with_key() - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[allow(non_camel_case_types)] -pub struct TrustedChannel { - id: u32, // Just take the id of the cached message? - revokation_outpoint: OutPoint, // revokation output is locked by the shared_secret - shared_secret: [u8; 32], - revoked_in_block: [u8; 32], - with: String, // Silent payment address -} - -impl TrustedChannel { - pub fn new(with: String) -> Result { - // check that with is valid silent payment address - SilentPaymentAddress::try_from(with.as_str())?; - - // Generating random id - let mut new = Self::default(); - let mut buf = [0u8; 4]; - thread_rng().fill_bytes(&mut buf); - new.id = u32::from_be_bytes(buf); - new.with = with; - - Ok(new) - } - - pub fn set_revokation( - &mut self, - revokation_outpoint: String, - shared_secret: String, - ) -> Result<()> { - let mut buf = [0u8; 32]; - buf.copy_from_slice(&Vec::from_hex(&shared_secret)?); - self.revokation_outpoint = OutPoint::from_str(&revokation_outpoint)?; - self.shared_secret.copy_from_slice(&buf); - - Ok(()) - } - - pub fn revoke(&mut self, revoked_in_block: String) -> Result<()> { - let block_hash = BlockHash::from_str(&revoked_in_block)?; - let mut buf = [0u8; 32]; - buf.copy_from_slice(&serialize::(&block_hash)); - self.revoked_in_block = buf; - - Ok(()) - } - - pub fn is_set(&self) -> bool { - self.revokation_outpoint == OutPoint::default() - } - - pub fn is_revoked(&self) -> bool { - self.revoked_in_block != [0u8; 32] - } - - pub fn encrypt_msg_for(&self, msg: String) -> Result { - let mut rng = thread_rng(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - let aes256_encryption = Aes256Encryption::import_key( - Purpose::Arbitrary, - msg.into_bytes(), - self.shared_secret, - nonce, - )?; - let cipher = aes256_encryption.encrypt_with_aes_key()?; - Ok(cipher.to_lower_hex_string()) + Err(Error::msg("Failed to decrypt message")) } } diff --git a/src/pcd.rs b/src/pcd.rs new file mode 100644 index 0000000..77f2626 --- /dev/null +++ b/src/pcd.rs @@ -0,0 +1,147 @@ +use std::str::FromStr; +use anyhow::{Result, Error}; + +use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit}; +use log::debug; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sp_client::{bitcoin::{hashes::{sha256t_hash_newtype, Hash, HashEngine}, hex::{DisplayHex, FromHex}, Txid}, silentpayments::utils::SilentPaymentAddress}; +use tsify::Tsify; + +use crate::crypto::AAD; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash, PartialOrd)] +pub enum Role { + User, + Manager, + Admin, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct Member { + nym: String, + sp_address_a: String, + sp_address_b: String, + role: Role, +} + +impl Member { + pub fn new( + nym: String, + sp_address_a: SilentPaymentAddress, + sp_address_b: SilentPaymentAddress, + role: Role, + ) -> Self { + Self { + nym, + sp_address_a: sp_address_a.into(), + sp_address_b: sp_address_b.into(), + role, + } + } + + pub fn get_nym(&self) -> String { + self.nym.clone() + } + + pub fn get_addresses(&self) -> (String, String) { + (self.sp_address_a.clone(), self.sp_address_b.clone()) + } + + pub fn get_role(&self) -> Role { + self.role + } +} + +sha256t_hash_newtype! { + pub struct AnkPcdTag = hash_str("4nk/Pcd"); + + #[hash_newtype(forward)] + pub struct AnkPcdHash(_); +} + +impl AnkPcdHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkPcdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPcdHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkPcdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPcdHash::from_engine(eng) + } +} + +pub trait Pcd<'a>: Serialize + Deserialize<'a> { + fn hash(&self) -> AnkPcdHash { + AnkPcdHash::from_value(&self.to_value()) + } + + fn encrypt_fields(&self, fields2keys: &mut Map, fields2cipher: &mut Map) -> Result<()> { + let as_value = self.to_value(); + let as_map = as_value.as_object().unwrap(); + let mut rng = thread_rng(); + for (key, value) in as_map { + let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); + let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); + fields2keys.insert(key.to_owned(), Value::String(aes_key.to_lower_hex_string())); + + let encryption = Aes256Gcm::new(&aes_key.into()); + let value_string = value.to_string(); + let payload = Payload { + msg: value_string.as_bytes(), + aad: AAD, + }; + let cipher = encryption.encrypt(&nonce.into(), payload) + .map_err(|e| Error::msg(format!("{}", e)))?; + + let mut res = Vec::with_capacity(nonce.len() + cipher.len()); + res.extend_from_slice(&nonce); + res.extend_from_slice(&cipher); + + fields2cipher.insert(key.to_owned(), Value::String(res.to_lower_hex_string())); + } + + Ok(()) + } + + fn decrypt_fields(&mut self, fields2keys: &Map) -> Result<()> { + let as_value = self.to_value(); + let as_map = as_value.as_object().unwrap(); + for (key, value) in as_map { + if let Some(aes_key) = fields2keys.get(key) { + let mut nonce = [0u8; 12]; + let mut key_buf = [0u8; 32]; + key_buf.copy_from_slice(&Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?); + let decrypt = Aes256Gcm::new(&key_buf.into()); + let raw_cipher = Vec::from_hex(&value.to_string().trim_matches('\"'))?; + nonce.copy_from_slice(&raw_cipher[..12]); + let payload = Payload { + msg: &raw_cipher[12..], + aad: AAD, + }; + let plain = decrypt.decrypt(&nonce.into(), payload) + .map_err(|_| Error::msg(format!("Failed to decrypt field {}", key)))?; + self.to_value() + .as_object_mut() + .unwrap() + .insert(key.to_owned(), Value::String(plain.to_lower_hex_string())); + } else { + continue; + } + } + + Ok(()) + } + + fn to_value(&self) -> Value { + Value::from_str(&serde_json::to_string(&self).unwrap()).unwrap() + } +} + +impl Pcd<'_> for Value {} diff --git a/src/prd.rs b/src/prd.rs new file mode 100644 index 0000000..b51570d --- /dev/null +++ b/src/prd.rs @@ -0,0 +1,208 @@ +use std::str::FromStr; + +use anyhow::{Result, Error}; +use serde::{Serialize, Deserialize}; + +use serde_json::{Map, Value}; +use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::spclient::SpWallet; +use sp_client::bitcoin::secp256k1::schnorr::Signature; +use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; +use tsify::Tsify; +use uuid::Uuid; + +use crate::pcd::{AnkPcdHash, Member}; +use crate::signature::{Proof}; + +#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub enum PrdType { + #[default] + None, + Message, + Init, // Create a new process + Update, // Update an existing process + List, // request a list of items + Response, + Confirm, +} + +sha256t_hash_newtype! { + pub struct AnkValidationYesTag = hash_str("4nk/yes"); + + #[hash_newtype(forward)] + pub struct AnkValidationYesHash(_); +} + +impl AnkValidationYesHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkValidationYesHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkValidationYesHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkValidationYesHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkValidationYesHash::from_engine(eng) + } +} + +sha256t_hash_newtype! { + pub struct AnkValidationNoTag = hash_str("4nk/no"); + + #[hash_newtype(forward)] + pub struct AnkValidationNoHash(_); +} + +impl AnkValidationNoHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkValidationNoHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkValidationNoHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkValidationNoHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkValidationNoHash::from_engine(eng) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +pub struct ValidationToken { + member: Member, + message: [u8; 32], + sig_a: Signature, + sig_b: Signature, // User must sign with its 2 paired devices +} + +sha256t_hash_newtype! { + pub struct AnkPrdTag = hash_str("4nk/Prd"); + + #[hash_newtype(forward)] + pub struct AnkPrdHash(_); +} + +impl AnkPrdHash { + pub fn from_value(value: &Value) -> Self { + let mut eng = AnkPrdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPrdHash::from_engine(eng) + } + + pub fn from_map(map: &Map) -> Self { + let value = Value::Object(map.clone()); + let mut eng = AnkPrdHash::engine(); + eng.input(value.to_string().as_bytes()); + AnkPrdHash::from_engine(eng) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[allow(non_camel_case_types)] +pub struct Prd { + pub prd_type: PrdType, + pub process_uuid: String, // stringification of Uuid + pub sender: String, + pub keys: Map, // key is a key in pcd, value is the key to decrypt it + pub validation_tokens: Vec, + pub pcd_commitment: String, + pub proof: Option, // This must be None up to the creation of the network message +} + +impl Prd { + pub fn new( + prd_type: PrdType, + uuid: Uuid, + sender: String, + encrypted_pcd: Map, + keys: Map + ) -> Result { + let res = Self { + prd_type, + process_uuid: uuid.to_string(), + sender, + validation_tokens: vec![], + keys, + pcd_commitment: AnkPcdHash::from_map(&encrypted_pcd).to_string(), + proof: None, + }; + + Ok(res) + } + + pub fn extract_from_message(plain: &[u8], commitment: [u8; 32]) -> Result { + let prd: Prd = serde_json::from_slice(plain)?; + // check that the hash of the prd is consistent with what's commited in the op_return + if prd.create_commitment().to_byte_array() != commitment { + return Err(anyhow::Error::msg("Received prd is not what was commited in the transaction")); + } + // check that the proof is consistent + let sender: Member = serde_json::from_str(&prd.sender)?; + if let Some(proof) = prd.proof { + // take the 2 spending keys in sender + let (address_a, address_b) = sender.get_addresses(); + let spend_key_a = ::try_from(address_a)?.get_spend_key().x_only_public_key().0; + let spend_key_b = ::try_from(address_b)?.get_spend_key().x_only_public_key().0; + // The key in proof must be one of the 2 sender keys + let proof_key = proof.get_key(); + if spend_key_a != proof_key && spend_key_b != proof_key { + return Err(anyhow::Error::msg("Proof signed with an unknown key")); + } + proof.verify()?; + } + Ok(prd) + } + + pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> { + match self.prd_type { + PrdType::Confirm => self.validation_tokens.push(validation_token), + _ => return Err(Error::msg("This Prd type doesn't allow validation tokens")) + } + Ok(()) + } + + pub fn filter_keys(&mut self, to_keep: Vec) { + let current_keys = self.keys.clone(); + let filtered_keys: Map = current_keys.into_iter() + .filter(|(field, _)| to_keep.contains(field)) + .collect(); + self.keys = filtered_keys; + } + + /// We commit to everything except the keys and the proof + /// Because 1) we need one commitment to common data for all recipients of the transaction + /// 2) we already commit to the keys in the sender proof anyway + pub fn create_commitment(&self) -> AnkPrdHash { + let mut to_commit = self.clone(); + to_commit.keys = Map::new(); + to_commit.proof = None; + AnkPrdHash::from_value(&to_commit.to_value()) + } + + /// Generate the signed proof and serialize to send over the network + pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result { + let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?; + let to_sign = self.to_string(); // we sign the whole prd, incl the keys, for each recipient + let proof = Proof::new(to_sign.as_bytes(), spend_sk); + + let mut res = self.clone(); + res.proof = Some(proof); + + Ok(res.to_string()) + } + + pub fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } + + pub fn to_value(&self) -> Value { + Value::from_str(&self.to_string()).unwrap() + } +} diff --git a/src/process.rs b/src/process.rs index 3fd021f..a42623f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -2,71 +2,27 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sp_client::bitcoin::hashes::Hash; -use sp_client::bitcoin::secp256k1::schnorr::Signature; -use sp_client::bitcoin::{OutPoint, Txid}; -use sp_client::silentpayments::utils::SilentPaymentAddress; +use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; use tsify::Tsify; use uuid::Uuid; use wasm_bindgen::prelude::*; -use crate::device::Device; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, PartialOrd)] -pub enum Role { - User, - Manager, - Admin, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -pub struct Member { - nym: String, - sp_address: String, - sp_address_alt: String, - role: Role, -} - -impl Member { - pub fn new( - nym: String, - sp_address: SilentPaymentAddress, - sp_address_alt: SilentPaymentAddress, - role: Role, - ) -> Self { - Self { - nym, - sp_address: sp_address.into(), - sp_address_alt: sp_address_alt.into(), - role, - } - } - - pub fn get_nym(&self) -> String { - self.nym.clone() - } - - pub fn get_addresses(&self) -> (String, String) { - (self.sp_address.clone(), self.sp_address_alt.clone()) - } - - pub fn get_role(&self) -> Role { - self.role - } -} +use crate::pcd::Role; #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct ValidationRules { quorum: f32, // Must be > 0.0, <= 1.0 min_permission: Role, // Only users with at least that Role can send a token + min_signatures_member: f32, // Must be > 0.0, <= 1.0, does each member need to sign with all it's devices? } impl ValidationRules { - pub fn new(quorum: f32, min_permission: Role) -> Self { + pub fn new(quorum: f32, min_permission: Role, min_signatures_member: f32) -> Self { Self { quorum, min_permission, + min_signatures_member, } } } @@ -76,95 +32,33 @@ impl ValidationRules { pub struct Process { pub uuid: String, pub name: String, - pub version: u32, - pub roles: Vec, pub validation_rules: ValidationRules, - pub initial_commit_tx: Txid, - pub latest_commit_tx: Txid, pub html: String, pub style: String, pub script: String, - pub pcd_template: Value + pub init_state: Value, + pub commited_in: OutPoint, } impl Process { pub fn new( name: String, - roles: Vec, validation_rules: ValidationRules, - initial_commit_tx: Txid, html: String, style: String, script: String, - pcd_template: Value, + init_state: Value, + commited_in: OutPoint, ) -> Self { Self { uuid: Uuid::new_v4().to_string(), name, - version: 1, - roles, validation_rules, - initial_commit_tx, - latest_commit_tx: initial_commit_tx, html, style, script, - pcd_template - } - } - - pub fn new_pairing_process( - pcd: PairingPcd, - initial_commit_tx: Txid, - ) -> Self { - let member = Member::new( - pcd.nym.clone(), - pcd.addresses[0].clone().try_into().unwrap(), - pcd.addresses[1].clone().try_into().unwrap(), - Role::Admin, - ); - let validation_rules = ValidationRules::new(1.0, Role::Admin); - Self::new( - "pairing".to_owned(), - vec![member], - validation_rules, - initial_commit_tx, - "".to_owned(), - "".to_owned(), - "".to_owned(), - Value::from_str(&serde_json::to_string(&pcd).unwrap()).unwrap() - ) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PairingPcd { - nym: String, - addresses: [String; 2], - session_index: u32, - revokation_index: u32, - pairing_txs: [Txid; 2], - current_session_txs: [Txid; 2] -} - -impl PairingPcd { - pub fn new( - nym: String, - local_address: SilentPaymentAddress, - remote_address: SilentPaymentAddress, - session_index: u32, - revokation_index: u32, - incoming_pairing_tx: Txid, - outgoing_pairing_tx: Txid, - ) -> Self { - let empty_txid = Txid::from_byte_array([0u8; Txid::LEN]); - Self { - nym, - addresses: [local_address.into(), remote_address.into()], - session_index, - revokation_index, - pairing_txs: [incoming_pairing_tx, outgoing_pairing_tx], - current_session_txs: [empty_txid, empty_txid] + init_state, + commited_in, } } } diff --git a/src/signature.rs b/src/signature.rs index b965962..468bcf0 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -1,5 +1,6 @@ use anyhow::Result; use rand::{thread_rng, RngCore}; +use serde::{Serialize, Deserialize}; use sp_client::bitcoin::key::Secp256k1; use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey}; @@ -20,26 +21,48 @@ impl AnkMessageHash { } } -pub fn sign_message(message: &[u8], signing_key: SecretKey) -> Result { - let message_hash = AnkMessageHash::from_message(message); - - let secp = Secp256k1::signing_only(); - - let keypair = Keypair::from_secret_key(&secp, &signing_key); - - let mut aux_rand = [0u8; 32]; - - thread_rng().fill_bytes(&mut aux_rand); - - let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand); - - Ok(sig) +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Proof { + signature: Signature, + message: AnkMessageHash, + key: XOnlyPublicKey } -pub fn verify(sig: Signature, message: &[u8], x_only_key: XOnlyPublicKey) -> Result<()> { - let secp = Secp256k1::verification_only(); - let message_hash = AnkMessageHash::from_message(message); - secp.verify_schnorr(&sig, &Message::from_digest(message_hash.to_byte_array()), &x_only_key)?; +impl Proof { + pub fn new(message: &[u8], signing_key: SecretKey) -> Self { + let message_hash = AnkMessageHash::from_message(message); + + let secp = Secp256k1::signing_only(); + + let keypair = Keypair::from_secret_key(&secp, &signing_key); + + let mut aux_rand = [0u8; 32]; + + thread_rng().fill_bytes(&mut aux_rand); + + let sig = secp.sign_schnorr_with_aux_rand(&Message::from_digest(message_hash.to_byte_array()), &keypair, &aux_rand); + + Self { + signature: sig, + message: message_hash, + key: keypair.x_only_public_key().0 + } + } + + pub fn get_key(&self) -> XOnlyPublicKey { + self.key + } + + pub fn verify(&self) -> Result<()> { + let secp = Secp256k1::verification_only(); + secp.verify_schnorr(&self.signature, &Message::from_digest(self.message.to_byte_array()), &self.key)?; + + Ok(()) + } + + pub fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } + +} - Ok(()) -} \ No newline at end of file diff --git a/src/silentpayments.rs b/src/silentpayments.rs index 78dc7b5..1e29c98 100644 --- a/src/silentpayments.rs +++ b/src/silentpayments.rs @@ -171,245 +171,247 @@ pub fn map_outputs_to_sp_address(psbt_str: &str) -> Result PublicKey { - let prevout = tx.input.get(0).unwrap().to_owned(); - let outpoint_data = ( - prevout.previous_output.txid.to_string(), - prevout.previous_output.vout, - ); - let input_pubkey = - get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap(); - let tweak_data = - calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap(); - tweak_data - } +// fn helper_get_tweak_data(tx: &Transaction, spk: ScriptBuf) -> PublicKey { +// let prevout = tx.input.get(0).unwrap().to_owned(); +// let outpoint_data = ( +// prevout.previous_output.txid.to_string(), +// prevout.previous_output.vout, +// ); +// let input_pubkey = +// get_pubkey_from_input(&vec![], &prevout.witness.to_vec(), spk.as_bytes()).unwrap(); +// let tweak_data = +// calculate_tweak_data(&vec![&input_pubkey.unwrap()], &vec![outpoint_data]).unwrap(); +// tweak_data +// } - fn helper_create_commitment(payload_to_hash: String) -> sha256::Hash { - let mut engine = sha256::HashEngine::default(); - engine.write_all(&payload_to_hash.as_bytes()).unwrap(); - let hash = sha256::Hash::from_engine(engine); - hash - } +// fn helper_create_commitment(payload_to_hash: String) -> sha256::Hash { +// let mut engine = sha256::HashEngine::default(); +// engine.write_all(&payload_to_hash.as_bytes()).unwrap(); +// let hash = sha256::Hash::from_engine(engine); +// hash +// } - #[test] - fn it_creates_notification_transaction() { - let recipient = Recipient { - address: BOB_ADDRESS.to_owned(), - amount: Amount::from_sat(1200), - nb_outputs: 1, - }; - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); - let pcd = Pcd::new("TEST".to_owned()); - let pcd_hash = helper_create_commitment(pcd.to_string()); - let mut key = [0u8; 32]; - key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); +// #[test] +// fn it_creates_notification_transaction() { +// let recipient = Recipient { +// address: BOB_ADDRESS.to_owned(), +// amount: Amount::from_sat(1200), +// nb_outputs: 1, +// }; +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET).unwrap(); +// let pcd = Pcd::new(Value::String("TEST".to_owned())); +// let pcd_hash = helper_create_commitment(pcd.to_string()); +// let mut key = [0u8; 32]; +// key.copy_from_slice(&Vec::from_hex(KEY).unwrap()); - let alice_member = Member::new( - "alice".to_owned(), - alice_wallet.get_client().get_receiving_address().try_into().unwrap(), - alice_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), - Role::Admin, - ); - let bob_member = Member::new( - "bob".to_owned(), - bob_wallet.get_client().get_receiving_address().try_into().unwrap(), - bob_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), - Role::User, - ); +// let alice_member = Member::new( +// "alice".to_owned(), +// alice_wallet.get_client().get_receiving_address().try_into().unwrap(), +// alice_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), +// Role::Admin, +// ); +// let bob_member = Member::new( +// "bob".to_owned(), +// bob_wallet.get_client().get_receiving_address().try_into().unwrap(), +// bob_wallet.get_client().sp_receiver.get_change_address().try_into().unwrap(), +// Role::User, +// ); - let validation_rules = ValidationRules::new(0.5, Role::User); +// let validation_rules = ValidationRules::new(0.5, Role::User); - let pcd_template = serde_json::json!({ - "int": 0, - "string": "exemple_data", - "array": [ - "element1", - "element2" - ] - }); +// let pcd_template = serde_json::json!({ +// "int": 0, +// "string": "exemple_data", +// "array": [ +// "element1", +// "element2" +// ] +// }); - let process = Process::new( - "default".to_owned(), - vec![alice_member, bob_member], - validation_rules, - Txid::from_str(INITIAL_COMMIT_TX).unwrap(), - "".to_owned(), - "".to_owned(), - "".to_owned(), - pcd_template, - ); +// let process = Process::new( +// "default".to_owned(), +// vec![alice_member, bob_member], +// validation_rules, +// Txid::from_str(INITIAL_COMMIT_TX).unwrap(), +// "".to_owned(), +// "".to_owned(), +// "".to_owned(), +// pcd_template, +// ); - let prd = Prd::new( - PrdType::Update, - process, - ALICE_ADDRESS.try_into().unwrap(), - key, - pcd_hash, - ).unwrap(); - let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap()); +// let prd = Prd::new( +// PrdType::Update, +// process, +// ALICE_ADDRESS.try_into().unwrap(), +// key, +// pcd_hash, +// ).unwrap(); +// let commitment = helper_create_commitment(serde_json::to_string(&prd).unwrap()); - let psbt = create_transaction( - &vec![], - &HashSet::new(), - &alice_wallet, - vec![recipient], - Some(commitment.as_byte_array().to_vec()), - FEE_RATE, - None, - ) - .unwrap(); +// let psbt = create_transaction( +// &vec![], +// &HashSet::new(), +// &alice_wallet, +// vec![recipient], +// Some(commitment.as_byte_array().to_vec()), +// FEE_RATE, +// None, +// ) +// .unwrap(); - let final_tx = psbt.extract_tx().unwrap(); - let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1"; +// let final_tx = psbt.extract_tx().unwrap(); +// let spk = "51205b7b324bb71d411e32f2c61fda5d1db23f5c7d6d416a77fab87c913a1b120be1"; - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } - #[test] - fn it_creates_confirmation_transaction() { - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap(); +// #[test] +// fn it_creates_confirmation_transaction() { +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_CONFIRMATION).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_CONFIRMATION).unwrap(); - // Bob must spend notification output - let (confirmation_outpoint, _) = bob_wallet - .get_outputs() - .get_outpoint( - OutPoint::from_str( - "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0", - ) - .unwrap(), - ) - .unwrap(); +// // Bob must spend notification output +// let (confirmation_outpoint, _) = bob_wallet +// .get_outputs() +// .get_outpoint( +// OutPoint::from_str( +// "148e0faa2f203b6e9488e2da696d8a49ebff4212946672f0bb072ced0909360d:0", +// ) +// .unwrap(), +// ) +// .unwrap(); - let recipient = Recipient { - address: ALICE_ADDRESS.to_owned(), - amount: Amount::from_sat(0), - nb_outputs: 1, - }; +// let recipient = Recipient { +// address: ALICE_ADDRESS.to_owned(), +// amount: Amount::from_sat(0), +// nb_outputs: 1, +// }; - let psbt = create_transaction( - &vec![&confirmation_outpoint], - &HashSet::new(), - &bob_wallet, - vec![recipient], - None, - FEE_RATE, - Some(ALICE_ADDRESS.to_owned()), - ) - .unwrap(); +// let psbt = create_transaction( +// &vec![&confirmation_outpoint], +// &HashSet::new(), +// &bob_wallet, +// vec![recipient], +// None, +// FEE_RATE, +// Some(ALICE_ADDRESS.to_owned()), +// ) +// .unwrap(); - let final_tx = psbt.extract_tx().unwrap(); - // println!( - // "{}", - // serialize::(&final_tx).to_lower_hex_string() - // ); - let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c"; +// let final_tx = psbt.extract_tx().unwrap(); +// // println!( +// // "{}", +// // serialize::(&final_tx).to_lower_hex_string() +// // ); +// let spk = "512010f06f764cbc923ec3198db946307bf0c06a1b4f09206055e47a6fec0a33d52c"; - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } - #[test] - fn it_creates_answer_transaction() { - let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap(); - let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap(); +// #[test] +// fn it_creates_answer_transaction() { +// let mut alice_wallet: SpWallet = serde_json::from_str(ALICE_WALLET_ANSWER).unwrap(); +// let mut bob_wallet: SpWallet = serde_json::from_str(BOB_WALLET_ANSWER).unwrap(); - // Bob must spend notification output - let (confirmation_outpoint, _) = alice_wallet - .get_outputs() - .get_outpoint( - OutPoint::from_str( - "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0", - ) - .unwrap(), - ) - .unwrap(); +// // Bob must spend notification output +// let (confirmation_outpoint, _) = alice_wallet +// .get_outputs() +// .get_outpoint( +// OutPoint::from_str( +// "bc207c02bc4f1d4359fcd604296c0938bf1e6ff827662a56410676b8cbd768d9:0", +// ) +// .unwrap(), +// ) +// .unwrap(); - let recipient = Recipient { - address: BOB_ADDRESS.to_owned(), - amount: Amount::from_sat(0), - nb_outputs: 1, - }; +// let recipient = Recipient { +// address: BOB_ADDRESS.to_owned(), +// amount: Amount::from_sat(0), +// nb_outputs: 1, +// }; - let psbt = create_transaction( - &vec![&confirmation_outpoint], - &HashSet::new(), - &alice_wallet, - vec![recipient], - None, - FEE_RATE, - Some(BOB_ADDRESS.to_owned()), - ) - .unwrap(); +// let psbt = create_transaction( +// &vec![&confirmation_outpoint], +// &HashSet::new(), +// &alice_wallet, +// vec![recipient], +// None, +// FEE_RATE, +// Some(BOB_ADDRESS.to_owned()), +// ) +// .unwrap(); - let final_tx = psbt.extract_tx().unwrap(); - // println!("{}", serialize::(&final_tx).to_lower_hex_string()); - let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa"; +// let final_tx = psbt.extract_tx().unwrap(); +// // println!("{}", serialize::(&final_tx).to_lower_hex_string()); +// let spk = "5120646bdb98d89a2573acc6064a5c806d00e34beb65588c91a32733b62255b4dafa"; - let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); +// let tweak_data = helper_get_tweak_data(&final_tx, ScriptBuf::from_hex(spk).unwrap()); - // Check that Alice and Bob are both able to find that transaction - let alice_update = alice_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(alice_update.len() > 0); - let bob_update = bob_wallet - .update_wallet_with_transaction(&final_tx, 0, tweak_data) - .unwrap(); - assert!(bob_update.len() > 0); - println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); - println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); - } -} +// // Check that Alice and Bob are both able to find that transaction +// let alice_update = alice_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(alice_update.len() > 0); +// let bob_update = bob_wallet +// .update_wallet_with_transaction(&final_tx, 0, tweak_data) +// .unwrap(); +// assert!(bob_update.len() > 0); +// println!("{:?}", alice_wallet.get_outputs().to_outpoints_list()); +// println!("{:?}", bob_wallet.get_outputs().to_outpoints_list()); +// } +// } From 97df8ea13d25e9b213224fd19838e99aac3244d4 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 30 Aug 2024 19:57:17 +0200 Subject: [PATCH 08/13] Update prd/pcd --- src/device.rs | 38 ++++--------------- src/pcd.rs | 100 +++++++++++++++++++++++++++++++++---------------- src/prd.rs | 28 +++++++++----- src/process.rs | 38 ++++--------------- 4 files changed, 101 insertions(+), 103 deletions(-) diff --git a/src/device.rs b/src/device.rs index 5e491b9..4c49cf9 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,10 +1,9 @@ -use serde_json::Value; use serde::{Deserialize, Serialize}; use tsify::Tsify; use uuid::Uuid; use wasm_bindgen::prelude::*; -use sp_client::{silentpayments::utils::SilentPaymentAddress, spclient::SpWallet}; +use sp_client::spclient::SpWallet; use crate::pcd::Member; @@ -13,8 +12,7 @@ use crate::pcd::Member; pub struct Device { sp_wallet: SpWallet, pairing_process_uuid: Option, - paired_devices: Vec, // map an address to the pairing process - latest_known_state: Value + paired_member: Option, } impl Device { @@ -22,8 +20,7 @@ impl Device { Self { sp_wallet, pairing_process_uuid: None, - paired_devices: vec![], - latest_known_state: Value::Null + paired_member: None, } } @@ -43,33 +40,12 @@ impl Device { self.pairing_process_uuid.clone() } - pub fn set_process_uuid(&mut self, uuid: Uuid) { + pub fn pair(&mut self, uuid: Uuid, member: Member) { self.pairing_process_uuid = Some(uuid.to_string()); + self.paired_member = Some(member); } - pub fn get_paired_devices(&self) -> Vec { - self.paired_devices.clone() - } - - pub fn push_paired_device(&mut self, new_device_address: SilentPaymentAddress) { - self.paired_devices.push(new_device_address.into()) - } - - pub fn to_member(&self) -> anyhow::Result { - if !self.is_linked() { - return Err(anyhow::Error::msg("device is not linked")); - } - - let member = self.latest_known_state.as_object().unwrap().get("member").unwrap().clone(); - let res: Member = serde_json::from_value(member)?; - Ok(res) - } - - pub fn get_latest_state(&self) -> Value { - self.latest_known_state.clone() - } - - pub fn update_latest_state(&mut self, update: Value) { - self.latest_known_state = update; + pub fn to_member(&self) -> Option { + self.paired_member.clone() } } \ No newline at end of file diff --git a/src/pcd.rs b/src/pcd.rs index 77f2626..97e57f2 100644 --- a/src/pcd.rs +++ b/src/pcd.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::{HashMap, HashSet}, str::FromStr}; use anyhow::{Result, Error}; use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit}; @@ -11,47 +11,38 @@ use tsify::Tsify; use crate::crypto::AAD; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash, PartialOrd)] -pub enum Role { - User, - Manager, - Admin, -} - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Member { - nym: String, - sp_address_a: String, - sp_address_b: String, - role: Role, + sp_addresses: Vec } impl Member { pub fn new( - nym: String, - sp_address_a: SilentPaymentAddress, - sp_address_b: SilentPaymentAddress, - role: Role, - ) -> Self { - Self { - nym, - sp_address_a: sp_address_a.into(), - sp_address_b: sp_address_b.into(), - role, + sp_addresses: Vec, + ) -> Result { + if sp_addresses.is_empty() { + return Err(Error::msg("empty address set")); } + + let mut seen = HashSet::new(); + for s in sp_addresses.iter() { + if !seen.insert(s.clone()) { + return Err(Error::msg("Duplicate addresses found")); + } + } + + let res: Vec = sp_addresses.iter() + .map(|a| Into::::into(*a)) + .collect(); + + Ok(Self { + sp_addresses: res + }) } - pub fn get_nym(&self) -> String { - self.nym.clone() - } - - pub fn get_addresses(&self) -> (String, String) { - (self.sp_address_a.clone(), self.sp_address_b.clone()) - } - - pub fn get_role(&self) -> Role { - self.role + pub fn get_addresses(&self) -> Vec { + self.sp_addresses.clone() } } @@ -145,3 +136,48 @@ pub trait Pcd<'a>: Serialize + Deserialize<'a> { } impl Pcd<'_> for Value {} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct ValidationRule { + quorum: f32, // Must be >= 0.0, <= 1.0, 0.0 means reading right + pub fields: Vec, // Which fields are concerned by this rule + min_sig_member: f32, // Must be >= 0.0, <= 1.0, does each member need to sign with all it's devices? +} + +impl ValidationRule { + pub fn new(quorum: f32, fields: Vec, min_sig_member: f32) -> Result { + if quorum < 0.0 || quorum > 1.0 { + return Err(Error::msg("quorum must be 0.0 < quorum <= 1.0")); + } + + if min_sig_member < 0.0 || min_sig_member > 1.0 { + return Err(Error::msg("min_signatures_member must be 0.0 < min_signatures_member <= 1.0")); + } + + if fields.is_empty() { + return Err(Error::msg("Fields can't be empty")); + } + + let res = Self { + quorum, + fields, + min_sig_member, + }; + + Ok(res) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +pub struct RoleDefinition { + pub members: Vec, + pub validation_rules: Vec, +} + +// #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +// #[tsify(into_wasm_abi, from_wasm_abi)] +// pub struct Roles { +// pub roles: HashMap +// } diff --git a/src/prd.rs b/src/prd.rs index b51570d..9066900 100644 --- a/src/prd.rs +++ b/src/prd.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::str::FromStr; use anyhow::{Result, Error}; @@ -5,6 +6,7 @@ use serde::{Serialize, Deserialize}; use serde_json::{Map, Value}; use sp_client::bitcoin::secp256k1::SecretKey; +use sp_client::bitcoin::XOnlyPublicKey; use sp_client::silentpayments::utils::SilentPaymentAddress; use sp_client::spclient::SpWallet; use sp_client::bitcoin::secp256k1::schnorr::Signature; @@ -77,8 +79,7 @@ impl AnkValidationNoHash { pub struct ValidationToken { member: Member, message: [u8; 32], - sig_a: Signature, - sig_b: Signature, // User must sign with its 2 paired devices + sigs: Vec, // User must sign with the requested number of devices } sha256t_hash_newtype! { @@ -146,13 +147,22 @@ impl Prd { // check that the proof is consistent let sender: Member = serde_json::from_str(&prd.sender)?; if let Some(proof) = prd.proof { - // take the 2 spending keys in sender - let (address_a, address_b) = sender.get_addresses(); - let spend_key_a = ::try_from(address_a)?.get_spend_key().x_only_public_key().0; - let spend_key_b = ::try_from(address_b)?.get_spend_key().x_only_public_key().0; - // The key in proof must be one of the 2 sender keys + // take the spending keys in sender + let addresses = sender.get_addresses(); + let mut spend_keys: Vec = vec![]; + for address in addresses { + spend_keys.push(::try_from(address)?.get_spend_key().x_only_public_key().0); + } + // The key in proof must be one of the sender keys let proof_key = proof.get_key(); - if spend_key_a != proof_key && spend_key_b != proof_key { + let mut known_key = false; + for key in spend_keys { + if key == proof_key { + known_key = true; + break; + } + } + if !known_key { return Err(anyhow::Error::msg("Proof signed with an unknown key")); } proof.verify()?; @@ -168,7 +178,7 @@ impl Prd { Ok(()) } - pub fn filter_keys(&mut self, to_keep: Vec) { + pub fn filter_keys(&mut self, to_keep: HashSet) { let current_keys = self.keys.clone(); let filtered_keys: Map = current_keys.into_iter() .filter(|(field, _)| to_keep.contains(field)) diff --git a/src/process.rs b/src/process.rs index a42623f..2ad347b 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,59 +1,35 @@ -use std::str::FromStr; +use std::collections::HashMap; + +use anyhow::{Error, Result}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; +use serde_json::{Map, Value}; +use sp_client::bitcoin::OutPoint; use tsify::Tsify; use uuid::Uuid; use wasm_bindgen::prelude::*; -use crate::pcd::Role; - -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct ValidationRules { - quorum: f32, // Must be > 0.0, <= 1.0 - min_permission: Role, // Only users with at least that Role can send a token - min_signatures_member: f32, // Must be > 0.0, <= 1.0, does each member need to sign with all it's devices? -} - -impl ValidationRules { - pub fn new(quorum: f32, min_permission: Role, min_signatures_member: f32) -> Self { - Self { - quorum, - min_permission, - min_signatures_member, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Process { pub uuid: String, - pub name: String, - pub validation_rules: ValidationRules, pub html: String, pub style: String, pub script: String, - pub init_state: Value, + pub init_state: Map, pub commited_in: OutPoint, } impl Process { pub fn new( - name: String, - validation_rules: ValidationRules, html: String, style: String, script: String, - init_state: Value, + init_state: Map, commited_in: OutPoint, ) -> Self { Self { uuid: Uuid::new_v4().to_string(), - name, - validation_rules, html, style, script, From 3cbe77c143f271fa9dfddec3a6a9e0e9219c483a Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 23 Sep 2024 12:43:56 +0200 Subject: [PATCH 09/13] Add MutexExt trait --- src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index f1b0d88..82316b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ +use std::sync::{Mutex, MutexGuard}; +use std::fmt::Debug; + pub use sp_client; pub use uuid; pub use log; +pub use aes_gcm; pub mod crypto; pub mod device; @@ -11,3 +15,16 @@ pub mod prd; pub mod process; pub mod silentpayments; pub mod signature; + +pub const MAX_PRD_PAYLOAD_SIZE: usize = u16::MAX as usize; // 64KiB sounds reasonable for now + +pub trait MutexExt { + fn lock_anyhow(&self) -> Result, anyhow::Error>; +} + +impl MutexExt for Mutex { + fn lock_anyhow(&self) -> Result, anyhow::Error> { + self.lock() + .map_err(|e| anyhow::Error::msg(format!("Failed to lock: {}", e))) + } +} From bbc9eaa96223d73999929d335fb5e1dce9babf67 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 23 Sep 2024 16:03:41 +0200 Subject: [PATCH 10/13] cleanup crypto --- src/crypto.rs | 384 ++++---------------------------------------------- 1 file changed, 27 insertions(+), 357 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index 0bbe5f1..7fecd13 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,20 +1,5 @@ -use std::collections::HashMap; - use anyhow::{Error, Result}; -use serde::{Deserialize, Serialize}; -use sp_client::{ - bitcoin::{ - hex::{DisplayHex, FromHex}, - key::constants::SECRET_KEY_SIZE, - Txid, - }, - silentpayments::{ - bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, - secp256k1::PublicKey, - utils::SilentPaymentAddress, - }, -}; -use tsify::Tsify; +use sp_client::silentpayments::{bitcoin_hashes::{sha256t_hash_newtype, Hash, HashEngine}, secp256k1::PublicKey}; use aes_gcm::aead::{Aead, Payload}; pub use aes_gcm::{AeadCore, Aes256Gcm, KeyInit}; @@ -22,43 +7,6 @@ use rand::thread_rng; pub const AAD: &[u8] = "4nk".as_bytes(); -const HALFKEYSIZE: usize = SECRET_KEY_SIZE / 2; - -const THIRTYTWO: usize = 32; - -#[derive(Debug)] -pub struct SharedPoint([u8; 64]); - -impl SharedPoint { - pub fn as_inner(&self) -> &[u8; 64] { - &self.0 - } -} - -#[derive(Debug, Serialize, Deserialize, Tsify, Clone, Default, PartialEq)] -#[tsify(from_wasm_abi, into_wasm_abi)] -pub struct AnkSharedSecret { - secret: String, -} - -impl AnkSharedSecret { - pub fn new(shared_point: PublicKey) -> Self { - let mut shared_point_bin = [0u8; 64]; - shared_point_bin.copy_from_slice(&shared_point.serialize_uncompressed()[1..]); - let secret = AnkSharedSecretHash::from_shared_point(shared_point_bin).to_byte_array(); - Self { - secret: secret.to_lower_hex_string(), - } - } - - pub fn to_byte_array(&self) -> [u8; 32] { - let bytes = Vec::from_hex(&self.secret).unwrap(); - let mut buf = [0u8; 32]; - buf.copy_from_slice(&bytes); - buf - } -} - sha256t_hash_newtype! { pub struct AnkSharedSecretTag = hash_str("4nk/AnkSharedSecret"); @@ -67,317 +15,39 @@ sha256t_hash_newtype! { } impl AnkSharedSecretHash { - pub fn from_shared_point(shared_point: [u8; 64]) -> Self { + pub fn from_shared_point(shared_point: PublicKey) -> Self { let mut eng = AnkSharedSecretHash::engine(); - eng.input(&shared_point); + eng.input(&shared_point.serialize_uncompressed()[1..]); AnkSharedSecretHash::from_engine(eng) } } -pub struct HalfKey([u8; HALFKEYSIZE]); +pub fn encrypt_with_key(key: &[u8; 32], plaintext: &[u8]) -> Result> { + let encryption_eng = Aes256Gcm::new(key.into()); + let nonce = Aes256Gcm::generate_nonce(&mut thread_rng()); + let payload = Payload { + msg: plaintext, + aad: AAD, + }; + let ciphertext = encryption_eng.encrypt(&nonce, payload) + .map_err(|e| anyhow::anyhow!(e))?; -impl TryFrom> for HalfKey { - type Error = anyhow::Error; - fn try_from(value: Vec) -> std::prelude::v1::Result { - if value.len() == HALFKEYSIZE { - let mut buf = [0u8; HALFKEYSIZE]; - buf.copy_from_slice(&value); - Ok(HalfKey(buf)) - } else { - Err(Error::msg("Invalid length for HalfKey")) - } - } + let mut res: Vec = Vec::with_capacity(nonce.len() + ciphertext.len()); + res.extend_from_slice(&nonce); + res.extend_from_slice(&ciphertext); + + Ok(res) } -impl HalfKey { - pub fn as_slice(&self) -> &[u8] { - &self.0 - } +pub fn decrypt_with_key(key: &[u8; 32], ciphertext: &[u8]) -> Result> { + let decryption_eng = Aes256Gcm::new(key.into()); + let nonce = &ciphertext[..12]; + let payload = Payload { + msg: &ciphertext[12..], + aad: AAD, + }; + let plaintext = decryption_eng.decrypt(nonce.into(), payload) + .map_err(|e| anyhow::anyhow!(e))?; - pub fn to_inner(&self) -> Vec { - self.0.to_vec() - } -} - -pub enum Purpose { - Login, - ThirtyTwoBytes, - Arbitrary, -} - -pub type CipherText = Vec; - -pub type EncryptedKey = Vec; - -pub struct Aes256Decryption { - pub purpose: Purpose, - cipher_text: CipherText, - aes_key: [u8; 32], - nonce: [u8; 12], -} - -impl Aes256Decryption { - pub fn new(purpose: Purpose, cipher_text: CipherText, aes_key: [u8; 32]) -> Result { - if cipher_text.len() <= 12 { - return Err(Error::msg("cipher_text is shorter than nonce length")); - } - let (message_nonce, message_cipher) = cipher_text.split_at(12); - let mut nonce = [0u8; 12]; - nonce.copy_from_slice(message_nonce); - Ok(Self { - purpose, - cipher_text: message_cipher.to_vec(), - aes_key, - nonce, - }) - } - - pub fn decrypt_with_key(&self) -> Result> { - match self.purpose { - Purpose::Login => { - let half_key = self.decrypt_login()?; - Ok(half_key.to_inner()) - } - Purpose::ThirtyTwoBytes => { - let thirty_two_buf = self.decrypt_thirty_two()?; - Ok(thirty_two_buf.to_vec()) - } - Purpose::Arbitrary => { - let arbitrary = self.decrypt_arbitrary()?; - Ok(arbitrary) - } - } - } - - fn decrypt_login(&self) -> Result { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != SECRET_KEY_SIZE / 2 { - return Err(Error::msg("Plain text of invalid lenght for a login")); - } - let mut key_half = [0u8; SECRET_KEY_SIZE / 2]; - key_half.copy_from_slice(&plain); - Ok(HalfKey(key_half)) - } - - fn decrypt_thirty_two(&self) -> Result<[u8; THIRTYTWO]> { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let plain = cipher - .decrypt(&self.nonce.into(), &*self.cipher_text) - .map_err(|e| Error::msg(format!("{}", e)))?; - if plain.len() != THIRTYTWO { - return Err(Error::msg("Plain text of invalid length, should be 32")); - } - let mut thirty_two = [0u8; THIRTYTWO]; - thirty_two.copy_from_slice(&plain); - Ok(thirty_two) - } - - fn decrypt_arbitrary(&self) -> Result> { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let payload = Payload { - msg: &self.cipher_text, - aad: AAD, - }; - let plain = cipher - .decrypt(&self.nonce.into(), payload) - .map_err(|e| Error::msg(format!("{}", e)))?; - Ok(plain) - } -} - -pub struct Aes256Encryption { - pub purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - shared_secrets: HashMap>, -} - -impl Aes256Encryption { - pub fn new(purpose: Purpose, plaintext: Vec) -> Result { - let mut rng = thread_rng(); - let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - Self::import_key(purpose, plaintext, aes_key, nonce) - } - - pub fn set_shared_secret( - &mut self, - shared_secrets: HashMap>, - ) { - self.shared_secrets = shared_secrets; - } - - pub fn encrypt_keys_with_shared_secrets( - &self, - ) -> Result> { - let mut res = HashMap::new(); - let mut rng = thread_rng(); - - for (_, sp_address2shared_secret) in self.shared_secrets.iter() { - for (sp_address, shared_secret) in sp_address2shared_secret { - let cipher = Aes256Gcm::new_from_slice(&shared_secret.to_byte_array()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let nonce = Aes256Gcm::generate_nonce(&mut rng); - let encrypted_key = cipher - .encrypt(&nonce, self.aes_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - - let mut ciphertext = Vec::::with_capacity(nonce.len() + encrypted_key.len()); - ciphertext.extend(nonce); - ciphertext.extend(encrypted_key); - - res.insert(sp_address.to_owned(), ciphertext); - } - } - Ok(res) - } - - pub fn import_key( - purpose: Purpose, - plaintext: Vec, - aes_key: [u8; 32], - nonce: [u8; 12], - ) -> Result { - if plaintext.len() == 0 { - return Err(Error::msg("Can't create encryption for an empty message")); - } - Ok(Self { - purpose, - plaintext, - aes_key, - nonce, - shared_secrets: HashMap::new(), - }) - } - - pub fn export_key(&self) -> [u8; 32] { - self.aes_key - } - - pub fn encrypt_with_aes_key(&self) -> Result { - match self.purpose { - Purpose::Login => self.encrypt_login(), - Purpose::ThirtyTwoBytes => self.encrypt_thirty_two(), - Purpose::Arbitrary => self.encrypt_arbitrary(), - } - } - - fn encrypt_login(&self) -> Result { - let half_key: HalfKey = self.plaintext.clone().try_into()?; - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), half_key.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } - - fn encrypt_thirty_two(&self) -> Result { - if self.plaintext.len() != 32 { - return Err(Error::msg("Invalid length, should be 32")); - } - let mut thirty_two = [0u8; 32]; - thirty_two.copy_from_slice(&self.plaintext); - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let cipher_text = cipher - .encrypt(&self.nonce.into(), thirty_two.as_slice()) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } - - fn encrypt_arbitrary(&self) -> Result { - let cipher = Aes256Gcm::new(&self.aes_key.into()); - let payload = Payload { - msg: &self.plaintext, - aad: AAD, - }; - let cipher_text = cipher - .encrypt(&self.nonce.into(), payload) - .map_err(|e| Error::msg(format!("{}", e)))?; - let mut res = Vec::with_capacity(self.nonce.len() + cipher_text.len()); - res.extend_from_slice(&self.nonce); - res.extend_from_slice(&cipher_text); - Ok(res) - } -} - -#[cfg(test)] -mod tests { - use std::{io::Read, str::FromStr}; - - use sp_client::bitcoin::hex::FromHex; - - use super::*; - - const ALICE_SP_ADDRESS: &str = "tsp1qqw3lqr6xravz9nf8ntazgwwl0fqv47kfjdxsnxs6eutavqfwyv5q6qk97mmyf6dtkdyzqlu2zv6h9j2ggclk7vn705q5u2phglpq7yw3dg5rwpdz"; - const BOB_SP_ADDRESS: &str = "tsp1qq2hlsgrj0gz8kcfkf9flqw5llz0u2vr04telqndku9mcqm6dl4fhvq60t8r78srrf56w9yr7w9e9dusc2wjqc30up6fjwnh9mw3e3veqegdmtf08"; - const TRANSACTION: &str = "4e6d03dec558e1b6624f813bf2da7cd8d8fb1c2296684c08cf38724dcfd8d10b"; - const ALICE_SHARED_SECRET: &str = - "ccf02d364c2641ca129a3fdf49de57b705896e233f7ba6d738991993ea7e2106"; - const BOB_SHARED_SECRET: &str = - "15ef3e377fb842e81de52dbaaea8ba30aeb051a81043ee19264afd27353da521"; - - #[test] - fn new_aes_empty_plaintext() { - let plaintext = Vec::new(); - let aes_enc = Aes256Encryption::new(Purpose::Login, plaintext); - - assert!(aes_enc.is_err()); - } - - #[test] - fn aes_encrypt_login_invalid_length() { - let plaintext = "example"; - let aes_enc_short = Aes256Encryption::new(Purpose::Login, plaintext.as_bytes().to_vec()); - - assert!(aes_enc_short.is_ok()); - - let cipher = aes_enc_short.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - - let plaintext = [1u8; 64]; - let aes_enc_long = Aes256Encryption::new(Purpose::Login, plaintext.to_vec()); - - assert!(aes_enc_long.is_ok()); - - let cipher = aes_enc_long.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_err()); - } - - #[test] - fn aes_encrypt_login() { - let plaintext = [1u8; HALFKEYSIZE]; - let aes_key = Aes256Gcm::generate_key(&mut thread_rng()); - let nonce = Aes256Gcm::generate_nonce(&mut thread_rng()); - let aes_enc = Aes256Encryption::import_key( - Purpose::Login, - plaintext.to_vec(), - aes_key.into(), - nonce.into(), - ); - - assert!(aes_enc.is_ok()); - - let cipher = aes_enc.unwrap().encrypt_with_aes_key(); - - assert!(cipher.is_ok()); - - let mut plain_key = [0u8; 32]; - plain_key.copy_from_slice(&aes_key.to_vec()); - - let aes_dec = Aes256Decryption::new(Purpose::Login, cipher.unwrap(), plain_key); - - assert!(aes_dec.is_ok()); - } + Ok(plaintext) } From 62b2137c77e4587cccdedd770ca54dfab9885a14 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 23 Sep 2024 16:10:59 +0200 Subject: [PATCH 11/13] Complete Process implementation --- src/process.rs | 102 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 28 deletions(-) diff --git a/src/process.rs b/src/process.rs index 2ad347b..b785313 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,40 +1,86 @@ -use std::collections::HashMap; - -use anyhow::{Error, Result}; +use std::{collections::HashMap, sync::{Mutex, MutexGuard, OnceLock}}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use sp_client::bitcoin::OutPoint; -use tsify::Tsify; -use uuid::Uuid; -use wasm_bindgen::prelude::*; +use sp_client::{bitcoin::OutPoint, silentpayments::utils::SilentPaymentAddress}; -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -pub struct Process { - pub uuid: String, - pub html: String, - pub style: String, - pub script: String, - pub init_state: Map, +use crate::{crypto::AnkSharedSecretHash, prd::Prd, signature::Proof, MutexExt}; + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ProcessState { pub commited_in: OutPoint, + pub encrypted_pcd: Value, // Some fields may be clear, if the owner of the process decides so + pub keys: Map, // We may not always have all the keys + pub validation_token: Vec, // This signs the encrypted pcd +} + +/// A process is basically a succession of states +/// If a process has nothing to do with us, shared_secrets and impending_requests will be empty +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct Process { + states: Vec, + shared_secrets: HashMap, + impending_requests: Vec, } impl Process { - pub fn new( - html: String, - style: String, - script: String, - init_state: Map, - commited_in: OutPoint, - ) -> Self { + pub fn new(states: Vec, shared_secrets: HashMap, impending_requests: Vec) -> Self { Self { - uuid: Uuid::new_v4().to_string(), - html, - style, - script, - init_state, - commited_in, + states, + shared_secrets: shared_secrets.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), + impending_requests, } } + + pub fn insert_shared_secret(&mut self, address: SilentPaymentAddress, secret: AnkSharedSecretHash) { + self.shared_secrets.insert(address.to_string(), secret); + } + + pub fn get_shared_secret_for_address(&self, address: &SilentPaymentAddress) -> Option { + self.shared_secrets.get(&address.to_string()).cloned() + } + + pub fn get_all_secrets(&self) -> HashMap { + self.shared_secrets.clone().into_iter().map(|(k, v)| (SilentPaymentAddress::try_from(k.as_str()).unwrap(), v)).collect() + } + + pub fn insert_state(&mut self, state: ProcessState) { + self.states.push(state); + } + + pub fn get_status_at(&self, index: usize) -> Option<&ProcessState> { + self.states.get(index) + } + + pub fn get_status_at_mut(&mut self, index: usize) -> Option<&mut ProcessState> { + self.states.get_mut(index) + } + + pub fn get_latest_state(&self) -> Option<&ProcessState> { + self.states.last() + } + + pub fn get_latest_state_mut(&mut self) -> Option<&mut ProcessState> { + self.states.last_mut() + } + + pub fn insert_impending_request(&mut self, request: Prd) { + self.impending_requests.push(request); + } + + pub fn get_impending_requests(&self) -> Vec<&Prd> { + self.impending_requests.iter().collect() + } + + pub fn get_impending_requests_mut(&mut self) -> Vec<&mut Prd> { + self.impending_requests.iter_mut().collect() + } +} + +pub static CACHEDPROCESSES: OnceLock>> = OnceLock::new(); + +pub fn lock_processes() -> Result>, anyhow::Error> { + CACHEDPROCESSES + .get_or_init(|| Mutex::new(HashMap::new())) + .lock_anyhow() } From f3aa1bc2d0c3f27dedf5e1211bfa1ddf5d540aa6 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 23 Sep 2024 16:11:46 +0200 Subject: [PATCH 12/13] Functional prd/pcd --- src/device.rs | 26 +++++--- src/pcd.rs | 118 ++++++++++++++++++++++++---------- src/prd.rs | 163 ++++++++++++++++++++++------------------------- src/signature.rs | 53 +++++++++++++-- 4 files changed, 227 insertions(+), 133 deletions(-) diff --git a/src/device.rs b/src/device.rs index 4c49cf9..4403170 100644 --- a/src/device.rs +++ b/src/device.rs @@ -3,7 +3,7 @@ use tsify::Tsify; use uuid::Uuid; use wasm_bindgen::prelude::*; -use sp_client::spclient::SpWallet; +use sp_client::{bitcoin::{hashes::Hash, OutPoint, Txid}, spclient::SpWallet}; use crate::pcd::Member; @@ -11,7 +11,7 @@ use crate::pcd::Member; #[tsify(into_wasm_abi, from_wasm_abi)] pub struct Device { sp_wallet: SpWallet, - pairing_process_uuid: Option, + pairing_process_commitment: Option, paired_member: Option, } @@ -19,7 +19,7 @@ impl Device { pub fn new(sp_wallet: SpWallet) -> Self { Self { sp_wallet, - pairing_process_uuid: None, + pairing_process_commitment: None, paired_member: None, } } @@ -32,16 +32,26 @@ impl Device { &mut self.sp_wallet } + pub fn is_linking(&self) -> bool { + match self.pairing_process_commitment { + Some(ref value) => value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0), + None => false, + } + } + pub fn is_linked(&self) -> bool { - self.pairing_process_uuid.is_some() + match self.pairing_process_commitment { + Some(ref value) => !value.as_raw_hash().as_byte_array().iter().all(|&b| b == 0), + None => false, + } } - pub fn get_process_uuid(&self) -> Option { - self.pairing_process_uuid.clone() + pub fn get_process_commitment(&self) -> Option { + self.pairing_process_commitment.clone() } - pub fn pair(&mut self, uuid: Uuid, member: Member) { - self.pairing_process_uuid = Some(uuid.to_string()); + pub fn pair(&mut self, commitment_tx: Txid, member: Member) { + self.pairing_process_commitment = Some(commitment_tx); self.paired_member = Some(member); } diff --git a/src/pcd.rs b/src/pcd.rs index 97e57f2..1cc64fa 100644 --- a/src/pcd.rs +++ b/src/pcd.rs @@ -1,4 +1,4 @@ -use std::{collections::{HashMap, HashSet}, str::FromStr}; +use std::{collections::HashSet, str::FromStr}; use anyhow::{Result, Error}; use aes_gcm::{aead::{Aead, Payload}, AeadCore, Aes256Gcm, KeyInit}; @@ -69,61 +69,68 @@ impl AnkPcdHash { } pub trait Pcd<'a>: Serialize + Deserialize<'a> { - fn hash(&self) -> AnkPcdHash { + fn tagged_hash(&self) -> AnkPcdHash { AnkPcdHash::from_value(&self.to_value()) } fn encrypt_fields(&self, fields2keys: &mut Map, fields2cipher: &mut Map) -> Result<()> { let as_value = self.to_value(); - let as_map = as_value.as_object().unwrap(); + let as_map = as_value.as_object().ok_or_else(|| Error::msg("Expected object"))?; let mut rng = thread_rng(); - for (key, value) in as_map { - let aes_key: [u8; 32] = Aes256Gcm::generate_key(&mut rng).into(); - let nonce: [u8; 12] = Aes256Gcm::generate_nonce(&mut rng).into(); - fields2keys.insert(key.to_owned(), Value::String(aes_key.to_lower_hex_string())); - let encryption = Aes256Gcm::new(&aes_key.into()); + for (field, value) in as_map { + let aes_key = Aes256Gcm::generate_key(&mut rng); + let nonce = Aes256Gcm::generate_nonce(&mut rng); + fields2keys.insert(field.to_owned(), Value::String(aes_key.to_lower_hex_string())); + + let encrypt_eng = Aes256Gcm::new(&aes_key); let value_string = value.to_string(); let payload = Payload { msg: value_string.as_bytes(), aad: AAD, }; - let cipher = encryption.encrypt(&nonce.into(), payload) - .map_err(|e| Error::msg(format!("{}", e)))?; + let cipher = encrypt_eng.encrypt(&nonce, payload) + .map_err(|e| Error::msg(format!("Encryption failed for field {}: {}", field, e)))?; let mut res = Vec::with_capacity(nonce.len() + cipher.len()); res.extend_from_slice(&nonce); res.extend_from_slice(&cipher); - fields2cipher.insert(key.to_owned(), Value::String(res.to_lower_hex_string())); + fields2cipher.insert(field.to_owned(), Value::String(res.to_lower_hex_string())); } Ok(()) } - fn decrypt_fields(&mut self, fields2keys: &Map) -> Result<()> { - let as_value = self.to_value(); - let as_map = as_value.as_object().unwrap(); - for (key, value) in as_map { - if let Some(aes_key) = fields2keys.get(key) { - let mut nonce = [0u8; 12]; - let mut key_buf = [0u8; 32]; - key_buf.copy_from_slice(&Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?); - let decrypt = Aes256Gcm::new(&key_buf.into()); - let raw_cipher = Vec::from_hex(&value.to_string().trim_matches('\"'))?; - nonce.copy_from_slice(&raw_cipher[..12]); + fn decrypt_fields(&self, fields2keys: &Map, fields2plain: &mut Map) -> Result<()> { + let value = self.to_value(); + let map = value.as_object().unwrap(); + + for (field, encrypted_value) in map.iter() { + if let Some(aes_key) = fields2keys.get(field) { + + let key_buf = Vec::from_hex(&aes_key.to_string().trim_matches('\"'))?; + + let decrypt_eng = Aes256Gcm::new(key_buf.as_slice().into()); + + let raw_cipher = Vec::from_hex(&encrypted_value.as_str().ok_or_else(|| Error::msg("Expected string"))?.trim_matches('\"'))?; + + if raw_cipher.len() < 28 { + return Err(Error::msg(format!("Invalid ciphertext length for field {}", field))); + } + let payload = Payload { msg: &raw_cipher[12..], aad: AAD, }; - let plain = decrypt.decrypt(&nonce.into(), payload) - .map_err(|_| Error::msg(format!("Failed to decrypt field {}", key)))?; - self.to_value() - .as_object_mut() - .unwrap() - .insert(key.to_owned(), Value::String(plain.to_lower_hex_string())); + + let plain = decrypt_eng.decrypt(raw_cipher[..12].into(), payload) + .map_err(|_| Error::msg(format!("Failed to decrypt field {}", field)))?; + let decrypted_value: String = String::from_utf8(plain)?; + + fields2plain.insert(field.to_owned(), Value::String(decrypted_value)); } else { - continue; + fields2plain.insert(field.to_owned(), Value::Null); } } @@ -176,8 +183,51 @@ pub struct RoleDefinition { pub validation_rules: Vec, } -// #[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -// #[tsify(into_wasm_abi, from_wasm_abi)] -// pub struct Roles { -// pub roles: HashMap -// } +pub fn compare_maps(map1: &Map, map2: &Map) -> bool { + // First, check if both maps have the same keys + if map1.keys().collect::>() != map2.keys().collect::>() { + return false; + } + + // Then, check if the corresponding values have the same type + for key in map1.keys() { + let value1 = map1.get(key).unwrap(); + let value2 = map2.get(key).unwrap(); + + if !compare_values(value1, value2) { + return false; + } + } + + true +} + +fn compare_values(value1: &Value, value2: &Value) -> bool { + if value1.is_null() && value2.is_null() { + return true; + } else if value1.is_boolean() && value2.is_boolean() { + return true; + } else if value1.is_number() && value2.is_number() { + return true; + } else if value1.is_string() && value2.is_string() { + return true; + } else if value1.is_array() && value2.is_array() { + return compare_arrays(value1.as_array().unwrap(), value2.as_array().unwrap()); + } else if value1.is_object() && value2.is_object() { + // Recursive comparison for nested objects + return compare_maps(value1.as_object().unwrap(), value2.as_object().unwrap()); + } else { + return false; + } +} + +fn compare_arrays(array1: &Vec, array2: &Vec) -> bool { + // Compare the type of each element in the arrays + for (elem1, elem2) in array1.iter().zip(array2.iter()) { + if !compare_values(elem1, elem2) { + return false; + } + } + + true +} diff --git a/src/prd.rs b/src/prd.rs index 9066900..9362da8 100644 --- a/src/prd.rs +++ b/src/prd.rs @@ -5,81 +5,29 @@ use anyhow::{Result, Error}; use serde::{Serialize, Deserialize}; use serde_json::{Map, Value}; +use sp_client::bitcoin::hex::FromHex; use sp_client::bitcoin::secp256k1::SecretKey; -use sp_client::bitcoin::XOnlyPublicKey; +use sp_client::bitcoin::{OutPoint, XOnlyPublicKey}; use sp_client::silentpayments::utils::SilentPaymentAddress; use sp_client::spclient::SpWallet; -use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; use tsify::Tsify; -use uuid::Uuid; -use crate::pcd::{AnkPcdHash, Member}; -use crate::signature::{Proof}; +use crate::pcd::{AnkPcdHash, Member, Pcd}; +use crate::signature::{AnkHash, AnkMessageHash, Proof}; -#[derive(Debug, Default, Clone, Serialize, Deserialize, Tsify)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub enum PrdType { #[default] None, Message, - Init, // Create a new process Update, // Update an existing process List, // request a list of items Response, Confirm, -} - -sha256t_hash_newtype! { - pub struct AnkValidationYesTag = hash_str("4nk/yes"); - - #[hash_newtype(forward)] - pub struct AnkValidationYesHash(_); -} - -impl AnkValidationYesHash { - pub fn from_value(value: &Value) -> Self { - let mut eng = AnkValidationYesHash::engine(); - eng.input(value.to_string().as_bytes()); - AnkValidationYesHash::from_engine(eng) - } - - pub fn from_map(map: &Map) -> Self { - let value = Value::Object(map.clone()); - let mut eng = AnkValidationYesHash::engine(); - eng.input(value.to_string().as_bytes()); - AnkValidationYesHash::from_engine(eng) - } -} - -sha256t_hash_newtype! { - pub struct AnkValidationNoTag = hash_str("4nk/no"); - - #[hash_newtype(forward)] - pub struct AnkValidationNoHash(_); -} - -impl AnkValidationNoHash { - pub fn from_value(value: &Value) -> Self { - let mut eng = AnkValidationNoHash::engine(); - eng.input(value.to_string().as_bytes()); - AnkValidationNoHash::from_engine(eng) - } - - pub fn from_map(map: &Map) -> Self { - let value = Value::Object(map.clone()); - let mut eng = AnkValidationNoHash::engine(); - eng.input(value.to_string().as_bytes()); - AnkValidationNoHash::from_engine(eng) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] -pub struct ValidationToken { - member: Member, - message: [u8; 32], - sigs: Vec, // User must sign with the requested number of devices + TxProposal, // Send a psbt asking for recipient signature, used for login not sure about other use cases } sha256t_hash_newtype! { @@ -104,45 +52,78 @@ impl AnkPrdHash { } } -#[derive(Debug, Clone, Serialize, Deserialize, Tsify)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Tsify)] #[tsify(into_wasm_abi, from_wasm_abi)] #[allow(non_camel_case_types)] pub struct Prd { pub prd_type: PrdType, - pub process_uuid: String, // stringification of Uuid + pub root_commitment: String, pub sender: String, pub keys: Map, // key is a key in pcd, value is the key to decrypt it - pub validation_tokens: Vec, - pub pcd_commitment: String, + pub validation_tokens: Vec, + pub payload: String, // Payload depends on the actual type pub proof: Option, // This must be None up to the creation of the network message } impl Prd { - pub fn new( - prd_type: PrdType, - uuid: Uuid, - sender: String, + pub fn new_update( + root_commitment: OutPoint, + sender: String, // Should take Member as argument encrypted_pcd: Map, keys: Map - ) -> Result { - let res = Self { - prd_type, - process_uuid: uuid.to_string(), + ) -> Self { + Self { + prd_type: PrdType::Update, + root_commitment: root_commitment.to_string(), sender, validation_tokens: vec![], keys, - pcd_commitment: AnkPcdHash::from_map(&encrypted_pcd).to_string(), + payload: Value::Object(encrypted_pcd).to_string(), proof: None, - }; - - Ok(res) + } } - pub fn extract_from_message(plain: &[u8], commitment: [u8; 32]) -> Result { + pub fn new_response( + root_commitment: OutPoint, + sender: String, + validation_token: Proof, + pcd_commitment: AnkPcdHash, + ) -> Self { + Self { + prd_type: PrdType::Response, + root_commitment: root_commitment.to_string(), + sender, + validation_tokens: vec![validation_token], + keys: Map::new(), + payload: pcd_commitment.to_string(), + proof: None, + } + } + + pub fn new_confirm( + root_commitment: OutPoint, + sender: Member, + pcd_commitment: AnkPcdHash, + ) -> Self { + Self { + prd_type: PrdType::Confirm, + root_commitment: root_commitment.to_string(), + sender: serde_json::to_string(&sender).unwrap(), + validation_tokens: vec![], + keys: Map::new(), + payload: pcd_commitment.to_string(), + proof: None, + } + } + + + fn _extract_from_message(plain: &[u8], commitment: Option<&AnkPrdHash>) -> Result { let prd: Prd = serde_json::from_slice(plain)?; - // check that the hash of the prd is consistent with what's commited in the op_return - if prd.create_commitment().to_byte_array() != commitment { - return Err(anyhow::Error::msg("Received prd is not what was commited in the transaction")); + if let Some(commitment) = commitment { + // check that the hash of the prd is consistent with what's commited in the op_return + if prd.create_commitment() != *commitment { + return Err(anyhow::Error::msg("Received prd is not what was commited in the transaction")); + } } // check that the proof is consistent let sender: Member = serde_json::from_str(&prd.sender)?; @@ -167,15 +148,17 @@ impl Prd { } proof.verify()?; } + // check that the commitment outpoint is valid, just in case + OutPoint::from_str(&prd.root_commitment)?; Ok(prd) } - pub fn add_validation_token(&mut self, validation_token: ValidationToken) -> Result<()> { - match self.prd_type { - PrdType::Confirm => self.validation_tokens.push(validation_token), - _ => return Err(Error::msg("This Prd type doesn't allow validation tokens")) - } - Ok(()) + pub fn extract_from_message(plain: &[u8]) -> Result { + Self::_extract_from_message(plain, None) + } + + pub fn extract_from_message_with_commitment(plain: &[u8], commitment: &AnkPrdHash) -> Result { + Self::_extract_from_message(plain, Some(commitment)) } pub fn filter_keys(&mut self, to_keep: HashSet) { @@ -193,14 +176,22 @@ impl Prd { let mut to_commit = self.clone(); to_commit.keys = Map::new(); to_commit.proof = None; + + if to_commit.payload.len() != 64 && Vec::from_hex(&to_commit.payload).is_err() { + to_commit.payload = Value::from_str(&to_commit.payload).unwrap().tagged_hash().to_string(); + } + AnkPrdHash::from_value(&to_commit.to_value()) } /// Generate the signed proof and serialize to send over the network pub fn to_network_msg(&self, sp_wallet: &SpWallet) -> Result { let spend_sk: SecretKey = sp_wallet.get_client().get_spend_key().try_into()?; - let to_sign = self.to_string(); // we sign the whole prd, incl the keys, for each recipient - let proof = Proof::new(to_sign.as_bytes(), spend_sk); + let to_sign = self.clone(); // we sign the whole prd, incl the keys, for each recipient + + let message_hash = AnkHash::Message(AnkMessageHash::from_message(to_sign.to_string().as_bytes())); + + let proof = Proof::new(message_hash, spend_sk); let mut res = self.clone(); res.proof = Some(proof); diff --git a/src/signature.rs b/src/signature.rs index 468bcf0..d58ecd6 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -6,11 +6,23 @@ use sp_client::bitcoin::secp256k1::schnorr::Signature; use sp_client::bitcoin::secp256k1::{Keypair, Message, SecretKey, XOnlyPublicKey}; use sp_client::bitcoin::hashes::{sha256t_hash_newtype, Hash, HashEngine}; +use crate::pcd::AnkPcdHash; + sha256t_hash_newtype! { pub struct AnkMessageTag = hash_str("4nk/Message"); #[hash_newtype(forward)] pub struct AnkMessageHash(_); + + pub struct AnkValidationYesTag = hash_str("4nk/yes"); + + #[hash_newtype(forward)] + pub struct AnkValidationYesHash(_); + + pub struct AnkValidationNoTag = hash_str("4nk/no"); + + #[hash_newtype(forward)] + pub struct AnkValidationNoHash(_); } impl AnkMessageHash { @@ -21,17 +33,48 @@ impl AnkMessageHash { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +impl AnkValidationYesHash { + pub fn from_commitment(commitment: AnkPcdHash) -> Self { + let mut eng = AnkValidationYesHash::engine(); + eng.input(&commitment.to_byte_array()); + AnkValidationYesHash::from_engine(eng) + } +} + +impl AnkValidationNoHash { + pub fn from_commitment(commitment: AnkPcdHash) -> Self { + let mut eng = AnkValidationNoHash::engine(); + eng.input(&commitment.to_byte_array()); + AnkValidationNoHash::from_engine(eng) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum AnkHash { + Message(AnkMessageHash), + ValidationYes(AnkValidationYesHash), + ValidationNo(AnkValidationNoHash), +} + +impl AnkHash { + pub fn to_byte_array(&self) -> [u8; 32] { + match self { + AnkHash::Message(hash) => hash.to_byte_array(), + AnkHash::ValidationYes(hash) => hash.to_byte_array(), + AnkHash::ValidationNo(hash) => hash.to_byte_array(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Proof { signature: Signature, - message: AnkMessageHash, + message: AnkHash, key: XOnlyPublicKey } impl Proof { - pub fn new(message: &[u8], signing_key: SecretKey) -> Self { - let message_hash = AnkMessageHash::from_message(message); - + pub fn new(message_hash: AnkHash, signing_key: SecretKey) -> Self { let secp = Secp256k1::signing_only(); let keypair = Keypair::from_secret_key(&secp, &signing_key); From 959f8a3058474980369b27294b9770dc2b1faaa4 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Mon, 23 Sep 2024 16:14:58 +0200 Subject: [PATCH 13/13] Rm uuid dependency --- Cargo.toml | 1 - src/device.rs | 1 - src/lib.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0521f2e..3fc049f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,4 @@ serde_json = { version = "1.0.108", features = ["preserve_order"]} # sp_client = { path = "../sp-client" } sp_client = { git = "https://github.com/Sosthene00/sp-client.git", branch = "master" } tsify = { git = "https://github.com/Sosthene00/tsify", branch = "next" } -uuid = { version = "1.10.0", features = ["v4"] } wasm-bindgen = "0.2.91" diff --git a/src/device.rs b/src/device.rs index 4403170..76c1b01 100644 --- a/src/device.rs +++ b/src/device.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; use tsify::Tsify; -use uuid::Uuid; use wasm_bindgen::prelude::*; use sp_client::{bitcoin::{hashes::Hash, OutPoint, Txid}, spclient::SpWallet}; diff --git a/src/lib.rs b/src/lib.rs index 82316b7..4e56d23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,6 @@ use std::sync::{Mutex, MutexGuard}; use std::fmt::Debug; pub use sp_client; -pub use uuid; pub use log; pub use aes_gcm;