use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, Mutex, OnceLock, RwLock, }; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use base64::{ engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, Engine as _, }; use mtp::client::{ClientConfig, MTPClient, MTPConnection, Policy, SendMode}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap}; use mtp::crypto::{ derive_encryption_key, AeadDecrypt, ChaCha20Poly1305, HybridKem, Keyring, PublicKeyBundle, }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use tauri::{AppHandle, Emitter}; use tokio::sync::mpsc; const EVENT_NAME: &str = "mtp://event"; const DISCONNECTED: u8 = 0; const CONNECTING: u8 = 1; const CONNECTED: u8 = 2; const CHAT_SECRET_SALT: &[u8] = b"tensamin-chat-secret-v1"; const CHAT_MESSAGE_SALT: &[u8] = b"tensamin-chat-message-v1"; const CHAT_SECRET_SCHEME: &str = "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"; const INITIAL_SYNC_TIMEOUT: Duration = Duration::from_secs(30); const MAX_BUFFERED_INITIAL_FRAMES: usize = 1_000; const NOTIFICATION_QUEUE_CAPACITY: usize = 32; #[cfg(target_os = "android")] const ROOT_YE_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL\n\ MAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWUUwHhcN\n\ MjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsG\n\ A1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAi\n\ A2IABDwS/6vhrcVqcbBo+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7X\n\ yFjWsPYhIPt/wzOqxTd2b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW8\n\ 7j77GaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\n\ BBYEFKPIJlqOoUzQNWP8myPIOq5W809WMAoGCCqGSM49BAMDA2cAMGQCMHhMr8N9\n\ LdL1VQKs9BdV81r76eXRB6mtjuNjzk6/lBsPNToWLTDzGYgtQKO1jl63uAIwGV7m\n\ onyF377c+MM1oqVNs17sgu7F9YKZwgLmVbeOMDbKAXHtKMDLbiGllCcs8f47\n\ -----END CERTIFICATE-----\n"; #[cfg(target_os = "android")] const YE1_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\n\ MIICizCCAhGgAwIBAgIQXd1w3TH4AchcGGp6BLgK/jAKBggqhkjOPQQDAzAuMQsw\n\ CQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTAeFw0y\n\ NTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYD\n\ VQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZRTEwdjAQBgcqhkjOPQIBBgUr\n\ gQQAIgNiAAQHZVB1/mimla2hfSurylScjPMZaOJXLz/NnAc2sylm8WDyhU9Ccp+z\n\ ASQi5vSwGGJjSGklkD9fdPR8GpyDIOIjCEfrnbt/v+ZSEPLLEGbaM6EccDbN7p9x\n\ teIm2Avf+ryjge4wgeswDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF\n\ BwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLsgykcL/tflnPmPCSqj\n\ jDdFsbzYMB8GA1UdIwQYMBaAFKPIJlqOoUzQNWP8myPIOq5W809WMDIGCCsGAQUF\n\ BwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3llLmkubGVuY3Iub3JnLzATBgNV\n\ HSAEDDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veWUuYy5s\n\ ZW5jci5vcmcvMAoGCCqGSM49BAMDA2gAMGUCMQDgjUEahFT/h3DRakqiPZpLvPgf\n\ Zwkt6K2EOMmh1nvEzl83eMLYcod4GCl3b0J1Nn0CMBNYmEQJb4CEG5WoOe7aRn/L\n\ VKu6saHmHEynI7ysIPd8zQsK1HdmhlHKlw9Z5GpGvA==\n\ -----END CERTIFICATE-----\n"; #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(rename_all = "camelCase")] pub struct MtpConfig { pub user_id: u64, pub keyring: String, pub omega_url: String, pub forced_omikron_url: Option, pub forced_omikron_public_key: Option, } #[derive(Clone, Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct MtpSnapshot { pub generation: u64, pub ready_state: u8, pub identified: bool, pub state: Option, pub error: Option, } #[derive(Clone, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] enum MtpEvent { State { snapshot: MtpSnapshot, }, Message { generation: u64, message: Value, }, Log { level: u8, message: String, details: Option, }, } pub struct MtpManager { runtime: tokio::runtime::Runtime, config: RwLock>, connection: RwLock>>, snapshot: RwLock, generation: AtomicU64, enabled: AtomicBool, ui_visible: AtomicBool, app: RwLock>, start_lock: Mutex<()>, } struct RequestIdAllocator { next: AtomicU64, } impl RequestIdAllocator { fn new() -> Self { Self { next: AtomicU64::new(1), } } fn next(&self) -> Result { let value = self.next.fetch_add(1, Ordering::Relaxed); u32::try_from(value) .map_err(|_| "MTP request ID space exhausted for this connection".to_string()) } } struct ManagedConnection { mtp: Arc, request_ids: RequestIdAllocator, } impl ManagedConnection { async fn next_request_id(&self) -> Result { let id = self.request_ids.next(); if id.is_err() { self.mtp.sender.close().await; } id } } struct PreparedConnection { connection: MTPConnection, request_ids: RequestIdAllocator, initial_state: Value, buffered_frames: Vec, } static MANAGER: OnceLock = OnceLock::new(); pub fn manager() -> &'static MtpManager { MANAGER.get_or_init(|| MtpManager { runtime: tokio::runtime::Builder::new_multi_thread() .enable_all() .thread_name("tensamin-mtp") .build() .expect("failed to create MTP runtime"), config: RwLock::new(None), connection: RwLock::new(None), snapshot: RwLock::new(MtpSnapshot { ready_state: DISCONNECTED, ..MtpSnapshot::default() }), generation: AtomicU64::new(0), enabled: AtomicBool::new(false), ui_visible: AtomicBool::new(false), app: RwLock::new(None), start_lock: Mutex::new(()), }) } impl MtpManager { pub fn attach_app(&self, app: AppHandle) { *self.app.write().expect("app lock poisoned") = Some(app); } pub fn configure_and_start(&'static self, config: MtpConfig) { let _guard = self.start_lock.lock().expect("start lock poisoned"); if self.enabled.load(Ordering::SeqCst) && self.config.read().expect("config lock poisoned").as_ref() == Some(&config) { return; } *self.config.write().expect("config lock poisoned") = Some(config); self.enabled.store(true, Ordering::SeqCst); let generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1; if let Some(connection) = self .connection .write() .expect("connection lock poisoned") .take() { self.runtime .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation, ready_state: CONNECTING, identified: false, state: None, error: None, }); self.runtime.spawn(supervise(generation)); } pub fn stop(&self) { self.enabled.store(false, Ordering::SeqCst); self.generation.fetch_add(1, Ordering::SeqCst); if let Some(connection) = self .connection .write() .expect("connection lock poisoned") .take() { self.runtime .spawn(async move { connection.mtp.sender.close().await }); } self.set_snapshot(MtpSnapshot { generation: self.generation.load(Ordering::SeqCst), ready_state: DISCONNECTED, identified: false, state: None, error: None, }); } pub fn set_ui_visible(&self, visible: bool) { self.ui_visible.store(visible, Ordering::SeqCst); } pub fn is_enabled(&self) -> bool { self.enabled.load(Ordering::SeqCst) } pub fn snapshot(&self) -> MtpSnapshot { self.snapshot .read() .expect("snapshot lock poisoned") .clone() } fn set_snapshot(&self, snapshot: MtpSnapshot) { if let Some(error) = snapshot.error.as_ref() { eprintln!("android: MTP connection failed: {error}"); } *self.snapshot.write().expect("snapshot lock poisoned") = snapshot.clone(); self.emit(MtpEvent::State { snapshot }); } fn emit(&self, event: MtpEvent) { if let Some(app) = self.app.read().expect("app lock poisoned").as_ref() { let _ = app.emit(EVENT_NAME, event); } } fn log(&self, level: u8, message: impl Into, details: Option) { let message = message.into(); eprintln!( "android: {message}{}", details .as_ref() .map(|value| format!(": {value}")) .unwrap_or_default() ); self.emit(MtpEvent::Log { level, message, details, }); } fn is_current(&self, generation: u64) -> bool { self.enabled.load(Ordering::SeqCst) && self.generation.load(Ordering::SeqCst) == generation } async fn request(&self, type_name: &str, data: Value) -> Result { let connection = self .connection .read() .map_err(|_| "MTP connection lock is unavailable")? .clone() .ok_or_else(|| "MTP is not connected".to_string())?; let request = json_to_frame(type_name, data, connection.next_request_id().await?)?; let response = connection .mtp .request(&request, None) .await .map_err(|error| error.to_string())?; frame_to_json(&response) } } async fn supervise(generation: u64) { let manager = manager(); let mut delay = Duration::from_secs(1); while manager.is_current(generation) { let config = manager.config.read().expect("config lock poisoned").clone(); let Some(config) = config else { return }; manager.log(2, "Starting native MTP connection", None); android_status("Connecting"); match connect(&config).await { Ok(prepared) => { delay = Duration::from_secs(1); let connection = Arc::new(ManagedConnection { mtp: Arc::new(prepared.connection), request_ids: prepared.request_ids, }); let stale = { let _guard = manager.start_lock.lock().expect("start lock poisoned"); let mut current = manager .connection .write() .expect("connection lock poisoned"); if !manager.is_current(generation) { true } else { *current = Some(connection.clone()); drop(current); manager.set_snapshot(MtpSnapshot { generation, ready_state: CONNECTED, identified: true, state: Some(prepared.initial_state), error: None, }); false } }; if stale { connection.mtp.sender.close().await; break; } android_status("Connected"); manager.log(2, "Native MTP connection established", None); let (notification_tx, mut notification_rx) = mpsc::channel(NOTIFICATION_QUEUE_CAPACITY); let notification_connection = connection.clone(); let notification_config = config.clone(); let notification_worker = tokio::spawn(async move { while let Some(frame) = notification_rx.recv().await { if !manager().is_current(generation) { break; } if let Err(error) = notify_message( ¬ification_config, notification_connection.clone(), &frame, ) .await { eprintln!("failed to create background message notification: {error}"); } } }); for frame in prepared.buffered_frames { handle_push(generation, ¬ification_tx, frame).await; } while manager.is_current(generation) { match connection.mtp.receive().await { Ok(frame) => handle_push(generation, ¬ification_tx, frame).await, Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { manager.set_snapshot(MtpSnapshot { generation, ready_state: DISCONNECTED, identified: false, state: None, error: Some(error.to_string()), }); } break; } } } drop(notification_tx); notification_worker.abort(); if let Err(error) = notification_worker.await { if !error.is_cancelled() { eprintln!("background notification worker failed: {error}"); } } let mut current = manager .connection .write() .expect("connection lock poisoned"); if current .as_ref() .is_some_and(|stored| Arc::ptr_eq(stored, &connection)) { current.take(); } } Err(error) => { let _guard = manager.start_lock.lock().expect("start lock poisoned"); if manager.is_current(generation) { manager.set_snapshot(MtpSnapshot { generation, ready_state: DISCONNECTED, identified: false, state: None, error: Some(error), }); } } } if !manager.is_current(generation) { break; } android_status("Reconnecting"); tokio::time::sleep(jittered_retry_delay(delay)).await; delay = (delay * 2).min(Duration::from_secs(60)); } } fn jittered_retry_delay(delay: Duration) -> Duration { let entropy = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.subsec_nanos()) .unwrap_or_default(); let percent = 80 + entropy % 41; delay.mul_f64(percent as f64 / 100.0) } async fn connect(config: &MtpConfig) -> Result { let (url, public_key) = resolve_endpoint(config) .await .map_err(|error| format!("endpoint discovery failed: {error}"))?; manager().log( 2, "Resolved Omikron endpoint", Some(Value::String(url.clone())), ); let keyring_bytes = decode_browser_base64(&config.keyring) .map_err(|error| format!("invalid MTP keyring: {error}"))?; let keyring = Keyring::from_bytes(&keyring_bytes) .map_err(|error| format!("invalid MTP keyring: {error}"))?; let host_key_bytes = decode_sdk_bytes(&public_key) .map_err(|error| format!("invalid Omikron public key: {error}"))?; let host_key = PublicKeyBundle::from_bytes(&host_key_bytes) .map_err(|error| format!("invalid Omikron public key: {error}"))?; host_key .validate() .map_err(|error| format!("invalid Omikron public key: {error}"))?; let client_config = ClientConfig::new(url) .with_client_id(config.user_id) .with_description("client") .with_policy(Policy::default().with_send_mode(SendMode::SingleStreamPerMessage)) .with_ping_interval(Duration::from_secs(30)) .with_max_missed_pings(3); #[cfg(target_os = "android")] let client_config = client_config.with_pinned_pem(android_root_certificates().clone()); let connection = tokio::time::timeout( Duration::from_secs(30), MTPClient::auth_connect(client_config, &keyring, &host_key), ) .await .map_err(|_| "transport authentication timed out".to_string())? .map_err(|error| format!("transport authentication failed: {error}"))?; manager().log(2, "Native MTP authentication completed", None); let (state, buffered_frames) = await_initial_state(&connection).await?; let request_ids = RequestIdAllocator::new(); let (initial_state, ack) = prepare_initial_state_ack(&state, &request_ids)?; let response = tokio::time::timeout(INITIAL_SYNC_TIMEOUT, connection.request(&ack, None)) .await .map_err(|_| "state acknowledgement timed out".to_string())? .map_err(|error| format!("state acknowledgement failed: {error}"))?; if response .get_type_name() .is_some_and(|name| name.starts_with("Error")) { return Err(format!("ClientStateAck failed: {response}")); } Ok(PreparedConnection { connection, request_ids, initial_state, buffered_frames, }) } fn prepare_initial_state_ack( state: &CommunicationValue, request_ids: &RequestIdAllocator, ) -> Result<(Value, CommunicationValue), String> { let initial_state = frame_data_to_json(state)?; validate_client_state_sync(&initial_state)?; let data = initial_state .as_object() .ok_or("ClientStateSync payload is not an object")?; let session_id = required_integer(data, "SessionId")?; let version = required_integer(data, "VersionNumber")?; let ack = CommunicationValue::new(CommunicationType::ClientStateAck) .with_id(request_ids.next()?) .add_typed_default(DataType::SessionId, number_to_data(session_id)) .add_typed_default(DataType::VersionNumber, number_to_data(version)); Ok((initial_state, ack)) } fn validate_client_state_sync(state: &Value) -> Result<(), String> { let data = state .as_object() .ok_or("ClientStateSync payload is not an object")?; if required_integer(data, "SessionId")? <= 0 { return Err("ClientStateSync SessionId must be a positive integer".into()); } for field in ["VersionNumber", "CacheSchemaVersion"] { if required_integer(data, field)? < 0 { return Err(format!("ClientStateSync {field} must be nonnegative")); } } match data.get("SyncMode").and_then(Value::as_str) { Some("full" | "delta") => {} _ => return Err("ClientStateSync SyncMode must be 'full' or 'delta'".into()), } for field in ["Contacts", "Communities", "Calls", "Messages"] { if !data.get(field).is_some_and(Value::is_array) { return Err(format!("ClientStateSync {field} must be an array")); } } for field in ["DeletedMessageIds", "DeletedContactIds"] { if let Some(value) = data.get(field) { let values = value .as_array() .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; if values.iter().any(|value| !value.is_number()) { return Err(format!("ClientStateSync {field} must contain numbers")); } } } validate_object_array(data, "Communities", |_| Ok(()))?; validate_object_array(data, "Contacts", validate_contact)?; validate_object_array(data, "Calls", validate_call)?; validate_object_array(data, "Messages", validate_message)?; Ok(()) } fn validate_object_array( data: &Map, field: &str, validate: impl Fn(&Map) -> Result<(), String>, ) -> Result<(), String> { let values = data .get(field) .and_then(Value::as_array) .ok_or_else(|| format!("ClientStateSync {field} must be an array"))?; for value in values { let object = value .as_object() .ok_or_else(|| format!("ClientStateSync {field} entries must be objects"))?; validate(object)?; } Ok(()) } fn validate_contact(contact: &Map) -> Result<(), String> { if !contact.get("UserId").is_some_and(Value::is_number) { return Err("ClientStateSync contact omitted numeric UserId".into()); } if let Some(messages) = contact.get("Messages") { let messages = messages .as_array() .ok_or("ClientStateSync contact Messages must be an array")?; for message in messages { validate_message( message .as_object() .ok_or("ClientStateSync contact message must be an object")?, )?; } } Ok(()) } fn validate_call(call: &Map) -> Result<(), String> { if !call.get("CallId").is_some_and(Value::is_string) { return Err("ClientStateSync call omitted string CallId".into()); } let members = call .get("CallMembers") .and_then(Value::as_array) .ok_or("ClientStateSync call omitted CallMembers array")?; if members.iter().any(|member| !member.is_number()) { return Err("ClientStateSync CallMembers must contain numbers".into()); } Ok(()) } fn validate_message(message: &Map) -> Result<(), String> { for field in ["SenderId", "SendTime"] { if !message.get(field).is_some_and(Value::is_number) { return Err(format!("ClientStateSync message omitted numeric {field}")); } } let content = message .get("Content") .and_then(Value::as_str) .ok_or("ClientStateSync message omitted string Content")?; STANDARD .decode(content) .or_else(|_| STANDARD_NO_PAD.decode(content)) .map_err(|_| "ClientStateSync message Content must be base64".to_string())?; if let Some(state) = message.get("MessageState") { match state.as_str() { Some("read" | "received" | "sent" | "sending" | "awaiting") => {} _ => return Err("ClientStateSync message has invalid MessageState".into()), } } Ok(()) } fn required_integer(data: &Map, field: &str) -> Result { let value = data .get(field) .ok_or_else(|| format!("ClientStateSync omitted {field}"))?; if let Some(value) = value.as_i64() { return Ok(value as i128); } value .as_u64() .map(|value| value as i128) .ok_or_else(|| format!("ClientStateSync {field} must be an integer")) } async fn await_initial_state( connection: &MTPConnection, ) -> Result<(CommunicationValue, Vec), String> { let mut buffered = Vec::new(); let deadline = tokio::time::Instant::now() + INITIAL_SYNC_TIMEOUT; loop { let frame = tokio::time::timeout_at(deadline, connection.receive()) .await .map_err(|_| "initial state synchronization timed out".to_string())? .map_err(|error| format!("initial state synchronization failed: {error}"))?; if frame.is_type(CommunicationType::ErrorNoIota) { return Err("No Iota is currently connected".into()); } if frame.is_type(CommunicationType::ClientStateSync) { return Ok((frame, buffered)); } if buffered.len() == MAX_BUFFERED_INITIAL_FRAMES { return Err("initial state synchronization buffered too many frames".into()); } buffered.push(frame); } } async fn resolve_endpoint(config: &MtpConfig) -> Result<(String, String), String> { if let (Some(url), Some(key)) = ( config.forced_omikron_url.as_ref(), config.forced_omikron_public_key.as_ref(), ) { return Ok((url.clone(), key.clone())); } #[derive(Deserialize)] struct Omikron { ip_address: String, port: u16, public_key: String, } let root = config.omega_url.trim_end_matches('/'); let client = reqwest::Client::builder() .connect_timeout(Duration::from_secs(10)) .timeout(Duration::from_secs(20)); #[cfg(target_os = "android")] let client = client.tls_certs_only( reqwest::Certificate::from_pem_bundle(android_root_certificates()) .map_err(|error| format!("invalid bundled root certificates: {error}"))?, ); let response = client .build() .map_err(|error| error.to_string())? .get(format!("{root}/api/get/omikron/{}", config.user_id)) .send() .await .map_err(|error| error.to_string())?; if !response.status().is_success() { return Err(format!("Omikron lookup failed: {}", response.status())); } let data: Omikron = response.json().await.map_err(|error| error.to_string())?; Ok(( format!("https://{}:{}", data.ip_address, data.port), data.public_key, )) } async fn handle_push( generation: u64, notification_tx: &mpsc::Sender, frame: CommunicationValue, ) { let manager = manager(); if let Ok(message) = frame_to_json(&frame) { manager.emit(MtpEvent::Message { generation, message, }); } if frame.is_type(CommunicationType::MessageState) && frame.get_str(DataType::MessageState) == Some("read") { if let Some(partner_id) = frame .get_data(DataType::ChatPartnerId) .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) { if let Err(error) = android_cancel_notification(partner_id) { eprintln!("failed to clear read message notification: {error}"); } } } if frame.is_type(CommunicationType::MessageLive) && !manager.ui_visible.load(Ordering::SeqCst) { if notification_tx.try_send(frame).is_err() { eprintln!("background message notification queue is full"); } } } async fn notify_message( config: &MtpConfig, connection: Arc, frame: &CommunicationValue, ) -> Result<(), String> { let sender_id = frame .get_data(DataType::SenderId) .and_then(DataValue::as_number) .and_then(|value| u64::try_from(value).ok()) .ok_or("MessageLive omitted SenderId")?; let message = frame .get_data(DataType::Message) .ok_or("MessageLive omitted Message")?; let content = container_value(message, DataType::AppContent) .and_then(DataValue::as_str) .ok_or("MessageLive omitted Content")?; let keyring_bytes = decode_browser_base64(&config.keyring)?; let keyring = Keyring::from_bytes(&keyring_bytes).map_err(|error| error.to_string())?; let chat_id = derive_chat_id(config.user_id, sender_id); let secret_id = format!("chat:{chat_id}:main"); let secret_request = CommunicationValue::new(CommunicationType::GetChatSecret) .with_id(connection.next_request_id().await?) .add_typed_default(DataType::UserId, DataValue::Str(config.user_id.to_string())) .add_typed_default(DataType::ChatId, DataValue::Str(chat_id.clone())) .add_typed_default(DataType::SecretId, DataValue::Str(secret_id.clone())); let secret = connection .mtp .request(&secret_request, None) .await .map_err(|error| error.to_string())?; if !secret.is_type(CommunicationType::ChatSecretResponse) { return Err(format!("GetChatSecret failed: {secret}")); } if secret.get_str(DataType::WrappingScheme) != Some(CHAT_SECRET_SCHEME) { return Err("unsupported chat secret wrapping scheme".into()); } let version = secret .get_data(DataType::VersionNumber) .and_then(DataValue::as_number) .ok_or("missing secret version")?; let encrypted_secret = secret .get_bytes(DataType::EncryptedSecret) .ok_or("missing encrypted secret")?; let kem_ciphertext = secret .get_bytes(DataType::KemCiphertext) .ok_or("missing KEM ciphertext")?; let shared = HybridKem::decapsulate(&keyring.kem_secret_key, kem_ciphertext) .map_err(|error| error.to_string())?; let wrapping_key = derive_encryption_key( &shared, CHAT_SECRET_SALT, format!("{chat_id}:{secret_id}:{version}").as_bytes(), ) .map_err(|error| error.to_string())?; let chat_secret = ChaCha20Poly1305::new(wrapping_key) .decrypt(encrypted_secret, b"") .map_err(|error| error.to_string())?; let message_key = derive_encryption_key(&chat_secret, CHAT_MESSAGE_SALT, b"message-content") .map_err(|error| error.to_string())?; let ciphertext = STANDARD .decode(content) .map_err(|error| error.to_string())?; let plaintext = ChaCha20Poly1305::new(message_key) .decrypt(&ciphertext, b"") .map_err(|error| error.to_string())?; let user_request = CommunicationValue::new(CommunicationType::GetUserData) .with_id(connection.next_request_id().await?) .add_typed_default( DataType::UserId, DataValue::UnsignedNumber(sender_id as u128), ); let user = connection .mtp .request(&user_request, None) .await .map_err(|error| error.to_string())?; let sender = user .get_str(DataType::Display) .or_else(|| user.get_str(DataType::Username)) .map(str::to_owned) .unwrap_or_else(|| format!("User {sender_id}")); let avatar = user .get_str(DataType::Avatar) .and_then(|avatar| decode_browser_base64(avatar).ok()); android_notify( sender_id, &sender, &String::from_utf8_lossy(&plaintext), avatar.as_deref(), )?; Ok(()) } fn derive_chat_id(own: u64, peer: u64) -> String { format!("{}:{}", own.min(peer), own.max(peer)) } fn decode_browser_base64(value: &str) -> Result, String> { let normalized: String = value .chars() .filter(|character| !character.is_ascii_whitespace()) .collect(); if normalized.is_empty() { return Err("value is empty".into()); } STANDARD .decode(&normalized) .or_else(|_| STANDARD_NO_PAD.decode(&normalized)) .map_err(|_| "invalid base64 encoding".into()) } fn decode_sdk_bytes(value: &str) -> Result, String> { let trimmed = value.trim(); let hex: String = trimmed .strip_prefix("0x") .or_else(|| trimmed.strip_prefix("0X")) .unwrap_or(trimmed) .chars() .filter(|character| !matches!(character, ' ' | '\t' | '\r' | '\n' | ':' | '_' | '-')) .collect(); if !hex.is_empty() && hex.chars().all(|character| character.is_ascii_hexdigit()) { if hex.len() % 2 != 0 { return Err("hex string has an odd length".into()); } return (0..hex.len()) .step_by(2) .map(|offset| { u8::from_str_radix(&hex[offset..offset + 2], 16) .map_err(|_| "invalid hex encoding".to_string()) }) .collect(); } decode_browser_base64(trimmed) } #[cfg(target_os = "android")] fn android_root_certificates() -> &'static Vec { static ROOTS: OnceLock> = OnceLock::new(); ROOTS.get_or_init(|| { let mut pem = Vec::new(); for certificate in webpki_root_certs::TLS_SERVER_ROOT_CERTS { pem.extend_from_slice(b"-----BEGIN CERTIFICATE-----\n"); let encoded = STANDARD.encode(certificate.as_ref()); for line in encoded.as_bytes().chunks(64) { pem.extend_from_slice(line); pem.push(b'\n'); } pem.extend_from_slice(b"-----END CERTIFICATE-----\n"); } pem.extend_from_slice(ROOT_YE_PEM); pem.extend_from_slice(YE1_PEM); pem }) } fn container_value(value: &DataValue, field: DataType) -> Option<&DataValue> { let id = field.try_to_id(&TypeMap::latest())?; value.get_field(id) } fn wire_field_name(name: &str) -> &str { match name { "Content" => "AppContent", "CreatedAt" => "AppCreatedAt", "MessageId" => "AppMessageId", _ => name, } } fn application_field_name(name: &str) -> &str { match name { "AppContent" => "Content", "AppCreatedAt" => "CreatedAt", "AppMessageId" => "MessageId", _ => name, } } fn json_to_frame(type_name: &str, data: Value, id: u32) -> Result { let comm_type = CommunicationType::from_name(type_name) .ok_or_else(|| format!("unknown communication type: {type_name}"))?; let mut frame = CommunicationValue::new(comm_type).with_id(id); let Value::Object(fields) = data else { return Err("MTP request data must be an object".into()); }; let mut translated_fields = std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { let wire_name = wire_field_name(&name).to_owned(); if !translated_fields.insert(wire_name.clone()) { return Err(format!( "duplicate MTP field after translation: {wire_name}" )); } let data_type = DataType::from_name(&wire_name).ok_or_else(|| format!("unknown data type: {name}"))?; frame = frame.add_typed_default(data_type, json_to_data(&wire_name, value)?); } Ok(frame) } fn json_to_data(field: &str, value: Value) -> Result { Ok(match value { Value::Null => DataValue::Null, Value::Bool(value) => DataValue::from(value), Value::Number(value) => { if let Some(integer) = value.as_u64() { DataValue::UnsignedNumber(integer as u128) } else if let Some(integer) = value.as_i64() { DataValue::SignedNumber(integer as i128) } else { DataValue::Float(value.as_f64().ok_or("invalid number")?) } } Value::String(value) => DataValue::Str(value), Value::Array(values) if is_bytes_field(field) => DataValue::Bytes( values .into_iter() .map(|value| { value .as_u64() .and_then(|n| u8::try_from(n).ok()) .ok_or("invalid byte") }) .collect::, _>>()?, ), Value::Array(values) => DataValue::Array( values .into_iter() .map(|value| json_to_data(field, value)) .collect::>()?, ), Value::Object(fields) => { let mut entries = Vec::with_capacity(fields.len()); let mut translated_fields = std::collections::HashSet::::with_capacity(fields.len()); for (name, value) in fields { let wire_name = wire_field_name(&name).to_owned(); if !translated_fields.insert(wire_name.clone()) { return Err(format!( "duplicate MTP field after translation: {wire_name}" )); } let data_type = DataType::from_name(&wire_name) .ok_or_else(|| format!("unknown nested data type: {name}"))?; let id = data_type .try_to_id(&TypeMap::latest()) .ok_or_else(|| format!("unmapped data type: {name}"))?; entries.push((id, json_to_data(&wire_name, value)?)); } DataValue::Container(entries) } }) } fn number_to_data(value: i128) -> DataValue { if value >= 0 { DataValue::UnsignedNumber(value as u128) } else { DataValue::SignedNumber(value) } } fn is_bytes_field(field: &str) -> bool { matches!( field, "EncryptedSecret" | "KemCiphertext" | "Payload" | "PublicKeys" ) } fn frame_to_json(frame: &CommunicationValue) -> Result { let mut result = Map::new(); if let Some(id) = frame.id() { result.insert("id".into(), Value::from(id)); } result.insert( "type".into(), Value::String(frame.get_type_name().unwrap_or("Unknown").to_owned()), ); result.insert("data".into(), frame_data_to_json(frame)?); Ok(Value::Object(result)) } fn frame_data_to_json(frame: &CommunicationValue) -> Result { let map = frame.type_map().cloned().unwrap_or_else(TypeMap::latest); let mut result = Map::new(); let entries = frame .data() .ok_or("MTP frame payload is not a data container")?; for (id, value) in entries { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown data type id: {}", id.0))?; let application_name = application_field_name(name); if result.contains_key(application_name) { return Err(format!( "duplicate MTP field after translation: {application_name}" )); } result.insert(application_name.to_owned(), data_to_json(value, &map)?); } Ok(Value::Object(result)) } fn data_to_json(value: &DataValue, map: &TypeMap) -> Result { Ok(match value { DataValue::BoolTrue => Value::Bool(true), DataValue::BoolFalse => Value::Bool(false), DataValue::Bool(value) => Value::Bool(*value), DataValue::SignedNumber(value) => number_to_json(*value)?, DataValue::UnsignedNumber(value) => { number_to_json(i128::try_from(*value).map_err(|_| "number exceeds JSON range")?)? } DataValue::Float(value) => serde_json::Number::from_f64(*value) .map(Value::Number) .ok_or("invalid float")?, DataValue::Str(value) => Value::String(value.clone()), DataValue::Bytes(value) => Value::Array(value.iter().copied().map(Value::from).collect()), DataValue::Array(values) => Value::Array( values .iter() .map(|value| data_to_json(value, map)) .collect::>()?, ), DataValue::Container(entries) => { let mut object = Map::new(); for (id, value) in entries { let name = map .data_type_name(id.0) .ok_or_else(|| format!("unknown nested data type id: {}", id.0))?; let application_name = application_field_name(name); if object.contains_key(application_name) { return Err(format!( "duplicate MTP field after translation: {application_name}" )); } object.insert(application_name.to_owned(), data_to_json(value, map)?); } Value::Object(object) } DataValue::Null => Value::Null, _ => return Err("encrypted protocol values cannot cross the frontend bridge".into()), }) } fn number_to_json(value: i128) -> Result { i64::try_from(value) .map(Value::from) .map_err(|_| "number exceeds JSON range".into()) } #[cfg(test)] mod tests { use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use serde_json::json; use super::{ container_value, decode_browser_base64, decode_sdk_bytes, frame_to_json, jittered_retry_delay, json_to_frame, prepare_initial_state_ack, RequestIdAllocator, }; #[test] fn browser_base64_accepts_file_whitespace_and_missing_padding() { assert_eq!(decode_browser_base64(" AQID\r\n").unwrap(), [1, 2, 3]); assert_eq!(decode_browser_base64("AQI").unwrap(), [1, 2]); } #[test] fn sdk_bytes_accept_hex_and_base64() { assert_eq!(decode_sdk_bytes("0x01:02-ff").unwrap(), [1, 2, 255]); assert_eq!(decode_sdk_bytes("AQI=").unwrap(), [1, 2]); } #[test] fn request_ids_are_nonzero_and_monotonic() { let ids = RequestIdAllocator::new(); assert_eq!(ids.next().unwrap(), 1); assert_eq!(ids.next().unwrap(), 2); } #[test] fn retry_jitter_stays_within_policy_bounds() { let delay = jittered_retry_delay(std::time::Duration::from_secs(10)); assert!(delay >= std::time::Duration::from_secs(8)); assert!(delay <= std::time::Duration::from_secs(12)); } #[test] fn json_content_uses_app_content_wire_type() { let frame = json_to_frame( "MessageEdit", json!({ "Content": "ciphertext", "ChatPartnerId": 42, "SendTime": 10, }), 1, ) .unwrap(); assert_eq!( frame .get_data(DataType::AppContent) .and_then(DataValue::as_str), Some("ciphertext") ); } #[test] fn nested_json_content_uses_app_content_wire_type() { let frame = json_to_frame( "MessageEdit", json!({ "Message": { "Content": "ciphertext" }, }), 1, ) .unwrap(); let message = frame.get_data(DataType::Message).unwrap(); assert_eq!( container_value(message, DataType::AppContent).and_then(DataValue::as_str), Some("ciphertext") ); } #[test] fn app_content_is_exposed_as_content_to_frontend() { let frame = CommunicationValue::new(CommunicationType::MessageEditLive) .with_id(1) .add_typed_default(DataType::AppContent, DataValue::Str("ciphertext".into())); let json = frame_to_json(&frame).unwrap(); assert_eq!(json["data"]["Content"], "ciphertext"); assert!(json["data"].get("AppContent").is_none()); } #[test] fn translated_field_collisions_are_rejected() { assert!(json_to_frame( "MessageEdit", json!({ "Content": "a", "AppContent": "b" }), 1, ) .is_err()); } fn valid_initial_state() -> CommunicationValue { CommunicationValue::new(CommunicationType::ClientStateSync) .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) .add_typed_default(DataType::Contacts, DataValue::Array(vec![])) .add_typed_default(DataType::Communities, DataValue::Array(vec![])) .add_typed_default(DataType::Calls, DataValue::Array(vec![])) .add_typed_default(DataType::Messages, DataValue::Array(vec![])) } #[test] fn valid_initial_state_is_prepared_before_ack() { let ids = RequestIdAllocator::new(); let (state, ack) = prepare_initial_state_ack(&valid_initial_state(), &ids).unwrap(); assert_eq!(state["SyncMode"], "full"); assert!(ack.is_type(CommunicationType::ClientStateAck)); assert_eq!(ack.id(), Some(1)); } #[test] fn malformed_initial_state_does_not_prepare_ack() { let ids = RequestIdAllocator::new(); let malformed = valid_initial_state() .add_typed_default(DataType::SyncMode, DataValue::Str("invalid".into())); assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); assert_eq!( ids.next().unwrap(), 1, "no acknowledgement ID was allocated" ); } #[test] fn malformed_nested_initial_state_does_not_prepare_ack() { let ids = RequestIdAllocator::new(); let malformed = CommunicationValue::new(CommunicationType::ClientStateSync) .add_typed_default(DataType::SessionId, DataValue::UnsignedNumber(1)) .add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::CacheSchemaVersion, DataValue::UnsignedNumber(0)) .add_typed_default(DataType::SyncMode, DataValue::Str("full".into())) .add_typed_default( DataType::Contacts, DataValue::Array(vec![DataValue::Container(vec![])]), ) .add_typed_default(DataType::Communities, DataValue::Array(vec![])) .add_typed_default(DataType::Calls, DataValue::Array(vec![])) .add_typed_default(DataType::Messages, DataValue::Array(vec![])); assert!(prepare_initial_state_ack(&malformed, &ids).is_err()); assert_eq!( ids.next().unwrap(), 1, "no acknowledgement ID was allocated" ); } } #[tauri::command] pub async fn mtp_request(type_name: String, data: Value) -> Result { manager().request(&type_name, data).await } #[tauri::command] pub fn mtp_status() -> MtpSnapshot { manager().snapshot() } #[tauri::command] pub fn mtp_store_credentials(config: MtpConfig) -> Result<(), String> { let serialized = serde_json::to_string(&config).map_err(|error| error.to_string())?; android_store_config(&serialized)?; *manager().config.write().expect("config lock poisoned") = Some(config); Ok(()) } #[tauri::command] pub fn mtp_has_credentials() -> Result { android_has_config() } #[tauri::command] pub fn mtp_load_keyring() -> Option { manager() .config .read() .expect("config lock poisoned") .as_ref() .map(|config| config.keyring.clone()) } #[tauri::command] pub fn mtp_set_enabled(enabled: bool) -> Result<(), String> { android_set_enabled(enabled)?; if !enabled { manager().stop(); } Ok(()) } #[tauri::command] pub fn mtp_is_ignoring_battery_optimizations() -> Result { android_is_ignoring_battery_optimizations() } #[tauri::command] pub fn mtp_request_battery_exemption() -> Result<(), String> { android_request_battery_exemption() } #[tauri::command] pub fn mtp_set_ui_visible(visible: bool) { manager().set_ui_visible(visible); } #[tauri::command] pub fn mtp_post_message_notification( sender_id: u64, sender: String, body: String, avatar: Option, ) -> Result { #[cfg(target_os = "android")] { let avatar = avatar.as_deref().map(decode_browser_base64).transpose()?; android_notify(sender_id, &sender, &body, avatar.as_deref())?; Ok(true) } #[cfg(not(target_os = "android"))] { let _ = (sender_id, sender, body, avatar); Ok(false) } } #[cfg(not(target_os = "android"))] fn android_store_config(_: &str) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_has_config() -> Result { Ok(manager() .config .read() .expect("config lock poisoned") .is_some()) } #[cfg(not(target_os = "android"))] fn android_set_enabled(_: bool) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_status(_: &str) {} #[cfg(not(target_os = "android"))] fn android_notify(_: u64, _: &str, _: &str, _: Option<&[u8]>) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_cancel_notification(_: u64) -> Result<(), String> { Ok(()) } #[cfg(not(target_os = "android"))] fn android_is_ignoring_battery_optimizations() -> Result { Ok(true) } #[cfg(not(target_os = "android"))] fn android_request_battery_exemption() -> Result<(), String> { Ok(()) } #[cfg(target_os = "android")] mod android { use std::sync::OnceLock; use jni::{ jni_sig, jni_str, objects::{Global, JClass, JObject, JString, JValue}, Env, EnvUnowned, JavaVM, }; use serde_json::Value; pub struct Host { vm: JavaVM, context: Global>, bridge: Global>, } static HOST: OnceLock = OnceLock::new(); pub fn attach(env: &mut Env, context: JObject) -> Result<(), String> { if HOST.get().is_some() { return Ok(()); } let class = env .find_class(jni_str!("net/tensamin/client/NativeMtpBridge")) .map_err(|e| e.to_string())?; let bridge = env .get_static_field( class, jni_str!("INSTANCE"), jni_sig!("Lnet/tensamin/client/NativeMtpBridge;"), ) .and_then(|value| value.l()) .map_err(|e| e.to_string())?; HOST.set(Host { vm: env.get_java_vm().map_err(|e| e.to_string())?, context: env.new_global_ref(context).map_err(|e| e.to_string())?, bridge: env.new_global_ref(bridge).map_err(|e| e.to_string())?, }) .map_err(|_| "Android MTP host is already attached".to_string()) } fn with_env(call: impl FnOnce(&mut Env, &Host) -> Result) -> Result { let host = HOST.get().ok_or("Android MTP host is not attached")?; host.vm .attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host))) .map_err(|e| e.to_string())? } pub fn store_config(config: &str) -> Result<(), String> { with_env(|env, host| { let value = env.new_string(config).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), jni_str!("storeConfig"), jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), &[ JValue::Object(host.context.as_obj()), JValue::Object(&value), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn has_config() -> Result { with_env(|env, host| { env.call_method( host.bridge.as_obj(), jni_str!("hasConfig"), jni_sig!("(Landroid/content/Context;)Z"), &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) .map_err(|e| e.to_string()) }) } pub fn set_enabled(enabled: bool) -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), jni_str!("setServiceEnabled"), jni_sig!("(Landroid/content/Context;Z)V"), &[ JValue::Object(host.context.as_obj()), JValue::Bool(enabled.into()), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn status(status: &str) { let _ = with_env(|env, host| { let status = env.new_string(status).map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), jni_str!("updateServiceStatus"), jni_sig!("(Landroid/content/Context;Ljava/lang/String;)V"), &[ JValue::Object(host.context.as_obj()), JValue::Object(&status), ], ) .map_err(|e| e.to_string())?; Ok(()) }); } pub fn notify( sender_id: u64, sender: &str, body: &str, avatar: Option<&[u8]>, ) -> Result<(), String> { with_env(|env, host| { let sender = env.new_string(sender).map_err(|e| e.to_string())?; let body = env.new_string(body).map_err(|e| e.to_string())?; let avatar = env .byte_array_from_slice(avatar.unwrap_or_default()) .map_err(|e| e.to_string())?; env.call_method( host.bridge.as_obj(), jni_str!("postMessageNotification"), jni_sig!("(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;[B)V"), &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), JValue::Object(&sender), JValue::Object(&body), JValue::Object(&avatar), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn cancel_notification(sender_id: u64) -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), jni_str!("cancelMessageNotification"), jni_sig!("(Landroid/content/Context;J)V"), &[ JValue::Object(host.context.as_obj()), JValue::Long(sender_id as i64), ], ) .map_err(|e| e.to_string())?; Ok(()) }) } pub fn is_ignoring_battery_optimizations() -> Result { with_env(|env, host| { env.call_method( host.bridge.as_obj(), jni_str!("isIgnoringBatteryOptimizations"), jni_sig!("(Landroid/content/Context;)Z"), &[JValue::Object(host.context.as_obj())], ) .and_then(|value| value.z()) .map_err(|e| e.to_string()) }) } pub fn request_battery_exemption() -> Result<(), String> { with_env(|env, host| { env.call_method( host.bridge.as_obj(), jni_str!("requestBatteryExemption"), jni_sig!("(Landroid/content/Context;)V"), &[JValue::Object(host.context.as_obj())], ) .map_err(|e| e.to_string())?; Ok(()) }) } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeAttach<'caller>( mut env: EnvUnowned<'caller>, _class: JClass, context: JObject<'caller>, ) { let _ = env.with_env(|env| { let _ = attach(env, context); Ok::<_, jni::errors::Error>(()) }); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStart<'caller>( mut env: EnvUnowned<'caller>, _class: JClass, config: JString<'caller>, ) { let _ = env.with_env(|env| { if let Ok(config) = config.mutf8_chars(env) { if let Ok(config) = serde_json::from_str(config.to_str().as_ref()) { super::manager().configure_and_start(config); } } Ok::<_, jni::errors::Error>(()) }); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeStop( _env: EnvUnowned, _class: JClass, ) { let _ = std::panic::catch_unwind(|| super::manager().stop()); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeSetUiState( _env: EnvUnowned, _class: JClass, visible: jni::sys::jboolean, ) { super::manager().set_ui_visible(visible); } #[no_mangle] pub extern "system" fn Java_net_tensamin_client_NativeMtpBridge_nativeLog<'caller>( mut env: EnvUnowned<'caller>, _class: JClass, level: jni::sys::jint, message: JString<'caller>, details: JString<'caller>, ) { let _ = env.with_env(|env| { if let (Ok(message), Ok(details)) = (message.mutf8_chars(env), details.mutf8_chars(env)) { let message = message.to_str(); let details = details.to_str(); super::manager().log( level.clamp(0, 3) as u8, message.into_owned(), (!details.is_empty()).then(|| Value::String(details.into_owned())), ); } Ok::<_, jni::errors::Error>(()) }); } } #[cfg(target_os = "android")] use android::{ cancel_notification as android_cancel_notification, has_config as android_has_config, is_ignoring_battery_optimizations as android_is_ignoring_battery_optimizations, notify as android_notify, request_battery_exemption as android_request_battery_exemption, set_enabled as android_set_enabled, status as android_status, store_config as android_store_config, };