Skip to content

Commit dcc32da

Browse files
authored
Merge pull request #3448 from matrix-org/rav/fix_backup_import
Crypto: fix backed-up keys being re-backed-up
2 parents 7fb57ea + 0777aa6 commit dcc32da

File tree

13 files changed

+311
-100
lines changed

13 files changed

+311
-100
lines changed

bindings/matrix-sdk-crypto-ffi/src/machine.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ impl OlmMachine {
991991
) -> Result<KeysImportResult, KeyImportError> {
992992
let keys = Cursor::new(keys);
993993
let keys = decrypt_room_key_export(keys, &passphrase)?;
994-
self.import_room_keys_helper(keys, false, progress_listener)
994+
self.import_room_keys_helper(keys, None, progress_listener)
995995
}
996996

997997
/// Import room keys from the given serialized unencrypted key export.
@@ -1001,6 +1001,9 @@ impl OlmMachine {
10011001
/// should be used if the room keys are coming from the server-side backup,
10021002
/// the method will mark all imported room keys as backed up.
10031003
///
1004+
/// **Note**: This has been deprecated. Use
1005+
/// [`OlmMachine::import_room_keys_from_backup`] instead.
1006+
///
10041007
/// # Arguments
10051008
///
10061009
/// * `keys` - The serialized version of the unencrypted key export.
@@ -1012,11 +1015,38 @@ impl OlmMachine {
10121015
keys: String,
10131016
progress_listener: Box<dyn ProgressListener>,
10141017
) -> Result<KeysImportResult, KeyImportError> {
1018+
// Assume that the keys came from the current backup version.
1019+
let backup_version = self.runtime.block_on(self.inner.backup_machine().backup_version());
10151020
let keys: Vec<Value> = serde_json::from_str(&keys)?;
1016-
10171021
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
1022+
self.import_room_keys_helper(keys, backup_version.as_deref(), progress_listener)
1023+
}
10181024

1019-
self.import_room_keys_helper(keys, true, progress_listener)
1025+
/// Import room keys from the given serialized unencrypted key export.
1026+
///
1027+
/// This method is the same as [`OlmMachine::import_room_keys`] but the
1028+
/// decryption step is skipped and should be performed by the caller. This
1029+
/// should be used if the room keys are coming from the server-side backup.
1030+
/// The method will mark all imported room keys as backed up.
1031+
///
1032+
/// # Arguments
1033+
///
1034+
/// * `keys` - The serialized version of the unencrypted key export.
1035+
///
1036+
/// * `backup_version` - The version of the backup that these keys came
1037+
/// from.
1038+
///
1039+
/// * `progress_listener` - A callback that can be used to introspect the
1040+
/// progress of the key import.
1041+
pub fn import_room_keys_from_backup(
1042+
&self,
1043+
keys: String,
1044+
backup_version: String,
1045+
progress_listener: Box<dyn ProgressListener>,
1046+
) -> Result<KeysImportResult, KeyImportError> {
1047+
let keys: Vec<Value> = serde_json::from_str(&keys)?;
1048+
let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect();
1049+
self.import_room_keys_helper(keys, Some(&backup_version), progress_listener)
10201050
}
10211051

10221052
/// Discard the currently active room key for the given room if there is
@@ -1506,16 +1536,18 @@ impl OlmMachine {
15061536
fn import_room_keys_helper(
15071537
&self,
15081538
keys: Vec<ExportedRoomKey>,
1509-
from_backup: bool,
1539+
from_backup_version: Option<&str>,
15101540
progress_listener: Box<dyn ProgressListener>,
15111541
) -> Result<KeysImportResult, KeyImportError> {
15121542
let listener = |progress: usize, total: usize| {
15131543
progress_listener.on_progress(progress as i32, total as i32)
15141544
};
15151545

1516-
#[allow(deprecated)]
1517-
let result =
1518-
self.runtime.block_on(self.inner.import_room_keys(keys, from_backup, listener))?;
1546+
let result = self.runtime.block_on(self.inner.store().import_room_keys(
1547+
keys,
1548+
from_backup_version,
1549+
listener,
1550+
))?;
15191551

15201552
Ok(KeysImportResult {
15211553
imported: result.imported_count as i64,

crates/matrix-sdk-crypto/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,19 @@ Breaking changes:
3131
- Add new `dehydrated` property to `olm::account::PickledAccount`.
3232
([#3164](https://github.com/matrix-org/matrix-rust-sdk/pull/3164))
3333

34+
- Remove deprecated `OlmMachine::import_room_keys`.
35+
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
36+
37+
Deprecations:
38+
39+
- Deprecate `BackupMachine::import_backed_up_room_keys`.
40+
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
41+
3442
Additions:
3543

44+
- Expose new method `CryptoStore::import_room_keys`.
45+
([#3448](https://github.com/matrix-org/matrix-rust-sdk/pull/3448))
46+
3647
- Expose new method `BackupMachine::backup_version`.
3748
([#3320](https://github.com/matrix-org/matrix-rust-sdk/pull/3320))
3849

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ impl BackupMachine {
600600
///
601601
/// Returns a [`RoomKeyImportResult`] containing information about room keys
602602
/// which were imported.
603+
#[deprecated(note = "Use the OlmMachine::store::import_room_keys method instead")]
603604
pub async fn import_backed_up_room_keys(
604605
&self,
605606
room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
@@ -619,7 +620,15 @@ impl BackupMachine {
619620
}
620621
}
621622

622-
self.store.import_room_keys(decrypted_room_keys, true, progress_listener).await
623+
// FIXME: This method is a bit flawed: we have no real idea which backup version
624+
// these keys came from. For example, we might have reset the backup
625+
// since the keys were downloaded. For now, let's assume they came from
626+
// the "current" backup version.
627+
let backup_version = self.backup_version().await;
628+
629+
self.store
630+
.import_room_keys(decrypted_room_keys, backup_version.as_deref(), progress_listener)
631+
.await
623632
}
624633
}
625634

@@ -826,6 +835,12 @@ mod tests {
826835
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
827836
let backup_machine = machine.backup_machine();
828837

838+
// We set up a backup key, so that we can test `backup_machine.backup()` later.
839+
let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
840+
let backup_key = decryption_key.megolm_v1_public_key();
841+
backup_key.set_version("1".to_owned());
842+
backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
843+
829844
let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
830845
let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
831846
let room_key = room_key();
@@ -839,19 +854,29 @@ mod tests {
839854

840855
assert!(session.is_none(), "Initially we should not have the session in the store");
841856

857+
#[allow(deprecated)]
842858
backup_machine
843859
.import_backed_up_room_keys(room_keys, |_, _| {})
844860
.await
845861
.expect("We should be able to import a room key");
846862

863+
// Now check that the session was correctly imported, and that it is marked as
864+
// backed up
847865
let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
848-
849866
assert_let!(Some(session) = session);
850867
assert!(
851868
session.backed_up(),
852869
"If a session was imported from a backup, it should be considered to be backed up"
853870
);
854871
assert!(session.has_been_imported());
872+
873+
// Also check that it is not returned by a backup request.
874+
let backup_request =
875+
backup_machine.backup().await.expect("We should be able to create a backup request");
876+
assert!(
877+
backup_request.is_none(),
878+
"If a session was imported from backup, it should not be backed up again."
879+
);
855880
}
856881

857882
#[async_test]

crates/matrix-sdk-crypto/src/file_encryption/key_export.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pub enum KeyExportError {
7474
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
7575
/// # let export = Cursor::new("".to_owned());
7676
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
77-
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
77+
/// machine.store().import_room_keys(exported_keys, None, |_, _| {}).await.unwrap();
7878
/// # };
7979
/// ```
8080
pub fn decrypt_room_key_export(

crates/matrix-sdk-crypto/src/machine.rs

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ use crate::{
6262
gossiping::GossipMachine,
6363
identities::{user::UserIdentities, Device, IdentityManager, UserDevices},
6464
olm::{
65-
Account, CrossSigningStatus, EncryptionSettings, ExportedRoomKey, IdentityKeys,
66-
InboundGroupSession, OlmDecryptionInfo, PrivateCrossSigningIdentity, SessionType,
67-
StaticAccountData,
65+
Account, CrossSigningStatus, EncryptionSettings, IdentityKeys, InboundGroupSession,
66+
OlmDecryptionInfo, PrivateCrossSigningIdentity, SessionType, StaticAccountData,
6867
},
6968
requests::{IncomingResponse, OutgoingRequest, UploadSigningKeysRequest},
7069
session_manager::{GroupSessionManager, SessionManager},
@@ -91,7 +90,7 @@ use crate::{
9190
utilities::timestamp_to_iso8601,
9291
verification::{Verification, VerificationMachine, VerificationRequest},
9392
CrossSigningKeyExport, CryptoStoreError, KeysQueryRequest, LocalTrust, ReadOnlyDevice,
94-
RoomKeyImportResult, SignatureError, ToDeviceRequest,
93+
SignatureError, ToDeviceRequest,
9594
};
9695

9796
/// State machine implementation of the Olm/Megolm encryption protocol used for
@@ -1783,49 +1782,6 @@ impl OlmMachine {
17831782
self.store().get_user_devices(user_id).await
17841783
}
17851784

1786-
/// Import the given room keys into our store.
1787-
///
1788-
/// # Arguments
1789-
///
1790-
/// * `exported_keys` - A list of previously exported keys that should be
1791-
/// imported into our store. If we already have a better version of a key
1792-
/// the key will *not* be imported.
1793-
///
1794-
/// * `from_backup` - Were the room keys imported from the backup, if true
1795-
/// will mark the room keys as already backed up. This will prevent backing
1796-
/// up keys that are already backed up.
1797-
///
1798-
/// Returns a tuple of numbers that represent the number of sessions that
1799-
/// were imported and the total number of sessions that were found in the
1800-
/// key export.
1801-
///
1802-
/// # Examples
1803-
///
1804-
/// ```no_run
1805-
/// # use std::io::Cursor;
1806-
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_room_key_export};
1807-
/// # use ruma::{device_id, user_id};
1808-
/// # let alice = user_id!("@alice:example.org");
1809-
/// # async {
1810-
/// # let machine = OlmMachine::new(&alice, device_id!("DEVICEID")).await;
1811-
/// # let export = Cursor::new("".to_owned());
1812-
/// let exported_keys = decrypt_room_key_export(export, "1234").unwrap();
1813-
/// machine.import_room_keys(exported_keys, false, |_, _| {}).await.unwrap();
1814-
/// # };
1815-
/// ```
1816-
#[deprecated(
1817-
since = "0.7.0",
1818-
note = "Use the OlmMachine::store::import_exported_room_keys method instead"
1819-
)]
1820-
pub async fn import_room_keys(
1821-
&self,
1822-
exported_keys: Vec<ExportedRoomKey>,
1823-
from_backup: bool,
1824-
progress_listener: impl Fn(usize, usize),
1825-
) -> StoreResult<RoomKeyImportResult> {
1826-
self.store().import_room_keys(exported_keys, from_backup, progress_listener).await
1827-
}
1828-
18291785
/// Get the status of the private cross signing keys.
18301786
///
18311787
/// This can be used to check which private cross signing keys we have

crates/matrix-sdk-crypto/src/olm/group_sessions/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ pub struct ExportedRoomKey {
9898
}
9999

100100
impl ExportedRoomKey {
101-
pub(crate) fn from_backed_up_room_key(
101+
/// Create an `ExportedRoomKey` from a `BackedUpRoomKey`.
102+
///
103+
/// This can be used when importing the keys from a backup into the store.
104+
pub fn from_backed_up_room_key(
102105
room_id: OwnedRoomId,
103106
session_id: String,
104107
room_key: BackedUpRoomKey,

crates/matrix-sdk-crypto/src/store/integration_tests.rs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,9 @@ macro_rules! cryptostore_integration_tests {
290290
);
291291
}
292292

293+
/// Test that we can import an inbound group session via [`CryptoStore::save_changes`]
293294
#[async_test]
294-
async fn save_inbound_group_session() {
295+
async fn save_changes_save_inbound_group_session() {
295296
let (account, store) = get_loaded_store("save_inbound_group_session").await;
296297

297298
let room_id = &room_id!("!test:localhost");
@@ -303,31 +304,96 @@ macro_rules! cryptostore_integration_tests {
303304
store.save_changes(changes).await.expect("Can't save group session");
304305
}
305306

307+
/// Test that we can import a backed-up group session via
308+
/// [`CryptoStore::save_inbound_group_sessions`]
306309
#[async_test]
307-
async fn save_inbound_group_session_for_backup() {
310+
async fn save_inbound_group_session_from_backup() {
308311
let (account, store) =
309-
get_loaded_store("save_inbound_group_session_for_backup").await;
312+
get_loaded_store("save_inbound_group_session_from_backup").await;
310313

311314
let room_id = &room_id!("!test:localhost");
312315
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
313316

314-
let changes =
315-
Changes { inbound_group_sessions: vec![session.clone()], ..Default::default() };
317+
session.mark_as_backed_up();
318+
store
319+
.save_inbound_group_sessions(vec![session.clone()], Some(&"bkpver1"))
320+
.await
321+
.expect("could not save sessions");
316322

317-
store.save_changes(changes).await.expect("Can't save group session");
323+
let loaded_session = store
324+
.get_inbound_group_session(&session.room_id, session.session_id())
325+
.await
326+
.expect("error when loading session")
327+
.expect("session not found in store");
328+
assert_eq!(session, loaded_session);
329+
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
330+
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
331+
332+
// It should *not* be returned by a request for backup for the same backup version
333+
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
334+
assert_eq!(to_back_up.len(), 0, "backup was returned by backup query");
335+
assert_eq!(
336+
store.inbound_group_session_counts(Some(&"bkpver1")).await.unwrap().backed_up, 1,
337+
"backed_up count",
338+
);
339+
}
340+
341+
/// Test that the behaviour of a key imported from an *old* backup is correct
342+
///
343+
/// This currently only works on the MemoryStore, so is ignored. The other stores
344+
/// are waiting for more work on https://github.com/element-hq/element-web/issues/26892.
345+
#[ignore]
346+
#[async_test]
347+
async fn save_inbound_group_session_from_old_backup() {
348+
let (account, store) =
349+
get_loaded_store("save_inbound_group_session_from_old_backup").await;
350+
351+
let room_id = &room_id!("!test:localhost");
352+
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
353+
354+
session.mark_as_backed_up();
355+
store
356+
.save_inbound_group_sessions(vec![session.clone()], Some(&"bkpver1"))
357+
.await
358+
.expect("could not save sessions");
359+
360+
// The session should be returned by a request for backup from a different backup version.
361+
let to_back_up = store.inbound_group_sessions_for_backup("bkpver2", 1).await.unwrap();
362+
assert_eq!(to_back_up, vec![session]);
363+
assert_eq!(
364+
store.inbound_group_session_counts(Some(&"bkpver2")).await.unwrap().backed_up, 0,
365+
"backed_up count for backup version 2",
366+
);
367+
}
368+
369+
/// Test that we can import a not-backed-up group session via
370+
/// [`CryptoStore::save_inbound_group_sessions`]
371+
#[async_test]
372+
async fn save_inbound_group_session_from_import() {
373+
let (account, store) =
374+
get_loaded_store("save_inbound_group_session_from_import").await;
375+
376+
let room_id = &room_id!("!test:localhost");
377+
let (_, session) = account.create_group_session_pair_with_defaults(room_id).await;
378+
379+
store
380+
.save_inbound_group_sessions(vec![session.clone()], None)
381+
.await
382+
.expect("could not save sessions");
318383

319384
let loaded_session = store
320385
.get_inbound_group_session(&session.room_id, session.session_id())
321386
.await
322-
.unwrap()
323-
.unwrap();
387+
.expect("error when loading session")
388+
.expect("session not found in store");
324389
assert_eq!(session, loaded_session);
325390
assert_eq!(store.get_inbound_group_sessions().await.unwrap().len(), 1);
326391
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().total, 1);
327392
assert_eq!(store.inbound_group_session_counts(None).await.unwrap().backed_up, 0);
328393

329-
let to_back_up = store.inbound_group_sessions_for_backup("bkpver", 1).await.unwrap();
330-
assert_eq!(to_back_up, vec![session])
394+
// It should be returned by a request for backup
395+
let to_back_up = store.inbound_group_sessions_for_backup("bkpver1", 1).await.unwrap();
396+
assert_eq!(to_back_up, vec![session]);
331397
}
332398

333399
#[async_test]

0 commit comments

Comments
 (0)