atagen

macltok

public
Star0
Watch0
fa9114fcf75f7bdf1a7cec3b3367d604cf7de472
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/encryption.rsRust866 lines35.0 KB
1
use std::sync::Arc;
2
3
use futures_util::StreamExt;
4
use matrix_sdk::{
5
    Client,
6
    encryption::{
7
        VerificationState,
8
        backups::BackupState,
9
        recovery::RecoveryState as SdkRecoveryState,
10
        verification::{
11
            SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState,
12
        },
13
    },
14
    ruma::events::key::verification::VerificationMethod,
15
};
16
use ruma::{OwnedDeviceId, UserId};
17
use tokio::{sync::Mutex, task::JoinHandle};
18
19
use crate::{
20
    CommandErrorKind, EncryptionState, Event, KeyBackupState, RecoveryState, VerificationAction,
21
    VerificationState as UiState, VerificationTarget, error::CommandError,
22
    model::VerificationFailure, transport::EventSender,
23
};
24
25
pub(crate) fn encryption_state(
26
    device_id: OwnedDeviceId,
27
    recovery: SdkRecoveryState,
28
    verification: VerificationState,
29
    backup: BackupState,
30
) -> EncryptionState {
31
    EncryptionState {
32
        device_id,
33
        device_verified: matches!(verification, VerificationState::Verified),
34
        recovery: match recovery {
35
            SdkRecoveryState::Unknown => RecoveryState::Unknown,
36
            SdkRecoveryState::Disabled => RecoveryState::Disabled,
37
            SdkRecoveryState::Incomplete => RecoveryState::Incomplete,
38
            SdkRecoveryState::Enabled => RecoveryState::Enabled,
39
        },
40
        key_backup: match backup {
41
            BackupState::Unknown => KeyBackupState::Unknown,
42
            BackupState::Creating => KeyBackupState::Creating,
43
            BackupState::Enabling => KeyBackupState::Enabling,
44
            BackupState::Resuming => KeyBackupState::Resuming,
45
            BackupState::Enabled => KeyBackupState::Enabled,
46
            BackupState::Downloading => KeyBackupState::Downloading,
47
            BackupState::Disabling => KeyBackupState::Disabling,
48
        },
49
    }
50
}
51
52
#[derive(Default)]
53
pub(crate) struct VerificationController {
54
    active: Arc<Mutex<VerificationSession>>,
55
    task: Option<JoinHandle<()>>,
56
}
57
58
#[derive(Default)]
59
struct VerificationSession {
60
    request: Option<VerificationRequest>,
61
    sas: Option<SasVerification>,
62
}
63
64
impl VerificationController {
65
    pub(crate) async fn apply(
66
        &mut self,
67
        _client: &Client,
68
        sender: EventSender,
69
        action: VerificationAction,
70
    ) -> Result<(), CommandError> {
71
        match action {
72
            VerificationAction::Confirm => {
73
                // Retain a handle across the operation: the watcher clears
74
                // `active` when it publishes a terminal event, but this clone
75
                // still observes the shared SDK state afterwards.
76
                let sas = self.active.lock().await.sas.clone();
77
                match self.confirm().await {
78
                    Ok(()) => Ok(()),
79
                    Err(error) if error.kind == CommandErrorKind::VerificationInactive => {
80
                        Err(error)
81
                    }
82
                    // The confirm advanced local SDK state before its send failed,
83
                    // so the retained session could report success on retry without
84
                    // network traffic. Best-effort cancel (no lock held across the
85
                    // network), clear for restart, then report the terminal state.
86
                    Err(error) => {
87
                        // Stop the watcher first so it cannot emit a stale
88
                        // Confirmed from the local transition we are unwinding.
89
                        self.stop_task().await;
90
                        // The peer can complete or cancel the verification
91
                        // while our own Done send fails. The stopped watcher may
92
                        // already have published that terminal outcome (losing the
93
                        // claim below); otherwise this path owns it and must emit
94
                        // it instead of regressing to Cancelled.
95
                        if sas.as_ref().is_some_and(|sas| sas.is_done()) {
96
                            if take_verification(&self.active).await {
97
                                let _ =
98
                                    sender.send(Event::VerificationChanged(UiState::Done)).await;
99
                            }
100
                            return Err(error);
101
                        }
102
                        if let Some(reason) = sas
103
                            .as_ref()
104
                            .and_then(|sas| sas.cancel_info())
105
                            .map(|info| info.reason().to_owned())
106
                        {
107
                            if take_verification(&self.active).await {
108
                                let _ = sender
109
                                    .send(Event::VerificationChanged(UiState::Cancelled(reason)))
110
                                    .await;
111
                            }
112
                            return Err(error);
113
                        }
114
                        if let Some(sas) = sas {
115
                            if sas.cancel().await.is_err() {
116
                                // The best-effort unwind already advanced
117
                                // local state; clearing here keeps the failed
118
                                // cancellation from looking retryable. The
119
                                // send is owned only if the watcher has not
120
                                // already claimed the session for its own
121
                                // terminal outcome.
122
                                if take_verification(&self.active).await {
123
                                    let _ = sender
124
                                        .send(Event::VerificationChanged(UiState::Cancelled(
125
                                            error.to_string(),
126
                                        )))
127
                                        .await;
128
                                }
129
                                return Err(error);
130
                            }
131
                            // The peer can complete while the unwind cancel is in
132
                            // flight and the SDK call no-ops; the Done then owns the
133
                            // terminal outcome, so report it instead of attributing
134
                            // it to the send failure.
135
                            if sas.is_done() {
136
                                if take_verification(&self.active).await {
137
                                    let _ = sender
138
                                        .send(Event::VerificationChanged(UiState::Done))
139
                                        .await;
140
                                }
141
                                return Err(error);
142
                            }
143
                            if let Some(info) = sas.cancel_info() {
144
                                // Our own unwind cancel just marked the flow; it does
145
                                // not own the outcome yet, so fall through and report
146
                                // the send failure as Cancelled below. Only a peer
147
                                // cancellation racing the unwind owns it instead.
148
                                if !info.cancelled_by_us() {
149
                                    if take_verification(&self.active).await {
150
                                        let _ = sender
151
                                            .send(Event::VerificationChanged(UiState::Cancelled(
152
                                                info.reason().to_owned(),
153
                                            )))
154
                                            .await;
155
                                    }
156
                                    return Err(CommandError::new(
157
                                        CommandErrorKind::VerificationInactive,
158
                                        info.reason().to_owned(),
159
                                    ));
160
                                }
161
                            }
162
                        }
163
                        if take_verification(&self.active).await {
164
                            let _ = sender
165
                                .send(Event::VerificationChanged(UiState::Cancelled(
166
                                    error.to_string(),
167
                                )))
168
                                .await;
169
                        }
170
                        Err(error)
171
                    }
172
                }
173
            }
174
            VerificationAction::Reject => self.reject().await,
175
            VerificationAction::Cancel => {
176
                // The SDK no-ops a cancel of an already-settled flow, so a
177
                // failed claim inside `cancel` already reported the true
178
                // outcome as an error and owns no event here. Only the claim
179
                // winner emits the local Cancelled.
180
                let claimed = self.cancel_claimed(Some(&sender)).await?;
181
                if claimed {
182
                    sender
183
                        .send(Event::VerificationChanged(UiState::Cancelled(
184
                            "cancelled by this client".to_owned(),
185
                        )))
186
                        .await
187
                        .map_err(|source| {
188
                            CommandError::with_source(
189
                                CommandErrorKind::EventReceiverClosed,
190
                                "event receiver has closed",
191
                                source,
192
                            )
193
                        })?;
194
                }
195
                Ok(())
196
            }
197
        }
198
    }
199
200
    pub(crate) async fn start(
201
        &mut self,
202
        client: &Client,
203
        sender: EventSender,
204
        target: VerificationTarget,
205
    ) -> Result<(), CommandError> {
206
        client
207
            .encryption()
208
            .wait_for_e2ee_initialization_tasks()
209
            .await;
210
        let own_user_id = client.user_id().ok_or_else(|| {
211
            CommandError::new(
212
                CommandErrorKind::Unauthenticated,
213
                "the Matrix client is not authenticated",
214
            )
215
        })?;
216
        if self.has_active_verification().await {
217
            self.cancel().await?;
218
        } else {
219
            self.stop_task().await;
220
        }
221
        let request = match &target {
222
            VerificationTarget::OwnIdentity => {
223
                request_identity_verification(client, own_user_id).await?
224
            }
225
            VerificationTarget::User(user_id) => {
226
                request_identity_verification(client, user_id).await?
227
            }
228
            VerificationTarget::Device { user_id, device_id } => {
229
                let device = client
230
                    .encryption()
231
                    .get_device(user_id, device_id)
232
                    .await
233
                    .map_err(|source| {
234
                        CommandError::with_source(
235
                            CommandErrorKind::Encryption,
236
                            "reading the verification device failed",
237
                            source,
238
                        )
239
                    })?
240
                    .ok_or_else(|| {
241
                        CommandError::new(
242
                            CommandErrorKind::VerificationUnsupported,
243
                            "the verification device is unavailable",
244
                        )
245
                    })?;
246
                device
247
                    .request_verification_with_methods(vec![VerificationMethod::SasV1])
248
                    .await
249
                    .map_err(|source| {
250
                        CommandError::from_sdk("starting device verification failed", source)
251
                    })?
252
            }
253
        };
254
        self.active.lock().await.request = Some(request.clone());
255
        sender
256
            .send(Event::VerificationTargetChanged(target))
257
            .await
258
            .map_err(|source| {
259
                CommandError::with_source(
260
                    CommandErrorKind::EventReceiverClosed,
261
                    "event receiver has closed",
262
                    source,
263
                )
264
            })?;
265
        sender
266
            .send(Event::VerificationChanged(UiState::Waiting))
267
            .await
268
            .map_err(|source| {
269
                CommandError::with_source(
270
                    CommandErrorKind::EventReceiverClosed,
271
                    "event receiver has closed",
272
                    source,
273
                )
274
            })?;
275
        let active = self.active.clone();
276
        self.task = Some(tokio::spawn(async move {
277
            follow_request(request, active, sender).await;
278
        }));
279
        Ok(())
280
    }
281
282
    async fn confirm(&self) -> Result<(), CommandError> {
283
        let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
284
            CommandError::new(
285
                CommandErrorKind::VerificationInactive,
286
                "verification has not reached comparison",
287
            )
288
        })?;
289
        // Report a settled flow truthfully instead of as "not reached
290
        // comparison": the SDK would no-op the confirm with success.
291
        if sas.is_done() {
292
            return Err(CommandError::new(
293
                CommandErrorKind::VerificationInactive,
294
                "verification is already complete",
295
            ));
296
        }
297
        if let Some(info) = sas.cancel_info() {
298
            return Err(CommandError::new(
299
                CommandErrorKind::VerificationInactive,
300
                info.reason().to_owned(),
301
            ));
302
        }
303
        // Only the short-auth-string stage is confirmable. The SDK reports the
304
        // remote-MAC-arrived state through the same presentable flag, while an
305
        // early confirm would locally no-op and falsely report success.
306
        if !sas.can_be_presented() {
307
            return Err(CommandError::new(
308
                CommandErrorKind::VerificationInactive,
309
                "verification has not reached comparison",
310
            ));
311
        }
312
        // Our own confirm can move the flow to Done, so only Done that predates
313
        // the send means the SDK no-op'd on a stale session; a cancellation
314
        // racing the send must not report success.
315
        let was_done = sas.is_done();
316
        sas.confirm()
317
            .await
318
            .map_err(|source| CommandError::from_sdk("confirming verification failed", source))?;
319
        if sas.is_done() {
320
            if was_done {
321
                return Err(CommandError::new(
322
                    CommandErrorKind::VerificationInactive,
323
                    "verification is already complete",
324
                ));
325
            }
326
        } else if sas.is_cancelled() {
327
            let reason = sas
328
                .cancel_info()
329
                .map(|info| info.reason().to_owned())
330
                .unwrap_or_else(|| "verification was cancelled".to_owned());
331
            return Err(CommandError::new(
332
                CommandErrorKind::VerificationInactive,
333
                reason,
334
            ));
335
        }
336
        Ok(())
337
    }
338
339
    async fn reject(&self) -> Result<(), CommandError> {
340
        let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
341
            CommandError::new(
342
                CommandErrorKind::VerificationInactive,
343
                "verification has not reached comparison",
344
            )
345
        })?;
346
        // Report a settled flow truthfully instead of as "not reached
347
        // comparison": the SDK would no-op the mismatch with success.
348
        if sas.is_done() {
349
            return Err(CommandError::new(
350
                CommandErrorKind::VerificationInactive,
351
                "verification is already complete",
352
            ));
353
        }
354
        if let Some(info) = sas.cancel_info() {
355
            return Err(CommandError::new(
356
                CommandErrorKind::VerificationInactive,
357
                info.reason().to_owned(),
358
            ));
359
        }
360
        // Rejecting before the comparison stage would cancel a flow the user
361
        // never compared, so it requires the same presentable state.
362
        if !sas.can_be_presented() {
363
            return Err(CommandError::new(
364
                CommandErrorKind::VerificationInactive,
365
                "verification has not reached comparison",
366
            ));
367
        }
368
        sas.mismatch()
369
            .await
370
            .map_err(|source| CommandError::from_sdk("rejecting verification failed", source))?;
371
        // A peer completing the flow concurrently must not be reported as
372
        // rejected. A remote cancellation racing the send turns the SDK call
373
        // into a no-op success, so report it instead of a false success.
374
        if sas.is_done() {
375
            return Err(CommandError::new(
376
                CommandErrorKind::VerificationInactive,
377
                "verification is already complete",
378
            ));
379
        }
380
        if let Some(info) = sas.cancel_info() {
381
            // Our own mismatch just marked the flow cancelled-by-us; that is the
382
            // expected reject path and stays Ok. Only a peer cancel reports an
383
            // error.
384
            if !info.cancelled_by_us() {
385
                return Err(CommandError::new(
386
                    CommandErrorKind::VerificationInactive,
387
                    info.reason().to_owned(),
388
                ));
389
            }
390
        }
391
        Ok(())
392
    }
393
    pub(crate) async fn cancel(&mut self) -> Result<(), CommandError> {
394
        self.cancel_claimed(None).await.map(|_| ())
395
    }
396
397
    /// Return whether this action won the claim for a local cancellation.
398
    /// The optional sender reports a peer terminal outcome when it races after
399
    /// the claim; starting a replacement verification does not publish it.
400
    async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, CommandError> {
401
        let (sas, request) = {
402
            let active = self.active.lock().await;
403
            (active.sas.clone(), active.request.clone())
404
        };
405
        if sas.is_none() && request.is_none() {
406
            return Err(CommandError::new(
407
                CommandErrorKind::VerificationInactive,
408
                "no verification is active",
409
            ));
410
        }
411
        // The watcher owns terminal emission and claims the session when it
412
        // publishes. If the SDK is already terminal, the watcher either already
413
        // reported it or is about to: leave session and watcher alone so the
414
        // outcome is emitted exactly once, and report it instead of letting the
415
        // SDK no-op success regress Done to "cancelled by this client".
416
        if let Some(sas) = sas.as_ref() {
417
            if sas.is_done() {
418
                return Err(CommandError::new(
419
                    CommandErrorKind::VerificationInactive,
420
                    "verification is already complete",
421
                ));
422
            }
423
            if let Some(info) = sas.cancel_info() {
424
                return Err(CommandError::new(
425
                    CommandErrorKind::VerificationInactive,
426
                    info.reason().to_owned(),
427
                ));
428
            }
429
        } else if let Some(request) = request.as_ref() {
430
            if request.is_done() {
431
                return Err(CommandError::new(
432
                    CommandErrorKind::VerificationInactive,
433
                    "verification is already complete",
434
                ));
435
            }
436
            if let Some(info) = request.cancel_info() {
437
                return Err(CommandError::new(
438
                    CommandErrorKind::VerificationInactive,
439
                    info.reason().to_owned(),
440
                ));
441
            }
442
        }
443
        // Own the terminal outcome from here: claim the session before
444
        // stopping the watcher so a watcher that already claimed (and will
445
        // emit its own terminal event) turns this into a truthful error
446
        // instead of a duplicate local cancel; no lock is held across sends.
447
        if !take_verification(&self.active).await {
448
            // Re-check the SDK: the claim loss may be a stale session after a
449
            // terminal outcome, or a concurrent start of a new flow. Only the
450
            // terminal case reports here; a live session belongs to the new
451
            // flow and must not be disturbed.
452
            if let Some(sas) = sas.as_ref() {
453
                if sas.is_done() {
454
                    return Err(CommandError::new(
455
                        CommandErrorKind::VerificationInactive,
456
                        "verification is already complete",
457
                    ));
458
                }
459
                if let Some(info) = sas.cancel_info() {
460
                    return Err(CommandError::new(
461
                        CommandErrorKind::VerificationInactive,
462
                        info.reason().to_owned(),
463
                    ));
464
                }
465
            } else if let Some(request) = request.as_ref() {
466
                if request.is_done() {
467
                    return Err(CommandError::new(
468
                        CommandErrorKind::VerificationInactive,
469
                        "verification is already complete",
470
                    ));
471
                }
472
                if let Some(info) = request.cancel_info() {
473
                    return Err(CommandError::new(
474
                        CommandErrorKind::VerificationInactive,
475
                        info.reason().to_owned(),
476
                    ));
477
                }
478
            }
479
            return Err(CommandError::new(
480
                CommandErrorKind::VerificationInactive,
481
                "no verification is active",
482
            ));
483
        }
484
        self.stop_task().await;
485
        if let Some(sas) = sas {
486
            let result = sas.cancel().await;
487
            if sas.is_done() {
488
                report_claimed_terminal(sender, UiState::Done).await;
489
                return Err(CommandError::new(
490
                    CommandErrorKind::VerificationInactive,
491
                    "verification is already complete",
492
                ));
493
            }
494
            if let Some(info) = sas.cancel_info()
495
                && !info.cancelled_by_us()
496
            {
497
                let reason = info.reason().to_owned();
498
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
499
                return Err(CommandError::new(
500
                    CommandErrorKind::VerificationInactive,
501
                    reason,
502
                ));
503
            }
504
            if let Err(source) = result {
505
                let error = CommandError::from_sdk("cancelling verification failed", source);
506
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
507
                return Err(error);
508
            }
509
            Ok(true)
510
        } else if let Some(request) = request {
511
            let result = request.cancel().await;
512
            if request.is_done() {
513
                report_claimed_terminal(sender, UiState::Done).await;
514
                return Err(CommandError::new(
515
                    CommandErrorKind::VerificationInactive,
516
                    "verification is already complete",
517
                ));
518
            }
519
            if let Some(info) = request.cancel_info()
520
                && !info.cancelled_by_us()
521
            {
522
                let reason = info.reason().to_owned();
523
                report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
524
                return Err(CommandError::new(
525
                    CommandErrorKind::VerificationInactive,
526
                    reason,
527
                ));
528
            }
529
            if let Err(source) = result {
530
                let error = CommandError::from_sdk("cancelling verification failed", source);
531
                report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
532
                return Err(error);
533
            }
534
            Ok(true)
535
        } else {
536
            Ok(false)
537
        }
538
    }
539
540
    async fn has_active_verification(&self) -> bool {
541
        let active = self.active.lock().await;
542
        active.request.is_some() || active.sas.is_some()
543
    }
544
545
    async fn stop_task(&mut self) {
546
        if let Some(task) = self.task.take() {
547
            task.abort();
548
            let _ = task.await;
549
        }
550
    }
551
552
    pub(crate) async fn stop(&mut self) {
553
        // Shutdown path: terminate the watcher without touching the SDK so a
554
        // shutdown under lock cannot hang on network sends. No terminal event
555
        // is emitted; the session is simply forgotten locally.
556
        self.stop_task().await;
557
        clear_verification(&self.active).await;
558
    }
559
}
560
561
async fn request_identity_verification(
562
    client: &Client,
563
    user_id: &UserId,
564
) -> Result<VerificationRequest, CommandError> {
565
    let identity = client
566
        .encryption()
567
        .request_user_identity(user_id)
568
        .await
569
        .map_err(|source| CommandError::from_sdk("requesting the user identity failed", source))?
570
        .ok_or_else(|| {
571
            CommandError::new(
572
                CommandErrorKind::NoCrossSigningIdentity,
573
                "the user has no cross-signing identity",
574
            )
575
        })?;
576
    identity
577
        .request_verification_with_methods(vec![VerificationMethod::SasV1])
578
        .await
579
        .map_err(|source| {
580
            CommandError::from_verification_request("starting verification failed", source)
581
        })
582
}
583
584
async fn follow_request(
585
    request: VerificationRequest,
586
    active: Arc<Mutex<VerificationSession>>,
587
    sender: EventSender,
588
) {
589
    let mut changes = request.changes();
590
    while let Some(state) = changes.next().await {
591
        match state {
592
            VerificationRequestState::Created { .. } => {}
593
            VerificationRequestState::Requested { .. } => {
594
                if let Err(error) = request
595
                    .accept_with_methods(vec![VerificationMethod::SasV1])
596
                    .await
597
                {
598
                    tracing::warn!(error = %error, "accepting verification request failed");
599
                    // Claim the session so this terminal outcome is emitted exactly
600
                    // once; a lost claim means an action path already owns it.
601
                    report_watcher_terminal(
602
                        &active,
603
                        &sender,
604
                        UiState::Failed(VerificationFailure::RequestAcceptance(error.to_string())),
605
                        "request acceptance failure",
606
                    )
607
                    .await;
608
                    return;
609
                }
610
            }
611
            VerificationRequestState::Ready { .. } => match request.start_sas().await {
612
                Ok(Some(sas)) => {
613
                    follow_sas(sas, active.clone(), sender.clone()).await;
614
                    return;
615
                }
616
                Ok(None) => {}
617
                Err(error) => {
618
                    tracing::warn!(error = %error, "starting SAS verification failed");
619
                    // Claim the session so this terminal outcome is emitted exactly
620
                    // once; a lost claim means an action path already owns it.
621
                    report_watcher_terminal(
622
                        &active,
623
                        &sender,
624
                        UiState::Failed(VerificationFailure::SasStartup(error.to_string())),
625
                        "SAS startup failure",
626
                    )
627
                    .await;
628
                    return;
629
                }
630
            },
631
            VerificationRequestState::Transitioned { verification } => {
632
                if let Verification::SasV1(sas) = verification {
633
                    follow_sas(sas, active.clone(), sender.clone()).await;
634
                } else {
635
                    report_watcher_terminal(
636
                        &active,
637
                        &sender,
638
                        UiState::Cancelled(
639
                            "the other device selected an unsupported method".into(),
640
                        ),
641
                        "unsupported verification method",
642
                    )
643
                    .await;
644
                }
645
                return;
646
            }
647
            VerificationRequestState::Done => {
648
                report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
649
                return;
650
            }
651
            VerificationRequestState::Cancelled(info) => {
652
                report_watcher_terminal(
653
                    &active,
654
                    &sender,
655
                    UiState::Cancelled(info.reason().to_owned()),
656
                    "cancellation",
657
                )
658
                .await;
659
                return;
660
            }
661
        }
662
    }
663
    tracing::debug!("verification request watcher ended unexpectedly");
664
    // The stream may end after the SDK already settled without delivering the
665
    // terminal update; report the settled outcome instead of StreamEnded, and
666
    // stay silent if an action path already claimed the session.
667
    if request.is_done() {
668
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
669
    } else if let Some(info) = request.cancel_info() {
670
        let reason = info.reason().to_owned();
671
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
672
    } else {
673
        report_watcher_terminal(
674
            &active,
675
            &sender,
676
            UiState::Failed(VerificationFailure::RequestStreamEnded),
677
            "request stream termination",
678
        )
679
        .await;
680
    }
681
}
682
683
async fn follow_sas(
684
    sas: SasVerification,
685
    active: Arc<Mutex<VerificationSession>>,
686
    sender: EventSender,
687
) {
688
    active.lock().await.sas = Some(sas.clone());
689
    let mut changes = sas.changes();
690
    while let Some(state) = changes.next().await {
691
        let update = match state {
692
            SasState::Created { .. } | SasState::Accepted { .. } => continue,
693
            SasState::Started { .. } => {
694
                if let Err(error) = sas.accept().await {
695
                    tracing::warn!(error = %error, "accepting SAS verification failed");
696
                    // Claim the session so this terminal outcome is emitted exactly
697
                    // once; a lost claim means an action path already owns it.
698
                    report_watcher_terminal(
699
                        &active,
700
                        &sender,
701
                        UiState::Failed(VerificationFailure::SasAcceptance(error.to_string())),
702
                        "SAS acceptance failure",
703
                    )
704
                    .await;
705
                    return;
706
                }
707
                continue;
708
            }
709
            SasState::KeysExchanged { emojis, decimals } => UiState::Comparison {
710
                emojis: emojis
711
                    .map(|short_auth| {
712
                        short_auth
713
                            .emojis
714
                            .into_iter()
715
                            .map(|emoji| (emoji.symbol.to_owned(), emoji.description.to_owned()))
716
                            .collect()
717
                    })
718
                    .unwrap_or_default(),
719
                decimals,
720
            },
721
            SasState::Confirmed => UiState::Confirmed,
722
            SasState::Done { .. } => UiState::Done,
723
            SasState::Cancelled(info) => UiState::Cancelled(info.reason().to_owned()),
724
        };
725
        let terminal = matches!(update, UiState::Done | UiState::Cancelled(_));
726
        // Claim the session before the terminal send so a racing Cancel observes
727
        // no active session and the SDK no-op cannot regress Done back to
728
        // "cancelled by this client". A lost claim means an action path already
729
        // owns the terminal outcome, so stay silent instead of duplicating it.
730
        if terminal && !take_verification(&active).await {
731
            return;
732
        }
733
        if sender
734
            .send(Event::VerificationChanged(update))
735
            .await
736
            .is_err()
737
        {
738
            tracing::debug!("SAS watcher stopped because the event receiver closed");
739
            return;
740
        }
741
        if terminal {
742
            return;
743
        }
744
    }
745
    tracing::debug!("SAS watcher ended unexpectedly");
746
    // The stream may end after the SDK already settled without delivering the
747
    // terminal update; report the settled outcome instead of StreamEnded, and
748
    // stay silent if an action path already claimed the session.
749
    if sas.is_done() {
750
        report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
751
    } else if let Some(info) = sas.cancel_info() {
752
        let reason = info.reason().to_owned();
753
        report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
754
    } else {
755
        report_watcher_terminal(
756
            &active,
757
            &sender,
758
            UiState::Failed(VerificationFailure::SasStreamEnded),
759
            "SAS stream termination",
760
        )
761
        .await;
762
    }
763
}
764
765
async fn report_watcher_terminal(
766
    active: &Arc<Mutex<VerificationSession>>,
767
    sender: &EventSender,
768
    state: UiState,
769
    context: &'static str,
770
) {
771
    if take_verification(active).await
772
        && sender
773
            .send(Event::VerificationChanged(state))
774
            .await
775
            .is_err()
776
    {
777
        tracing::debug!(
778
            context,
779
            "verification watcher could not report terminal outcome"
780
        );
781
    }
782
}
783
784
async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {
785
    if let Some(sender) = sender {
786
        let _ = sender.send(Event::VerificationChanged(state)).await;
787
    }
788
}
789
790
async fn clear_verification(active: &Arc<Mutex<VerificationSession>>) {
791
    let mut active = active.lock().await;
792
    active.sas.take();
793
    active.request.take();
794
}
795
796
/// Claim the active session for terminal emission, if one is still present.
797
///
798
/// Exactly one party (watcher or action path) wins the claim and may emit the
799
/// terminal event; the loser stays silent so Done is never overwritten by a
800
/// later local Cancelled/Failed.
801
async fn take_verification(active: &Arc<Mutex<VerificationSession>>) -> bool {
802
    let mut active = active.lock().await;
803
    let had = active.sas.is_some() || active.request.is_some();
804
    active.sas.take();
805
    active.request.take();
806
    had
807
}
808
809
#[cfg(test)]
810
mod tests {
811
    use matrix_sdk::encryption::VerificationState;
812
    use matrix_sdk::encryption::backups::BackupState;
813
    use ruma::owned_device_id;
814
815
    use super::*;
816
817
    #[test]
818
    fn sdk_security_state_is_normalized() {
819
        assert_eq!(
820
            encryption_state(
821
                owned_device_id!("DEVICE"),
822
                SdkRecoveryState::Incomplete,
823
                VerificationState::Unverified,
824
                BackupState::Unknown,
825
            ),
826
            EncryptionState {
827
                device_id: owned_device_id!("DEVICE"),
828
                device_verified: false,
829
                recovery: RecoveryState::Incomplete,
830
                key_backup: KeyBackupState::Unknown,
831
            }
832
        );
833
        assert_eq!(
834
            encryption_state(
835
                owned_device_id!("DEVICE"),
836
                SdkRecoveryState::Enabled,
837
                VerificationState::Verified,
838
                BackupState::Enabled,
839
            ),
840
            EncryptionState {
841
                device_id: owned_device_id!("DEVICE"),
842
                device_verified: true,
843
                recovery: RecoveryState::Enabled,
844
                key_backup: KeyBackupState::Enabled,
845
            }
846
        );
847
    }
848
849
    #[tokio::test]
850
    async fn comparison_actions_require_an_active_sas() {
851
        let mut controller = VerificationController::default();
852
853
        assert_eq!(
854
            controller.confirm().await.unwrap_err().to_string(),
855
            "verification has not reached comparison"
856
        );
857
        assert_eq!(
858
            controller.reject().await.unwrap_err().to_string(),
859
            "verification has not reached comparison"
860
        );
861
        assert_eq!(
862
            controller.cancel().await.unwrap_err().to_string(),
863
            "no verification is active"
864
        );
865
    }
866
}