From: Erik Mackdanz Date: Sun, 22 Feb 2026 19:08:50 +0000 (-0600) Subject: implement SAS device verification (auto-accepted) X-Git-Url: https://git.humopery.space/?a=commitdiff_plain;h=3535771358dd499d2c70e31c7f0bcbd77c18085d;p=private%2Fmemberbot.git implement SAS device verification (auto-accepted) --- diff --git a/.gitignore b/.gitignore index fedaa2b..3b05ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target .env +memberbot.db +matrix_store.db/* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 402a893..1fad3c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2120,6 +2120,7 @@ version = "0.1.0" dependencies = [ "anyhow", "dotenvy", + "futures-util", "matrix-sdk", "matrix-sdk-crypto", "matrix-sdk-sqlite", diff --git a/Cargo.toml b/Cargo.toml index 885496b..7c3f429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -matrix-sdk = "0.16.0" +matrix-sdk = { version = "0.16.0", features = ["e2e-encryption"] } matrix-sdk-sqlite = "0.16.0" matrix-sdk-crypto = "0.16.0" sqlx = { version = "0.9.0-alpha.1", features = ["runtime-tokio", "sqlite"] } @@ -16,4 +16,5 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.123" dotenvy = "0.15.7" thiserror = "1.0.69" +futures-util = "0.3.30" diff --git a/README.md b/README.md index 27b01db..0c42b43 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,14 @@ cargo test ## Deployment +### Initializing a new database + +```bash +sqlite3 memberbot.db Result<()> { info!("Starting bot event listener..."); + // Start verification handler + self.start_verification_handler().await?; + // Register event handler let db = self.db.clone(); self.client @@ -79,6 +87,48 @@ impl Bot { Ok(()) } + async fn start_verification_handler(&self) -> Result<()> { + info!("Setting up verification handlers..."); + + let client = self.client.clone(); + + // To-device verification request handler + client.add_event_handler( + |ev: ToDeviceKeyVerificationRequestEvent, client: Client| async move { + info!("Received to-device verification request from {}", ev.sender); + if let Some(request) = client + .encryption() + .get_verification_request(&ev.sender, &ev.content.transaction_id) + .await + { + tokio::spawn(handle_verification_request(client, request)); + } else { + warn!("Failed to get verification request object for transaction {}", ev.content.transaction_id); + } + }, + ); + + // Room verification request handler + let client2 = self.client.clone(); + client2.add_event_handler(|ev: OriginalSyncRoomMessageEvent, client: Client| async move { + if let MessageType::VerificationRequest(_) = &ev.content.msgtype { + info!("Received room verification request from {}", ev.sender); + if let Some(request) = client + .encryption() + .get_verification_request(&ev.sender, &ev.event_id) + .await + { + tokio::spawn(handle_verification_request(client, request)); + } else { + warn!("Failed to get verification request object for event {}", ev.event_id); + } + } + }); + + info!("Verification handlers setup complete"); + Ok(()) + } + pub async fn send_message(&self, room_id: &OwnedRoomId, message: &str) -> Result<()> { let content = RoomMessageEventContent::text_plain(message); if let Some(room) = self.client.get_room(room_id) { @@ -408,3 +458,115 @@ async fn handle_verify_command( Ok(()) } +async fn handle_verification_request(client: Client, request: VerificationRequest) { + info!("Processing verification request from {}", request.other_user_id()); + + // Accept the verification request + match request.accept().await { + Ok(_) => info!("Accepted verification request from {}", request.other_user_id()), + Err(e) => { + error!("Failed to accept verification request from {}: {}", request.other_user_id(), e); + return; + } + } + + // Monitor the verification request state changes + let mut stream = request.changes(); + + while let Some(state) = stream.next().await { + match state { + VerificationRequestState::Transitioned { verification } => { + info!("Verification transitioned to specific method"); + match verification { + Verification::SasV1(sas) => { + tokio::spawn(handle_sas_verification(client, sas)); + break; + } + // QR code verification requires the qrcode feature which we don't have enabled + // So we only handle SAS verification + _ => { + info!("Unsupported verification method requested"); + break; + } + } + } + VerificationRequestState::Done => { + info!("Verification completed successfully with {}", request.other_user_id()); + break; + } + VerificationRequestState::Cancelled(cancel_info) => { + info!("Verification cancelled by {}: {}", request.other_user_id(), cancel_info.reason()); + break; + } + VerificationRequestState::Created { .. } + | VerificationRequestState::Requested { .. } + | VerificationRequestState::Ready { .. } => { + // These are intermediate states, we can ignore them + } + } + } +} + +async fn handle_sas_verification(_client: Client, sas: SasVerification) { + let user_id = sas.other_device().user_id(); + let device_id = sas.other_device().device_id(); + + info!("Starting SAS verification with {} ({})", user_id, device_id); + + // Accept the SAS verification + match sas.accept().await { + Ok(_) => info!("Accepted SAS verification with {} ({})", user_id, device_id), + Err(e) => { + error!("Failed to accept SAS verification with {} ({}): {}", user_id, device_id, e); + return; + } + } + + // Monitor the SAS verification state changes + let mut stream = sas.changes(); + + while let Some(state) = stream.next().await { + match state { + SasState::KeysExchanged { emojis, decimals } => { + info!("SAS keys exchanged with {} ({})", user_id, device_id); + + // Log the emojis for auditing purposes + if let Some(emoji_list) = emojis { + info!("SAS emojis: {:?}", emoji_list.emojis); + } + + // Log the decimal codes for auditing (decimals is a tuple of three u16 values) + info!("SAS decimals: {} {} {}", decimals.0, decimals.1, decimals.2); + + // Auto-confirm the SAS verification (bot doesn't need manual confirmation) + match sas.confirm().await { + Ok(_) => info!("Confirmed SAS verification with {} ({})", user_id, device_id), + Err(e) => error!("Failed to confirm SAS verification with {} ({}): {}", user_id, device_id, e), + } + } + SasState::Done { .. } => { + info!("SAS verification completed successfully with {} ({})", user_id, device_id); + + // The device is now verified locally + let device = sas.other_device(); + info!( + "Device {} {} is now verified locally with trust state: {:?}", + device.user_id(), + device.device_id(), + device.local_trust_state() + ); + break; + } + SasState::Cancelled(cancel_info) => { + info!("SAS verification cancelled by {} ({}): {}", user_id, device_id, cancel_info.reason()); + break; + } + SasState::Created { .. } + | SasState::Started { .. } + | SasState::Accepted { .. } + | SasState::Confirmed => { + // These are intermediate states, we can ignore them + } + } + } +} \ No newline at end of file