merge into one single crate

This commit is contained in:
otsmr 2026-06-08 22:58:01 +02:00
parent 053fbeb66e
commit 37551cacce
38 changed files with 79 additions and 2034 deletions

38
rust/Cargo.lock generated
View file

@ -391,12 +391,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.1" version = "0.10.1"
@ -562,7 +556,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [ dependencies = [
"const-oid 0.9.6", "const-oid",
"pem-rfc7468", "pem-rfc7468",
"zeroize", "zeroize",
] ]
@ -594,7 +588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer 0.10.4", "block-buffer 0.10.4",
"const-oid 0.9.6", "const-oid",
"crypto-common 0.1.7", "crypto-common 0.1.7",
"subtle", "subtle",
] ]
@ -606,7 +600,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [ dependencies = [
"block-buffer 0.12.0", "block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.1", "crypto-common 0.2.1",
"ctutils", "ctutils",
] ]
@ -1857,25 +1850,6 @@ dependencies = [
"prost", "prost",
] ]
[[package]]
name = "protocols"
version = "0.1.0"
dependencies = [
"base64",
"blahaj",
"hmac 0.13.0",
"prost",
"prost-build",
"rand 0.10.1",
"serde",
"serde_json",
"sha2 0.11.0",
"sqlx",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@ -1991,7 +1965,7 @@ version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [ dependencies = [
"const-oid 0.9.6", "const-oid",
"digest 0.10.7", "digest 0.10.7",
"num-bigint-dig", "num-bigint-dig",
"num-integer", "num-integer",
@ -2012,20 +1986,24 @@ dependencies = [
"aes-gcm", "aes-gcm",
"android-native-keyring-store", "android-native-keyring-store",
"apple-native-keyring-store", "apple-native-keyring-store",
"base64",
"blahaj",
"chrono", "chrono",
"flutter_rust_bridge", "flutter_rust_bridge",
"hex", "hex",
"hkdf", "hkdf",
"hmac 0.13.0",
"keyring-core", "keyring-core",
"libsqlite3-sys", "libsqlite3-sys",
"paste", "paste",
"postcard", "postcard",
"pretty_env_logger", "pretty_env_logger",
"prost",
"prost-build", "prost-build",
"protocols",
"rand 0.10.1", "rand 0.10.1",
"scrypt", "scrypt",
"serde", "serde",
"serde_json",
"sha2 0.10.9", "sha2 0.10.9",
"sqlx", "sqlx",
"tempfile", "tempfile",

View file

@ -36,7 +36,11 @@ libsqlite3-sys = { version = "0.35.0", features = [
tokio = { version = "1.44", features = ["full"] } tokio = { version = "1.44", features = ["full"] }
tracing = "0.1.44" tracing = "0.1.44"
rand = "0.10.1" rand = "0.10.1"
protocols = { path = "../rust_dependencies/protocols" } prost = "0.14.1"
blahaj = "0.6.0"
serde_json = "1.0"
base64 = "0.22.1"
hmac = "0.13.0"
hkdf = "0.12.4" hkdf = "0.12.4"
sha2 = "0.10.8" sha2 = "0.10.8"
aes-gcm = "0.10.3" aes-gcm = "0.10.3"

5
rust/build.rs Normal file
View file

@ -0,0 +1,5 @@
use std::io::Result;
fn main() -> Result<()> {
prost_build::compile_protos(&["src/user_discovery/types.proto"], &["src/"])?;
Ok(())
}

View file

@ -52,7 +52,7 @@ impl BackupIdentity {
let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?; let key_manager: KeyManager = postcard::from_bytes(&decrypted_bytes)?;
key_manager.store_to_keychain(&secure_storage)?; key_manager.store_to_keychain(secure_storage)?;
Ok(()) Ok(())
} }

View file

@ -2,8 +2,8 @@ pub(crate) mod log;
mod macros; mod macros;
pub(crate) mod user_discovery; pub(crate) mod user_discovery;
use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion};
use flutter_rust_bridge::DartFnFuture; use flutter_rust_bridge::DartFnFuture;
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion};
use crate::error::{Result, TwonlyError}; use crate::error::{Result, TwonlyError};
use crate::{callback_generator, frb_generated::StreamSink}; use crate::{callback_generator, frb_generated::StreamSink};
@ -50,7 +50,9 @@ pub(crate) fn get_callbacks() -> Result<FlutterCallbacks> {
let caller_opt = CURRENT_CALLBACK_ID.try_with(|&c| c).ok(); let caller_opt = CURRENT_CALLBACK_ID.try_with(|&c| c).ok();
let lock = FLUTTER_CALLBACKS.read().unwrap(); let lock = FLUTTER_CALLBACKS.read().unwrap();
let map = lock.as_ref().ok_or(TwonlyError::MissingCallbackInitialization)?; let map = lock
.as_ref()
.ok_or(TwonlyError::MissingCallbackInitialization)?;
if let Some(id) = caller_opt { if let Some(id) = caller_opt {
if let Some(cb) = map.get(&id) { if let Some(cb) = map.get(&id) {

View file

@ -1,9 +1,10 @@
use crate::bridge::callbacks::get_callbacks; use crate::bridge::callbacks::get_callbacks;
use crate::bridge::get_twonly_flutter; use crate::bridge::get_twonly_flutter;
use crate::error::TwonlyError; use crate::error::TwonlyError;
use protocols::user_discovery::error::{Result, UserDiscoveryError}; use crate::user_discovery::error::{Result, UserDiscoveryError};
use protocols::user_discovery::traits::UserDiscoveryUtils; use crate::user_discovery::traits::UserDiscoveryUtils;
use protocols::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore}; use crate::user_discovery::traits::{AnnouncedUser, OtherPromotion, UserDiscoveryStore};
#[cfg(test)]
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@ -148,6 +149,7 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
.ok_or(TwonlyError::DartError.into()) .ok_or(TwonlyError::DartError.into())
} }
#[cfg(test)]
async fn get_all_announced_users( async fn get_all_announced_users(
&self, &self,
) -> Result<HashMap<AnnouncedUser, Vec<(i64, Option<i64>)>>> { ) -> Result<HashMap<AnnouncedUser, Vec<(i64, Option<i64>)>>> {

View file

@ -13,12 +13,12 @@ use crate::error::Result;
use crate::error::TwonlyError; use crate::error::TwonlyError;
use crate::keys::KeyManager; use crate::keys::KeyManager;
use crate::secure_storage::SecureStorage; use crate::secure_storage::SecureStorage;
use crate::user_discovery::UserDiscovery;
use crate::utils::Shared; use crate::utils::Shared;
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use protocols::user_discovery::UserDiscovery;
pub use protocols::user_discovery::traits::AnnouncedUser; pub use crate::user_discovery::traits::AnnouncedUser;
pub use protocols::user_discovery::traits::OtherPromotion; pub use crate::user_discovery::traits::OtherPromotion;
use tokio::sync::Mutex; use tokio::sync::Mutex;
pub struct InitConfig { pub struct InitConfig {
@ -57,9 +57,9 @@ pub(crate) struct TwonlyFlutter {
pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> { pub(super) fn get_twonly_flutter() -> Result<&'static TwonlyFlutter> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
if let Context::Flutter(twonly) = ctx { if let Context::Flutter(twonly) = ctx {
return Ok(twonly); Ok(twonly)
} else { } else {
return Err(TwonlyError::Initialization); Err(TwonlyError::Initialization)
} }
} }

View file

@ -53,7 +53,7 @@ impl RustBackupIdentity {
pub async fn get_identity_backup_bytes() -> Result<Vec<u8>> { pub async fn get_identity_backup_bytes() -> Result<Vec<u8>> {
let key_manager = get_twonly_flutter()?.key_manager.lock().await; let key_manager = get_twonly_flutter()?.key_manager.lock().await;
return BackupIdentity::encrypt_key_manager(&key_manager); BackupIdentity::encrypt_key_manager(&key_manager)
} }
pub async fn restore_identity_backup( pub async fn restore_identity_backup(
@ -70,7 +70,7 @@ impl RustBackupIdentity {
impl RustBackupArchive { impl RustBackupArchive {
pub async fn create_backup_archive() -> Result<(String, String)> { pub async fn create_backup_archive() -> Result<(String, String)> {
let ctx = Context::get_static()?; let ctx = Context::get_static()?;
let path = BackupArchive::create_backup(&ctx).await?; let path = BackupArchive::create_backup(ctx).await?;
let key_manager = get_twonly_flutter()?.key_manager.lock().await; let key_manager = get_twonly_flutter()?.key_manager.lock().await;
let token = hex::encode(key_manager.main_key.get_backup_download_token()); let token = hex::encode(key_manager.main_key.get_backup_download_token());
Ok((token, path.canonicalize()?.to_string_lossy().to_string())) Ok((token, path.canonicalize()?.to_string_lossy().to_string()))

View file

@ -1,3 +1,4 @@
use crate::user_discovery::UserDiscovery;
use crate::{ use crate::{
bridge::{ bridge::{
callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter}, callbacks::user_discovery::{UserDiscoveryStoreFlutter, UserDiscoveryUtilsFlutter},
@ -9,7 +10,6 @@ use crate::{
log::init_tracing, log::init_tracing,
utils::Shared, utils::Shared,
}; };
use protocols::user_discovery::UserDiscovery;
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use tokio::sync::{Mutex, OnceCell}; use tokio::sync::{Mutex, OnceCell};
use zeroize::Zeroize; use zeroize::Zeroize;

View file

@ -88,11 +88,10 @@ macro_rules! generate_test_select {
#[cfg(test)] #[cfg(test)]
#[tokio::test] #[tokio::test]
async fn [<test_ $select_fn>]() { async fn [<test_ $select_fn>]() {
use crate::database::Database;
use tempfile::tempdir; use tempfile::tempdir;
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let db_path = dir.path().join("test.sqlite").display().to_string(); let db_path = dir.path().join("test.sqlite").display().to_string();
let db = Database::new(&db_path, None, false).await.unwrap(); let db = crate::database::Database::new(&db_path, None, false).await.unwrap();
db.run_migrations().await.unwrap(); db.run_migrations().await.unwrap();
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap(); $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();

View file

@ -1,5 +1,5 @@
use crate::user_discovery::error::UserDiscoveryError;
use hex::FromHexError; use hex::FromHexError;
use protocols::user_discovery::error::UserDiscoveryError;
use scrypt::errors::{InvalidOutputLen, InvalidParams}; use scrypt::errors::{InvalidOutputLen, InvalidParams};
use thiserror::Error; use thiserror::Error;
use zip::result::ZipError; use zip::result::ZipError;

View file

@ -61,7 +61,7 @@ impl MainKey {
self.decrypt_with_info(b"backup_key", encrypted_backup) self.decrypt_with_info(b"backup_key", encrypted_backup)
} }
/// Encrypts a newly generated media key using the derived Media Main Key. // Encrypts a newly generated media key using the derived Media Main Key.
// pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> { // pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> {
// self.encrypt_with_info(b"media_main_key", media_key) // self.encrypt_with_info(b"media_main_key", media_key)
// } // }

View file

@ -6,6 +6,8 @@ mod error;
mod frb_generated; mod frb_generated;
mod keys; mod keys;
mod log; mod log;
mod passwordless_recovery;
mod secure_storage; mod secure_storage;
mod standalone; mod standalone;
mod user_discovery;
mod utils; mod utils;

View file

@ -4,9 +4,10 @@ pub mod stores;
pub mod tests; pub mod tests;
pub mod traits; pub mod traits;
use std::collections::{HashMap, HashSet}; #[cfg(test)]
use std::collections::HashMap;
use std::collections::{HashSet};
use std::sync::Arc; use std::sync::Arc;
use std::u8;
use blahaj::{Share, Sharks}; use blahaj::{Share, Sharks};
use prost::Message; use prost::Message;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -184,6 +185,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
/// * `Ok(HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>)` - All connections the user has discovered /// * `Ok(HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>)` - All connections the user has discovered
/// * `Err(UserDiscoveryError)` - If there where erros in the store. /// * `Err(UserDiscoveryError)` - If there where erros in the store.
/// ///
#[cfg(test)]
pub async fn get_all_announced_users( pub async fn get_all_announced_users(
&self, &self,
) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> { ) -> Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>> {
@ -384,7 +386,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
config.promotion_version += 1; config.promotion_version += 1;
new_promotion_version = config.promotion_version; new_promotion_version = config.promotion_version;
announcement_version = config.announcement_version; announcement_version = config.announcement_version;
}).await?; })
.await?;
let message = UserDiscoveryMessage { let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion { version: Some(UserDiscoveryVersion {
@ -430,7 +433,7 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
} }
.encode_to_vec(); .encode_to_vec();
let sharks = Sharks(config.threshold as u8); let sharks = Sharks(config.threshold);
let dealer = sharks.dealer(&encrypted_announcement); let dealer = sharks.dealer(&encrypted_announcement);
let mut shares: Vec<Vec<u8>> = dealer let mut shares: Vec<Vec<u8>> = dealer
@ -476,12 +479,10 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
where where
F: FnOnce(&mut UserDiscoveryConfig), F: FnOnce(&mut UserDiscoveryConfig),
{ {
let _lock = tokio::time::timeout( let _lock =
std::time::Duration::from_secs(10), tokio::time::timeout(std::time::Duration::from_secs(10), self.config_lock.lock())
self.config_lock.lock(), .await
) .ok();
.await
.ok();
let mut config: UserDiscoveryConfig = let mut config: UserDiscoveryConfig =
serde_json::from_str(&self.store.get_config().await?)?; serde_json::from_str(&self.store.get_config().await?)?;
mutate(&mut config); mutate(&mut config);
@ -539,9 +540,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
.verify_stored_pubkey(contact_id, &signed_data.public_key) .verify_stored_pubkey(contact_id, &signed_data.public_key)
.await? .await?
{ {
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!( return Err(UserDiscoveryError::MaliciousAnnouncementData(
"public key does not match with stored one", "public key does not match with stored one".to_string(),
))); ));
} }
if !self if !self
@ -553,9 +554,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
) )
.await? .await?
{ {
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!( return Err(UserDiscoveryError::MaliciousAnnouncementData(
"signature invalid", "signature invalid".to_string(),
))); ));
} }
// Only add this user to the promotions if the users enabled this feature // Only add this user to the promotions if the users enabled this feature
@ -567,7 +568,8 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
config.promotion_version += 1; config.promotion_version += 1;
new_promotion_version = config.promotion_version; new_promotion_version = config.promotion_version;
announcement_version = config.announcement_version; announcement_version = config.announcement_version;
}).await?; })
.await?;
let message = UserDiscoveryMessage { let message = UserDiscoveryMessage {
version: Some(UserDiscoveryVersion { version: Some(UserDiscoveryVersion {
@ -624,11 +626,10 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
self.store self.store
.push_new_user_relation( .push_new_user_relation(
promotion.from_contact_id, promotion.from_contact_id,
announced_user, announced_user.clone(),
promotion.public_key_verified_timestamp, promotion.public_key_verified_timestamp,
) )
.await?; .await?;
return Ok(());
} }
Ok(()) Ok(())
@ -731,9 +732,9 @@ impl<Store: UserDiscoveryStore, Utils: UserDiscoveryUtils> UserDiscovery<Store,
) )
.await? .await?
{ {
return Err(UserDiscoveryError::MaliciousAnnouncementData(format!( return Err(UserDiscoveryError::MaliciousAnnouncementData(
"signature is invalid", "signature is invalid".to_string(),
))); ));
} }
tracing::debug!("Announcement valid."); tracing::debug!("Announcement valid.");
@ -790,4 +791,3 @@ impl Default for UserDiscoveryConfig {
} }
} }
} }

View file

@ -91,7 +91,7 @@ impl UserDiscoveryStore for InMemoryStore {
async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> { async fn get_own_promotions_after_version(&self, version: u32) -> Result<Vec<Vec<u8>>> {
let storage = self.storage(); let storage = self.storage();
let elements = storage.own_promotions[(version as usize)..] let elements = storage.own_promotions[(version as usize)..]
.into_iter() .iter()
.map(|(_, promotion)| promotion.to_owned()) .map(|(_, promotion)| promotion.to_owned())
.collect(); .collect();
Ok(elements) Ok(elements)
@ -107,7 +107,7 @@ impl UserDiscoveryStore for InMemoryStore {
if let Some(element) = element { if let Some(element) = element {
return Ok(Some(element.1.to_owned())); return Ok(Some(element.1.to_owned()));
} }
return Ok(None); Ok(None)
} }
async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> { async fn store_other_promotion(&self, promotion: OtherPromotion) -> Result<()> {
@ -158,7 +158,7 @@ impl UserDiscoveryStore for InMemoryStore {
let entry = storage let entry = storage
.announced_users .announced_users
.entry(announced_user.clone()) .entry(announced_user.clone())
.or_insert(vec![]); .or_default();
if announced_user.user_id != from_contact_id { if announced_user.user_id != from_contact_id {
if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) { if let Some(found) = entry.iter_mut().find(|x| x.0 == from_contact_id) {
found.1 = public_key_verified_timestamp; found.1 = public_key_verified_timestamp;

View file

@ -0,0 +1,4 @@
#[cfg(test)]
mod in_memory_store;
#[cfg(test)]
pub(super) use in_memory_store::InMemoryStore;

View file

@ -1,9 +1,14 @@
#[cfg(test)]
use std::collections::HashMap; use std::collections::HashMap;
use crate::user_discovery::error::Result; use crate::user_discovery::error::Result;
use crate::user_discovery::UserID; use crate::user_discovery::UserID;
use std::future::Future; use std::future::Future;
/// Type alias used in `UserDiscoveryStore::get_all_announced_users`.
#[cfg(test)]
pub type AnnouncedUserMap = HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>;
#[derive(Clone, sqlx::FromRow)] #[derive(Clone, sqlx::FromRow)]
pub struct OtherPromotion { pub struct OtherPromotion {
pub promotion_id: u32, pub promotion_id: u32,
@ -65,9 +70,8 @@ pub trait UserDiscoveryStore {
public_key_verified_timestamp: Option<i64>, public_key_verified_timestamp: Option<i64>,
) -> impl Future<Output = Result<()>> + Send; ) -> impl Future<Output = Result<()>> + Send;
fn get_all_announced_users( #[cfg(test)]
&self, fn get_all_announced_users(&self) -> impl Future<Output = Result<AnnouncedUserMap>> + Send;
) -> impl Future<Output = Result<HashMap<AnnouncedUser, Vec<(UserID, Option<i64>)>>>> + Send;
fn get_contact_promotion( fn get_contact_promotion(
&self, &self,

View file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
[workspace]
members = ["protocols"]
resolver = "3"

View file

@ -1,29 +0,0 @@
[package]
name = "protocols"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["rlib", "cdylib", "staticlib"]
[dependencies]
thiserror = "2.0.18"
tracing = "0.1.44"
serde = "1.0.228"
prost = "0.14.1"
rand = "0.10.1"
blahaj = "0.6.0"
serde_json = "1.0"
base64 = "0.22.1"
hmac = "0.13.0"
sha2 = "0.11.0"
tokio = { version = "1.44", features = ["full"] }
sqlx = { version = "0.9.0-alpha.1", default-features = false, features = [
"derive",
] }
[dev-dependencies]
pretty_env_logger = "0.5.0"
[build-dependencies]
prost-build = "0.14.1"

View file

@ -1,11 +0,0 @@
use std::io::Result;
fn main() -> Result<()> {
prost_build::compile_protos(
&[
"src/user_discovery/types.proto",
"src/key_verification/types.proto",
],
&["src/"],
)?;
Ok(())
}

View file

@ -1,30 +0,0 @@
use prost::DecodeError;
use thiserror::Error;
pub type Result<T> = core::result::Result<T, KeyVerificationError>;
#[derive(Error, Debug)]
pub enum KeyVerificationError {
#[error("The prefix deeplink url must start with https:// and end with a #")]
InvalidDeeplinkPrefix,
#[error("Invalid qr text")]
InvalidQrText,
#[error(
"Contact user_id is known and the stored public_key does not match the received user id"
)]
InvalidPublicKeyAndUserIdCombination,
#[error("Store error: `{0}`")]
Store(String),
#[error("`{0}`")]
Base64(#[from] base64::DecodeError),
#[error("`{0}`")]
Prost(#[from] DecodeError),
#[error("`{0}`")]
Hmac(#[from] hmac::digest::InvalidLength),
}

View file

@ -1,194 +0,0 @@
use crate::key_verification::{error::KeyVerificationError, traits::KeyVerificationStore};
use crate::user_discovery::UserID;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use error::Result;
use hmac::{Hmac, KeyInit, Mac};
use prost::Message;
use sha2::Sha256;
pub(crate) mod error;
pub mod stores;
pub mod traits;
include!(concat!(env!("OUT_DIR"), "/key_verification.rs"));
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
/// The link MUST start with a https:// and end with a #
/// The link should contain the username of the user so the application can show the scanned user without internet
/// Example: https://me.twonly.eu/tobi#
deeplink_prefix: String,
/// The user ID used to calculate the verification proof
user_id: UserID,
/// The public_key of the user to calculate the verification proof
public_key: Vec<u8>,
}
pub struct ScannedUser {
pub user_id: UserID,
pub public_key: Vec<u8>,
pub verification_proof: Vec<u8>,
}
pub struct KeyVerification<Store: KeyVerificationStore> {
store: Store,
config: KeyVerificationConfig,
}
impl<Store: KeyVerificationStore> KeyVerification<Store> {
pub fn new(store: Store, config: KeyVerificationConfig) -> Result<KeyVerification<Store>> {
if !config.deeplink_prefix.starts_with("https://") || !config.deeplink_prefix.ends_with("#")
{
return Err(KeyVerificationError::InvalidDeeplinkPrefix);
}
Ok(Self { store, config })
}
/// Generates the a string which should be displayed in the UI so others can scan it.
pub fn generate_qr_text(&self) -> Result<String> {
// 10 Bytes should be enough. Tokens are only valid for one day and then deleted.
let secret_verification_token: Vec<u8> = rand::random_iter().take(16).collect();
self.store
.push_new_secret_verification_token(&secret_verification_token)?;
let verification_data = VerificationData {
user_id: self.config.user_id,
public_key: self.config.public_key.clone(),
secret_verification_token,
};
let verification_data_bytes = verification_data.encode_to_vec();
let encoded = URL_SAFE_NO_PAD.encode(verification_data_bytes);
Ok(format!("{}{}", self.config.deeplink_prefix, encoded))
}
/// Handles the scanned qr code text and creates a response message
/// which can be send to the other person
pub fn get_user_from_scanned_qr_text(&self, received_text: &str) -> Result<ScannedUser> {
let splitted: Vec<_> = received_text.split('#').collect();
if splitted.len() != 2 {
tracing::info!("Scanned qr text does not contain a #");
return Err(KeyVerificationError::InvalidQrText);
}
let verification_data_bytes = URL_SAFE_NO_PAD.decode(splitted[1])?;
let verification_data = VerificationData::decode(verification_data_bytes.as_slice())?;
let mut mac = Hmac::<Sha256>::new_from_slice(&verification_data.secret_verification_token)?;
mac.update(&self.config.user_id.to_le_bytes());
mac.update(&self.config.public_key);
mac.update(&verification_data.user_id.to_le_bytes());
mac.update(&verification_data.public_key);
let verification_proof = mac.finalize().into_bytes().to_vec();
Ok(ScannedUser {
user_id: verification_data.user_id,
public_key: verification_data.public_key,
verification_proof,
})
}
/// Checks whether the received verification proof is valid
pub fn is_received_verification_proof_valid(
&self,
from_user_id: UserID,
public_key: Vec<u8>,
verification_proof: Vec<u8>,
) -> Result<bool> {
let verification_tokens = self.store.get_all_valid_verification_tokens()?;
for verification_token in &verification_tokens {
let calculated_verification_proof = {
let mut mac = Hmac::<Sha256>::new_from_slice(verification_token)?;
mac.update(&from_user_id.to_le_bytes());
mac.update(&public_key);
mac.update(&self.config.user_id.to_le_bytes());
mac.update(&self.config.public_key);
mac.finalize().into_bytes().to_vec()
};
if calculated_verification_proof == verification_proof {
return Ok(true);
}
}
Ok(false)
}
}
#[cfg(test)]
mod tests {
use crate::key_verification::{stores::InMemoryStore, KeyVerification, KeyVerificationConfig};
#[test]
fn test_key_verification() {
let _ = pretty_env_logger::try_init();
const ALICE_ID: i64 = 10;
const BOB_ID: i64 = 11;
let alice_kv = KeyVerification::new(
InMemoryStore::default(),
KeyVerificationConfig {
user_id: ALICE_ID,
public_key: vec![ALICE_ID as u8; 32],
deeplink_prefix: "https://me.twonly.eu/alice#".into(),
},
)
.unwrap();
let bob_kv = KeyVerification::new(
InMemoryStore::default(),
KeyVerificationConfig {
user_id: BOB_ID,
public_key: vec![BOB_ID as u8; 32],
deeplink_prefix: "https://me.twonly.eu/bob#".into(),
},
)
.unwrap();
let qr_code_text = alice_kv.generate_qr_text().unwrap();
assert_eq!(qr_code_text.len(), 99);
tracing::debug!("Generated QR-Code-Link: {qr_code_text}");
let scanned_user = bob_kv.get_user_from_scanned_qr_text(&qr_code_text).unwrap();
// THIS must be done by the application
assert_eq!(scanned_user.user_id, ALICE_ID);
assert_eq!(scanned_user.public_key, vec![ALICE_ID as u8; 32]);
// SEND scanned_user.verification_proof over the establish e2ee protected session if public_key verification was valid.
let valid_verification_proof = alice_kv
.is_received_verification_proof_valid(
BOB_ID,
vec![BOB_ID as u8; 32],
scanned_user.verification_proof.clone(),
)
.unwrap();
assert_eq!(valid_verification_proof, true);
let valid_verification_proof = alice_kv
.is_received_verification_proof_valid(
BOB_ID,
vec![(BOB_ID + 1) as u8; 32],
scanned_user.verification_proof.clone(),
)
.unwrap();
assert_eq!(valid_verification_proof, false);
let mut modified_proof = scanned_user.verification_proof;
modified_proof[0] = modified_proof[0] + 1;
let valid_verification_proof = alice_kv
.is_received_verification_proof_valid(BOB_ID, vec![BOB_ID as u8; 32], modified_proof)
.unwrap();
assert_eq!(valid_verification_proof, false);
}
}

View file

@ -1,22 +0,0 @@
use std::sync::{Arc, Mutex};
use crate::key_verification::{error::Result, traits::KeyVerificationStore};
#[derive(Default)]
pub struct InMemoryStore {
verification_tokens: Arc<Mutex<Vec<Vec<u8>>>>,
}
impl KeyVerificationStore for InMemoryStore {
fn push_new_secret_verification_token(&self, token: &[u8]) -> Result<()> {
self.verification_tokens
.lock()
.unwrap()
.push(token.to_vec());
Ok(())
}
fn get_all_valid_verification_tokens(&self) -> Result<Vec<Vec<u8>>> {
Ok(self.verification_tokens.lock().unwrap().clone())
}
}

View file

@ -1,3 +0,0 @@
mod in_memory_store;
pub use in_memory_store::InMemoryStore;

View file

@ -1,7 +0,0 @@
use super::error::Result;
pub trait KeyVerificationStore {
fn push_new_secret_verification_token(&self, token: &[u8]) -> Result<()>;
/// This function should return all tokens from the last 24h
/// All other tokens can be removed from the database
fn get_all_valid_verification_tokens(&self) -> Result<Vec<Vec<u8>>>;
}

View file

@ -1,12 +0,0 @@
syntax = "proto3";
package key_verification;
message VerificationData {
int64 user_id = 1;
bytes public_key = 2;
bytes secret_verification_token = 3;
}
message VerificationMessage {
bytes calculated_mac = 1;
}

View file

@ -1,3 +0,0 @@
pub mod key_verification;
pub mod passwordless_recovery;
pub mod user_discovery;

View file

@ -1,2 +0,0 @@
mod in_memory_store;
pub use in_memory_store::InMemoryStore;

View file

@ -18,7 +18,7 @@ protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "qr.proto"
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "data.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "data.proto"
mkdir "$GENERATED_DIR/user_discovery/" &>/dev/null mkdir "$GENERATED_DIR/user_discovery/" &>/dev/null
protoc --proto_path="./rust_dependencies/protocols/src/user_discovery/" --dart_out="$GENERATED_DIR/user_discovery/" "types.proto" protoc --proto_path="./rust/src/user_discovery/" --dart_out="$GENERATED_DIR/user_discovery/" "types.proto"
protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto" protoc --proto_path="$CLIENT_DIR" --dart_out="$GENERATED_DIR" "push_notification.proto"
protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto" protoc --proto_path="$CLIENT_DIR" --swift_out="./ios/NotificationService/" "push_notification.proto"