atagen

macltok

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