atagen

macltok

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