atagen

macltok

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