Skip to content

Commit 9de2d70

Browse files
authored
Provide recover_and_fix_backup method to fix backup if needed (#6252)
Part of element-hq/element-meta#3059
1 parent 6d141c0 commit 9de2d70

9 files changed

Lines changed: 390 additions & 18 deletions

File tree

bindings/matrix-sdk-ffi/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ All notable changes to this project will be documented in this file.
3131

3232
### Features
3333

34+
- Add `Encryption::recover_and_fix_backup` to automatically fix key storage backup if the
35+
private backup decryption key is missing, invalid or inconsistent with the public key.
36+
([#6252](https://github.com/matrix-org/matrix-rust-sdk/pull/6252))
3437
- Add support for [MSC3489](https://github.com/matrix-org/matrix-spec-proposals/pull/3489)
3538
live location sharing through a new `TimelineItemContent::LiveLocation` variant.
3639
([#6232](https://github.com/matrix-org/matrix-rust-sdk/pull/6232))

bindings/matrix-sdk-ffi/src/encryption.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ impl Encryption {
412412
Ok(None)
413413
}
414414

415+
/// Download identity and key backup information from Recovery
415416
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
416417
let result = self.inner.recovery().recover(&recovery_key).await;
417418

@@ -420,6 +421,23 @@ impl Encryption {
420421
Ok(result?)
421422
}
422423

424+
/// Download identity and key backup information from Recovery, and, if the
425+
/// key backup information is inconsistent, create a new key backup.
426+
///
427+
/// This will create a new key backup if:
428+
///
429+
/// * Key backup is enabled and the backup decryption key is missing from
430+
/// Recovery, or
431+
/// * Key backup is enabled and the backup decryption key does not match the
432+
/// public key
433+
pub async fn recover_and_fix_backup(&self, mut recovery_key: String) -> Result<()> {
434+
let result = self.inner.recovery().recover_and_fix_backup(&recovery_key).await;
435+
436+
recovery_key.zeroize();
437+
438+
Ok(result?)
439+
}
440+
423441
pub fn verification_state(&self) -> VerificationState {
424442
self.inner.verification_state().get().into()
425443
}

crates/matrix-sdk/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
88

99
### Features
1010

11+
- Add `Recovery::recover_and_fix_backup` to automatically fix key storage backup if the
12+
private backup decryption key is missing, invalid or inconsistent with the public key.
13+
([#6252](https://github.com/matrix-org/matrix-rust-sdk/pull/6252))
1114
- Attempt to import stored room key bundles for rooms awaiting bundles at
1215
client startup.
1316
([#6215](https://github.com/matrix-org/matrix-rust-sdk/pull/6215))

crates/matrix-sdk/src/encryption/backups/mod.rs

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,7 @@ impl Backups {
783783
pub(crate) async fn maybe_enable_backups(
784784
&self,
785785
maybe_recovery_key: &str,
786-
) -> Result<bool, Error> {
786+
) -> Result<bool, EnableBackupError> {
787787
let _guard = self.client.locks().backup_modify_lock.lock().await;
788788

789789
// Create a future here which allows us to catch any failure that might happen
@@ -879,7 +879,7 @@ impl Backups {
879879
this backup version"
880880
);
881881

882-
Ok(false)
882+
Err(EnableBackupError::InconsistentBackupDecryptionKey)
883883
}
884884
};
885885

@@ -932,8 +932,17 @@ impl Backups {
932932
let secrets = olm_machine.store().get_secrets_from_inbox(&SecretName::RecoveryKey).await?;
933933

934934
for secret in secrets {
935-
if self.maybe_enable_backups(&secret.event.content.secret).await? {
936-
break;
935+
match self.maybe_enable_backups(&secret.event.content.secret).await {
936+
Ok(enabled) => {
937+
if enabled {
938+
break;
939+
}
940+
}
941+
Err(EnableBackupError::InconsistentBackupDecryptionKey) => {
942+
// Ignore a bad backup decryption key here. We already
943+
// logged the details inside maybe_enable_backups().
944+
}
945+
Err(EnableBackupError::Error(e)) => return Err(e),
937946
}
938947
}
939948

@@ -1043,12 +1052,41 @@ impl Backups {
10431052
}
10441053
}
10451054

1055+
/// An error that happened while we were attempting to enable key backups.
1056+
#[derive(Debug, thiserror::Error)]
1057+
pub enum EnableBackupError {
1058+
/// The private decryption key we found does not match the public key for
1059+
/// the enabled backup.
1060+
#[error("The backup decryption key does not match the latest backup version")]
1061+
InconsistentBackupDecryptionKey,
1062+
1063+
/// A general error occurred while enabling key backup.
1064+
#[error(transparent)]
1065+
Error(Error),
1066+
}
1067+
1068+
impl<T: Into<Error>> From<T> for EnableBackupError {
1069+
fn from(value: T) -> Self {
1070+
Self::Error(value.into())
1071+
}
1072+
}
1073+
10461074
#[cfg(all(test, not(target_family = "wasm")))]
10471075
mod test {
10481076
use std::time::Duration;
10491077

1078+
use assert_matches2::assert_matches;
1079+
use matrix_sdk_base::crypto::{
1080+
GossipRequest, GossippedSecret, SecretInfo,
1081+
store::types::Changes,
1082+
types::events::{
1083+
olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
1084+
secret_send::SecretSendContent,
1085+
},
1086+
};
10501087
use matrix_sdk_test::async_test;
10511088
use serde_json::json;
1089+
use vodozemac::Curve25519PublicKey;
10521090
use wiremock::{
10531091
Mock, MockServer, ResponseTemplate,
10541092
matchers::{header, method, path},
@@ -1119,6 +1157,93 @@ mod test {
11191157
assert_eq!(client.encryption().backups().state(), BackupState::Unknown);
11201158
}
11211159

1160+
#[async_test]
1161+
async fn test_resuming_backups_when_keys_are_consistent_makes_backups_enabled() {
1162+
let server = MatrixMockServer::new().await;
1163+
let client = server.client_builder().build().await;
1164+
let backups = client.encryption().backups();
1165+
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
1166+
1167+
let matching_public_key = derive_public_key_from(&backup_decryption_key);
1168+
1169+
server
1170+
.mock_room_keys_version()
1171+
.exists_with_key(&matching_public_key.to_base64())
1172+
.expect(1)
1173+
.mount()
1174+
.await;
1175+
1176+
// Given there is a backup decryption key waiting in the secrets inbox, which is
1177+
// consistent with the public key
1178+
queue_backup_decryption_key_secret(client, &backup_decryption_key.to_base64()).await;
1179+
1180+
// When we resume backups
1181+
let res = backups.maybe_resume_backups().await;
1182+
1183+
// Then no error is returned
1184+
assert_matches!(res, Ok(_));
1185+
1186+
// And the backup state is now enabled
1187+
assert_eq!(backups.state(), BackupState::Enabled);
1188+
}
1189+
1190+
#[async_test]
1191+
async fn test_resuming_backups_when_keys_are_inconsistent_has_no_effect() {
1192+
// Note: this was written when we added new error-surfacing logic to
1193+
// matrix_sdk::encryption::backups::Backups::maybe_enable_backups, and
1194+
// this test checks that the previous behaviour is preserved: we ignore
1195+
// inconsistent backup keys when resuming backups. This may not turn out
1196+
// to be the correct behaviour, so this test may need updating if we
1197+
// decide this condition should be handled differently.
1198+
1199+
let server = MatrixMockServer::new().await;
1200+
let client = server.client_builder().build().await;
1201+
let backups = client.encryption().backups();
1202+
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
1203+
1204+
let non_matching_public_key = derive_public_key_from(&BackupDecryptionKey::new().unwrap());
1205+
1206+
server
1207+
.mock_room_keys_version()
1208+
.exists_with_key(&non_matching_public_key.to_base64())
1209+
.expect(1)
1210+
.mount()
1211+
.await;
1212+
1213+
// Given there is a backup decryption key waiting in the secrets inbox, but it
1214+
// doesn't match the public backup key
1215+
queue_backup_decryption_key_secret(client, &backup_decryption_key.to_base64()).await;
1216+
1217+
// When we attempt to resume backups
1218+
let res = backups.maybe_resume_backups().await;
1219+
1220+
// Then no error is returned ...
1221+
assert_matches!(res, Ok(_));
1222+
1223+
// ... even though the backup setup failed because the decryption keys were
1224+
// inconsistent
1225+
assert_eq!(backups.state(), BackupState::Unknown);
1226+
}
1227+
1228+
#[async_test]
1229+
async fn test_errors_when_resuming_backups_are_propagated() {
1230+
let server = MatrixMockServer::new().await;
1231+
let client = server.client_builder().build().await;
1232+
let backups = client.encryption().backups();
1233+
1234+
// Given an invalid backup decryption key is waiting in the secrets inbox
1235+
queue_backup_decryption_key_secret(client, "not valid base64").await;
1236+
1237+
// When we attempt to resume backups
1238+
let res = backups.maybe_resume_backups().await;
1239+
1240+
// Then an error is returned
1241+
assert_matches!(res, Err(Error::SerdeJson(_)));
1242+
1243+
// And the backup state is unknown
1244+
assert_eq!(backups.state(), BackupState::Unknown);
1245+
}
1246+
11221247
#[async_test]
11231248
async fn test_backup_disabling_after_remote_deletion() {
11241249
let server = MockServer::start().await;
@@ -1434,4 +1559,61 @@ mod test {
14341559

14351560
assert_eq!(old_duration, current_duration);
14361561
}
1562+
1563+
/// Given a private backup decryption key, return the matching public key
1564+
/// for that backup
1565+
fn derive_public_key_from(backup_decryption_key: &BackupDecryptionKey) -> Curve25519PublicKey {
1566+
let backup_info = backup_decryption_key.to_backup_info();
1567+
match backup_info {
1568+
RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(megolm_v1_auth_data) => {
1569+
megolm_v1_auth_data.public_key
1570+
}
1571+
RoomKeyBackupInfo::Other { .. } => {
1572+
panic!("Unexpected backup info type")
1573+
}
1574+
}
1575+
}
1576+
1577+
/// Add a new secret to the secrets inbox of this client's OlmMachine
1578+
/// containing the supplied backup decyption key.
1579+
async fn queue_backup_decryption_key_secret(
1580+
client: Client,
1581+
secret_backup_decryption_key: &str,
1582+
) {
1583+
let _guard = client.olm_machine().await;
1584+
let machine = _guard.as_ref().unwrap();
1585+
let transaction_id = TransactionId::new();
1586+
let secret_info = SecretInfo::SecretRequest(SecretName::RecoveryKey);
1587+
let user_id = machine.user_id().to_owned();
1588+
1589+
let gossip_request = GossipRequest {
1590+
request_recipient: machine.user_id().to_owned(),
1591+
request_id: transaction_id.clone(),
1592+
info: secret_info.clone(),
1593+
sent_out: true,
1594+
};
1595+
1596+
let event = DecryptedSecretSendEvent {
1597+
sender: user_id.clone(),
1598+
recipient: user_id.clone(),
1599+
keys: OlmV1Keys { ed25519: machine.identity_keys().ed25519 },
1600+
recipient_keys: OlmV1Keys { ed25519: machine.identity_keys().ed25519 },
1601+
sender_device_keys: None,
1602+
content: SecretSendContent::new(
1603+
transaction_id.to_owned(),
1604+
secret_backup_decryption_key.to_owned(),
1605+
),
1606+
};
1607+
1608+
let gossipped_secret =
1609+
GossippedSecret { secret_name: SecretName::RecoveryKey, gossip_request, event };
1610+
1611+
let changes = Changes { secrets: vec![gossipped_secret], ..Default::default() };
1612+
1613+
machine
1614+
.store()
1615+
.save_changes(changes)
1616+
.await
1617+
.expect("We should be able to import a room key");
1618+
}
14371619
}

crates/matrix-sdk/src/encryption/recovery/mod.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ use crate::encryption::{
109109
backups::Backups,
110110
secret_storage::{SecretStorage, SecretStore},
111111
};
112-
use crate::{Client, client::WeakClient, encryption::backups::BackupState};
112+
use crate::{
113+
Client,
114+
client::WeakClient,
115+
encryption::{backups::BackupState, secret_storage::SecretStorageError},
116+
};
113117

114118
pub mod futures;
115119
mod types;
@@ -490,6 +494,65 @@ impl Recovery {
490494
Ok(())
491495
}
492496

497+
/// Recover all the secrets from the homeserver, and, if the
498+
/// key backup information is inconsistent, create a new key backup.
499+
///
500+
/// Please read the documentation for [`SecretStore::import_secrets()`]
501+
/// for more information about the recovery of identity information.
502+
///
503+
/// This will create a new key backup if:
504+
///
505+
/// * Key backup is enabled and the backup decryption key is missing from
506+
/// Recovery, or
507+
/// * Key backup is enabled and the backup decryption key does not match the
508+
/// public key
509+
///
510+
/// # Examples
511+
///
512+
/// ```no_run
513+
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
514+
/// # use url::Url;
515+
/// # async {
516+
/// # let homeserver = Url::parse("http://example.com")?;
517+
/// # let client = Client::new(homeserver).await?;
518+
/// let recovery = client.encryption().recovery();
519+
///
520+
/// recovery.recover_and_fix_backup("my recovery key or passphrase").await;
521+
///
522+
/// assert_eq!(recovery.state(), RecoveryState::Enabled);
523+
/// # anyhow::Ok(()) };
524+
/// ```
525+
#[instrument(skip_all)]
526+
pub async fn recover_and_fix_backup(&self, recovery_key: &str) -> Result<()> {
527+
let store =
528+
self.client.encryption().secret_storage().open_secret_store(recovery_key).await?;
529+
530+
let delete_and_recreate_backup = match store.import_secrets().await {
531+
Ok(()) => false,
532+
Err(SecretStorageError::InconsistentBackupDecryptionKey) => {
533+
warn!(
534+
"Key storage decryption key does not match the current backup - creating a new key backup"
535+
);
536+
true
537+
}
538+
Err(SecretStorageError::MissingOrInvalidBackupDecryptionKey) => {
539+
warn!("Missing or invalid backup decryption key - creating a new key backup");
540+
true
541+
}
542+
Err(e) => return Err(e.into()),
543+
};
544+
545+
if delete_and_recreate_backup {
546+
self.client.encryption().backups().disable_and_delete().await?;
547+
self.enable_backup().await?;
548+
store.export_secrets().await?;
549+
}
550+
551+
self.update_recovery_state().await?;
552+
553+
Ok(())
554+
}
555+
493556
/// Is this device the last device the user has?
494557
///
495558
/// This method is useful to check if we should recommend to the user that

crates/matrix-sdk/src/encryption/secret_storage/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ mod secret_store;
8888
pub use futures::CreateStore;
8989
pub use secret_store::SecretStore;
9090

91-
/// Convenicence type alias for the secret-storage specific results.
91+
/// Convenience type alias for the secret-storage specific results.
9292
pub type Result<T, E = SecretStorageError> = std::result::Result<T, E>;
9393

9494
/// Error type for errors when importing a secret from secret storage.
@@ -171,6 +171,16 @@ pub enum SecretStorageError {
171171
/// Error describing a decryption failure of a secret.
172172
#[error(transparent)]
173173
Decryption(#[from] DecryptionError),
174+
175+
/// The private decryption key we found does not match the public key for
176+
/// the enabled backup.
177+
#[error("The backup decryption key does not match the latest backup version")]
178+
InconsistentBackupDecryptionKey,
179+
180+
/// The private backup decryption key is missing, even though backups are
181+
/// enabled.
182+
#[error("The backup decryption key is missing")]
183+
MissingOrInvalidBackupDecryptionKey,
174184
}
175185

176186
impl SecretStorageError {

0 commit comments

Comments
 (0)