From fc73e313eabdd6e48cfa0e92b7add477f418dfc7 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 14 Apr 2026 01:47:31 +0200 Subject: [PATCH] documentation and new modules --- rust/src/key_verification/mod.rs | 29 ++ rust/src/key_verification/traits.rs | 1 + rust/src/lib.rs | 2 + rust/src/passwordless_recovery/mod.rs | 1 + rust/src/passwordless_recovery/traits.rs | 0 rust/src/twonly/log/mod.rs | 2 +- rust/src/user_discovery/README.md | 78 +++++- rust/src/user_discovery/mod.rs | 337 +++++++++++++---------- 8 files changed, 304 insertions(+), 146 deletions(-) create mode 100644 rust/src/key_verification/mod.rs create mode 100644 rust/src/key_verification/traits.rs create mode 100644 rust/src/passwordless_recovery/mod.rs create mode 100644 rust/src/passwordless_recovery/traits.rs diff --git a/rust/src/key_verification/mod.rs b/rust/src/key_verification/mod.rs new file mode 100644 index 0000000..c347d67 --- /dev/null +++ b/rust/src/key_verification/mod.rs @@ -0,0 +1,29 @@ +use crate::key_verification::traits::KeyVerificationStore; + +pub mod traits; + +pub struct KeyVerificationConfig { + /// The link prefix for the qr code which should be registered as a deeplink on Android and a universal link on iOS + deeplink_prefix: String, +} + +struct KeyVerification { + store: Store, + config: KeyVerificationConfig, +} + +impl KeyVerification { + pub fn new(store: Store, config: KeyVerificationConfig) -> KeyVerification { + Self { store, config } + } + + /// Generates the a string which should be displayed in the UI so others can scan it. + pub fn generate_qr_code_data() -> String { + todo!(); + } + + /// Generates the a string which should be displayed in the UI so others can scan it. + pub fn handle_qr_code_data() -> String { + todo!(); + } +} diff --git a/rust/src/key_verification/traits.rs b/rust/src/key_verification/traits.rs new file mode 100644 index 0000000..1fc4c49 --- /dev/null +++ b/rust/src/key_verification/traits.rs @@ -0,0 +1 @@ +pub trait KeyVerificationStore {} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 71cb3df..0b278cd 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,3 +1,5 @@ mod frb_generated; +mod key_verification; +mod passwordless_recovery; mod twonly; mod user_discovery; diff --git a/rust/src/passwordless_recovery/mod.rs b/rust/src/passwordless_recovery/mod.rs new file mode 100644 index 0000000..3ecd121 --- /dev/null +++ b/rust/src/passwordless_recovery/mod.rs @@ -0,0 +1 @@ +mod traits; diff --git a/rust/src/passwordless_recovery/traits.rs b/rust/src/passwordless_recovery/traits.rs new file mode 100644 index 0000000..e69de29 diff --git a/rust/src/twonly/log/mod.rs b/rust/src/twonly/log/mod.rs index dc1bdf2..528f989 100644 --- a/rust/src/twonly/log/mod.rs +++ b/rust/src/twonly/log/mod.rs @@ -28,7 +28,7 @@ pub(crate) fn init_tracing(logs_dir: &std::path::Path) { fn build_writers(logs_dir: &std::path::Path) -> (NonBlocking, NonBlocking) { let file_appender = tracing_appender::rolling::RollingFileAppender::builder() .rotation(tracing_appender::rolling::Rotation::DAILY) - .filename_prefix("whitenoise") + .filename_prefix("twonly") .filename_suffix("log") .build(logs_dir) .expect("Failed to create file appender"); diff --git a/rust/src/user_discovery/README.md b/rust/src/user_discovery/README.md index 1a5368b..af442b9 100644 --- a/rust/src/user_discovery/README.md +++ b/rust/src/user_discovery/README.md @@ -1,10 +1,74 @@ +# User Discovery + +User Discovery is a feature that allows users to discover other users in a decentralized system without any central authority. It uses Shamir's Secret Sharing to securely share and discover user information. + +## Getting started + +The User Discovery module is composed of the following components: + +- **UserDiscovery** - The main struct which initializes the user discovery and provides access to the user discovery functionality. + +- **UserDiscoveryStore** - A trait which has to be implemented. It is used to store and retrieve the user discovery data. +- **UserDiscoveryUtils** - A trait which has to be implemented. It is used to perform signature verification and signing. + +```rust +use crate::user_discovery::{UserDiscovery, UserID}; +use crate::user_discovery::stores::InMemoryStore; // Replace with your persistent store +use crate::user_discovery::traits::tests::TestingUtils; // Replace with your utils + +const THRESHOLD: u8 = 2; + +// Initialize user discovery for Alice +const ALICE_ID: UserID = 1; +let alice_ud = UserDiscovery::new(InMemoryStore::default(), TestingUtils::default()).unwrap(); + +// Set threshold, user ID, and the user's public key +alice_ud.initialize_or_update(THRESHOLD, ALICE_ID, vec![0; 32]).unwrap(); + +// Initialize user discovery for Bob +const BOB_ID: UserID = 2; +let bob_ud = UserDiscovery::new(InMemoryStore::default(), TestingUtils::default()).unwrap(); +bob_ud.initialize_or_update(THRESHOLD, BOB_ID, vec![0; 32]).unwrap(); + - // **Store** promotions: - // version_id, contact_id, Option - // -> In case a contact_id deleted his account deleted or was removed - // - Remove the previous row (version_id must be increased...) - // - Create a new entry with User Discovery Recall - // -> Otherwise this promotion contains the Promotion - // \ No newline at end of file +// Simulate network communication: Alice sends her current version to Bob +let bob_received_version_from_alice = alice_ud.get_current_version().unwrap(); + +// SEND FROM ALICE TO BOB: bob_received_version_from_alice + +// Bob checks if he should request new messages +if bob_ud.should_request_new_messages(ALICE_ID, &bob_received_version_from_alice).unwrap() { + + // Bob has a old version and must now request to get the new messages + + // Bob fetches his current known version and sends it via the network to Alice + let bob_stored_alice_version = bob_ud.get_contact_version(ALICE_ID) + .unwrap() + .unwrap_or_else(|| vec![0, 0]); // Note: In practice use actual default encoded version + + // SEND FROM BOB TO ALICE: bob_stored_alice_version + + // Alice loads the new messages for Bob. These only conclude changes since the provided version. + let new_messages = alice_ud.get_new_messages(BOB_ID, &bob_stored_alice_version).unwrap(); + + // SEND FROM ALICE TO BOB: new_messages + + // Bob processes the received user discovery messages + bob_ud.handle_user_discovery_messages(ALICE_ID, new_messages).unwrap(); + + // BOB is now able to promote ALICE to his other contacts +} + +// + +// 4. Retrieve all newly discovered users and relationships +// In this example now new users where discovered, to see a more comprehensive example +// see the test in the `mod.rs` fil. +let discovered_users = bob_ud.get_all_announced_users().unwrap(); +for (user, connections) in discovered_users { + println!("Discovered User: {} (Public ID: {})", user.user_id, user.public_id); +} +``` + diff --git a/rust/src/user_discovery/mod.rs b/rust/src/user_discovery/mod.rs index 55ee1b4..89372fa 100644 --- a/rust/src/user_discovery/mod.rs +++ b/rust/src/user_discovery/mod.rs @@ -1,9 +1,8 @@ mod error; pub mod stores; -mod traits; +pub mod traits; use std::collections::HashMap; - use blahaj::{Share, Sharks}; use postcard::{from_bytes, to_allocvec}; use prost::Message; @@ -19,6 +18,8 @@ pub use traits::UserDiscoveryStore; static TRANSMITTED_NETWORK_BYTES: std::sync::OnceLock> = std::sync::OnceLock::new(); +/// Type of the user id, this must be consistent with the user id defined in +/// the types.proto pub type UserID = i64; include!(concat!(env!("OUT_DIR"), "/_.rs")); @@ -41,20 +42,11 @@ struct UserDiscoveryConfig { user_id: UserID, } -impl Default for UserDiscoveryConfig { - fn default() -> Self { - Self { - threshold: 2, - total_number_of_shares: 255, - announcement_version: 0, - promotion_version: 0, - verification_shares: vec![], - public_id: 0, - user_id: 0, - } - } -} - +/// +/// The main struct to access the user discovery functionality. +/// +/// As generic values it requires a UserDiscoveryStore and a UserDiscoveryUtils. +/// pub struct UserDiscovery where Store: UserDiscoveryStore, @@ -65,10 +57,26 @@ where } impl UserDiscovery { + /// Creates a new instance of the user discovery. pub fn new(store: Store, utils: Utils) -> Result { Ok(Self { store, utils }) } + /// Initializes or updates the user discovery. + /// + /// This function will generate new verification shares and update the config. + /// + /// # Arguments + /// + /// * `threshold` - The number of required shares to get the secret + /// * `user_id` - The owner's user id + /// * `public_key` - The owner's public key + /// + /// # Returns + /// + /// * `Ok(())` - If the user discovery was initialized or updated successfully + /// * `Err(UserDiscoveryError)` - If the user discovery was not initialized or updated successfully + /// pub fn initialize_or_update( &self, threshold: u8, @@ -109,6 +117,167 @@ impl UserDiscovery)` - The current version of the user discovery + /// * `Err(UserDiscoveryError)` - If there where errors in the store. + /// + pub fn get_current_version(&self) -> Result> { + let config = self.get_config()?; + Ok(UserDiscoveryVersion { + announcement: config.announcement_version, + promotion: config.promotion_version, + } + .encode_to_vec()) + } + + /// + /// Returns all users discovery though the user discovery and there relations + /// + /// # Returns + /// + /// * `Ok(HashMap)>>)` - All connections the user has discovered + /// * `Err(UserDiscoveryError)` - If there where erros in the store. + /// + pub fn get_all_announced_users( + &self, + ) -> Result)>>> { + self.store.get_all_announced_users() + } + + /// + /// Returns all new user discovery messages for the provided contact and his current version. + /// + /// # Arguments + /// + /// * `contact_id` - The contact id of the user + /// * `received_version` - The version of the user discovery the contact has received so far + /// + /// # Returns + /// + /// * `Ok(Vec>)` - The new user discovery messages + /// * `Err(UserDiscoveryError)` - If there where errors in the store or if the received version is invalid. + /// + pub fn get_new_messages( + &self, + contact_id: UserID, + received_version: &[u8], + ) -> Result>> { + let mut messages = vec![]; + let received_version = UserDiscoveryVersion::decode(received_version)?; + + let config = self.get_config()?; + let version = Some(UserDiscoveryVersion { + announcement: config.announcement_version, + promotion: config.promotion_version, + }); + + if received_version.announcement < config.announcement_version { + tracing::info!("New announcement message available for {}", contact_id); + + let announcement_share = self.store.get_share_for_contact(contact_id)?; + + let user_discovery_announcement = Some(UserDiscoveryAnnouncement { + public_id: config.public_id, + threshold: config.threshold as u32, + announcement_share, + verification_shares: config.verification_shares, + }); + + messages.push( + UserDiscoveryMessage { + user_discovery_announcement, + version, + ..Default::default() + } + .encode_to_vec(), + ); + } + if received_version.promotion < config.promotion_version { + tracing::info!("New promotion message available for user {}", contact_id); + let promoting_messages = self + .store + .get_own_promotions_after_version(received_version.promotion)?; + messages.extend_from_slice(&promoting_messages); + } + #[cfg(test)] + { + let mut count = TRANSMITTED_NETWORK_BYTES.get().unwrap().lock().unwrap(); + for message in &messages { + *count += message.len(); + } + } + Ok(messages) + } + + /// + /// Checks if the provided user has new announcements and a request of update should be send. + /// + /// # Arguments + /// + /// * `contact_id` - The contact id of the user + /// * `version` - The current version of the user discovery from the contact + /// + /// # Returns + /// + /// * `Ok(bool)` - True if the user has new announcements + /// * `Err(UserDiscoveryError)` - If there where errors in the store or if the received version is invalid. + /// + pub fn should_request_new_messages(&self, contact_id: UserID, version: &[u8]) -> Result { + let received_version = UserDiscoveryVersion::decode(version)?; + let stored_version = match self.store.get_contact_version(contact_id)? { + Some(buf) => UserDiscoveryVersion::decode(buf.as_slice())?, + None => UserDiscoveryVersion { + announcement: 0, + promotion: 0, + }, + }; + Ok(received_version.announcement > stored_version.announcement + || received_version.promotion > stored_version.promotion) + } + + pub(crate) fn get_contact_version(&self, contact_id: UserID) -> Result>> { + self.store.get_contact_version(contact_id) + } + + /// Returns the latest version for this discovery. + /// Before calling this function the application must sure that contact_id is qualified to be announced. + pub fn handle_user_discovery_messages( + &self, + contact_id: UserID, + messages: Vec>, + ) -> Result<()> { + for message in messages { + let message = UserDiscoveryMessage::decode(message.as_slice())?; + let Some(version) = message.version else { + continue; + }; + + if let Some(uda) = message.user_discovery_announcement { + self.handle_user_discovery_announcement(contact_id, uda)?; + } else if let Some(udp) = message.user_discovery_promotion { + self.handle_user_discovery_promotion(contact_id, udp)?; + } else { + tracing::warn!("Got unknown user discovery messaging. Ignoring it."); + continue; + } + + self.store + .set_contact_version(contact_id, version.encode_to_vec())?; + } + + Ok(()) + } + fn setup_announcements( &self, config: &UserDiscoveryConfig, @@ -164,128 +333,6 @@ impl UserDiscovery Result> { - let config = self.get_config()?; - Ok(UserDiscoveryVersion { - announcement: config.announcement_version, - promotion: config.promotion_version, - } - .encode_to_vec()) - } - - /// Returns all users discovery though the user discovery and there relations - pub fn get_all_announced_users( - &self, - ) -> Result)>>> { - self.store.get_all_announced_users() - } - - /// Returns all new user discovery messages for the provided contact - pub fn get_new_messages( - &self, - contact_id: UserID, - received_version: &[u8], - ) -> Result>> { - let mut messages = vec![]; - let received_version = UserDiscoveryVersion::decode(received_version)?; - - let config = self.get_config()?; - let version = Some(UserDiscoveryVersion { - announcement: config.announcement_version, - promotion: config.promotion_version, - }); - - if received_version.announcement < config.announcement_version { - tracing::info!("New announcement message available for {}", contact_id); - - let announcement_share = self.store.get_share_for_contact(contact_id)?; - - let user_discovery_announcement = Some(UserDiscoveryAnnouncement { - public_id: config.public_id, - threshold: config.threshold as u32, - announcement_share, - verification_shares: config.verification_shares, - }); - - messages.push( - UserDiscoveryMessage { - user_discovery_announcement, - version, - ..Default::default() - } - .encode_to_vec(), - ); - - // messages.push(value); - } - if received_version.promotion < config.promotion_version { - tracing::info!("New promotion message available for user {}", contact_id); - let promoting_messages = self - .store - .get_own_promotions_after_version(received_version.promotion)?; - messages.extend_from_slice(&promoting_messages); - } - #[cfg(test)] - { - let mut count = TRANSMITTED_NETWORK_BYTES.get().unwrap().lock().unwrap(); - for message in &messages { - *count += message.len(); - } - } - // static TRANSMITTED_NETWORK_BYTES: std::sync::OnceLock = std::sync::OnceLock::new(); - Ok(messages) - } - - /// Checks if the provided user has new announcements. - /// In this case the user should be requested to send there updates. - pub fn should_request_new_messages(&self, contact_id: UserID, version: &[u8]) -> Result { - let received_version = UserDiscoveryVersion::decode(version)?; - let stored_version = match self.store.get_contact_version(contact_id)? { - Some(buf) => UserDiscoveryVersion::decode(buf.as_slice())?, - None => UserDiscoveryVersion { - announcement: 0, - promotion: 0, - }, - }; - // tracing::debug!("{received_version:?} > {stored_version:?}"); - Ok(received_version.announcement > stored_version.announcement - || received_version.promotion > stored_version.promotion) - } - - pub(crate) fn get_contact_version(&self, contact_id: UserID) -> Result>> { - self.store.get_contact_version(contact_id) - } - - /// Returns the latest version for this discovery. - /// Before calling this function the application must sure that contact_id is qualified to be announced. - pub fn handle_user_discovery_messages( - &self, - contact_id: UserID, - messages: Vec>, - ) -> Result<()> { - for message in messages { - let message = UserDiscoveryMessage::decode(message.as_slice())?; - let Some(version) = message.version else { - continue; - }; - - if let Some(uda) = message.user_discovery_announcement { - self.handle_user_discovery_announcement(contact_id, uda)?; - } else if let Some(udp) = message.user_discovery_promotion { - self.handle_user_discovery_promotion(contact_id, udp)?; - } else { - tracing::warn!("Got unknown user discovery messaging. Ignoring it."); - continue; - } - - self.store - .set_contact_version(contact_id, version.encode_to_vec())?; - } - - Ok(()) - } - fn handle_user_discovery_announcement( &self, contact_id: UserID, @@ -505,6 +552,20 @@ impl UserDiscovery Self { + Self { + threshold: 2, + total_number_of_shares: 255, + announcement_version: 0, + promotion_version: 0, + verification_shares: vec![], + public_id: 0, + user_id: 0, + } + } +} + #[cfg(test)] mod tests {