diff --git a/MatrixSDK.xcodeproj/project.pbxproj b/MatrixSDK.xcodeproj/project.pbxproj index aca441293d..df5a338e32 100644 --- a/MatrixSDK.xcodeproj/project.pbxproj +++ b/MatrixSDK.xcodeproj/project.pbxproj @@ -1767,6 +1767,12 @@ ED35652D281150310002BF6A /* MXOlmInboundGroupSessionUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED35652B281150310002BF6A /* MXOlmInboundGroupSessionUnitTests.swift */; }; ED35652F281153480002BF6A /* MXMegolmSessionDataUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED35652E281153480002BF6A /* MXMegolmSessionDataUnitTests.swift */; }; ED356530281153480002BF6A /* MXMegolmSessionDataUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED35652E281153480002BF6A /* MXMegolmSessionDataUnitTests.swift */; }; + ED44F01128180BCC00452A5D /* MXSharedHistoryKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01028180BCC00452A5D /* MXSharedHistoryKeyRequest.swift */; }; + ED44F01228180BCC00452A5D /* MXSharedHistoryKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01028180BCC00452A5D /* MXSharedHistoryKeyRequest.swift */; }; + ED44F01428180EAB00452A5D /* MXSharedHistoryKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01328180EAB00452A5D /* MXSharedHistoryKeyManager.swift */; }; + ED44F01528180EAB00452A5D /* MXSharedHistoryKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01328180EAB00452A5D /* MXSharedHistoryKeyManager.swift */; }; + ED44F01A28180F4000452A5D /* MXSharedHistoryKeyManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01728180F1C00452A5D /* MXSharedHistoryKeyManagerUnitTests.swift */; }; + ED44F01B28180F4000452A5D /* MXSharedHistoryKeyManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED44F01728180F1C00452A5D /* MXSharedHistoryKeyManagerUnitTests.swift */; }; ED5AE8C52816C8CF00105072 /* MXCoreDataRoomSummaryStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ED5AE8C22816C8CF00105072 /* MXCoreDataRoomSummaryStore.xcdatamodeld */; }; ED5AE8C62816C8CF00105072 /* MXCoreDataRoomSummaryStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = ED5AE8C22816C8CF00105072 /* MXCoreDataRoomSummaryStore.xcdatamodeld */; }; ED88999127F2065D00718486 /* MXRoomAliasResolution.h in Headers */ = {isa = PBXBuildFile; fileRef = ED88998F27F2065C00718486 /* MXRoomAliasResolution.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -2768,6 +2774,9 @@ ED2F344856EFFCA383E37B22 /* Pods-SDK-MatrixSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDK-MatrixSDK.release.xcconfig"; path = "Target Support Files/Pods-SDK-MatrixSDK/Pods-SDK-MatrixSDK.release.xcconfig"; sourceTree = ""; }; ED35652B281150310002BF6A /* MXOlmInboundGroupSessionUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXOlmInboundGroupSessionUnitTests.swift; sourceTree = ""; }; ED35652E281153480002BF6A /* MXMegolmSessionDataUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXMegolmSessionDataUnitTests.swift; sourceTree = ""; }; + ED44F01028180BCC00452A5D /* MXSharedHistoryKeyRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXSharedHistoryKeyRequest.swift; sourceTree = ""; }; + ED44F01328180EAB00452A5D /* MXSharedHistoryKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXSharedHistoryKeyManager.swift; sourceTree = ""; }; + ED44F01728180F1C00452A5D /* MXSharedHistoryKeyManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXSharedHistoryKeyManagerUnitTests.swift; sourceTree = ""; }; ED5AE8C32816C8CF00105072 /* MXRoomSummaryCoreDataStore2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MXRoomSummaryCoreDataStore2.xcdatamodel; sourceTree = ""; }; ED5AE8C42816C8CF00105072 /* MXRoomSummaryCoreDataStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MXRoomSummaryCoreDataStore.xcdatamodel; sourceTree = ""; }; ED88998F27F2065C00718486 /* MXRoomAliasResolution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MXRoomAliasResolution.h; sourceTree = ""; }; @@ -4113,6 +4122,7 @@ 32FA10C01FA1C9EE00E54233 /* MXOutgoingRoomKeyRequestManager.m */, 32A30B161FB4813400C8309E /* MXIncomingRoomKeyRequestManager.h */, 32A30B171FB4813400C8309E /* MXIncomingRoomKeyRequestManager.m */, + ED44F01328180EAB00452A5D /* MXSharedHistoryKeyManager.swift */, ); path = KeySharing; sourceTree = ""; @@ -4126,6 +4136,7 @@ 32F945F11FAB83D800622468 /* MXIncomingRoomKeyRequestCancellation.m */, 32FA10C81FA1C9F700E54233 /* MXOutgoingRoomKeyRequest.h */, 32FA10C91FA1C9F700E54233 /* MXOutgoingRoomKeyRequest.m */, + ED44F01028180BCC00452A5D /* MXSharedHistoryKeyRequest.swift */, ); path = Data; sourceTree = ""; @@ -4873,6 +4884,7 @@ ED21F67A28104B9A002FF83D /* Crypto */ = { isa = PBXGroup; children = ( + ED44F01628180F1300452A5D /* KeySharing */, ED35652A281150230002BF6A /* Data */, ED21F67B28104BA1002FF83D /* Algorithms */, ); @@ -4904,6 +4916,14 @@ path = Data; sourceTree = ""; }; + ED44F01628180F1300452A5D /* KeySharing */ = { + isa = PBXGroup; + children = ( + ED44F01728180F1C00452A5D /* MXSharedHistoryKeyManagerUnitTests.swift */, + ); + path = KeySharing; + sourceTree = ""; + }; ED8943D127E3474A000FC39C /* Store */ = { isa = PBXGroup; children = ( @@ -6031,7 +6051,9 @@ 8EC511062568216B00EC4E5B /* MXTaggedEventInfo.m in Sources */, 3AC135DB2640335100EE1E74 /* MXDehydrationService.m in Sources */, 32792BDD2296B90A00F4FC9D /* MXAggregatedEditsUpdater.m in Sources */, + ED44F01428180EAB00452A5D /* MXSharedHistoryKeyManager.swift in Sources */, 3259CD541DF860C300186944 /* MXRealmCryptoStore.m in Sources */, + ED44F01128180BCC00452A5D /* MXSharedHistoryKeyRequest.swift in Sources */, EC60EDAA265CFE3B00B39A4E /* MXRoomSyncTimeline.m in Sources */, EC0B9438271DB0D600B4D440 /* MXMemoryRoomSummaryStore.m in Sources */, EC8A53E225B1BCC6004E0802 /* MXThirdPartyUserInstance.m in Sources */, @@ -6486,6 +6508,7 @@ 32FCAB4D19E578860049C555 /* MXRestClientTests.m in Sources */, 32C78BA7256D227D008130B1 /* MXCryptoMigrationTests.m in Sources */, ED21F68528104DA2002FF83D /* MXMegolmEncryptionTests.swift in Sources */, + ED44F01A28180F4000452A5D /* MXSharedHistoryKeyManagerUnitTests.swift in Sources */, 322985CB26FAF898001890BC /* MXSession.swift in Sources */, EC131B192779D8D500712964 /* MXThreadEventTimelineUnitTests.swift in Sources */, B135067427EB201E00BD3276 /* MXLocationServiceTests.swift in Sources */, @@ -6574,7 +6597,9 @@ B14EF1E82397E90400758AF0 /* MXRoomPowerLevels.m in Sources */, 32EEA85E260401490041425B /* MXSummable.swift in Sources */, B14EF1E92397E90400758AF0 /* MXRealmMediaScanMapper.m in Sources */, + ED44F01528180EAB00452A5D /* MXSharedHistoryKeyManager.swift in Sources */, EC8A53E725B1BCC6004E0802 /* MXThirdPartyProtocol.m in Sources */, + ED44F01228180BCC00452A5D /* MXSharedHistoryKeyRequest.swift in Sources */, B19A30D724042F2700FB6F35 /* MXSelfVerifyingMasterKeyNotTrustedQRCodeData.m in Sources */, EC0B9439271DB0D600B4D440 /* MXMemoryRoomSummaryStore.m in Sources */, B14EF1EA2397E90400758AF0 /* MXRealmMediaScan.m in Sources */, @@ -7029,6 +7054,7 @@ EC116598270FCA8B0089FA56 /* MXBackgroundTaskUnitTests.swift in Sources */, B1E09A322397FD750057C069 /* MXRoomTests.m in Sources */, ED21F68628104DA2002FF83D /* MXMegolmEncryptionTests.swift in Sources */, + ED44F01B28180F4000452A5D /* MXSharedHistoryKeyManagerUnitTests.swift in Sources */, 322985CC26FAF898001890BC /* MXSession.swift in Sources */, EC131B1A2779D8D500712964 /* MXThreadEventTimelineUnitTests.swift in Sources */, B135067527EB201E00BD3276 /* MXLocationServiceTests.swift in Sources */, diff --git a/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.h b/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.h index 697b1a0f9e..00071f56e4 100644 --- a/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.h +++ b/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.h @@ -22,7 +22,9 @@ #import "MXDecrypting.h" -@interface MXMegolmDecryption : NSObject +@protocol MXSharedHistoryKeyService; + +@interface MXMegolmDecryption : NSObject @end diff --git a/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.m b/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.m index 9d58b25e05..bfe02508d7 100644 --- a/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.m +++ b/MatrixSDK/Crypto/Algorithms/Megolm/MXMegolmDecryption.m @@ -328,50 +328,75 @@ - (MXHTTPOperation*)shareKeysWithDevice:(MXIncomingRoomKeyRequest*)keyRequest NSString *deviceId = keyRequest.deviceId; MXDeviceInfo *deviceInfo = [crypto.deviceList storedDevice:userId deviceId:deviceId]; NSDictionary *body = keyRequest.requestBody; + NSString *roomId, *senderKey, *sessionId; + MXJSONModelSetString(roomId, body[@"room_id"]); + MXJSONModelSetString(senderKey, body[@"sender_key"]); + MXJSONModelSetString(sessionId, body[@"session_id"]); + + return [self shareKeysWitUserId:userId + devices:@[deviceInfo] + forceEnsureOlmSessions:NO + roomId:roomId + sessionId:sessionId + senderKey:senderKey + success:success + failure:failure]; +} + +#pragma mark - Private methods +- (MXHTTPOperation *)shareKeysWitUserId:(NSString *)userId + devices:(NSArray *)devices + forceEnsureOlmSessions:(BOOL)forceEnsureOlmSessions + roomId:(NSString *)roomId + sessionId:(NSString *)sessionId + senderKey:(NSString *)senderKey + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ MXHTTPOperation *operation; MXWeakify(self); operation = [crypto ensureOlmSessionsForDevices:@{ - userId: @[deviceInfo] + userId: devices } - force:NO + force:forceEnsureOlmSessions success:^(MXUsersDevicesMap *results) { - MXStrongifyAndReturnIfNil(self); - - MXOlmSessionResult *olmSessionResult = [results objectForDevice:deviceId forUser:userId]; - if (!olmSessionResult.sessionId) - { - // no session with this device, probably because there - // were no one-time keys. - // - // ensureOlmSessionsForUsers has already done the logging, - // so just skip it. - if (success) - { - success(); - } - return; - } - - NSString *roomId, *senderKey, *sessionId; - MXJSONModelSetString(roomId, body[@"room_id"]); - MXJSONModelSetString(senderKey, body[@"sender_key"]); - MXJSONModelSetString(sessionId, body[@"session_id"]); - - MXLogDebug(@"[MXMegolmDecryption] shareKeysWithDevice: sharing keys for session %@|%@ with device %@:%@", senderKey, sessionId, userId, deviceId); - - NSDictionary *payload = [self->crypto buildMegolmKeyForwardingMessage:roomId senderKey:senderKey sessionId:sessionId chainIndex:nil]; - - MXDeviceInfo *deviceInfo = olmSessionResult.device; - - MXUsersDevicesMap *contentMap = [[MXUsersDevicesMap alloc] init]; - [contentMap setObject:[self->crypto encryptMessage:payload forDevices:@[deviceInfo]] - forUser:userId andDevice:deviceId]; - - MXHTTPOperation *operation2 = [self->crypto.matrixRestClient sendToDevice:kMXEventTypeStringRoomEncrypted contentMap:contentMap txnId:nil success:success failure:failure]; - [operation mutateTo:operation2]; + MXStrongifyAndReturnIfNil(self); + NSDictionary *payload = [self->crypto buildMegolmKeyForwardingMessage:roomId + senderKey:senderKey + sessionId:sessionId + chainIndex:nil]; + + MXUsersDevicesMap *contentMap = [[MXUsersDevicesMap alloc] init]; + for (MXDeviceInfo *deviceInfo in devices) { + MXOlmSessionResult *olmSessionResult = [results objectForDevice:deviceInfo.deviceId forUser:userId]; + if (olmSessionResult.sessionId) { + NSDictionary *message = [self->crypto encryptMessage:payload forDevices:@[deviceInfo]]; + [contentMap setObject:message forUser:userId andDevice:deviceInfo.deviceId]; + } else { + MXLogDebug(@"[MXMegolmDecryption] No session with device %@, cannot share keys", deviceInfo.deviceId); + } + } + + if (contentMap.count == 0) { + MXLogDebug(@"[MXMegolmDecryption] No devices available for user %@, cannot share keys", userId); + if (success) + { + success(); + } + return; + } + + MXLogDebug(@"[MXMegolmDecryption] shareKeysWithDevices: sharing keys for session %@|%@ with devices of user %@", senderKey, sessionId, userId); + MXHTTPOperation *operation2 = [self->crypto.matrixRestClient sendToDevice:kMXEventTypeStringRoomEncrypted + contentMap:contentMap + txnId:nil + success:success + failure:failure]; + [operation mutateTo:operation2]; + } failure:failure]; return operation; @@ -498,6 +523,29 @@ - (void)requestKeysForEvent:(MXEvent*)event } } +#pragma mark - MXSharedHistoryKeyStore + +- (BOOL)hasSharedHistoryWithSessionId:(NSString *)sessionId senderKey:(NSString *)senderKey +{ + MXOlmInboundGroupSession *session = [crypto.store inboundGroupSessionWithId:sessionId + andSenderKey:senderKey]; + return session.sharedHistory; +} + +- (void)shareKeysWithRequest:(MXSharedHistoryKeyRequest *)request + success:(void (^)(void))success + failure:(void (^)(NSError *error))failure +{ + [self shareKeysWitUserId:request.userId + devices:request.devices + forceEnsureOlmSessions:YES + roomId:request.roomId + sessionId:request.sessionId + senderKey:request.senderKey + success:success + failure:failure]; +} + @end #endif diff --git a/MatrixSDK/Crypto/KeySharing/Data/MXSharedHistoryKeyRequest.swift b/MatrixSDK/Crypto/KeySharing/Data/MXSharedHistoryKeyRequest.swift new file mode 100644 index 0000000000..04e14b9e2d --- /dev/null +++ b/MatrixSDK/Crypto/KeySharing/Data/MXSharedHistoryKeyRequest.swift @@ -0,0 +1,40 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// Request to share the key for a given `sessionId` and `senderKey` +/// with selected user and all of their devices +@objcMembers +public class MXSharedHistoryKeyRequest: NSObject { + public let userId: String + public let devices: [MXDeviceInfo] + public let roomId: String + public let sessionId: String + public let senderKey: String + + public init( + userId: String, + devices: [MXDeviceInfo], + roomId: String, + sessionId: String, + senderKey: String + ) { + self.userId = userId + self.devices = devices + self.roomId = roomId + self.sessionId = sessionId + self.senderKey = senderKey + } +} diff --git a/MatrixSDK/Crypto/KeySharing/MXSharedHistoryKeyManager.swift b/MatrixSDK/Crypto/KeySharing/MXSharedHistoryKeyManager.swift new file mode 100644 index 0000000000..d28f86f397 --- /dev/null +++ b/MatrixSDK/Crypto/KeySharing/MXSharedHistoryKeyManager.swift @@ -0,0 +1,123 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// Object managing the session keys and responsible for executing key share requests +@objc +public protocol MXSharedHistoryKeyService { + func hasSharedHistory(sessionId: String, senderKey: String) -> Bool + func shareKeys(request: MXSharedHistoryKeyRequest, success: (() -> Void)?, failure: ((NSError?) -> Void)?) +} + +/// Manager responsible for sharing keys of messages in a room with an invited user +/// +/// The intent of sharing keys with different users on invite is to allow them to see any immediate +/// context of the conversation that may have led to the invite. The amount of keys to be shared +/// is configurable, based on the number of messages that should be readable. +/// +/// Note that after the initial key share by the inviting user, there is no mechanism by which the invited +/// user can request additional keys. There is also no retry mechanism if any of the initial key sharing fails. +@objc +public class MXSharedHistoryKeyManager: NSObject { + struct SessionInfo: Hashable { + let roomId: String + let sessionId: String + let senderKey: String + } + + private let crypto: MXCrypto + private let service: MXSharedHistoryKeyService + + @objc public init(crypto: MXCrypto, service: MXSharedHistoryKeyService) { + self.crypto = crypto + self.service = service + } + + @objc public func shareMessageKeys(withUserId userId: String, messageEnumerator: MXEventsEnumerator, limit: Int) { + // Convert the last few messages into session information + let sessions = extractMessages(from: messageEnumerator, limit: limit) + .compactMap(sessionInfo) + + // We need to force download all keys for a given user, as we may not have any of them locally yet + crypto.downloadKeys([userId], forceDownload: true) { [weak self] userDevices, _ in + guard + let devices = userDevices?.objects(forUser: userId), + !devices.isEmpty else + { + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] No known devices for user %@, cannot share keys", userId) + return + } + + self?.shareSessions(Set(sessions), userId: userId, devices: devices) + } failure: { + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] Failed downloading user keys - \(String(describing: $0?.localizedDescription))") + } + } + + private func shareSessions(_ sessions: Set, userId: String, devices: [MXDeviceInfo]) { + for session in sessions { + + let request = MXSharedHistoryKeyRequest( + userId: userId, + devices: devices, + roomId: session.roomId, + sessionId: session.sessionId, + senderKey: session.senderKey + ) + + service.shareKeys(request: request) { + // Success does not trigger any further action / user notification, so we only log the outcome + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] Shared key successfully") + } failure: { + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] Failed sharing key - \(String(describing: $0?.localizedDescription))") + } + } + } + + private func extractMessages(from enumerator: MXEventsEnumerator, limit: Int) -> [MXEvent] { + var messages = [MXEvent]() + while let event = enumerator.nextEvent, messages.count < limit { + if event.wireEventType == .roomEncrypted { + messages.append(event) + } + } + return messages + } + + private func sessionInfo(for message: MXEvent) -> SessionInfo? { + let content = message.wireContent + guard + let roomId = message.roomId, + let sessionId = content?["session_id"] as? String, + let senderKey = content?["sender_key"] as? String + else { + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] Cannot create key request") + return nil + } + + guard service.hasSharedHistory(sessionId: sessionId, senderKey: senderKey) else { + MXLog.debug("[MXSharedHistoryRoomKeyRequestManager] Skipping keys for message without shared history") + return nil + } + + return .init( + roomId: roomId, + sessionId: sessionId, + senderKey: senderKey + ) + } +} diff --git a/MatrixSDK/Data/MXRoom.m b/MatrixSDK/Data/MXRoom.m index 8079d0c7c2..dc262c1dde 100644 --- a/MatrixSDK/Data/MXRoom.m +++ b/MatrixSDK/Data/MXRoom.m @@ -80,6 +80,11 @@ The list of room operations (sending of text, images...) that must be sent FIFO queue of failure blocks waiting for [self members:]. */ NSMutableArray *pendingMembersFailureBlocks; + + /** + The manager for sharing keys of messages with invited users + */ + MXSharedHistoryKeyManager *sharedHistoryKeyManager; } @end @@ -116,6 +121,11 @@ - (id)initWithRoomId:(NSString *)roomId matrixSession:(MXSession *)mxSession2 an { _roomId = roomId; mxSession = mxSession2; + + if (mxSession.crypto) { + MXMegolmDecryption *decryption = [[MXMegolmDecryption alloc] initWithCrypto:mxSession.crypto]; + sharedHistoryKeyManager = [[MXSharedHistoryKeyManager alloc] initWithCrypto:mxSession.crypto service:decryption]; + } if (store) { @@ -1932,9 +1942,23 @@ - (MXHTTPOperation*)inviteUser:(NSString*)userId success:(void (^)(void))success failure:(void (^)(NSError *error))failure { + if (MXSDKOptions.sharedInstance.enableRoomSharedHistoryOnInvite) { + [self shareRoomKeysWith:userId]; + } return [mxSession.matrixRestClient inviteUser:userId toRoom:self.roomId success:success failure:failure]; } +- (void)shareRoomKeysWith:(NSString *)userId +{ + // The value of 20 is arbitrary and imprecise, we merely want to ensure that when a user is invited to a room + // they are able to read any immediately preciding messages that may be relevant to the invite. + NSInteger numberOfSharedMessage = 20; + id enumerator = [self enumeratorForStoredMessagesWithTypeIn:@[kMXEventTypeStringRoomMessage]]; + [sharedHistoryKeyManager shareMessageKeysWithUserId:userId + messageEnumerator:enumerator + limit:numberOfSharedMessage]; +} + - (MXHTTPOperation*)inviteUserByEmail:(NSString*)email success:(void (^)(void))success failure:(void (^)(NSError *error))failure diff --git a/MatrixSDK/MXSDKOptions.h b/MatrixSDK/MXSDKOptions.h index 780569b1f3..417e4e5b3b 100644 --- a/MatrixSDK/MXSDKOptions.h +++ b/MatrixSDK/MXSDKOptions.h @@ -196,6 +196,14 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL enableThreads; +/** + Enable sharing of session keys for an immediate historical context (e.g. last 10-20 messages) + when inviting a new user to a room with shared history. + + @remark YES by default. + */ +@property (nonatomic) BOOL enableRoomSharedHistoryOnInvite; + @end NS_ASSUME_NONNULL_END diff --git a/MatrixSDK/MXSDKOptions.m b/MatrixSDK/MXSDKOptions.m index dd3385aa8a..2c0318cdba 100644 --- a/MatrixSDK/MXSDKOptions.m +++ b/MatrixSDK/MXSDKOptions.m @@ -53,6 +53,7 @@ - (instancetype)init _clientPermalinkBaseUrl = nil; _authEnableRefreshTokens = NO; _enableThreads = NO; + _enableRoomSharedHistoryOnInvite = YES; } return self; diff --git a/MatrixSDKTests/Crypto/KeySharing/MXSharedHistoryKeyManagerUnitTests.swift b/MatrixSDKTests/Crypto/KeySharing/MXSharedHistoryKeyManagerUnitTests.swift new file mode 100644 index 0000000000..135e472b54 --- /dev/null +++ b/MatrixSDKTests/Crypto/KeySharing/MXSharedHistoryKeyManagerUnitTests.swift @@ -0,0 +1,204 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import XCTest +@testable import MatrixSDK + +class MXSharedHistoryKeyManagerUnitTests: XCTestCase { + class CryptoStub: MXCrypto { + var devices = MXUsersDevicesMap() + + override func downloadKeys(_ userIds: [String]!, forceDownload: Bool, success: ((MXUsersDevicesMap?, [String : MXCrossSigningInfo]?) -> Void)!, failure: ((Error?) -> Void)!) -> MXHTTPOperation! { + success(devices, nil) + return MXHTTPOperation() + } + } + + class SpyService: MXSharedHistoryKeyService { + var sharedHistory: Set? + func hasSharedHistory(sessionId: String, senderKey: String) -> Bool { + guard let sharedHistory = sharedHistory else { + return true + } + return sharedHistory.contains(sessionId) + } + + var requests = [MXSharedHistoryKeyRequest]() + func shareKeys(request: MXSharedHistoryKeyRequest, success: (() -> Void)?, failure: ((NSError?) -> Void)?) { + requests.append(request) + success?() + } + } + + class EnumeratorStub: NSObject, MXEventsEnumerator { + var messages: [MXEvent] = [] + + func nextEventsBatch(_ eventsCount: UInt, threadId: String!) -> [MXEvent]! { + return nil + } + + var nextEvent: MXEvent? { + if messages.isEmpty { + return nil + } + return messages.removeFirst() + } + + var remaining: UInt { + return UInt(messages.count) + } + } + + var enumerator: EnumeratorStub! + var crypto: CryptoStub! + var service: SpyService! + var manager: MXSharedHistoryKeyManager! + + override func setUp() { + super.setUp() + + enumerator = EnumeratorStub() + crypto = CryptoStub() + crypto.devices.setObject(MXDeviceInfo(deviceId: "1"), forUser: "user1", andDevice: "1") + + service = SpyService() + manager = MXSharedHistoryKeyManager(crypto: crypto, service: service) + } + + private func makeEvent( + sessionId: String = "123", + senderKey: String = "456" + ) -> MXEvent { + MXEvent(fromJSON: [ + "room_id": "123", + "type": kMXEventTypeStringRoomEncrypted, + "content": [ + "session_id": sessionId, + "sender_key": senderKey, + ] + ]) + } + + func testDoesNotCreateRequestIfNoKnownDevices() { + enumerator.messages = [ + makeEvent(sessionId: "A", senderKey: "B") + ] + crypto.devices = MXUsersDevicesMap() + + manager.shareMessageKeys(withUserId: "user1", messageEnumerator: enumerator, limit: .max) + + XCTAssertEqual(service.requests.count, 0) + } + + func testCreateRequestForSingleMessage() { + enumerator.messages = [ + makeEvent(sessionId: "A", senderKey: "B") + ] + crypto.devices.setObject(MXDeviceInfo(deviceId: "1"), forUser: "user1", andDevice: "1") + crypto.devices.setObject(MXDeviceInfo(deviceId: "2"), forUser: "user1", andDevice: "2") + crypto.devices.setObject(MXDeviceInfo(deviceId: "3"), forUser: "user2", andDevice: "3") + + manager.shareMessageKeys(withUserId: "user1", messageEnumerator: enumerator, limit: .max) + + XCTAssertEqual(service.requests.count, 1) + XCTAssertEqual( + service.requests.first, + MXSharedHistoryKeyRequest( + userId: "user1", + devices: [ + MXDeviceInfo(deviceId: "1"), + MXDeviceInfo(deviceId: "2") + ], + roomId: "123", + sessionId: "A", + senderKey: "B" + ) + ) + } + + func testCreateOneRequestPerSessionIdAndSenderKey() { + enumerator.messages = [ + makeEvent(sessionId: "1", senderKey: "A"), + makeEvent(sessionId: "1", senderKey: "B"), + makeEvent(sessionId: "1", senderKey: "A"), + makeEvent(sessionId: "2", senderKey: "A"), + makeEvent(sessionId: "3", senderKey: "A"), + makeEvent(sessionId: "2", senderKey: "A"), + makeEvent(sessionId: "3", senderKey: "B"), + ] + + manager.shareMessageKeys(withUserId: "user1", messageEnumerator: enumerator, limit: .max) + + let identifiers = service.requests.map { [$0.sessionId, $0.senderKey] } + XCTAssertEqual(service.requests.count, 5) + XCTAssertTrue(identifiers.contains(["1", "A"])) + XCTAssertTrue(identifiers.contains(["1", "B"])) + XCTAssertTrue(identifiers.contains(["2", "A"])) + XCTAssertTrue(identifiers.contains(["3", "A"])) + XCTAssertTrue(identifiers.contains(["3", "B"])) + } + + func testCreateRequestsWithinLimit() { + enumerator.messages = [ + makeEvent(sessionId: "5"), + makeEvent(sessionId: "4"), + makeEvent(sessionId: "3"), + makeEvent(sessionId: "2"), + makeEvent(sessionId: "1"), + ] + + manager.shareMessageKeys(withUserId: "user1", messageEnumerator: enumerator, limit: 3) + + let identifiers = service.requests.map { $0.sessionId } + XCTAssertEqual(service.requests.count, 3) + XCTAssertEqual(Set(identifiers), ["5", "4", "3"]) + } + + func testCreateRequestsOnlyForSessionsWithSharedHistory() { + enumerator.messages = [ + makeEvent(sessionId: "1"), + makeEvent(sessionId: "2"), + makeEvent(sessionId: "3"), + makeEvent(sessionId: "4"), + makeEvent(sessionId: "5"), + ] + service.sharedHistory = [ + "1", + "2", + "4", + ] + + manager.shareMessageKeys(withUserId: "user1", messageEnumerator: enumerator, limit: .max) + + let identifiers = service.requests.map { $0.sessionId } + XCTAssertEqual(service.requests.count, 3) + XCTAssertEqual(Set(identifiers), ["1", "2", "4"]) + } +} + +extension MXSharedHistoryKeyRequest { + public override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? MXSharedHistoryKeyRequest else { + return false + } + return object.userId == userId + && object.devices.map { $0.deviceId } == devices.map { $0.deviceId } + && object.roomId == roomId + && object.sessionId == sessionId + && object.senderKey == senderKey + } +} diff --git a/MatrixSDKTests/TestPlans/UnitTests.xctestplan b/MatrixSDKTests/TestPlans/UnitTests.xctestplan index 1bec327787..17e48c7063 100644 --- a/MatrixSDKTests/TestPlans/UnitTests.xctestplan +++ b/MatrixSDKTests/TestPlans/UnitTests.xctestplan @@ -55,6 +55,7 @@ "MXQRCodeDataUnitTests", "MXReplyEventParserUnitTests", "MXResponseUnitTests", + "MXSharedHistoryKeyManagerUnitTests", "MXStoreRoomListDataManagerUnitTests", "MXSyncResponseUnitTests", "MXThreadEventTimelineUnitTests", diff --git a/changelog.d/4947.change b/changelog.d/4947.change index 8110c2cada..1abb2486b0 100644 --- a/changelog.d/4947.change +++ b/changelog.d/4947.change @@ -1 +1 @@ -Crypto: Store `sharedHistory` flag for inbound Megolm sessions +Crypto: Share Megolm session keys when inviting a new user