atagen

macltok

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