mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-18 14:22:53 +00:00
documentation and new modules
This commit is contained in:
parent
51f51f768b
commit
fc73e313ea
8 changed files with 304 additions and 146 deletions
29
rust/src/key_verification/mod.rs
Normal file
29
rust/src/key_verification/mod.rs
Normal 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!();
|
||||
}
|
||||
}
|
||||
1
rust/src/key_verification/traits.rs
Normal file
1
rust/src/key_verification/traits.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub trait KeyVerificationStore {}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
mod frb_generated;
|
||||
mod key_verification;
|
||||
mod passwordless_recovery;
|
||||
mod twonly;
|
||||
mod user_discovery;
|
||||
|
|
|
|||
1
rust/src/passwordless_recovery/mod.rs
Normal file
1
rust/src/passwordless_recovery/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
mod traits;
|
||||
0
rust/src/passwordless_recovery/traits.rs
Normal file
0
rust/src/passwordless_recovery/traits.rs
Normal 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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue