]> Humopery - private/memberbot.git/commitdiff
implement SAS device verification (auto-accepted)
authorErik Mackdanz <erikmack@gmail.com>
Sun, 22 Feb 2026 19:08:50 +0000 (13:08 -0600)
committerErik Mackdanz <erikmack@gmail.com>
Sun, 22 Feb 2026 19:08:50 +0000 (13:08 -0600)
.gitignore
Cargo.lock
Cargo.toml
README.md
src/bot.rs

index fedaa2b1d2a80a9dc199386a1f248ca92f4cf54c..3b05ad8a9c2a32f848e27885fada85f7f4e0bcea 100644 (file)
@@ -1,2 +1,4 @@
 /target
 .env
+memberbot.db
+matrix_store.db/*
\ No newline at end of file
index 402a8939513848e590ba27bd749759931f8a567f..1fad3c241a03c8cd5e13de1e70ec8b009caa1374 100644 (file)
@@ -2120,6 +2120,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "dotenvy",
+ "futures-util",
  "matrix-sdk",
  "matrix-sdk-crypto",
  "matrix-sdk-sqlite",
index 885496beb49c50ae13d065c45c0b588f4f59d954..7c3f42931441ca39ee02e21cdc6691d4ec643541 100644 (file)
@@ -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"
 
index 27b01db3d49ae7312a2e9eba0ca855a9f7278290..0c42b43bd3d6696d989ad50db46761519c6a7f86 100644 (file)
--- a/README.md
+++ b/README.md
@@ -131,6 +131,14 @@ cargo test
 
 ## Deployment
 
+### Initializing a new database
+
+```bash
+sqlite3 memberbot.db <migrations/001_initial_schema.sql
+```
+
+TODO: add first admin user
+
 ### Docker (Coming Soon)
 
 ```bash
index f82c76e59569bc1b04673e343bb3fc4ab7d8ad2a..6af3de3a52df6da8ca4b1d7c75e0673c58a8937e 100644 (file)
@@ -1,16 +1,21 @@
 use std::sync::Arc;
 
 use anyhow::Result;
+use futures_util::StreamExt;
 use matrix_sdk::{
     config::SyncSettings,
+    encryption::verification::{SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState},
     room::Room,
     ruma::{
-        events::room::message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
+        events::{
+            key::verification::request::ToDeviceKeyVerificationRequestEvent,
+            room::message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent},
+        },
         OwnedRoomId, UserId,
     },
     Client,
 };
-use tracing::{error, info};
+use tracing::{error, info, warn};
 
 use crate::{
     config::Config,
@@ -59,6 +64,9 @@ impl Bot {
     pub async fn run(&self) -> 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