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);