twonly-app/rust/src/user_discovery.rs

1114 lines
40 KiB
Rust

/*
* Copyright (c) 2026, Tobias Müller git@tsmr.eu
*
*/
use std::collections::HashSet;
use crate::api::server::Server;
use crate::bridge::api::ServerResult;
use crate::context::Context;
use std::path::PathBuf;
use std::sync::Arc;
use blahaj::{Share, Sharks};
use libsignal_protocol::{IdentityKey, IdentityKeyPair};
use prost::Message;
use rand::SeedableRng;
use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use crate::database::signal::Database;
use crate::keys::KeyManager;
use crate::error::{Result, TwonlyError};
use crate::user_discovery::user_discovery_message::{UserDiscoveryAnnouncement, UserDiscoveryPromotion};
use crate::user_discovery::user_discovery_message::user_discovery_promotion::AnnouncementShareDecrypted;
use crate::user_discovery::user_discovery_message::user_discovery_promotion::announcement_share_decrypted::SignedData;
/// 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"), "/user_discovery.rs"));
#[derive(Clone, sqlx::FromRow)]
pub struct OtherPromotion {
pub promotion_id: u32,
pub public_id: i64,
pub from_contact_id: UserID,
pub threshold: u8,
pub announcement_share: Vec<u8>,
pub public_key_verified_timestamp: Option<i64>,
}
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
pub struct AnnouncedUser {
pub user_id: UserID,
pub public_key: Vec<u8>,
pub public_id: i64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
struct UserDiscoveryConfig {
/// The number of required shares to get the secret
threshold: u8,
/// Currently limited to <= 255 as GF 256 is used
total_number_of_shares: u8,
/// Version of announcements
announcement_version: u32,
/// Version of promotions
promotion_version: u32,
/// This is a random public_id associated with a single announcement.
public_id: i64,
/// Verification shares
verification_shares: Vec<Vec<u8>>,
// The users' id:
user_id: UserID,
// If others user should promote the promotion to other users
share_promotion: bool,
}
pub struct UserDiscovery {
config_path: PathBuf,
key_manager: Arc<Mutex<KeyManager>>,
rust_db: Arc<RwLock<Arc<Database>>>,
config_lock: Arc<Mutex<()>>,
}
impl UserDiscovery {
/// Refreshes server-owned data for announcements after the API connection
/// has authenticated. Cryptographic discovery state remains owned here;
/// the API runtime only invokes this lifecycle hook.
pub async fn on_connected(&self, ctx: &Arc<Context>) -> Result<()> {
let database = ctx.app_db.read().await.clone();
let announcements = sqlx::query!(
r#"SELECT announced_user_id, announced_public_key
FROM user_discovery_announced_users WHERE username IS NULL"#
)
.fetch_all(&database.pool)
.await?;
for announcement in announcements {
let user = match Server::get_user_by_id(ctx, announcement.announced_user_id).await? {
ServerResult::Ok(user) => user,
ServerResult::ErrorCode(code) => {
tracing::warn!(
user_id = announcement.announced_user_id,
code,
"could not refresh announced user"
);
continue;
}
};
if user.public_identity_key.as_deref()
!= Some(announcement.announced_public_key.as_slice())
{
tracing::warn!(
user_id = announcement.announced_user_id,
"server returned a different identity key for announced user"
);
continue;
}
let Some(username) = user.username else {
continue;
};
let username = String::from_utf8(username)?;
sqlx::query!(
"UPDATE user_discovery_announced_users SET username = ? WHERE announced_user_id = ?",
username,
announcement.announced_user_id,
)
.execute(&database.pool)
.await?;
}
Ok(())
}
pub fn new(
data_dir: &str,
key_manager: Arc<Mutex<KeyManager>>,
rust_db: Arc<RwLock<Arc<Database>>>,
) -> Result<Self> {
Ok(Self {
config_path: PathBuf::from(data_dir).join("user_discovery_config.json"),
key_manager,
rust_db,
config_lock: Arc::default(),
})
}
pub async fn initialize_from_config(&self, ctx: &Context) -> Result<()> {
let config = crate::user_config::UserConfig::load_required_from(ctx)?;
if !config.is_user_discovery_enabled {
return Ok(());
}
let discovery_config_path =
PathBuf::from(&ctx.config.data_dir).join("user_discovery_config.json");
let settings_are_current = std::fs::read_to_string(&discovery_config_path)
.ok()
.and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok())
.is_some_and(|value| {
value.get("threshold").and_then(serde_json::Value::as_u64)
== Some(u64::from(config.user_discovery_threshold))
&& value
.get("share_promotion")
.and_then(serde_json::Value::as_bool)
== Some(config.user_discovery_share_promotion)
});
if settings_are_current {
let database = ctx.app_db.read().await.clone();
let has_shares =
sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM user_discovery_shares LIMIT 1)")
.fetch_one(&database.pool)
.await?
!= 0;
if has_shares {
return Ok(());
}
}
let key_manager = ctx.key_manager.lock().await;
let user_id = key_manager.user_id.ok_or_else(|| {
TwonlyError::Generic("cannot initialize user discovery without user ID".into())
})?;
let identity = key_manager
.signal_identity
.as_ref()
.ok_or(TwonlyError::SignalIdentityNotFound)?;
let identity = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice())
.map_err(|error| {
TwonlyError::Generic(format!("invalid Signal identity: {error}"))
})?;
let public_key = identity.identity_key().serialize().to_vec();
drop(key_manager);
let database = ctx.app_db.read().await.clone();
let mut transaction = database.pool.begin().await?;
self.initialize_or_update(
config.user_discovery_threshold,
user_id,
public_key,
config.user_discovery_share_promotion,
&mut transaction,
)
.await?;
transaction.commit().await?;
Ok(())
}
async fn sign_data(&self, input_data: &[u8]) -> Result<Vec<u8>> {
let key_manager = self.key_manager.lock().await;
let identity = key_manager
.signal_identity
.as_ref()
.ok_or_else(|| TwonlyError::UserDiscoveryStore("no Signal identity found".into()))?;
let key_pair = IdentityKeyPair::try_from(identity.identity_key_pair_structure.as_slice())
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
let mut csprng = rand::rngs::StdRng::from_os_rng();
key_pair
.private_key()
.calculate_signature_for_multipart_message(&[input_data], &mut csprng)
.map(|signature| signature.to_vec())
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))
}
async fn verify_signature(
&self,
input_data: &[u8],
public_key: &[u8],
signature: &[u8],
) -> Result<bool> {
let identity = match IdentityKey::decode(public_key) {
Ok(identity) => identity,
Err(_) => return Ok(false),
};
Ok(identity
.public_key()
.verify_signature(input_data, signature))
}
async fn verify_stored_pubkey(&self, contact_id: UserID, public_key: &[u8]) -> Result<bool> {
let database = self.rust_db.read().await.clone();
let stored = sqlx::query_scalar!(
"SELECT identity_key FROM signal_identities WHERE name = ?",
contact_id.to_string(),
)
.fetch_optional(&database.pool)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
Ok(stored.as_deref() == Some(public_key))
}
/// 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(TwonlyError)` - If the user discovery was not initialized or updated successfully
///
pub async fn initialize_or_update(
&self,
threshold: u8,
user_id: UserID,
public_key: Vec<u8>,
share_promotion: bool,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
tracing::info!("Protocols: initialize_or_update started, getting config from store");
let config = match self.read_config() {
Ok(config) => {
let mut config: UserDiscoveryConfig = serde_json::from_str(&config)?;
config.threshold = threshold;
config
}
Err(_) => UserDiscoveryConfig {
threshold,
user_id,
total_number_of_shares: 255,
..Default::default()
},
};
let public_id = rand::random();
let signed_data = SignedData {
public_id,
user_id,
public_key,
};
tracing::info!("Protocols: signing data");
let signature = self.sign_data(&signed_data.encode_to_vec()).await?;
debug_assert_eq!(threshold, config.threshold);
tracing::info!("Protocols: setting up announcements");
let verification_shares = self
.setup_announcements(&config, signed_data, signature, t)
.await?;
debug_assert_eq!(verification_shares.len(), threshold as usize - 1);
tracing::info!("Protocols: updating config in store");
{
let mut final_config = match self.read_config() {
Ok(c) => serde_json::from_str(&c)?,
Err(_) => UserDiscoveryConfig {
threshold,
user_id,
..Default::default()
},
};
final_config.public_id = public_id;
final_config.announcement_version += 1;
final_config.verification_shares = verification_shares;
final_config.share_promotion = share_promotion;
final_config.threshold = threshold;
self.write_config(&final_config)?;
}
tracing::info!("Protocols: initialize_or_update finished");
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(TwonlyError)` - If there where errors in the store.
///
pub async fn get_current_version(&self) -> Result<Vec<u8>> {
let config = self.get_config_snapshot().await?;
Ok(UserDiscoveryVersion {
announcement: config.announcement_version,
promotion: config.promotion_version,
}
.encode_to_vec())
}
///
/// 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(TwonlyError)` - If there where errors in the store or if the received version is invalid.
///
pub async fn get_new_messages(
&self,
contact_id: UserID,
received_version: &[u8],
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<Vec<Vec<u8>>> {
let mut messages = vec![];
let received_version = UserDiscoveryVersion::decode(received_version)?;
let config = self.get_config_snapshot().await?;
let version = Some(UserDiscoveryVersion {
announcement: config.announcement_version,
promotion: config.promotion_version,
});
if received_version.announcement < config.announcement_version {
let announcement_share = if let Some(share) = sqlx::query_scalar!(
"SELECT share FROM user_discovery_shares WHERE contact_id = ? LIMIT 1",
contact_id,
)
.fetch_optional(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?
{
share
} else {
let row = sqlx::query!(
"SELECT share_id, share FROM user_discovery_shares WHERE contact_id IS NULL LIMIT 1"
)
.fetch_optional(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?
.ok_or(TwonlyError::NoSharesLeft)?;
sqlx::query!(
"UPDATE user_discovery_shares SET contact_id = ? WHERE share_id = ?",
contact_id,
row.share_id,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
row.share
};
messages.push(
UserDiscoveryMessage {
user_discovery_announcement: Some(UserDiscoveryAnnouncement {
public_id: config.public_id,
threshold: config.threshold as u32,
announcement_share,
verification_shares: config.verification_shares.clone(),
share_promotion: config.share_promotion,
}),
version: version.clone(),
..Default::default()
}
.encode_to_vec(),
);
}
if received_version.promotion < config.promotion_version {
let promoting_messages = sqlx::query_scalar!(
"SELECT promotion FROM user_discovery_own_promotions WHERE version_id > ?",
received_version.promotion,
)
.fetch_all(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
let size = promoting_messages.len();
let mut filtered: Vec<Vec<u8>> = promoting_messages
.into_iter()
.filter(|message| !message.is_empty())
.collect();
if filtered.len() != size {
filtered.push(
UserDiscoveryMessage {
version,
..Default::default()
}
.encode_to_vec(),
);
}
messages.extend(filtered);
}
Ok(messages)
}
/// Returns the latest version for this discovery.
/// Before calling this function the application must sure that contact_id is qualified to be announced.
pub async fn handle_new_messages(
&self,
contact_id: UserID,
public_key_verified_timestamp: Option<i64>,
messages: Vec<Vec<u8>>,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
for message in messages {
let Ok(message) = UserDiscoveryMessage::decode(message.as_slice()) else {
tracing::warn!("Could not parse the message. Continue to the next message...");
continue;
};
let Some(version) = message.version else {
continue;
};
if let Some(uda) = message.user_discovery_announcement {
if let Err(err) = self
.handle_user_discovery_announcement(
contact_id,
public_key_verified_timestamp,
uda,
t,
)
.await
{
tracing::warn!("Ignoring: {err}");
}
} else if let Some(udp) = message.user_discovery_promotion {
if let Err(err) = self
.handle_user_discovery_promotion(contact_id, udp, t)
.await
{
tracing::warn!("Ignoring: {err}");
}
}
// Always update the version...
let version = version.encode_to_vec();
sqlx::query!(
"UPDATE contacts SET user_discovery_version = ? WHERE user_id = ?",
version,
contact_id,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
}
Ok(())
}
pub async fn update_verification_state_for_user(
&self,
contact_id: UserID,
public_key_verified_timestamp: Option<i64>,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
let current_promotion = sqlx::query_scalar!(
r#"SELECT promotion FROM user_discovery_own_promotions
WHERE contact_id = ? ORDER BY version_id DESC LIMIT 1"#,
contact_id,
)
.fetch_optional(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
let Some(current_promotion) = current_promotion else {
// User does not participate...
return Ok(());
};
let old_message = UserDiscoveryMessage::decode(current_promotion.as_slice())?;
let Some(old_promotion) = old_message.user_discovery_promotion else {
tracing::warn!("A contact should only have a promotion message...");
return Ok(());
};
// Read-modify-write the config to get the new promotion_version
let mut new_promotion_version = 0u32;
let mut announcement_version = 0u32;
self.read_modify_write_config(|config| {
config.promotion_version += 1;
new_promotion_version = config.promotion_version;
announcement_version = config.announcement_version;
})
.await?;
let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion {
announcement: announcement_version,
promotion: new_promotion_version,
}),
user_discovery_promotion: Some(UserDiscoveryPromotion {
promotion_id: rand::random(),
public_id: old_promotion.public_id,
threshold: old_promotion.threshold,
announcement_share: old_promotion.announcement_share,
public_key_verified_timestamp,
}),
..Default::default()
};
let promotion = message.encode_to_vec();
sqlx::query!(
"UPDATE user_discovery_own_promotions SET promotion = X'' WHERE contact_id = ?",
contact_id,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
sqlx::query!(
"INSERT INTO user_discovery_own_promotions(contact_id, promotion) VALUES (?, ?)",
contact_id,
promotion,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
Ok(())
}
async fn setup_announcements(
&self,
config: &UserDiscoveryConfig,
signed_data: SignedData,
signature: Vec<u8>,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<Vec<Vec<u8>>> {
tracing::debug!(
"Initializing user discovery with {} total shares and with a threshold of {}",
config.total_number_of_shares,
config.threshold
);
let encrypted_announcement = AnnouncementShareDecrypted {
signed_data: Some(signed_data),
signature,
}
.encode_to_vec();
let sharks = Sharks(config.threshold);
let dealer = sharks.dealer(&encrypted_announcement);
let mut shares: Vec<Vec<u8>> = dealer
.take(config.total_number_of_shares as usize)
.map(|x| Vec::from(&x))
.collect();
if shares.len() != config.total_number_of_shares as usize
|| shares.is_empty()
|| shares.len() <= (config.threshold as usize * 2)
{
return Err(TwonlyError::ShamirsSecret(
"Invalid length of shares where generated".to_string(),
));
}
tracing::debug!(
"Generated {} shares each with a size of: {}",
shares.len(),
shares[0].len()
);
let mut verification_shares = vec![];
let split_index = shares.len() - (config.threshold - 1) as usize;
verification_shares.extend(shares.drain(split_index..));
// `WHERE 1` is load-bearing: without a WHERE clause SQLite takes the
// truncate optimization, which drops every row without ever invoking
// the update hook the change stream is built on, so the UI would never
// learn the shares were replaced.
sqlx::query!("DELETE FROM user_discovery_shares WHERE 1")
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
for share in shares {
sqlx::query!(
"INSERT INTO user_discovery_shares (share) VALUES (?)",
share
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
}
Ok(verification_shares)
}
/// Reads the config from the store without holding any lock.
/// Use this for read-only access to the config.
async fn get_config_snapshot(&self) -> Result<UserDiscoveryConfig> {
Ok(serde_json::from_str(&self.read_config()?)?)
}
fn read_config(&self) -> Result<String> {
if !self.config_path.is_file() {
return Err(TwonlyError::UserDiscoveryNotInitialized);
}
Ok(std::fs::read_to_string(&self.config_path)?)
}
fn write_config(&self, config: &UserDiscoveryConfig) -> Result<()> {
std::fs::write(&self.config_path, serde_json::to_string_pretty(config)?)?;
Ok(())
}
/// Atomically reads the config, applies the mutation, and writes it back.
/// The config_lock is only held during the read-modify-write cycle,
/// NOT across any async Dart callbacks from the caller.
async fn read_modify_write_config<F>(&self, mutate: F) -> Result<()>
where
F: FnOnce(&mut UserDiscoveryConfig),
{
let _lock =
tokio::time::timeout(std::time::Duration::from_secs(10), self.config_lock.lock())
.await
.ok();
let mut config: UserDiscoveryConfig = serde_json::from_str(&self.read_config()?)?;
mutate(&mut config);
self.write_config(&config)?;
Ok(())
}
async fn handle_user_discovery_announcement(
&self,
contact_id: UserID,
public_key_verified_timestamp: Option<i64>,
uda: UserDiscoveryAnnouncement,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
tracing::info!("Got a user discovery announcement from {contact_id}.");
if uda.threshold as usize != uda.verification_shares.len() + 1 {
tracing::warn!(
"UDA contains to few shares to verify: {} != {} + 1.",
uda.threshold,
uda.verification_shares.len(),
);
return Ok(());
}
let sharks = Sharks(uda.threshold as u8);
let mut all_shares = uda.verification_shares.clone();
all_shares.push(uda.announcement_share.clone());
let shares: Vec<_> = all_shares
.iter()
.filter_map(|x| Share::try_from(x.as_slice()).ok())
.collect();
match sharks.recover(&shares) {
Ok(secret) => {
let asd = AnnouncementShareDecrypted::decode(secret.as_slice())?;
let Some(signed_data) = asd.signed_data else {
return Err(TwonlyError::MaliciousAnnouncementData(
"missing signed data".into(),
));
};
if contact_id != signed_data.user_id {
return Err(TwonlyError::MaliciousAnnouncementData(format!(
"contact_id ({contact_id}) != signed_data.user_id ({})",
signed_data.user_id
)));
}
if !self
.verify_stored_pubkey(contact_id, &signed_data.public_key)
.await?
{
return Err(TwonlyError::MaliciousAnnouncementData(
"public key does not match with stored one".to_string(),
));
}
if !self
.verify_signature(
&signed_data.encode_to_vec(),
&signed_data.public_key,
&asd.signature,
)
.await?
{
return Err(TwonlyError::MaliciousAnnouncementData(
"signature invalid".to_string(),
));
}
// Only add this user to the promotions if the users enabled this feature
if uda.share_promotion {
// Read-modify-write the config to get the new promotion_version
let mut new_promotion_version = 0u32;
let mut announcement_version = 0u32;
self.read_modify_write_config(|config| {
config.promotion_version += 1;
new_promotion_version = config.promotion_version;
announcement_version = config.announcement_version;
})
.await?;
let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion {
announcement: announcement_version,
promotion: new_promotion_version,
}),
user_discovery_promotion: Some(UserDiscoveryPromotion {
promotion_id: rand::random(),
public_id: signed_data.public_id,
threshold: uda.threshold,
announcement_share: uda.announcement_share,
public_key_verified_timestamp,
}),
..Default::default()
};
let promotion = message.encode_to_vec();
sqlx::query!(
"UPDATE user_discovery_own_promotions SET promotion = X'' WHERE contact_id = ?",
contact_id,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
sqlx::query!(
"INSERT INTO user_discovery_own_promotions(contact_id, promotion) VALUES (?, ?)",
contact_id,
promotion,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
}
let announced_user = AnnouncedUser {
user_id: signed_data.user_id,
public_key: signed_data.public_key,
public_id: uda.public_id,
};
tracing::debug!(
"NEW PROMOTION 3: {} knows {}",
contact_id,
announced_user.user_id
);
// User is known, so add him to thr users relations
self.push_new_user_relation(
contact_id,
announced_user.clone(),
public_key_verified_timestamp,
t,
)
.await?;
// As we no now the public_id from the user, all promotions up to this point are also known, so add these to the relations database as well
let promotions = self
.get_other_promotions_by_public_id(uda.public_id, t)
.await?;
for promotion in promotions {
self.push_new_user_relation(
promotion.from_contact_id,
announced_user.clone(),
promotion.public_key_verified_timestamp,
t,
)
.await?;
}
Ok(())
}
Err(err) => Err(TwonlyError::ShamirsSecret(err.to_string())),
}
}
async fn handle_user_discovery_promotion(
&self,
from_contact_id: UserID,
udp: UserDiscoveryPromotion,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
tracing::debug!("Received a new UDP with public_id = {}.", &udp.public_id);
if udp.announcement_share.is_empty() {
tracing::info!("Got empty announcement share. Ignoring it..");
return Ok(());
}
self.store_other_promotion(
OtherPromotion {
from_contact_id,
promotion_id: udp.promotion_id,
threshold: udp.threshold as u8,
public_id: udp.public_id,
announcement_share: udp.announcement_share,
public_key_verified_timestamp: udp.public_key_verified_timestamp,
},
t,
)
.await?;
if let Some(contact) = self
.get_announced_user_by_public_id(udp.public_id, t)
.await?
{
tracing::debug!(
"NEW PROMOTION 2: {} knows {}",
from_contact_id,
contact.user_id
);
// The user is already known, just propagate the relation ship
self.push_new_user_relation(
from_contact_id,
contact,
udp.public_key_verified_timestamp,
t,
)
.await?;
return Ok(());
}
let promotions = self
.get_other_promotions_by_public_id(udp.public_id, t)
.await?;
// Deduplicate shares by their raw bytes to prevent invalid Shamir's Secret Sharing recoveries.
// Multiple identical shares (e.g. due to contact resending promotions, or DB duplicate writes)
// will cause `recover` to interpolate incorrectly and return garbage bytes.
let mut unique_shares_set = HashSet::new();
let mut unique_promotions = Vec::new();
for p in promotions {
if unique_shares_set.insert(p.announcement_share.clone()) {
unique_promotions.push(p);
}
}
if unique_promotions.len() < udp.threshold as usize {
tracing::debug!(
"Not enough unique shares ({} < {}) to decrypt announcement. Waiting for next share.",
unique_promotions.len(),
udp.threshold
);
return Ok(());
}
tracing::debug!("Enough shares decrypting announcement.");
let shares: Vec<_> = unique_promotions
.iter()
.map(|x| x.announcement_share.to_owned())
.filter_map(|x| Share::try_from(x.as_slice()).ok())
.collect();
match Sharks(udp.threshold as u8).recover(&shares) {
Ok(secret) => {
tracing::debug!("Could decrypt announcement.");
let asd = AnnouncementShareDecrypted::decode(secret.as_slice())?;
if let Some(signed_data) = asd.signed_data {
if udp.public_id != signed_data.public_id {
tracing::warn!(
"Mismatch of the announced public id and the signed public id "
);
return Ok(());
}
if !self
.verify_signature(
&signed_data.encode_to_vec(),
&signed_data.public_key,
&asd.signature,
)
.await?
{
return Err(TwonlyError::MaliciousAnnouncementData(
"signature is invalid".to_string(),
));
}
tracing::debug!("Announcement valid.");
let announced_user = AnnouncedUser {
user_id: signed_data.user_id,
public_key: signed_data.public_key,
public_id: udp.public_id,
};
let config = self.get_config_snapshot().await?;
let user_id = config.user_id;
for promotion in unique_promotions {
// Do not store the announcement of the users itself.
// Or in case the promotion promotes myself
if promotion.from_contact_id == announced_user.user_id
|| announced_user.user_id == user_id
{
continue;
}
tracing::debug!(
"NEW PROMOTION: {:x} knows {:x}",
promotion.from_contact_id,
announced_user.user_id
);
self.push_new_user_relation(
promotion.from_contact_id,
announced_user.clone(),
promotion.public_key_verified_timestamp,
t,
)
.await?;
}
}
Ok(())
}
Err(err) => Err(TwonlyError::ShamirsSecret(err.to_string())),
}
}
async fn store_other_promotion(
&self,
promotion: OtherPromotion,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
sqlx::query!(
r#"
INSERT INTO user_discovery_other_promotions (
from_contact_id, promotion_id, public_id, threshold,
announcement_share, public_key_verified_timestamp
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(from_contact_id, public_id) DO UPDATE SET
promotion_id = excluded.promotion_id,
threshold = excluded.threshold,
announcement_share = excluded.announcement_share,
public_key_verified_timestamp = excluded.public_key_verified_timestamp
"#,
promotion.from_contact_id,
promotion.promotion_id,
promotion.public_id,
promotion.threshold,
promotion.announcement_share,
promotion.public_key_verified_timestamp,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
Ok(())
}
async fn get_announced_user_by_public_id(
&self,
public_id: i64,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<Option<AnnouncedUser>> {
let row = sqlx::query!(
r#"
SELECT announced_user_id, announced_public_key, public_id
FROM user_discovery_announced_users
WHERE public_id = ?
"#,
public_id,
)
.fetch_optional(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
Ok(row.map(|row| AnnouncedUser {
user_id: row.announced_user_id,
public_key: row.announced_public_key,
public_id: row.public_id,
}))
}
async fn get_other_promotions_by_public_id(
&self,
public_id: i64,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<Vec<OtherPromotion>> {
let rows = sqlx::query!(
r#"
SELECT promotion_id, public_id, from_contact_id, threshold,
announcement_share, public_key_verified_timestamp
FROM user_discovery_other_promotions
WHERE public_id = ?
"#,
public_id,
)
.fetch_all(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
rows.into_iter()
.map(|row| {
Ok(OtherPromotion {
promotion_id: u32::try_from(row.promotion_id)
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?,
public_id: row.public_id,
from_contact_id: row.from_contact_id,
threshold: u8::try_from(row.threshold)
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?,
announcement_share: row.announcement_share,
public_key_verified_timestamp: row.public_key_verified_timestamp,
})
})
.collect()
}
async fn push_new_user_relation(
&self,
from_contact_id: UserID,
announced_user: AnnouncedUser,
public_key_verified_timestamp: Option<i64>,
t: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
) -> Result<()> {
sqlx::query!(
r#"
INSERT INTO user_discovery_announced_users (
announced_user_id, announced_public_key, public_id
) VALUES (?, ?, ?)
ON CONFLICT DO UPDATE SET
announced_user_id = excluded.announced_user_id,
announced_public_key = excluded.announced_public_key,
public_id = excluded.public_id
"#,
announced_user.user_id,
announced_user.public_key,
announced_user.public_id,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
sqlx::query!(
r#"
INSERT INTO user_discovery_user_relations (
announced_user_id, from_contact_id, public_key_verified_timestamp
) VALUES (?, ?, ?)
ON CONFLICT(announced_user_id, from_contact_id) DO UPDATE SET
public_key_verified_timestamp = excluded.public_key_verified_timestamp
"#,
announced_user.user_id,
from_contact_id,
public_key_verified_timestamp,
)
.execute(&mut **t)
.await
.map_err(|error| TwonlyError::UserDiscoveryStore(error.to_string()))?;
Ok(())
}
}
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,
share_promotion: true,
user_id: 0,
}
}
}