Commit 743123167e29
tarball7 files changed+193-29
@@ -16,11 +16,11 @@ use matrix_sdk::{
1616
},1717
send_queue::{LocalEchoContent, SendHandle},1818
};19
-use tokio::sync::mpsc;19
+use tokio::sync::{mpsc, watch};2020
2121
use crate::{22
- EncryptionCommand, Error, ErrorKind, InteractiveAuth, InteractiveAuthChallenge,23
- NotificationCommand, RoomNotificationMode, TimelineId,22
+ CommandOutput, EncryptionCommand, Error, ErrorKind, InteractiveAuth, InteractiveAuthChallenge,23
+ NotificationCommand, RecoveryKey, RoomNotificationMode, TimelineId,2424
encryption::VerificationController,2525
transport::{EventSender, Responder},2626
};@@ -55,14 +55,15 @@ pub(super) async fn run_encryption(
5555
events: EventSender,5656
mut jobs: mpsc::Receiver<Job<EncryptionCommand>>,5757
mut incoming: mpsc::UnboundedReceiver<VerificationRequest>,58
+ enabling: watch::Sender<bool>,5859
) {5960
let mut verification = VerificationController::default();6061
loop {6162
tokio::select! {6263
job = jobs.recv() => {6364
let Some(Job { command, reply }) = job else { return };64
- let work = encryption(&client, &events, &mut verification, command);65
- reply.done(guarded("encryption command", work).await);65
+ let work = encryption(&client, &events, &mut verification, &enabling, command);66
+ reply.send(guarded("encryption command", work).await);6667
}6768
Some(request) = incoming.recv() => {6869
verification.adopt_incoming(events.clone(), request).await;@@ -75,29 +76,58 @@ async fn encryption(
7576
client: &Client,7677
events: &EventSender,7778
verification: &mut VerificationController,79
+ enabling: &watch::Sender<bool>,7880
command: EncryptionCommand,79
-) -> Result<(), Error> {81
+) -> Result<CommandOutput, Error> {8082
let encryption = client.encryption();83
+ let recovery = encryption.recovery();84
+ let key = |key: String| CommandOutput::RecoveryKey(RecoveryKey::new(key));85
+ let done = CommandOutput::Done;8186
match command {82
- EncryptionCommand::Recover(key) => encryption83
- .recovery()87
+ EncryptionCommand::EnableRecovery { passphrase } => {88
+ let _enabling = Enabling::start(enabling);89
+ let enable = recovery.enable();90
+ match passphrase.as_deref() {91
+ Some(passphrase) => enable.with_passphrase(passphrase).await,92
+ None => enable.await,93
+ }94
+ .map(key)95
+ .map_err(|source| Error::from_recovery("enabling recovery failed", source))96
+ }97
+ EncryptionCommand::ResetRecoveryKey { passphrase } => {98
+ let reset = recovery.reset_key();99
+ match passphrase.as_deref() {100
+ Some(passphrase) => reset.with_passphrase(passphrase).await,101
+ None => reset.await,102
+ }103
+ .map(key)104
+ .map_err(|source| Error::from_recovery("resetting the recovery key failed", source))105
+ }106
+ EncryptionCommand::DisableRecovery => recovery107
+ .disable()108
+ .await109
+ .map(|()| done)110
+ .map_err(|source| Error::from_recovery("disabling recovery failed", source)),111
+ EncryptionCommand::Recover(key) => recovery84112
.recover(&key)85113
.await114
+ .map(|()| done)86115
.map_err(|source| Error::from_recovery("account recovery failed", source)),87
- EncryptionCommand::RecoverKeyBackup(key) => encryption88
- .recovery()116
+ EncryptionCommand::RecoverKeyBackup(key) => recovery89117
.recover_and_fix_backup(&key)90118
.await119
+ .map(|()| done)91120
.map_err(|source| Error::from_recovery("recovering key backup failed", source)),92121
EncryptionCommand::CreateKeyBackup => encryption93122
.backups()94123
.create()95124
.await125
+ .map(|()| done)96126
.map_err(|source| Error::from_sdk("creating key backup failed", source)),97
- EncryptionCommand::EnableKeyBackup => encryption98
- .recovery()127
+ EncryptionCommand::EnableKeyBackup => recovery99128
.enable_backup()100129
.await130
+ .map(|()| done)101131
.map_err(|source| Error::from_recovery("enabling key backup failed", source)),102132
EncryptionCommand::DisableKeyBackup { delete_from_server } => {103133
let backups = encryption.backups();@@ -106,12 +136,33 @@ async fn encryption(
106136
} else {107137
backups.disable().await108138
}139
+ .map(|()| done)109140
.map_err(|source| Error::from_sdk("disabling key backup failed", source))110141
}111
- EncryptionCommand::StartVerification(target) => {112
- verification.start(client, events.clone(), target).await113
- }114
- EncryptionCommand::Verify(action) => verification.apply(events.clone(), action).await,142
+ EncryptionCommand::StartVerification(target) => verification143
+ .start(client, events.clone(), target)144
+ .await145
+ .map(|()| done),146
+ EncryptionCommand::Verify(action) => verification147
+ .apply(events.clone(), action)148
+ .await149
+ .map(|()| done),150
+ }151
+}152
+153
+/// a panicking enable must not leave the state saying it is still running154
+struct Enabling<'a>(&'a watch::Sender<bool>);155
+156
+impl<'a> Enabling<'a> {157
+ fn start(sender: &'a watch::Sender<bool>) -> Self {158
+ sender.send_replace(true);159
+ Self(sender)160
+ }161
+}162
+163
+impl Drop for Enabling<'_> {164
+ fn drop(&mut self) {165
+ self.0.send_replace(false);115166
}116167
}117168
@@ -600,6 +600,7 @@ impl Engine {
600600
let client = &self.client;601601
let events = || self.port.event_sender();602602
let (notices, notice_queue) = mpsc::unbounded_channel();603
+ let (enabling_recovery, recovery_enabling) = watch::channel(false);603604
let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 15] = [604605
(605606
"upload tracker",@@ -658,7 +659,11 @@ impl Engine {
658659
),659660
(660661
"encryption state watcher",661
- Box::pin(watchers::encryption(client.clone(), events())),662
+ Box::pin(watchers::encryption(663
+ client.clone(),664
+ events(),665
+ recovery_enabling,666
+ )),662667
),663668
(664669
"room list watcher",@@ -702,6 +707,7 @@ impl Engine {
702707
events(),703708
queues.encryption,704709
incoming_verifications,710
+ enabling_recovery,705711
)),706712
),707713
(@@ -376,7 +376,11 @@ fn is_attachment_echo(content: &LocalEchoContent) -> bool {
376376
)377377
}378378
379
-pub(super) async fn encryption(client: Client, events: EventSender) {379
+pub(super) async fn encryption(380
+ client: Client,381
+ events: EventSender,382
+ mut enabling: watch::Receiver<bool>,383
+) {380384
let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {381385
tracing::warn!("encryption watcher stopped because the client has no device id");382386
return;@@ -390,17 +394,21 @@ pub(super) async fn encryption(client: Client, events: EventSender) {
390394
let mut backup_state = backups.state();391395
let mut verification_state = verification_stream.get();392396
loop {393
- let state = encryption_state(397
+ let mut state = encryption_state(394398
device_id.clone(),395399
recovery_state,396400
verification_state,397401
backup_state,398402
);403
+ if *enabling.borrow_and_update() {404
+ state.recovery = crate::RecoveryState::Enabling;405
+ }399406
if events.send(Event::EncryptionChanged(state)).await.is_err() {400407
return;401408
}402409
tokio::select! {403410
Some(update) = recovery_stream.next() => recovery_state = update,411
+ Ok(()) = enabling.changed() => {}404412
Some(update) = verification_stream.next() => verification_state = update,405413
Some(update) = backup_stream.next() => {406414
backup_state = update.unwrap_or_else(|_| backups.state());@@ -66,6 +66,8 @@ pub enum ErrorKind {
6666
VerificationUnsupported,6767
NoCrossSigningIdentity,6868
RecoveryRejected,69
+ /// the server already holds a key backup, which recovery has to open or replace first70
+ KeyBackupExists,6971
Network,7072
/// the homeserver wants us to slow down. try again later7173
RateLimited,@@ -223,9 +225,8 @@ impl Error {
223225
ErrorKind::RecoveryRejected224226
}225227
RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => ErrorKind::Store,226
- RecoveryError::BackupExistsOnServer | RecoveryError::SecretStorage(_) => {227
- ErrorKind::RecoveryRejected228
- }228
+ RecoveryError::BackupExistsOnServer => ErrorKind::KeyBackupExists,229
+ RecoveryError::SecretStorage(_) => ErrorKind::RecoveryRejected,229230
};230231
Self::with_source(kind, message, source)231232
}@@ -67,8 +67,8 @@ pub use model::{
6767
MessageEditContent, MessageKind, MessageMentions, NotificationCommand, OwnDevice, PackCommand,6868
PackImage, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile, PushNotification,6969
PushNotificationEvent, PushNotificationRoom, PushNotificationSender, PushNotificationStatus,70
- Reaction, RecentHistoryState, RecoveryState, ReplyDetails, ReplyReference, Retain, RichText,71
- RoomCategory, RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership,70
+ Reaction, RecentHistoryState, RecoveryKey, RecoveryState, ReplyDetails, ReplyReference, Retain,71
+ RichText, RoomCategory, RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership,7272
RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary,7373
RoomVisibility, SendState, Sender, SpaceChild, Termination, ThreadListCloseReason,7474
ThreadListCommand, ThreadListEntry, ThreadSummary, ThreadUnread, TimelineCommand,@@ -629,8 +629,19 @@ pub enum EncryptionCommand {
629629
Recover(String),630630
CreateKeyBackup,631631
EnableKeyBackup,632
- DisableKeyBackup { delete_from_server: bool },632
+ DisableKeyBackup {633
+ delete_from_server: bool,634
+ },633635
RecoverKeyBackup(String),636
+ /// creates secret storage holding the cross-signing and backup keys, turns key backup on,637
+ /// and answers with the recovery key. refused while a backup this device can't open exists638
+ EnableRecovery {639
+ passphrase: Option<String>,640
+ },641
+ ResetRecoveryKey {642
+ passphrase: Option<String>,643
+ },644
+ DisableRecovery,634645
StartVerification(VerificationTarget),635646
Verify(VerificationAction),636647
}@@ -649,6 +660,15 @@ impl fmt::Debug for EncryptionCommand {
649660
.debug_tuple("RecoverKeyBackup")650661
.field(&"...")651662
.finish(),663
+ Self::EnableRecovery { passphrase } => formatter664
+ .debug_struct("EnableRecovery")665
+ .field("passphrase", &passphrase.as_ref().map(|_| "..."))666
+ .finish(),667
+ Self::ResetRecoveryKey { passphrase } => formatter668
+ .debug_struct("ResetRecoveryKey")669
+ .field("passphrase", &passphrase.as_ref().map(|_| "..."))670
+ .finish(),671
+ Self::DisableRecovery => formatter.write_str("DisableRecovery"),652672
Self::StartVerification(target) => formatter653673
.debug_tuple("StartVerification")654674
.field(target)@@ -701,6 +721,7 @@ pub enum CommandOutput {
701721
SearchSession(crate::SearchSession),702722
SearchPage(crate::SearchPage),703723
SearchCoverage(crate::SearchCoverage),724
+ RecoveryKey(RecoveryKey),704725
}705726
706727
#[derive(Clone, Debug, Eq, PartialEq)]@@ -2296,10 +2317,39 @@ pub enum KeyBackupState {
22962317
pub enum RecoveryState {22972318
Unknown,22982319
Disabled,2320
+ /// `enable_recovery` is running2321
+ Enabling,22992322
Incomplete,23002323
Enabled,23012324
}23022325
2326
+/// shown to a person once. macltok never stores or logs it, and Debug hides it2327
+#[derive(Clone, Eq, PartialEq)]2328
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]2329
+pub struct RecoveryKey(String);2330
+2331
+impl RecoveryKey {2332
+ pub(crate) fn new(key: String) -> Self {2333
+ Self(key)2334
+ }2335
+2336
+ pub fn as_str(&self) -> &str {2337
+ &self.02338
+ }2339
+}2340
+2341
+impl fmt::Debug for RecoveryKey {2342
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {2343
+ formatter.write_str("RecoveryKey(...)")2344
+ }2345
+}2346
+2347
+impl fmt::Display for RecoveryKey {2348
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {2349
+ formatter.write_str(&self.0)2350
+ }2351
+}2352
+23032353
#[derive(Clone, Copy, Debug, Eq, PartialEq)]23042354
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]23052355
#[non_exhaustive]@@ -35,10 +35,10 @@ use crate::{
3535
MentionsBoundary, MentionsCommand, MentionsPage, MessageContent, MessageEditContent,3636
NotificationCommand, OwnDevice, PackCommand, PackImageInfo, PackUsage, Patch,3737
PowerLevelChanges, Profile, PushNotification, PushNotificationEvent, PushNotificationStatus,38
- RoomCommand, RoomListFilter, RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels,39
- RoomSummary, SavedMentionContext, SearchCommand, SearchCoverage, SearchPage, SearchScope,40
- SearchSession, SpaceChild, ThreadListCommand, TimelineCommand, TimelineId, VerificationAction,41
- VerificationTarget,38
+ RecoveryKey, RoomCommand, RoomListFilter, RoomNotificationMode, RoomNotificationSettings,39
+ RoomPowerLevels, RoomSummary, SavedMentionContext, SearchCommand, SearchCoverage, SearchPage,40
+ SearchScope, SearchSession, SpaceChild, ThreadListCommand, TimelineCommand, TimelineId,41
+ VerificationAction, VerificationTarget,4242
encryption::encryption_state,4343
engine::{MediaService, StoreLock},4444
model::{PushNotificationRoom, PushNotificationSender},@@ -800,6 +800,54 @@ impl ClientHandle {
800800
.await801801
}802802
803
+ /// the key is in the answer and nowhere else, so show it before dropping it804
+ pub async fn enable_recovery(&self, passphrase: Option<String>) -> Result<RecoveryKey, Error> {805
+ self.recovery_key(EncryptionCommand::EnableRecovery { passphrase })806
+ .await807
+ }808
+809
+ pub async fn reset_recovery_key(810
+ &self,811
+ passphrase: Option<String>,812
+ ) -> Result<RecoveryKey, Error> {813
+ self.recovery_key(EncryptionCommand::ResetRecoveryKey { passphrase })814
+ .await815
+ }816
+817
+ async fn recovery_key(&self, command: EncryptionCommand) -> Result<RecoveryKey, Error> {818
+ match self.execute(Command::Encryption(command)).await? {819
+ CommandOutput::RecoveryKey(key) => Ok(key),820
+ output => Err(unexpected("recovery key", &output)),821
+ }822
+ }823
+824
+ pub async fn disable_recovery(&self) -> Result<(), Error> {825
+ self.run(Command::Encryption(EncryptionCommand::DisableRecovery))826
+ .await827
+ }828
+829
+ /// checks a typed key or passphrase against secret storage without importing anything830
+ pub async fn verify_recovery_key(&self, recovery_key: &str) -> Result<bool, Error> {831
+ use matrix_sdk::encryption::secret_storage::SecretStorageError;832
+ match self833
+ .client834
+ .encryption()835
+ .secret_storage()836
+ .open_secret_store(recovery_key)837
+ .await838
+ {839
+ Ok(_) => Ok(true),840
+ Err(SecretStorageError::SecretStorageKey(_)) => Ok(false),841
+ Err(SecretStorageError::MissingKeyInfo { .. }) => {842
+ Err(Error::new(ErrorKind::Invalid, "recovery is not set up"))843
+ }844
+ Err(source) => Err(Error::from_recovery(845
+ "checking the recovery key failed",846
+ source.into(),847
+ )),848
+ }849
+ }850
+803851
pub async fn create_key_backup(&self) -> Result<(), Error> {804852
self.run(Command::Encryption(EncryptionCommand::CreateKeyBackup))805853
.await