atagen

macltok

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