atagen

macltok

public
Star0
Watch0
fa9114fcf75f7bdf1a7cec3b3367d604cf7de472
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/encryption.rs

96fdf90aacd6atagen2026-09-08 08:24 UTC1
use std::sync::Arc;
96fdf90aacd6atagen2026-09-08 08:24 UTC2
96fdf90aacd6atagen2026-09-08 08:24 UTC3
use futures_util::StreamExt;
96fdf90aacd6atagen2026-09-08 08:24 UTC4
use matrix_sdk::{
96fdf90aacd6atagen2026-09-08 08:24 UTC5
    Client,
96fdf90aacd6atagen2026-09-08 08:24 UTC6
    encryption::{
96fdf90aacd6atagen2026-09-08 08:24 UTC7
        VerificationState,
96fdf90aacd6atagen2026-09-08 08:24 UTC8
        backups::BackupState,
96fdf90aacd6atagen2026-09-08 08:24 UTC9
        recovery::RecoveryState as SdkRecoveryState,
96fdf90aacd6atagen2026-09-08 08:24 UTC10
        verification::{
96fdf90aacd6atagen2026-09-08 08:24 UTC11
            SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState,
96fdf90aacd6atagen2026-09-08 08:24 UTC12
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC13
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC14
    ruma::events::key::verification::VerificationMethod,
96fdf90aacd6atagen2026-09-08 08:24 UTC15
};
96fdf90aacd6atagen2026-09-08 08:24 UTC16
use ruma::{OwnedDeviceId, UserId};
96fdf90aacd6atagen2026-09-08 08:24 UTC17
use tokio::{sync::Mutex, task::JoinHandle};
96fdf90aacd6atagen2026-09-08 08:24 UTC18
96fdf90aacd6atagen2026-09-08 08:24 UTC19
use crate::{
96fdf90aacd6atagen2026-09-08 08:24 UTC20
    CommandErrorKind, EncryptionState, Event, KeyBackupState, RecoveryState, VerificationAction,
96fdf90aacd6atagen2026-09-08 08:24 UTC21
    VerificationState as UiState, VerificationTarget, error::CommandError,
96fdf90aacd6atagen2026-09-08 08:24 UTC22
    model::VerificationFailure, transport::EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC23
};
96fdf90aacd6atagen2026-09-08 08:24 UTC24
96fdf90aacd6atagen2026-09-08 08:24 UTC25
pub(crate) fn encryption_state(
96fdf90aacd6atagen2026-09-08 08:24 UTC26
    device_id: OwnedDeviceId,
96fdf90aacd6atagen2026-09-08 08:24 UTC27
    recovery: SdkRecoveryState,
96fdf90aacd6atagen2026-09-08 08:24 UTC28
    verification: VerificationState,
96fdf90aacd6atagen2026-09-08 08:24 UTC29
    backup: BackupState,
96fdf90aacd6atagen2026-09-08 08:24 UTC30
) -> EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC31
    EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC32
        device_id,
96fdf90aacd6atagen2026-09-08 08:24 UTC33
        device_verified: matches!(verification, VerificationState::Verified),
96fdf90aacd6atagen2026-09-08 08:24 UTC34
        recovery: match recovery {
96fdf90aacd6atagen2026-09-08 08:24 UTC35
            SdkRecoveryState::Unknown => RecoveryState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC36
            SdkRecoveryState::Disabled => RecoveryState::Disabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC37
            SdkRecoveryState::Incomplete => RecoveryState::Incomplete,
96fdf90aacd6atagen2026-09-08 08:24 UTC38
            SdkRecoveryState::Enabled => RecoveryState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC39
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC40
        key_backup: match backup {
96fdf90aacd6atagen2026-09-08 08:24 UTC41
            BackupState::Unknown => KeyBackupState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC42
            BackupState::Creating => KeyBackupState::Creating,
96fdf90aacd6atagen2026-09-08 08:24 UTC43
            BackupState::Enabling => KeyBackupState::Enabling,
96fdf90aacd6atagen2026-09-08 08:24 UTC44
            BackupState::Resuming => KeyBackupState::Resuming,
96fdf90aacd6atagen2026-09-08 08:24 UTC45
            BackupState::Enabled => KeyBackupState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC46
            BackupState::Downloading => KeyBackupState::Downloading,
96fdf90aacd6atagen2026-09-08 08:24 UTC47
            BackupState::Disabling => KeyBackupState::Disabling,
96fdf90aacd6atagen2026-09-08 08:24 UTC48
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC49
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC50
}
96fdf90aacd6atagen2026-09-08 08:24 UTC51
96fdf90aacd6atagen2026-09-08 08:24 UTC52
#[derive(Default)]
96fdf90aacd6atagen2026-09-08 08:24 UTC53
pub(crate) struct VerificationController {
96fdf90aacd6atagen2026-09-08 08:24 UTC54
    active: Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC55
    task: Option<JoinHandle<()>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC56
}
96fdf90aacd6atagen2026-09-08 08:24 UTC57
96fdf90aacd6atagen2026-09-08 08:24 UTC58
#[derive(Default)]
96fdf90aacd6atagen2026-09-08 08:24 UTC59
struct VerificationSession {
96fdf90aacd6atagen2026-09-08 08:24 UTC60
    request: Option<VerificationRequest>,
96fdf90aacd6atagen2026-09-08 08:24 UTC61
    sas: Option<SasVerification>,
96fdf90aacd6atagen2026-09-08 08:24 UTC62
}
96fdf90aacd6atagen2026-09-08 08:24 UTC63
96fdf90aacd6atagen2026-09-08 08:24 UTC64
impl VerificationController {
96fdf90aacd6atagen2026-09-08 08:24 UTC65
    pub(crate) async fn apply(
96fdf90aacd6atagen2026-09-08 08:24 UTC66
        &mut self,
96fdf90aacd6atagen2026-09-08 08:24 UTC67
        _client: &Client,
96fdf90aacd6atagen2026-09-08 08:24 UTC68
        sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC69
        action: VerificationAction,
96fdf90aacd6atagen2026-09-08 08:24 UTC70
    ) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC71
        match action {
96fdf90aacd6atagen2026-09-08 08:24 UTC72
            VerificationAction::Confirm => {
96fdf90aacd6atagen2026-09-08 08:24 UTC73
                // Retain a handle across the operation: the watcher clears
96fdf90aacd6atagen2026-09-08 08:24 UTC74
                // `active` when it publishes a terminal event, but this clone
96fdf90aacd6atagen2026-09-08 08:24 UTC75
                // still observes the shared SDK state afterwards.
96fdf90aacd6atagen2026-09-08 08:24 UTC76
                let sas = self.active.lock().await.sas.clone();
96fdf90aacd6atagen2026-09-08 08:24 UTC77
                match self.confirm().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC78
                    Ok(()) => Ok(()),
96fdf90aacd6atagen2026-09-08 08:24 UTC79
                    Err(error) if error.kind == CommandErrorKind::VerificationInactive => {
96fdf90aacd6atagen2026-09-08 08:24 UTC80
                        Err(error)
96fdf90aacd6atagen2026-09-08 08:24 UTC81
                    }
96fdf90aacd6atagen2026-09-08 08:24 UTC82
                    // The confirm advanced local SDK state before its send failed,
96fdf90aacd6atagen2026-09-08 08:24 UTC83
                    // so the retained session could report success on retry without
96fdf90aacd6atagen2026-09-08 08:24 UTC84
                    // network traffic. Best-effort cancel (no lock held across the
96fdf90aacd6atagen2026-09-08 08:24 UTC85
                    // network), clear for restart, then report the terminal state.
96fdf90aacd6atagen2026-09-08 08:24 UTC86
                    Err(error) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC87
                        // Stop the watcher first so it cannot emit a stale
96fdf90aacd6atagen2026-09-08 08:24 UTC88
                        // Confirmed from the local transition we are unwinding.
96fdf90aacd6atagen2026-09-08 08:24 UTC89
                        self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC90
                        // The peer can complete or cancel the verification
96fdf90aacd6atagen2026-09-08 08:24 UTC91
                        // while our own Done send fails. The stopped watcher may
96fdf90aacd6atagen2026-09-08 08:24 UTC92
                        // already have published that terminal outcome (losing the
96fdf90aacd6atagen2026-09-08 08:24 UTC93
                        // claim below); otherwise this path owns it and must emit
96fdf90aacd6atagen2026-09-08 08:24 UTC94
                        // it instead of regressing to Cancelled.
96fdf90aacd6atagen2026-09-08 08:24 UTC95
                        if sas.as_ref().is_some_and(|sas| sas.is_done()) {
96fdf90aacd6atagen2026-09-08 08:24 UTC96
                            if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC97
                                let _ =
96fdf90aacd6atagen2026-09-08 08:24 UTC98
                                    sender.send(Event::VerificationChanged(UiState::Done)).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC99
                            }
96fdf90aacd6atagen2026-09-08 08:24 UTC100
                            return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC101
                        }
96fdf90aacd6atagen2026-09-08 08:24 UTC102
                        if let Some(reason) = sas
96fdf90aacd6atagen2026-09-08 08:24 UTC103
                            .as_ref()
96fdf90aacd6atagen2026-09-08 08:24 UTC104
                            .and_then(|sas| sas.cancel_info())
96fdf90aacd6atagen2026-09-08 08:24 UTC105
                            .map(|info| info.reason().to_owned())
96fdf90aacd6atagen2026-09-08 08:24 UTC106
                        {
96fdf90aacd6atagen2026-09-08 08:24 UTC107
                            if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC108
                                let _ = sender
96fdf90aacd6atagen2026-09-08 08:24 UTC109
                                    .send(Event::VerificationChanged(UiState::Cancelled(reason)))
96fdf90aacd6atagen2026-09-08 08:24 UTC110
                                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC111
                            }
96fdf90aacd6atagen2026-09-08 08:24 UTC112
                            return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC113
                        }
96fdf90aacd6atagen2026-09-08 08:24 UTC114
                        if let Some(sas) = sas {
96fdf90aacd6atagen2026-09-08 08:24 UTC115
                            if sas.cancel().await.is_err() {
96fdf90aacd6atagen2026-09-08 08:24 UTC116
                                // The best-effort unwind already advanced
96fdf90aacd6atagen2026-09-08 08:24 UTC117
                                // local state; clearing here keeps the failed
96fdf90aacd6atagen2026-09-08 08:24 UTC118
                                // cancellation from looking retryable. The
96fdf90aacd6atagen2026-09-08 08:24 UTC119
                                // send is owned only if the watcher has not
96fdf90aacd6atagen2026-09-08 08:24 UTC120
                                // already claimed the session for its own
96fdf90aacd6atagen2026-09-08 08:24 UTC121
                                // terminal outcome.
96fdf90aacd6atagen2026-09-08 08:24 UTC122
                                if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC123
                                    let _ = sender
96fdf90aacd6atagen2026-09-08 08:24 UTC124
                                        .send(Event::VerificationChanged(UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC125
                                            error.to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC126
                                        )))
96fdf90aacd6atagen2026-09-08 08:24 UTC127
                                        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC128
                                }
96fdf90aacd6atagen2026-09-08 08:24 UTC129
                                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC130
                            }
96fdf90aacd6atagen2026-09-08 08:24 UTC131
                            // The peer can complete while the unwind cancel is in
96fdf90aacd6atagen2026-09-08 08:24 UTC132
                            // flight and the SDK call no-ops; the Done then owns the
96fdf90aacd6atagen2026-09-08 08:24 UTC133
                            // terminal outcome, so report it instead of attributing
96fdf90aacd6atagen2026-09-08 08:24 UTC134
                            // it to the send failure.
96fdf90aacd6atagen2026-09-08 08:24 UTC135
                            if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC136
                                if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC137
                                    let _ = sender
96fdf90aacd6atagen2026-09-08 08:24 UTC138
                                        .send(Event::VerificationChanged(UiState::Done))
96fdf90aacd6atagen2026-09-08 08:24 UTC139
                                        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC140
                                }
96fdf90aacd6atagen2026-09-08 08:24 UTC141
                                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC142
                            }
96fdf90aacd6atagen2026-09-08 08:24 UTC143
                            if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC144
                                // Our own unwind cancel just marked the flow; it does
96fdf90aacd6atagen2026-09-08 08:24 UTC145
                                // not own the outcome yet, so fall through and report
96fdf90aacd6atagen2026-09-08 08:24 UTC146
                                // the send failure as Cancelled below. Only a peer
96fdf90aacd6atagen2026-09-08 08:24 UTC147
                                // cancellation racing the unwind owns it instead.
96fdf90aacd6atagen2026-09-08 08:24 UTC148
                                if !info.cancelled_by_us() {
96fdf90aacd6atagen2026-09-08 08:24 UTC149
                                    if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC150
                                        let _ = sender
96fdf90aacd6atagen2026-09-08 08:24 UTC151
                                            .send(Event::VerificationChanged(UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC152
                                                info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC153
                                            )))
96fdf90aacd6atagen2026-09-08 08:24 UTC154
                                            .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC155
                                    }
96fdf90aacd6atagen2026-09-08 08:24 UTC156
                                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC157
                                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC158
                                        info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC159
                                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC160
                                }
96fdf90aacd6atagen2026-09-08 08:24 UTC161
                            }
96fdf90aacd6atagen2026-09-08 08:24 UTC162
                        }
96fdf90aacd6atagen2026-09-08 08:24 UTC163
                        if take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC164
                            let _ = sender
96fdf90aacd6atagen2026-09-08 08:24 UTC165
                                .send(Event::VerificationChanged(UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC166
                                    error.to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC167
                                )))
96fdf90aacd6atagen2026-09-08 08:24 UTC168
                                .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC169
                        }
96fdf90aacd6atagen2026-09-08 08:24 UTC170
                        Err(error)
96fdf90aacd6atagen2026-09-08 08:24 UTC171
                    }
96fdf90aacd6atagen2026-09-08 08:24 UTC172
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC173
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC174
            VerificationAction::Reject => self.reject().await,
96fdf90aacd6atagen2026-09-08 08:24 UTC175
            VerificationAction::Cancel => {
96fdf90aacd6atagen2026-09-08 08:24 UTC176
                // The SDK no-ops a cancel of an already-settled flow, so a
96fdf90aacd6atagen2026-09-08 08:24 UTC177
                // failed claim inside `cancel` already reported the true
96fdf90aacd6atagen2026-09-08 08:24 UTC178
                // outcome as an error and owns no event here. Only the claim
96fdf90aacd6atagen2026-09-08 08:24 UTC179
                // winner emits the local Cancelled.
96fdf90aacd6atagen2026-09-08 08:24 UTC180
                let claimed = self.cancel_claimed(Some(&sender)).await?;
96fdf90aacd6atagen2026-09-08 08:24 UTC181
                if claimed {
96fdf90aacd6atagen2026-09-08 08:24 UTC182
                    sender
96fdf90aacd6atagen2026-09-08 08:24 UTC183
                        .send(Event::VerificationChanged(UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC184
                            "cancelled by this client".to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC185
                        )))
96fdf90aacd6atagen2026-09-08 08:24 UTC186
                        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC187
                        .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC188
                            CommandError::with_source(
96fdf90aacd6atagen2026-09-08 08:24 UTC189
                                CommandErrorKind::EventReceiverClosed,
96fdf90aacd6atagen2026-09-08 08:24 UTC190
                                "event receiver has closed",
96fdf90aacd6atagen2026-09-08 08:24 UTC191
                                source,
96fdf90aacd6atagen2026-09-08 08:24 UTC192
                            )
96fdf90aacd6atagen2026-09-08 08:24 UTC193
                        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC194
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC195
                Ok(())
96fdf90aacd6atagen2026-09-08 08:24 UTC196
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC197
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC198
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC199
96fdf90aacd6atagen2026-09-08 08:24 UTC200
    pub(crate) async fn start(
96fdf90aacd6atagen2026-09-08 08:24 UTC201
        &mut self,
96fdf90aacd6atagen2026-09-08 08:24 UTC202
        client: &Client,
96fdf90aacd6atagen2026-09-08 08:24 UTC203
        sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC204
        target: VerificationTarget,
96fdf90aacd6atagen2026-09-08 08:24 UTC205
    ) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC206
        client
96fdf90aacd6atagen2026-09-08 08:24 UTC207
            .encryption()
96fdf90aacd6atagen2026-09-08 08:24 UTC208
            .wait_for_e2ee_initialization_tasks()
96fdf90aacd6atagen2026-09-08 08:24 UTC209
            .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC210
        let own_user_id = client.user_id().ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC211
            CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC212
                CommandErrorKind::Unauthenticated,
96fdf90aacd6atagen2026-09-08 08:24 UTC213
                "the Matrix client is not authenticated",
96fdf90aacd6atagen2026-09-08 08:24 UTC214
            )
96fdf90aacd6atagen2026-09-08 08:24 UTC215
        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC216
        if self.has_active_verification().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC217
            self.cancel().await?;
96fdf90aacd6atagen2026-09-08 08:24 UTC218
        } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC219
            self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC220
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC221
        let request = match &target {
96fdf90aacd6atagen2026-09-08 08:24 UTC222
            VerificationTarget::OwnIdentity => {
96fdf90aacd6atagen2026-09-08 08:24 UTC223
                request_identity_verification(client, own_user_id).await?
96fdf90aacd6atagen2026-09-08 08:24 UTC224
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC225
            VerificationTarget::User(user_id) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC226
                request_identity_verification(client, user_id).await?
96fdf90aacd6atagen2026-09-08 08:24 UTC227
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC228
            VerificationTarget::Device { user_id, device_id } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC229
                let device = client
96fdf90aacd6atagen2026-09-08 08:24 UTC230
                    .encryption()
96fdf90aacd6atagen2026-09-08 08:24 UTC231
                    .get_device(user_id, device_id)
96fdf90aacd6atagen2026-09-08 08:24 UTC232
                    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC233
                    .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC234
                        CommandError::with_source(
96fdf90aacd6atagen2026-09-08 08:24 UTC235
                            CommandErrorKind::Encryption,
96fdf90aacd6atagen2026-09-08 08:24 UTC236
                            "reading the verification device failed",
96fdf90aacd6atagen2026-09-08 08:24 UTC237
                            source,
96fdf90aacd6atagen2026-09-08 08:24 UTC238
                        )
96fdf90aacd6atagen2026-09-08 08:24 UTC239
                    })?
96fdf90aacd6atagen2026-09-08 08:24 UTC240
                    .ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC241
                        CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC242
                            CommandErrorKind::VerificationUnsupported,
96fdf90aacd6atagen2026-09-08 08:24 UTC243
                            "the verification device is unavailable",
96fdf90aacd6atagen2026-09-08 08:24 UTC244
                        )
96fdf90aacd6atagen2026-09-08 08:24 UTC245
                    })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC246
                device
96fdf90aacd6atagen2026-09-08 08:24 UTC247
                    .request_verification_with_methods(vec![VerificationMethod::SasV1])
96fdf90aacd6atagen2026-09-08 08:24 UTC248
                    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC249
                    .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC250
                        CommandError::from_sdk("starting device verification failed", source)
96fdf90aacd6atagen2026-09-08 08:24 UTC251
                    })?
96fdf90aacd6atagen2026-09-08 08:24 UTC252
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC253
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC254
        self.active.lock().await.request = Some(request.clone());
96fdf90aacd6atagen2026-09-08 08:24 UTC255
        sender
96fdf90aacd6atagen2026-09-08 08:24 UTC256
            .send(Event::VerificationTargetChanged(target))
96fdf90aacd6atagen2026-09-08 08:24 UTC257
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC258
            .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC259
                CommandError::with_source(
96fdf90aacd6atagen2026-09-08 08:24 UTC260
                    CommandErrorKind::EventReceiverClosed,
96fdf90aacd6atagen2026-09-08 08:24 UTC261
                    "event receiver has closed",
96fdf90aacd6atagen2026-09-08 08:24 UTC262
                    source,
96fdf90aacd6atagen2026-09-08 08:24 UTC263
                )
96fdf90aacd6atagen2026-09-08 08:24 UTC264
            })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC265
        sender
96fdf90aacd6atagen2026-09-08 08:24 UTC266
            .send(Event::VerificationChanged(UiState::Waiting))
96fdf90aacd6atagen2026-09-08 08:24 UTC267
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC268
            .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC269
                CommandError::with_source(
96fdf90aacd6atagen2026-09-08 08:24 UTC270
                    CommandErrorKind::EventReceiverClosed,
96fdf90aacd6atagen2026-09-08 08:24 UTC271
                    "event receiver has closed",
96fdf90aacd6atagen2026-09-08 08:24 UTC272
                    source,
96fdf90aacd6atagen2026-09-08 08:24 UTC273
                )
96fdf90aacd6atagen2026-09-08 08:24 UTC274
            })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC275
        let active = self.active.clone();
96fdf90aacd6atagen2026-09-08 08:24 UTC276
        self.task = Some(tokio::spawn(async move {
96fdf90aacd6atagen2026-09-08 08:24 UTC277
            follow_request(request, active, sender).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC278
        }));
96fdf90aacd6atagen2026-09-08 08:24 UTC279
        Ok(())
96fdf90aacd6atagen2026-09-08 08:24 UTC280
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC281
96fdf90aacd6atagen2026-09-08 08:24 UTC282
    async fn confirm(&self) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC283
        let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC284
            CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC285
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC286
                "verification has not reached comparison",
96fdf90aacd6atagen2026-09-08 08:24 UTC287
            )
96fdf90aacd6atagen2026-09-08 08:24 UTC288
        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC289
        // Report a settled flow truthfully instead of as "not reached
96fdf90aacd6atagen2026-09-08 08:24 UTC290
        // comparison": the SDK would no-op the confirm with success.
96fdf90aacd6atagen2026-09-08 08:24 UTC291
        if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC292
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC293
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC294
                "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC295
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC296
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC297
        if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC298
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC299
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC300
                info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC301
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC302
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC303
        // Only the short-auth-string stage is confirmable. The SDK reports the
96fdf90aacd6atagen2026-09-08 08:24 UTC304
        // remote-MAC-arrived state through the same presentable flag, while an
96fdf90aacd6atagen2026-09-08 08:24 UTC305
        // early confirm would locally no-op and falsely report success.
96fdf90aacd6atagen2026-09-08 08:24 UTC306
        if !sas.can_be_presented() {
96fdf90aacd6atagen2026-09-08 08:24 UTC307
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC308
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC309
                "verification has not reached comparison",
96fdf90aacd6atagen2026-09-08 08:24 UTC310
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC311
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC312
        // Our own confirm can move the flow to Done, so only Done that predates
96fdf90aacd6atagen2026-09-08 08:24 UTC313
        // the send means the SDK no-op'd on a stale session; a cancellation
96fdf90aacd6atagen2026-09-08 08:24 UTC314
        // racing the send must not report success.
96fdf90aacd6atagen2026-09-08 08:24 UTC315
        let was_done = sas.is_done();
96fdf90aacd6atagen2026-09-08 08:24 UTC316
        sas.confirm()
96fdf90aacd6atagen2026-09-08 08:24 UTC317
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC318
            .map_err(|source| CommandError::from_sdk("confirming verification failed", source))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC319
        if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC320
            if was_done {
96fdf90aacd6atagen2026-09-08 08:24 UTC321
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC322
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC323
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC324
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC325
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC326
        } else if sas.is_cancelled() {
96fdf90aacd6atagen2026-09-08 08:24 UTC327
            let reason = sas
96fdf90aacd6atagen2026-09-08 08:24 UTC328
                .cancel_info()
96fdf90aacd6atagen2026-09-08 08:24 UTC329
                .map(|info| info.reason().to_owned())
96fdf90aacd6atagen2026-09-08 08:24 UTC330
                .unwrap_or_else(|| "verification was cancelled".to_owned());
96fdf90aacd6atagen2026-09-08 08:24 UTC331
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC332
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC333
                reason,
96fdf90aacd6atagen2026-09-08 08:24 UTC334
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC335
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC336
        Ok(())
96fdf90aacd6atagen2026-09-08 08:24 UTC337
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC338
96fdf90aacd6atagen2026-09-08 08:24 UTC339
    async fn reject(&self) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC340
        let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC341
            CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC342
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC343
                "verification has not reached comparison",
96fdf90aacd6atagen2026-09-08 08:24 UTC344
            )
96fdf90aacd6atagen2026-09-08 08:24 UTC345
        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC346
        // Report a settled flow truthfully instead of as "not reached
96fdf90aacd6atagen2026-09-08 08:24 UTC347
        // comparison": the SDK would no-op the mismatch with success.
96fdf90aacd6atagen2026-09-08 08:24 UTC348
        if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC349
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC350
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC351
                "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC352
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC353
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC354
        if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC355
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC356
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC357
                info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC358
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC359
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC360
        // Rejecting before the comparison stage would cancel a flow the user
96fdf90aacd6atagen2026-09-08 08:24 UTC361
        // never compared, so it requires the same presentable state.
96fdf90aacd6atagen2026-09-08 08:24 UTC362
        if !sas.can_be_presented() {
96fdf90aacd6atagen2026-09-08 08:24 UTC363
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC364
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC365
                "verification has not reached comparison",
96fdf90aacd6atagen2026-09-08 08:24 UTC366
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC367
        }
fa9114fcf75fatagen2026-09-24 12:51 UTC368
        sas.mismatch()
fa9114fcf75fatagen2026-09-24 12:51 UTC369
            .await
fa9114fcf75fatagen2026-09-24 12:51 UTC370
            .map_err(|source| CommandError::from_sdk("rejecting verification failed", source))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC371
        // A peer completing the flow concurrently must not be reported as
96fdf90aacd6atagen2026-09-08 08:24 UTC372
        // rejected. A remote cancellation racing the send turns the SDK call
96fdf90aacd6atagen2026-09-08 08:24 UTC373
        // into a no-op success, so report it instead of a false success.
96fdf90aacd6atagen2026-09-08 08:24 UTC374
        if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC375
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC376
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC377
                "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC378
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC379
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC380
        if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC381
            // Our own mismatch just marked the flow cancelled-by-us; that is the
96fdf90aacd6atagen2026-09-08 08:24 UTC382
            // expected reject path and stays Ok. Only a peer cancel reports an
96fdf90aacd6atagen2026-09-08 08:24 UTC383
            // error.
96fdf90aacd6atagen2026-09-08 08:24 UTC384
            if !info.cancelled_by_us() {
96fdf90aacd6atagen2026-09-08 08:24 UTC385
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC386
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC387
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC388
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC389
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC390
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC391
        Ok(())
96fdf90aacd6atagen2026-09-08 08:24 UTC392
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC393
    pub(crate) async fn cancel(&mut self) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC394
        self.cancel_claimed(None).await.map(|_| ())
96fdf90aacd6atagen2026-09-08 08:24 UTC395
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC396
96fdf90aacd6atagen2026-09-08 08:24 UTC397
    /// Return whether this action won the claim for a local cancellation.
96fdf90aacd6atagen2026-09-08 08:24 UTC398
    /// The optional sender reports a peer terminal outcome when it races after
96fdf90aacd6atagen2026-09-08 08:24 UTC399
    /// the claim; starting a replacement verification does not publish it.
96fdf90aacd6atagen2026-09-08 08:24 UTC400
    async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC401
        let (sas, request) = {
96fdf90aacd6atagen2026-09-08 08:24 UTC402
            let active = self.active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC403
            (active.sas.clone(), active.request.clone())
96fdf90aacd6atagen2026-09-08 08:24 UTC404
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC405
        if sas.is_none() && request.is_none() {
96fdf90aacd6atagen2026-09-08 08:24 UTC406
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC407
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC408
                "no verification is active",
96fdf90aacd6atagen2026-09-08 08:24 UTC409
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC410
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC411
        // The watcher owns terminal emission and claims the session when it
96fdf90aacd6atagen2026-09-08 08:24 UTC412
        // publishes. If the SDK is already terminal, the watcher either already
96fdf90aacd6atagen2026-09-08 08:24 UTC413
        // reported it or is about to: leave session and watcher alone so the
96fdf90aacd6atagen2026-09-08 08:24 UTC414
        // outcome is emitted exactly once, and report it instead of letting the
96fdf90aacd6atagen2026-09-08 08:24 UTC415
        // SDK no-op success regress Done to "cancelled by this client".
96fdf90aacd6atagen2026-09-08 08:24 UTC416
        if let Some(sas) = sas.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC417
            if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC418
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC419
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC420
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC421
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC422
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC423
            if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC424
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC425
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC426
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC427
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC428
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC429
        } else if let Some(request) = request.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC430
            if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC431
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC432
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC433
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC434
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC435
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC436
            if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC437
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC438
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC439
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC440
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC441
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC442
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC443
        // Own the terminal outcome from here: claim the session before
96fdf90aacd6atagen2026-09-08 08:24 UTC444
        // stopping the watcher so a watcher that already claimed (and will
96fdf90aacd6atagen2026-09-08 08:24 UTC445
        // emit its own terminal event) turns this into a truthful error
96fdf90aacd6atagen2026-09-08 08:24 UTC446
        // instead of a duplicate local cancel; no lock is held across sends.
96fdf90aacd6atagen2026-09-08 08:24 UTC447
        if !take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC448
            // Re-check the SDK: the claim loss may be a stale session after a
96fdf90aacd6atagen2026-09-08 08:24 UTC449
            // terminal outcome, or a concurrent start of a new flow. Only the
96fdf90aacd6atagen2026-09-08 08:24 UTC450
            // terminal case reports here; a live session belongs to the new
96fdf90aacd6atagen2026-09-08 08:24 UTC451
            // flow and must not be disturbed.
96fdf90aacd6atagen2026-09-08 08:24 UTC452
            if let Some(sas) = sas.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC453
                if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC454
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC455
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC456
                        "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC457
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC458
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC459
                if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC460
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC461
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC462
                        info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC463
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC464
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC465
            } else if let Some(request) = request.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC466
                if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC467
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC468
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC469
                        "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC470
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC471
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC472
                if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC473
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC474
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC475
                        info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC476
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC477
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC478
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC479
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC480
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC481
                "no verification is active",
96fdf90aacd6atagen2026-09-08 08:24 UTC482
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC483
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC484
        self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC485
        if let Some(sas) = sas {
96fdf90aacd6atagen2026-09-08 08:24 UTC486
            let result = sas.cancel().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC487
            if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC488
                report_claimed_terminal(sender, UiState::Done).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC489
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC490
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC491
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC492
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC493
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC494
            if let Some(info) = sas.cancel_info()
96fdf90aacd6atagen2026-09-08 08:24 UTC495
                && !info.cancelled_by_us()
96fdf90aacd6atagen2026-09-08 08:24 UTC496
            {
96fdf90aacd6atagen2026-09-08 08:24 UTC497
                let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC498
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC499
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC500
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC501
                    reason,
96fdf90aacd6atagen2026-09-08 08:24 UTC502
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC503
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC504
            if let Err(source) = result {
96fdf90aacd6atagen2026-09-08 08:24 UTC505
                let error = CommandError::from_sdk("cancelling verification failed", source);
96fdf90aacd6atagen2026-09-08 08:24 UTC506
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC507
                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC508
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC509
            Ok(true)
96fdf90aacd6atagen2026-09-08 08:24 UTC510
        } else if let Some(request) = request {
96fdf90aacd6atagen2026-09-08 08:24 UTC511
            let result = request.cancel().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC512
            if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC513
                report_claimed_terminal(sender, UiState::Done).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC514
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC515
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC516
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC517
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC518
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC519
            if let Some(info) = request.cancel_info()
96fdf90aacd6atagen2026-09-08 08:24 UTC520
                && !info.cancelled_by_us()
96fdf90aacd6atagen2026-09-08 08:24 UTC521
            {
96fdf90aacd6atagen2026-09-08 08:24 UTC522
                let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC523
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC524
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC525
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC526
                    reason,
96fdf90aacd6atagen2026-09-08 08:24 UTC527
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC528
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC529
            if let Err(source) = result {
96fdf90aacd6atagen2026-09-08 08:24 UTC530
                let error = CommandError::from_sdk("cancelling verification failed", source);
96fdf90aacd6atagen2026-09-08 08:24 UTC531
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC532
                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC533
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC534
            Ok(true)
96fdf90aacd6atagen2026-09-08 08:24 UTC535
        } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC536
            Ok(false)
96fdf90aacd6atagen2026-09-08 08:24 UTC537
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC538
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC539
96fdf90aacd6atagen2026-09-08 08:24 UTC540
    async fn has_active_verification(&self) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC541
        let active = self.active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC542
        active.request.is_some() || active.sas.is_some()
96fdf90aacd6atagen2026-09-08 08:24 UTC543
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC544
96fdf90aacd6atagen2026-09-08 08:24 UTC545
    async fn stop_task(&mut self) {
96fdf90aacd6atagen2026-09-08 08:24 UTC546
        if let Some(task) = self.task.take() {
96fdf90aacd6atagen2026-09-08 08:24 UTC547
            task.abort();
96fdf90aacd6atagen2026-09-08 08:24 UTC548
            let _ = task.await;
96fdf90aacd6atagen2026-09-08 08:24 UTC549
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC550
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC551
96fdf90aacd6atagen2026-09-08 08:24 UTC552
    pub(crate) async fn stop(&mut self) {
96fdf90aacd6atagen2026-09-08 08:24 UTC553
        // Shutdown path: terminate the watcher without touching the SDK so a
96fdf90aacd6atagen2026-09-08 08:24 UTC554
        // shutdown under lock cannot hang on network sends. No terminal event
96fdf90aacd6atagen2026-09-08 08:24 UTC555
        // is emitted; the session is simply forgotten locally.
96fdf90aacd6atagen2026-09-08 08:24 UTC556
        self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC557
        clear_verification(&self.active).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC558
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC559
}
96fdf90aacd6atagen2026-09-08 08:24 UTC560
96fdf90aacd6atagen2026-09-08 08:24 UTC561
async fn request_identity_verification(
96fdf90aacd6atagen2026-09-08 08:24 UTC562
    client: &Client,
96fdf90aacd6atagen2026-09-08 08:24 UTC563
    user_id: &UserId,
96fdf90aacd6atagen2026-09-08 08:24 UTC564
) -> Result<VerificationRequest, CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC565
    let identity = client
96fdf90aacd6atagen2026-09-08 08:24 UTC566
        .encryption()
96fdf90aacd6atagen2026-09-08 08:24 UTC567
        .request_user_identity(user_id)
96fdf90aacd6atagen2026-09-08 08:24 UTC568
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC569
        .map_err(|source| CommandError::from_sdk("requesting the user identity failed", source))?
96fdf90aacd6atagen2026-09-08 08:24 UTC570
        .ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC571
            CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC572
                CommandErrorKind::NoCrossSigningIdentity,
96fdf90aacd6atagen2026-09-08 08:24 UTC573
                "the user has no cross-signing identity",
96fdf90aacd6atagen2026-09-08 08:24 UTC574
            )
96fdf90aacd6atagen2026-09-08 08:24 UTC575
        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC576
    identity
96fdf90aacd6atagen2026-09-08 08:24 UTC577
        .request_verification_with_methods(vec![VerificationMethod::SasV1])
96fdf90aacd6atagen2026-09-08 08:24 UTC578
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC579
        .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC580
            CommandError::from_verification_request("starting verification failed", source)
96fdf90aacd6atagen2026-09-08 08:24 UTC581
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC582
}
96fdf90aacd6atagen2026-09-08 08:24 UTC583
96fdf90aacd6atagen2026-09-08 08:24 UTC584
async fn follow_request(
96fdf90aacd6atagen2026-09-08 08:24 UTC585
    request: VerificationRequest,
96fdf90aacd6atagen2026-09-08 08:24 UTC586
    active: Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC587
    sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC588
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC589
    let mut changes = request.changes();
96fdf90aacd6atagen2026-09-08 08:24 UTC590
    while let Some(state) = changes.next().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC591
        match state {
96fdf90aacd6atagen2026-09-08 08:24 UTC592
            VerificationRequestState::Created { .. } => {}
96fdf90aacd6atagen2026-09-08 08:24 UTC593
            VerificationRequestState::Requested { .. } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC594
                if let Err(error) = request
96fdf90aacd6atagen2026-09-08 08:24 UTC595
                    .accept_with_methods(vec![VerificationMethod::SasV1])
96fdf90aacd6atagen2026-09-08 08:24 UTC596
                    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC597
                {
96fdf90aacd6atagen2026-09-08 08:24 UTC598
                    tracing::warn!(error = %error, "accepting verification request failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC599
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC600
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC601
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC602
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC603
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC604
                        UiState::Failed(VerificationFailure::RequestAcceptance(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC605
                        "request acceptance failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC606
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC607
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC608
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC609
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC610
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC611
            VerificationRequestState::Ready { .. } => match request.start_sas().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC612
                Ok(Some(sas)) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC613
                    follow_sas(sas, active.clone(), sender.clone()).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC614
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC615
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC616
                Ok(None) => {}
96fdf90aacd6atagen2026-09-08 08:24 UTC617
                Err(error) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC618
                    tracing::warn!(error = %error, "starting SAS verification failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC619
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC620
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC621
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC622
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC623
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC624
                        UiState::Failed(VerificationFailure::SasStartup(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC625
                        "SAS startup failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC626
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC627
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC628
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC629
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC630
            },
96fdf90aacd6atagen2026-09-08 08:24 UTC631
            VerificationRequestState::Transitioned { verification } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC632
                if let Verification::SasV1(sas) = verification {
96fdf90aacd6atagen2026-09-08 08:24 UTC633
                    follow_sas(sas, active.clone(), sender.clone()).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC634
                } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC635
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC636
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC637
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC638
                        UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC639
                            "the other device selected an unsupported method".into(),
96fdf90aacd6atagen2026-09-08 08:24 UTC640
                        ),
96fdf90aacd6atagen2026-09-08 08:24 UTC641
                        "unsupported verification method",
96fdf90aacd6atagen2026-09-08 08:24 UTC642
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC643
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC644
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC645
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC646
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC647
            VerificationRequestState::Done => {
96fdf90aacd6atagen2026-09-08 08:24 UTC648
                report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC649
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC650
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC651
            VerificationRequestState::Cancelled(info) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC652
                report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC653
                    &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC654
                    &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC655
                    UiState::Cancelled(info.reason().to_owned()),
96fdf90aacd6atagen2026-09-08 08:24 UTC656
                    "cancellation",
96fdf90aacd6atagen2026-09-08 08:24 UTC657
                )
96fdf90aacd6atagen2026-09-08 08:24 UTC658
                .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC659
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC660
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC661
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC662
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC663
    tracing::debug!("verification request watcher ended unexpectedly");
96fdf90aacd6atagen2026-09-08 08:24 UTC664
    // The stream may end after the SDK already settled without delivering the
96fdf90aacd6atagen2026-09-08 08:24 UTC665
    // terminal update; report the settled outcome instead of StreamEnded, and
96fdf90aacd6atagen2026-09-08 08:24 UTC666
    // stay silent if an action path already claimed the session.
96fdf90aacd6atagen2026-09-08 08:24 UTC667
    if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC668
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC669
    } else if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC670
        let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC671
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC672
    } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC673
        report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC674
            &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC675
            &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC676
            UiState::Failed(VerificationFailure::RequestStreamEnded),
96fdf90aacd6atagen2026-09-08 08:24 UTC677
            "request stream termination",
96fdf90aacd6atagen2026-09-08 08:24 UTC678
        )
96fdf90aacd6atagen2026-09-08 08:24 UTC679
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC680
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC681
}
96fdf90aacd6atagen2026-09-08 08:24 UTC682
96fdf90aacd6atagen2026-09-08 08:24 UTC683
async fn follow_sas(
96fdf90aacd6atagen2026-09-08 08:24 UTC684
    sas: SasVerification,
96fdf90aacd6atagen2026-09-08 08:24 UTC685
    active: Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC686
    sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC687
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC688
    active.lock().await.sas = Some(sas.clone());
96fdf90aacd6atagen2026-09-08 08:24 UTC689
    let mut changes = sas.changes();
96fdf90aacd6atagen2026-09-08 08:24 UTC690
    while let Some(state) = changes.next().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC691
        let update = match state {
96fdf90aacd6atagen2026-09-08 08:24 UTC692
            SasState::Created { .. } | SasState::Accepted { .. } => continue,
96fdf90aacd6atagen2026-09-08 08:24 UTC693
            SasState::Started { .. } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC694
                if let Err(error) = sas.accept().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC695
                    tracing::warn!(error = %error, "accepting SAS verification failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC696
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC697
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC698
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC699
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC700
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC701
                        UiState::Failed(VerificationFailure::SasAcceptance(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC702
                        "SAS acceptance failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC703
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC704
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC705
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC706
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC707
                continue;
96fdf90aacd6atagen2026-09-08 08:24 UTC708
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC709
            SasState::KeysExchanged { emojis, decimals } => UiState::Comparison {
96fdf90aacd6atagen2026-09-08 08:24 UTC710
                emojis: emojis
96fdf90aacd6atagen2026-09-08 08:24 UTC711
                    .map(|short_auth| {
96fdf90aacd6atagen2026-09-08 08:24 UTC712
                        short_auth
96fdf90aacd6atagen2026-09-08 08:24 UTC713
                            .emojis
96fdf90aacd6atagen2026-09-08 08:24 UTC714
                            .into_iter()
96fdf90aacd6atagen2026-09-08 08:24 UTC715
                            .map(|emoji| (emoji.symbol.to_owned(), emoji.description.to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC716
                            .collect()
96fdf90aacd6atagen2026-09-08 08:24 UTC717
                    })
96fdf90aacd6atagen2026-09-08 08:24 UTC718
                    .unwrap_or_default(),
96fdf90aacd6atagen2026-09-08 08:24 UTC719
                decimals,
96fdf90aacd6atagen2026-09-08 08:24 UTC720
            },
96fdf90aacd6atagen2026-09-08 08:24 UTC721
            SasState::Confirmed => UiState::Confirmed,
96fdf90aacd6atagen2026-09-08 08:24 UTC722
            SasState::Done { .. } => UiState::Done,
96fdf90aacd6atagen2026-09-08 08:24 UTC723
            SasState::Cancelled(info) => UiState::Cancelled(info.reason().to_owned()),
96fdf90aacd6atagen2026-09-08 08:24 UTC724
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC725
        let terminal = matches!(update, UiState::Done | UiState::Cancelled(_));
96fdf90aacd6atagen2026-09-08 08:24 UTC726
        // Claim the session before the terminal send so a racing Cancel observes
96fdf90aacd6atagen2026-09-08 08:24 UTC727
        // no active session and the SDK no-op cannot regress Done back to
96fdf90aacd6atagen2026-09-08 08:24 UTC728
        // "cancelled by this client". A lost claim means an action path already
96fdf90aacd6atagen2026-09-08 08:24 UTC729
        // owns the terminal outcome, so stay silent instead of duplicating it.
96fdf90aacd6atagen2026-09-08 08:24 UTC730
        if terminal && !take_verification(&active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC731
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC732
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC733
        if sender
96fdf90aacd6atagen2026-09-08 08:24 UTC734
            .send(Event::VerificationChanged(update))
96fdf90aacd6atagen2026-09-08 08:24 UTC735
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC736
            .is_err()
96fdf90aacd6atagen2026-09-08 08:24 UTC737
        {
96fdf90aacd6atagen2026-09-08 08:24 UTC738
            tracing::debug!("SAS watcher stopped because the event receiver closed");
96fdf90aacd6atagen2026-09-08 08:24 UTC739
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC740
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC741
        if terminal {
96fdf90aacd6atagen2026-09-08 08:24 UTC742
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC743
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC744
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC745
    tracing::debug!("SAS watcher ended unexpectedly");
96fdf90aacd6atagen2026-09-08 08:24 UTC746
    // The stream may end after the SDK already settled without delivering the
96fdf90aacd6atagen2026-09-08 08:24 UTC747
    // terminal update; report the settled outcome instead of StreamEnded, and
96fdf90aacd6atagen2026-09-08 08:24 UTC748
    // stay silent if an action path already claimed the session.
96fdf90aacd6atagen2026-09-08 08:24 UTC749
    if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC750
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC751
    } else if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC752
        let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC753
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC754
    } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC755
        report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC756
            &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC757
            &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC758
            UiState::Failed(VerificationFailure::SasStreamEnded),
96fdf90aacd6atagen2026-09-08 08:24 UTC759
            "SAS stream termination",
96fdf90aacd6atagen2026-09-08 08:24 UTC760
        )
96fdf90aacd6atagen2026-09-08 08:24 UTC761
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC762
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC763
}
96fdf90aacd6atagen2026-09-08 08:24 UTC764
96fdf90aacd6atagen2026-09-08 08:24 UTC765
async fn report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC766
    active: &Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC767
    sender: &EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC768
    state: UiState,
96fdf90aacd6atagen2026-09-08 08:24 UTC769
    context: &'static str,
96fdf90aacd6atagen2026-09-08 08:24 UTC770
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC771
    if take_verification(active).await
96fdf90aacd6atagen2026-09-08 08:24 UTC772
        && sender
96fdf90aacd6atagen2026-09-08 08:24 UTC773
            .send(Event::VerificationChanged(state))
96fdf90aacd6atagen2026-09-08 08:24 UTC774
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC775
            .is_err()
96fdf90aacd6atagen2026-09-08 08:24 UTC776
    {
96fdf90aacd6atagen2026-09-08 08:24 UTC777
        tracing::debug!(
96fdf90aacd6atagen2026-09-08 08:24 UTC778
            context,
96fdf90aacd6atagen2026-09-08 08:24 UTC779
            "verification watcher could not report terminal outcome"
96fdf90aacd6atagen2026-09-08 08:24 UTC780
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC781
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC782
}
96fdf90aacd6atagen2026-09-08 08:24 UTC783
96fdf90aacd6atagen2026-09-08 08:24 UTC784
async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {
96fdf90aacd6atagen2026-09-08 08:24 UTC785
    if let Some(sender) = sender {
96fdf90aacd6atagen2026-09-08 08:24 UTC786
        let _ = sender.send(Event::VerificationChanged(state)).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC787
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC788
}
96fdf90aacd6atagen2026-09-08 08:24 UTC789
96fdf90aacd6atagen2026-09-08 08:24 UTC790
async fn clear_verification(active: &Arc<Mutex<VerificationSession>>) {
96fdf90aacd6atagen2026-09-08 08:24 UTC791
    let mut active = active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC792
    active.sas.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC793
    active.request.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC794
}
96fdf90aacd6atagen2026-09-08 08:24 UTC795
96fdf90aacd6atagen2026-09-08 08:24 UTC796
/// Claim the active session for terminal emission, if one is still present.
96fdf90aacd6atagen2026-09-08 08:24 UTC797
///
96fdf90aacd6atagen2026-09-08 08:24 UTC798
/// Exactly one party (watcher or action path) wins the claim and may emit the
96fdf90aacd6atagen2026-09-08 08:24 UTC799
/// terminal event; the loser stays silent so Done is never overwritten by a
96fdf90aacd6atagen2026-09-08 08:24 UTC800
/// later local Cancelled/Failed.
96fdf90aacd6atagen2026-09-08 08:24 UTC801
async fn take_verification(active: &Arc<Mutex<VerificationSession>>) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC802
    let mut active = active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC803
    let had = active.sas.is_some() || active.request.is_some();
96fdf90aacd6atagen2026-09-08 08:24 UTC804
    active.sas.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC805
    active.request.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC806
    had
96fdf90aacd6atagen2026-09-08 08:24 UTC807
}
96fdf90aacd6atagen2026-09-08 08:24 UTC808
96fdf90aacd6atagen2026-09-08 08:24 UTC809
#[cfg(test)]
96fdf90aacd6atagen2026-09-08 08:24 UTC810
mod tests {
96fdf90aacd6atagen2026-09-08 08:24 UTC811
    use matrix_sdk::encryption::VerificationState;
96fdf90aacd6atagen2026-09-08 08:24 UTC812
    use matrix_sdk::encryption::backups::BackupState;
96fdf90aacd6atagen2026-09-08 08:24 UTC813
    use ruma::owned_device_id;
96fdf90aacd6atagen2026-09-08 08:24 UTC814
96fdf90aacd6atagen2026-09-08 08:24 UTC815
    use super::*;
96fdf90aacd6atagen2026-09-08 08:24 UTC816
96fdf90aacd6atagen2026-09-08 08:24 UTC817
    #[test]
96fdf90aacd6atagen2026-09-08 08:24 UTC818
    fn sdk_security_state_is_normalized() {
96fdf90aacd6atagen2026-09-08 08:24 UTC819
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC820
            encryption_state(
96fdf90aacd6atagen2026-09-08 08:24 UTC821
                owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC822
                SdkRecoveryState::Incomplete,
96fdf90aacd6atagen2026-09-08 08:24 UTC823
                VerificationState::Unverified,
96fdf90aacd6atagen2026-09-08 08:24 UTC824
                BackupState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC825
            ),
96fdf90aacd6atagen2026-09-08 08:24 UTC826
            EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC827
                device_id: owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC828
                device_verified: false,
96fdf90aacd6atagen2026-09-08 08:24 UTC829
                recovery: RecoveryState::Incomplete,
96fdf90aacd6atagen2026-09-08 08:24 UTC830
                key_backup: KeyBackupState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC831
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC832
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC833
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC834
            encryption_state(
96fdf90aacd6atagen2026-09-08 08:24 UTC835
                owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC836
                SdkRecoveryState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC837
                VerificationState::Verified,
96fdf90aacd6atagen2026-09-08 08:24 UTC838
                BackupState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC839
            ),
96fdf90aacd6atagen2026-09-08 08:24 UTC840
            EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC841
                device_id: owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC842
                device_verified: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC843
                recovery: RecoveryState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC844
                key_backup: KeyBackupState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC845
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC846
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC847
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC848
96fdf90aacd6atagen2026-09-08 08:24 UTC849
    #[tokio::test]
96fdf90aacd6atagen2026-09-08 08:24 UTC850
    async fn comparison_actions_require_an_active_sas() {
96fdf90aacd6atagen2026-09-08 08:24 UTC851
        let mut controller = VerificationController::default();
96fdf90aacd6atagen2026-09-08 08:24 UTC852
96fdf90aacd6atagen2026-09-08 08:24 UTC853
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC854
            controller.confirm().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC855
            "verification has not reached comparison"
96fdf90aacd6atagen2026-09-08 08:24 UTC856
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC857
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC858
            controller.reject().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC859
            "verification has not reached comparison"
96fdf90aacd6atagen2026-09-08 08:24 UTC860
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC861
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC862
            controller.cancel().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC863
            "no verification is active"
96fdf90aacd6atagen2026-09-08 08:24 UTC864
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC865
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC866
}