@@ -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" ) ) ) ]
10471075mod 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}
0 commit comments