documentation and new modules

This commit is contained in:
otsmr 2026-04-14 01:47:31 +02:00
parent 51f51f768b
commit fc73e313ea
8 changed files with 304 additions and 146 deletions

View file

@ -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: KeyVerificationStore> {
store: Store,
config: KeyVerificationConfig,
}
impl<Store: KeyVerificationStore> KeyVerification<Store> {
pub fn new(store: Store, config: KeyVerificationConfig) -> KeyVerification<Store> {
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!();
}
}

View file

@ -0,0 +1 @@
pub trait KeyVerificationStore {}

View file

@ -1,3 +1,5 @@
mod frb_generated;
mod key_verification;
mod passwordless_recovery;
mod twonly;
mod user_discovery;

View file

@ -0,0 +1 @@
mod traits;

View file

View file

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

View file

@ -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<UserDiscoveryMessage>
// -> 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
//
// 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
}
// <Involve more users>
// 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);
}
```

View file

@ -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::Mutex<usize>> =
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<Store, Utils>
where
Store: UserDiscoveryStore,
@ -65,10 +57,26 @@ where
}
impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store, Utils> {
/// Creates a new instance of the user discovery.
pub fn new(store: Store, utils: Utils) -> Result<Self> {
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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
Ok(())
}
///
/// Returns the current version of the owner's user discovery state.
///
/// The version is incremented every time the user discovery is initialized or updated.
/// It should be send contacts which participate in the user discovery, so they can
/// check if there is new data available for them using the `get_new_messages` function
/// on there side.
///
///
/// # Returns
///
/// * `Ok(Vec<u8>)` - The current version of the user discovery
/// * `Err(UserDiscoveryError)` - If there where errors in the store.
///
pub fn get_current_version(&self) -> Result<Vec<u8>> {
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<AnnouncedUser, Vec<(UserID, Option<i64>)>>)` - All connections the user has discovered
/// * `Err(UserDiscoveryError)` - If there where erros in the store.
///
pub fn get_all_announced_users(
&self,
) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
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<Vec<u8>>)` - 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<Vec<Vec<u8>>> {
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<bool> {
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<Option<Vec<u8>>> {
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<Vec<u8>>,
) -> 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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
Ok(from_bytes(&self.store.get_config()?)?)
}
/// Returns the current version of the user discovery.
pub fn get_current_version(&self) -> Result<Vec<u8>> {
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<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
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<Vec<Vec<u8>>> {
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<usize> = 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<bool> {
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<Option<Vec<u8>>> {
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<Vec<u8>>,
) -> 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<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
}
}
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,
}
}
}
#[cfg(test)]
mod tests {