atagen

macltok

public
Star0
Watch0
96fdf90aacd6dc2b8bd3d63773234245b389d28c
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
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC368
        // A peer completing the flow concurrently must not be reported as
96fdf90aacd6atagen2026-09-08 08:24 UTC369
        // rejected. A remote cancellation racing the send turns the SDK call
96fdf90aacd6atagen2026-09-08 08:24 UTC370
        // into a no-op success, so report it instead of a false success.
96fdf90aacd6atagen2026-09-08 08:24 UTC371
        if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC372
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC373
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC374
                "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC375
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC376
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC377
        if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC378
            // Our own mismatch just marked the flow cancelled-by-us; that is the
96fdf90aacd6atagen2026-09-08 08:24 UTC379
            // expected reject path and stays Ok. Only a peer cancel reports an
96fdf90aacd6atagen2026-09-08 08:24 UTC380
            // error.
96fdf90aacd6atagen2026-09-08 08:24 UTC381
            if !info.cancelled_by_us() {
96fdf90aacd6atagen2026-09-08 08:24 UTC382
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC383
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC384
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC385
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC386
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC387
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC388
        Ok(())
96fdf90aacd6atagen2026-09-08 08:24 UTC389
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC390
    pub(crate) async fn cancel(&mut self) -> Result<(), CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC391
        self.cancel_claimed(None).await.map(|_| ())
96fdf90aacd6atagen2026-09-08 08:24 UTC392
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC393
96fdf90aacd6atagen2026-09-08 08:24 UTC394
    /// Return whether this action won the claim for a local cancellation.
96fdf90aacd6atagen2026-09-08 08:24 UTC395
    /// The optional sender reports a peer terminal outcome when it races after
96fdf90aacd6atagen2026-09-08 08:24 UTC396
    /// the claim; starting a replacement verification does not publish it.
96fdf90aacd6atagen2026-09-08 08:24 UTC397
    async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC398
        let (sas, request) = {
96fdf90aacd6atagen2026-09-08 08:24 UTC399
            let active = self.active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC400
            (active.sas.clone(), active.request.clone())
96fdf90aacd6atagen2026-09-08 08:24 UTC401
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC402
        if sas.is_none() && request.is_none() {
96fdf90aacd6atagen2026-09-08 08:24 UTC403
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC404
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC405
                "no verification is active",
96fdf90aacd6atagen2026-09-08 08:24 UTC406
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC407
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC408
        // The watcher owns terminal emission and claims the session when it
96fdf90aacd6atagen2026-09-08 08:24 UTC409
        // publishes. If the SDK is already terminal, the watcher either already
96fdf90aacd6atagen2026-09-08 08:24 UTC410
        // reported it or is about to: leave session and watcher alone so the
96fdf90aacd6atagen2026-09-08 08:24 UTC411
        // outcome is emitted exactly once, and report it instead of letting the
96fdf90aacd6atagen2026-09-08 08:24 UTC412
        // SDK no-op success regress Done to "cancelled by this client".
96fdf90aacd6atagen2026-09-08 08:24 UTC413
        if let Some(sas) = sas.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC414
            if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC415
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC416
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC417
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC418
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC419
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC420
            if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC421
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC422
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC423
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC424
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC425
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC426
        } else if let Some(request) = request.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC427
            if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC428
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC429
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC430
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC431
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC432
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC433
            if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC434
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC435
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC436
                    info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC437
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC438
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC439
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC440
        // Own the terminal outcome from here: claim the session before
96fdf90aacd6atagen2026-09-08 08:24 UTC441
        // stopping the watcher so a watcher that already claimed (and will
96fdf90aacd6atagen2026-09-08 08:24 UTC442
        // emit its own terminal event) turns this into a truthful error
96fdf90aacd6atagen2026-09-08 08:24 UTC443
        // instead of a duplicate local cancel; no lock is held across sends.
96fdf90aacd6atagen2026-09-08 08:24 UTC444
        if !take_verification(&self.active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC445
            // Re-check the SDK: the claim loss may be a stale session after a
96fdf90aacd6atagen2026-09-08 08:24 UTC446
            // terminal outcome, or a concurrent start of a new flow. Only the
96fdf90aacd6atagen2026-09-08 08:24 UTC447
            // terminal case reports here; a live session belongs to the new
96fdf90aacd6atagen2026-09-08 08:24 UTC448
            // flow and must not be disturbed.
96fdf90aacd6atagen2026-09-08 08:24 UTC449
            if let Some(sas) = sas.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC450
                if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC451
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC452
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC453
                        "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC454
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC455
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC456
                if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC457
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC458
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC459
                        info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC460
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC461
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC462
            } else if let Some(request) = request.as_ref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC463
                if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC464
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC465
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC466
                        "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC467
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC468
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC469
                if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC470
                    return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC471
                        CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC472
                        info.reason().to_owned(),
96fdf90aacd6atagen2026-09-08 08:24 UTC473
                    ));
96fdf90aacd6atagen2026-09-08 08:24 UTC474
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC475
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC476
            return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC477
                CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC478
                "no verification is active",
96fdf90aacd6atagen2026-09-08 08:24 UTC479
            ));
96fdf90aacd6atagen2026-09-08 08:24 UTC480
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC481
        self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC482
        if let Some(sas) = sas {
96fdf90aacd6atagen2026-09-08 08:24 UTC483
            let result = sas.cancel().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC484
            if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC485
                report_claimed_terminal(sender, UiState::Done).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC486
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC487
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC488
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC489
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC490
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC491
            if let Some(info) = sas.cancel_info()
96fdf90aacd6atagen2026-09-08 08:24 UTC492
                && !info.cancelled_by_us()
96fdf90aacd6atagen2026-09-08 08:24 UTC493
            {
96fdf90aacd6atagen2026-09-08 08:24 UTC494
                let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC495
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC496
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC497
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC498
                    reason,
96fdf90aacd6atagen2026-09-08 08:24 UTC499
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC500
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC501
            if let Err(source) = result {
96fdf90aacd6atagen2026-09-08 08:24 UTC502
                let error = CommandError::from_sdk("cancelling verification failed", source);
96fdf90aacd6atagen2026-09-08 08:24 UTC503
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC504
                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC505
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC506
            Ok(true)
96fdf90aacd6atagen2026-09-08 08:24 UTC507
        } else if let Some(request) = request {
96fdf90aacd6atagen2026-09-08 08:24 UTC508
            let result = request.cancel().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC509
            if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC510
                report_claimed_terminal(sender, UiState::Done).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC511
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC512
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC513
                    "verification is already complete",
96fdf90aacd6atagen2026-09-08 08:24 UTC514
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC515
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC516
            if let Some(info) = request.cancel_info()
96fdf90aacd6atagen2026-09-08 08:24 UTC517
                && !info.cancelled_by_us()
96fdf90aacd6atagen2026-09-08 08:24 UTC518
            {
96fdf90aacd6atagen2026-09-08 08:24 UTC519
                let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC520
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC521
                return Err(CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC522
                    CommandErrorKind::VerificationInactive,
96fdf90aacd6atagen2026-09-08 08:24 UTC523
                    reason,
96fdf90aacd6atagen2026-09-08 08:24 UTC524
                ));
96fdf90aacd6atagen2026-09-08 08:24 UTC525
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC526
            if let Err(source) = result {
96fdf90aacd6atagen2026-09-08 08:24 UTC527
                let error = CommandError::from_sdk("cancelling verification failed", source);
96fdf90aacd6atagen2026-09-08 08:24 UTC528
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC529
                return Err(error);
96fdf90aacd6atagen2026-09-08 08:24 UTC530
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC531
            Ok(true)
96fdf90aacd6atagen2026-09-08 08:24 UTC532
        } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC533
            Ok(false)
96fdf90aacd6atagen2026-09-08 08:24 UTC534
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC535
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC536
96fdf90aacd6atagen2026-09-08 08:24 UTC537
    async fn has_active_verification(&self) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC538
        let active = self.active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC539
        active.request.is_some() || active.sas.is_some()
96fdf90aacd6atagen2026-09-08 08:24 UTC540
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC541
96fdf90aacd6atagen2026-09-08 08:24 UTC542
    async fn stop_task(&mut self) {
96fdf90aacd6atagen2026-09-08 08:24 UTC543
        if let Some(task) = self.task.take() {
96fdf90aacd6atagen2026-09-08 08:24 UTC544
            task.abort();
96fdf90aacd6atagen2026-09-08 08:24 UTC545
            let _ = task.await;
96fdf90aacd6atagen2026-09-08 08:24 UTC546
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC547
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC548
96fdf90aacd6atagen2026-09-08 08:24 UTC549
    pub(crate) async fn stop(&mut self) {
96fdf90aacd6atagen2026-09-08 08:24 UTC550
        // Shutdown path: terminate the watcher without touching the SDK so a
96fdf90aacd6atagen2026-09-08 08:24 UTC551
        // shutdown under lock cannot hang on network sends. No terminal event
96fdf90aacd6atagen2026-09-08 08:24 UTC552
        // is emitted; the session is simply forgotten locally.
96fdf90aacd6atagen2026-09-08 08:24 UTC553
        self.stop_task().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC554
        clear_verification(&self.active).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC555
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC556
}
96fdf90aacd6atagen2026-09-08 08:24 UTC557
96fdf90aacd6atagen2026-09-08 08:24 UTC558
async fn request_identity_verification(
96fdf90aacd6atagen2026-09-08 08:24 UTC559
    client: &Client,
96fdf90aacd6atagen2026-09-08 08:24 UTC560
    user_id: &UserId,
96fdf90aacd6atagen2026-09-08 08:24 UTC561
) -> Result<VerificationRequest, CommandError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC562
    let identity = client
96fdf90aacd6atagen2026-09-08 08:24 UTC563
        .encryption()
96fdf90aacd6atagen2026-09-08 08:24 UTC564
        .request_user_identity(user_id)
96fdf90aacd6atagen2026-09-08 08:24 UTC565
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC566
        .map_err(|source| CommandError::from_sdk("requesting the user identity failed", source))?
96fdf90aacd6atagen2026-09-08 08:24 UTC567
        .ok_or_else(|| {
96fdf90aacd6atagen2026-09-08 08:24 UTC568
            CommandError::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC569
                CommandErrorKind::NoCrossSigningIdentity,
96fdf90aacd6atagen2026-09-08 08:24 UTC570
                "the user has no cross-signing identity",
96fdf90aacd6atagen2026-09-08 08:24 UTC571
            )
96fdf90aacd6atagen2026-09-08 08:24 UTC572
        })?;
96fdf90aacd6atagen2026-09-08 08:24 UTC573
    identity
96fdf90aacd6atagen2026-09-08 08:24 UTC574
        .request_verification_with_methods(vec![VerificationMethod::SasV1])
96fdf90aacd6atagen2026-09-08 08:24 UTC575
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC576
        .map_err(|source| {
96fdf90aacd6atagen2026-09-08 08:24 UTC577
            CommandError::from_verification_request("starting verification failed", source)
96fdf90aacd6atagen2026-09-08 08:24 UTC578
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC579
}
96fdf90aacd6atagen2026-09-08 08:24 UTC580
96fdf90aacd6atagen2026-09-08 08:24 UTC581
async fn follow_request(
96fdf90aacd6atagen2026-09-08 08:24 UTC582
    request: VerificationRequest,
96fdf90aacd6atagen2026-09-08 08:24 UTC583
    active: Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC584
    sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC585
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC586
    let mut changes = request.changes();
96fdf90aacd6atagen2026-09-08 08:24 UTC587
    while let Some(state) = changes.next().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC588
        match state {
96fdf90aacd6atagen2026-09-08 08:24 UTC589
            VerificationRequestState::Created { .. } => {}
96fdf90aacd6atagen2026-09-08 08:24 UTC590
            VerificationRequestState::Requested { .. } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC591
                if let Err(error) = request
96fdf90aacd6atagen2026-09-08 08:24 UTC592
                    .accept_with_methods(vec![VerificationMethod::SasV1])
96fdf90aacd6atagen2026-09-08 08:24 UTC593
                    .await
96fdf90aacd6atagen2026-09-08 08:24 UTC594
                {
96fdf90aacd6atagen2026-09-08 08:24 UTC595
                    tracing::warn!(error = %error, "accepting verification request failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC596
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC597
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC598
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC599
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC600
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC601
                        UiState::Failed(VerificationFailure::RequestAcceptance(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC602
                        "request acceptance failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC603
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC604
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC605
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC606
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC607
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC608
            VerificationRequestState::Ready { .. } => match request.start_sas().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC609
                Ok(Some(sas)) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC610
                    follow_sas(sas, active.clone(), sender.clone()).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC611
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC612
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC613
                Ok(None) => {}
96fdf90aacd6atagen2026-09-08 08:24 UTC614
                Err(error) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC615
                    tracing::warn!(error = %error, "starting SAS verification failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC616
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC617
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC618
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC619
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC620
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC621
                        UiState::Failed(VerificationFailure::SasStartup(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC622
                        "SAS startup failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC623
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC624
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC625
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC626
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC627
            },
96fdf90aacd6atagen2026-09-08 08:24 UTC628
            VerificationRequestState::Transitioned { verification } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC629
                if let Verification::SasV1(sas) = verification {
96fdf90aacd6atagen2026-09-08 08:24 UTC630
                    follow_sas(sas, active.clone(), sender.clone()).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC631
                } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC632
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC633
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC634
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC635
                        UiState::Cancelled(
96fdf90aacd6atagen2026-09-08 08:24 UTC636
                            "the other device selected an unsupported method".into(),
96fdf90aacd6atagen2026-09-08 08:24 UTC637
                        ),
96fdf90aacd6atagen2026-09-08 08:24 UTC638
                        "unsupported verification method",
96fdf90aacd6atagen2026-09-08 08:24 UTC639
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC640
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC641
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC642
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC643
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC644
            VerificationRequestState::Done => {
96fdf90aacd6atagen2026-09-08 08:24 UTC645
                report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC646
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC647
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC648
            VerificationRequestState::Cancelled(info) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC649
                report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC650
                    &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC651
                    &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC652
                    UiState::Cancelled(info.reason().to_owned()),
96fdf90aacd6atagen2026-09-08 08:24 UTC653
                    "cancellation",
96fdf90aacd6atagen2026-09-08 08:24 UTC654
                )
96fdf90aacd6atagen2026-09-08 08:24 UTC655
                .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC656
                return;
96fdf90aacd6atagen2026-09-08 08:24 UTC657
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC658
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC659
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC660
    tracing::debug!("verification request watcher ended unexpectedly");
96fdf90aacd6atagen2026-09-08 08:24 UTC661
    // The stream may end after the SDK already settled without delivering the
96fdf90aacd6atagen2026-09-08 08:24 UTC662
    // terminal update; report the settled outcome instead of StreamEnded, and
96fdf90aacd6atagen2026-09-08 08:24 UTC663
    // stay silent if an action path already claimed the session.
96fdf90aacd6atagen2026-09-08 08:24 UTC664
    if request.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC665
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC666
    } else if let Some(info) = request.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC667
        let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC668
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC669
    } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC670
        report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC671
            &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC672
            &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC673
            UiState::Failed(VerificationFailure::RequestStreamEnded),
96fdf90aacd6atagen2026-09-08 08:24 UTC674
            "request stream termination",
96fdf90aacd6atagen2026-09-08 08:24 UTC675
        )
96fdf90aacd6atagen2026-09-08 08:24 UTC676
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC677
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC678
}
96fdf90aacd6atagen2026-09-08 08:24 UTC679
96fdf90aacd6atagen2026-09-08 08:24 UTC680
async fn follow_sas(
96fdf90aacd6atagen2026-09-08 08:24 UTC681
    sas: SasVerification,
96fdf90aacd6atagen2026-09-08 08:24 UTC682
    active: Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC683
    sender: EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC684
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC685
    active.lock().await.sas = Some(sas.clone());
96fdf90aacd6atagen2026-09-08 08:24 UTC686
    let mut changes = sas.changes();
96fdf90aacd6atagen2026-09-08 08:24 UTC687
    while let Some(state) = changes.next().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC688
        let update = match state {
96fdf90aacd6atagen2026-09-08 08:24 UTC689
            SasState::Created { .. } | SasState::Accepted { .. } => continue,
96fdf90aacd6atagen2026-09-08 08:24 UTC690
            SasState::Started { .. } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC691
                if let Err(error) = sas.accept().await {
96fdf90aacd6atagen2026-09-08 08:24 UTC692
                    tracing::warn!(error = %error, "accepting SAS verification failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC693
                    // Claim the session so this terminal outcome is emitted exactly
96fdf90aacd6atagen2026-09-08 08:24 UTC694
                    // once; a lost claim means an action path already owns it.
96fdf90aacd6atagen2026-09-08 08:24 UTC695
                    report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC696
                        &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC697
                        &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC698
                        UiState::Failed(VerificationFailure::SasAcceptance(error.to_string())),
96fdf90aacd6atagen2026-09-08 08:24 UTC699
                        "SAS acceptance failure",
96fdf90aacd6atagen2026-09-08 08:24 UTC700
                    )
96fdf90aacd6atagen2026-09-08 08:24 UTC701
                    .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC702
                    return;
96fdf90aacd6atagen2026-09-08 08:24 UTC703
                }
96fdf90aacd6atagen2026-09-08 08:24 UTC704
                continue;
96fdf90aacd6atagen2026-09-08 08:24 UTC705
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC706
            SasState::KeysExchanged { emojis, decimals } => UiState::Comparison {
96fdf90aacd6atagen2026-09-08 08:24 UTC707
                emojis: emojis
96fdf90aacd6atagen2026-09-08 08:24 UTC708
                    .map(|short_auth| {
96fdf90aacd6atagen2026-09-08 08:24 UTC709
                        short_auth
96fdf90aacd6atagen2026-09-08 08:24 UTC710
                            .emojis
96fdf90aacd6atagen2026-09-08 08:24 UTC711
                            .into_iter()
96fdf90aacd6atagen2026-09-08 08:24 UTC712
                            .map(|emoji| (emoji.symbol.to_owned(), emoji.description.to_owned()))
96fdf90aacd6atagen2026-09-08 08:24 UTC713
                            .collect()
96fdf90aacd6atagen2026-09-08 08:24 UTC714
                    })
96fdf90aacd6atagen2026-09-08 08:24 UTC715
                    .unwrap_or_default(),
96fdf90aacd6atagen2026-09-08 08:24 UTC716
                decimals,
96fdf90aacd6atagen2026-09-08 08:24 UTC717
            },
96fdf90aacd6atagen2026-09-08 08:24 UTC718
            SasState::Confirmed => UiState::Confirmed,
96fdf90aacd6atagen2026-09-08 08:24 UTC719
            SasState::Done { .. } => UiState::Done,
96fdf90aacd6atagen2026-09-08 08:24 UTC720
            SasState::Cancelled(info) => UiState::Cancelled(info.reason().to_owned()),
96fdf90aacd6atagen2026-09-08 08:24 UTC721
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC722
        let terminal = matches!(update, UiState::Done | UiState::Cancelled(_));
96fdf90aacd6atagen2026-09-08 08:24 UTC723
        // Claim the session before the terminal send so a racing Cancel observes
96fdf90aacd6atagen2026-09-08 08:24 UTC724
        // no active session and the SDK no-op cannot regress Done back to
96fdf90aacd6atagen2026-09-08 08:24 UTC725
        // "cancelled by this client". A lost claim means an action path already
96fdf90aacd6atagen2026-09-08 08:24 UTC726
        // owns the terminal outcome, so stay silent instead of duplicating it.
96fdf90aacd6atagen2026-09-08 08:24 UTC727
        if terminal && !take_verification(&active).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC728
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC729
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC730
        if sender
96fdf90aacd6atagen2026-09-08 08:24 UTC731
            .send(Event::VerificationChanged(update))
96fdf90aacd6atagen2026-09-08 08:24 UTC732
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC733
            .is_err()
96fdf90aacd6atagen2026-09-08 08:24 UTC734
        {
96fdf90aacd6atagen2026-09-08 08:24 UTC735
            tracing::debug!("SAS watcher stopped because the event receiver closed");
96fdf90aacd6atagen2026-09-08 08:24 UTC736
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC737
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC738
        if terminal {
96fdf90aacd6atagen2026-09-08 08:24 UTC739
            return;
96fdf90aacd6atagen2026-09-08 08:24 UTC740
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC741
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC742
    tracing::debug!("SAS watcher ended unexpectedly");
96fdf90aacd6atagen2026-09-08 08:24 UTC743
    // The stream may end after the SDK already settled without delivering the
96fdf90aacd6atagen2026-09-08 08:24 UTC744
    // terminal update; report the settled outcome instead of StreamEnded, and
96fdf90aacd6atagen2026-09-08 08:24 UTC745
    // stay silent if an action path already claimed the session.
96fdf90aacd6atagen2026-09-08 08:24 UTC746
    if sas.is_done() {
96fdf90aacd6atagen2026-09-08 08:24 UTC747
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC748
    } else if let Some(info) = sas.cancel_info() {
96fdf90aacd6atagen2026-09-08 08:24 UTC749
        let reason = info.reason().to_owned();
96fdf90aacd6atagen2026-09-08 08:24 UTC750
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
96fdf90aacd6atagen2026-09-08 08:24 UTC751
    } else {
96fdf90aacd6atagen2026-09-08 08:24 UTC752
        report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC753
            &active,
96fdf90aacd6atagen2026-09-08 08:24 UTC754
            &sender,
96fdf90aacd6atagen2026-09-08 08:24 UTC755
            UiState::Failed(VerificationFailure::SasStreamEnded),
96fdf90aacd6atagen2026-09-08 08:24 UTC756
            "SAS stream termination",
96fdf90aacd6atagen2026-09-08 08:24 UTC757
        )
96fdf90aacd6atagen2026-09-08 08:24 UTC758
        .await;
96fdf90aacd6atagen2026-09-08 08:24 UTC759
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC760
}
96fdf90aacd6atagen2026-09-08 08:24 UTC761
96fdf90aacd6atagen2026-09-08 08:24 UTC762
async fn report_watcher_terminal(
96fdf90aacd6atagen2026-09-08 08:24 UTC763
    active: &Arc<Mutex<VerificationSession>>,
96fdf90aacd6atagen2026-09-08 08:24 UTC764
    sender: &EventSender,
96fdf90aacd6atagen2026-09-08 08:24 UTC765
    state: UiState,
96fdf90aacd6atagen2026-09-08 08:24 UTC766
    context: &'static str,
96fdf90aacd6atagen2026-09-08 08:24 UTC767
) {
96fdf90aacd6atagen2026-09-08 08:24 UTC768
    if take_verification(active).await
96fdf90aacd6atagen2026-09-08 08:24 UTC769
        && sender
96fdf90aacd6atagen2026-09-08 08:24 UTC770
            .send(Event::VerificationChanged(state))
96fdf90aacd6atagen2026-09-08 08:24 UTC771
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC772
            .is_err()
96fdf90aacd6atagen2026-09-08 08:24 UTC773
    {
96fdf90aacd6atagen2026-09-08 08:24 UTC774
        tracing::debug!(
96fdf90aacd6atagen2026-09-08 08:24 UTC775
            context,
96fdf90aacd6atagen2026-09-08 08:24 UTC776
            "verification watcher could not report terminal outcome"
96fdf90aacd6atagen2026-09-08 08:24 UTC777
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC778
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC779
}
96fdf90aacd6atagen2026-09-08 08:24 UTC780
96fdf90aacd6atagen2026-09-08 08:24 UTC781
async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {
96fdf90aacd6atagen2026-09-08 08:24 UTC782
    if let Some(sender) = sender {
96fdf90aacd6atagen2026-09-08 08:24 UTC783
        let _ = sender.send(Event::VerificationChanged(state)).await;
96fdf90aacd6atagen2026-09-08 08:24 UTC784
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC785
}
96fdf90aacd6atagen2026-09-08 08:24 UTC786
96fdf90aacd6atagen2026-09-08 08:24 UTC787
async fn clear_verification(active: &Arc<Mutex<VerificationSession>>) {
96fdf90aacd6atagen2026-09-08 08:24 UTC788
    let mut active = active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC789
    active.sas.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC790
    active.request.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC791
}
96fdf90aacd6atagen2026-09-08 08:24 UTC792
96fdf90aacd6atagen2026-09-08 08:24 UTC793
/// Claim the active session for terminal emission, if one is still present.
96fdf90aacd6atagen2026-09-08 08:24 UTC794
///
96fdf90aacd6atagen2026-09-08 08:24 UTC795
/// Exactly one party (watcher or action path) wins the claim and may emit the
96fdf90aacd6atagen2026-09-08 08:24 UTC796
/// terminal event; the loser stays silent so Done is never overwritten by a
96fdf90aacd6atagen2026-09-08 08:24 UTC797
/// later local Cancelled/Failed.
96fdf90aacd6atagen2026-09-08 08:24 UTC798
async fn take_verification(active: &Arc<Mutex<VerificationSession>>) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC799
    let mut active = active.lock().await;
96fdf90aacd6atagen2026-09-08 08:24 UTC800
    let had = active.sas.is_some() || active.request.is_some();
96fdf90aacd6atagen2026-09-08 08:24 UTC801
    active.sas.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC802
    active.request.take();
96fdf90aacd6atagen2026-09-08 08:24 UTC803
    had
96fdf90aacd6atagen2026-09-08 08:24 UTC804
}
96fdf90aacd6atagen2026-09-08 08:24 UTC805
96fdf90aacd6atagen2026-09-08 08:24 UTC806
#[cfg(test)]
96fdf90aacd6atagen2026-09-08 08:24 UTC807
mod tests {
96fdf90aacd6atagen2026-09-08 08:24 UTC808
    use matrix_sdk::encryption::VerificationState;
96fdf90aacd6atagen2026-09-08 08:24 UTC809
    use matrix_sdk::encryption::backups::BackupState;
96fdf90aacd6atagen2026-09-08 08:24 UTC810
    use ruma::owned_device_id;
96fdf90aacd6atagen2026-09-08 08:24 UTC811
96fdf90aacd6atagen2026-09-08 08:24 UTC812
    use super::*;
96fdf90aacd6atagen2026-09-08 08:24 UTC813
96fdf90aacd6atagen2026-09-08 08:24 UTC814
    #[test]
96fdf90aacd6atagen2026-09-08 08:24 UTC815
    fn sdk_security_state_is_normalized() {
96fdf90aacd6atagen2026-09-08 08:24 UTC816
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC817
            encryption_state(
96fdf90aacd6atagen2026-09-08 08:24 UTC818
                owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC819
                SdkRecoveryState::Incomplete,
96fdf90aacd6atagen2026-09-08 08:24 UTC820
                VerificationState::Unverified,
96fdf90aacd6atagen2026-09-08 08:24 UTC821
                BackupState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC822
            ),
96fdf90aacd6atagen2026-09-08 08:24 UTC823
            EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC824
                device_id: owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC825
                device_verified: false,
96fdf90aacd6atagen2026-09-08 08:24 UTC826
                recovery: RecoveryState::Incomplete,
96fdf90aacd6atagen2026-09-08 08:24 UTC827
                key_backup: KeyBackupState::Unknown,
96fdf90aacd6atagen2026-09-08 08:24 UTC828
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC829
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC830
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC831
            encryption_state(
96fdf90aacd6atagen2026-09-08 08:24 UTC832
                owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC833
                SdkRecoveryState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC834
                VerificationState::Verified,
96fdf90aacd6atagen2026-09-08 08:24 UTC835
                BackupState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC836
            ),
96fdf90aacd6atagen2026-09-08 08:24 UTC837
            EncryptionState {
96fdf90aacd6atagen2026-09-08 08:24 UTC838
                device_id: owned_device_id!("DEVICE"),
96fdf90aacd6atagen2026-09-08 08:24 UTC839
                device_verified: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC840
                recovery: RecoveryState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC841
                key_backup: KeyBackupState::Enabled,
96fdf90aacd6atagen2026-09-08 08:24 UTC842
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC843
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC844
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC845
96fdf90aacd6atagen2026-09-08 08:24 UTC846
    #[tokio::test]
96fdf90aacd6atagen2026-09-08 08:24 UTC847
    async fn comparison_actions_require_an_active_sas() {
96fdf90aacd6atagen2026-09-08 08:24 UTC848
        let mut controller = VerificationController::default();
96fdf90aacd6atagen2026-09-08 08:24 UTC849
96fdf90aacd6atagen2026-09-08 08:24 UTC850
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC851
            controller.confirm().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC852
            "verification has not reached comparison"
96fdf90aacd6atagen2026-09-08 08:24 UTC853
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC854
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC855
            controller.reject().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC856
            "verification has not reached comparison"
96fdf90aacd6atagen2026-09-08 08:24 UTC857
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC858
        assert_eq!(
96fdf90aacd6atagen2026-09-08 08:24 UTC859
            controller.cancel().await.unwrap_err().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC860
            "no verification is active"
96fdf90aacd6atagen2026-09-08 08:24 UTC861
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC862
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC863
}