diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 2ce02efde..c71bfd95f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -229,7 +229,7 @@ extension MessageReceiver { private static func handleClosedGroupControlMessage(_ message: ClosedGroupControlMessage, using transaction: Any) { switch message.kind! { case .new: handleNewClosedGroup(message, using: transaction) - case .update: handleClosedGroupUpdated(message, using: transaction) + case .update: handleClosedGroupUpdated(message, using: transaction) // Deprecated case .encryptionKeyPair: handleClosedGroupEncryptionKeyPair(message, using: transaction) case .nameChange: handleClosedGroupNameChanged(message, using: transaction) case .usersAdded: handleClosedGroupMembersAdded(message, using: transaction) @@ -267,7 +267,176 @@ extension MessageReceiver { let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate) infoMessage.save(with: transaction) } + + private static func handleClosedGroupEncryptionKeyPair(_ message: ClosedGroupControlMessage, using transaction: Any) { + // Prepare + guard case let .encryptionKeyPair(wrappers) = message.kind, let groupPublicKey = message.groupPublicKey else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + let userPublicKey = getUserHexEncodedPublicKey() + guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { + return SNLog("Couldn't find user X25519 key pair.") + } + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + return SNLog("Ignoring closed group encryption key pair for nonexistent group.") + } + guard thread.groupModel.groupAdminIds.contains(message.sender!) else { + return SNLog("Ignoring closed group encryption key pair from non-admin.") + } + // Find our wrapper and decrypt it if possible + guard let wrapper = wrappers.first(where: { $0.publicKey == userPublicKey }), let encryptedKeyPair = wrapper.encryptedKeyPair else { return } + let plaintext: Data + do { + plaintext = try MessageReceiver.decryptWithSessionProtocol(ciphertext: encryptedKeyPair, using: userKeyPair).plaintext + } catch { + return SNLog("Couldn't decrypt closed group encryption key pair.") + } + // Parse it + let proto: SNProtoDataMessageClosedGroupControlMessageKeyPair + do { + proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.parseData(plaintext) + } catch { + return SNLog("Couldn't parse closed group encryption key pair.") + } + let keyPair: ECKeyPair + do { + keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey) + } catch { + return SNLog("Couldn't parse closed group encryption key pair.") + } + // Store it + Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction) + SNLog("Received a new closed group encryption key pair.") + } + + private static func handleClosedGroupNameChanged(_ message: ClosedGroupControlMessage, using transaction: Any) { + guard case let .nameChange(name) = message.kind else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + performIfValid(for: message, using: transaction) { groupID, thread, group in + // Update the group + let newGroupModel = TSGroupModel(title: name, memberIds: group.groupMemberIds, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user if needed + guard name != group.groupName else { return } + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + } + + private static func handleClosedGroupMembersAdded(_ message: ClosedGroupControlMessage, using transaction: Any) { + guard case let .usersAdded(membersAsData) = message.kind else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + let members = membersAsData.map { $0.toHexString() } + performIfValid(for: message, using: transaction) { groupID, thread, group in + // Update the group + let newGroupModel = TSGroupModel(title: group.groupName, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user if needed + guard Set(members) != Set(group.groupMemberIds) else { return } + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + } + + private static func handleClosedGroupMembersRemoved(_ message: ClosedGroupControlMessage, using transaction: Any) { + guard case let .usersRemoved(membersAsData) = message.kind else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + let members = membersAsData.map { $0.toHexString() } + guard let groupPublicKey = message.groupPublicKey else { return } + performIfValid(for: message, using: transaction) { groupID, thread, group in + // Check that the admin wasn't removed unless the group was destroyed entirely + guard members.contains(group.groupAdminIds.first!) || members.isEmpty else { + return SNLog("Ignoring invalid closed group update.") + } + // If the current user was removed: + // • Stop polling for the group + // • Remove the key pairs associated with the group + // • Notify the PN server + let userPublicKey = getUserHexEncodedPublicKey() + let wasCurrentUserRemoved = !members.contains(userPublicKey) + if wasCurrentUserRemoved { + Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction) + Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) + let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) + } + // Generate and distribute a new encryption key pair if needed + let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey()) + if isCurrentUserAdmin && !wasCurrentUserRemoved { + do { + try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: Set(members), using: transaction) + } catch { + SNLog("Couldn't distribute new encryption key pair.") + } + } + // Update the group + let newGroupModel = TSGroupModel(title: group.groupName, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user if needed + guard Set(members) != Set(group.groupMemberIds) else { return } + let infoMessageType: TSInfoMessageType = wasCurrentUserRemoved ? .typeGroupQuit : .typeGroupUpdate + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: infoMessageType, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + } + + private static func handleClosedGroupMemberLeft(_ message: ClosedGroupControlMessage, using transaction: Any) { + guard case .userLeft = message.kind else { return } + let transaction = transaction as! YapDatabaseReadWriteTransaction + guard let groupPublicKey = message.groupPublicKey else { return } + performIfValid(for: message, using: transaction) { groupID, thread, group in + let members = Set(group.groupMemberIds).subtracting([ message.sender! ]) + // Guard against self-sends + guard message.sender != getUserHexEncodedPublicKey() else { + return SNLog("Ignoring invalid closed group update.") + } + // Generate and distribute a new encryption key pair if needed + let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey()) + if isCurrentUserAdmin { + do { + try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction) + } catch { + SNLog("Couldn't distribute new encryption key pair.") + } + } + // Update the group + let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user if needed + guard members != Set(group.groupMemberIds) else { return } + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + } + + private static func performIfValid(for message: ClosedGroupControlMessage, using transaction: Any, _ update: (Data, TSGroupThread, TSGroupModel) -> Void) { + // Prepare + let transaction = transaction as! YapDatabaseReadWriteTransaction + // Get the group + guard let groupPublicKey = message.groupPublicKey else { return } + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + return SNLog("Ignoring closed group update for nonexistent group.") + } + let group = thread.groupModel + // Check that the message isn't from before the group was created + guard Double(message.sentTimestamp!) > thread.creationDate.timeIntervalSince1970 * 1000 else { + return SNLog("Ignoring closed group update from before thread was created.") + } + // Check that the sender is a member of the group + guard Set(group.groupMemberIds).contains(message.sender!) else { + return SNLog("Ignoring closed group update from non-member.") + } + // Perform the update + update(groupID, thread, group) + } + /// - Note: Deprecated. private static func handleClosedGroupUpdated(_ message: ClosedGroupControlMessage, using transaction: Any) { // Prepare guard case let .update(name, membersAsData) = message.kind else { return } @@ -326,62 +495,4 @@ extension MessageReceiver { infoMessage.save(with: transaction) } } - - private static func handleClosedGroupEncryptionKeyPair(_ message: ClosedGroupControlMessage, using transaction: Any) { - // Prepare - guard case let .encryptionKeyPair(wrappers) = message.kind, let groupPublicKey = message.groupPublicKey else { return } - let transaction = transaction as! YapDatabaseReadWriteTransaction - let userPublicKey = getUserHexEncodedPublicKey() - guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { - return SNLog("Couldn't find user X25519 key pair.") - } - let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - let threadID = TSGroupThread.threadId(fromGroupId: groupID) - guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { - return SNLog("Ignoring closed group encryption key pair for nonexistent group.") - } - guard thread.groupModel.groupAdminIds.contains(message.sender!) else { - return SNLog("Ignoring closed group encryption key pair from non-admin.") - } - // Find our wrapper and decrypt it if possible - guard let wrapper = wrappers.first(where: { $0.publicKey == userPublicKey }), let encryptedKeyPair = wrapper.encryptedKeyPair else { return } - let plaintext: Data - do { - plaintext = try MessageReceiver.decryptWithSessionProtocol(ciphertext: encryptedKeyPair, using: userKeyPair).plaintext - } catch { - return SNLog("Couldn't decrypt closed group encryption key pair.") - } - // Parse it - let proto: SNProtoDataMessageClosedGroupControlMessageKeyPair - do { - proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.parseData(plaintext) - } catch { - return SNLog("Couldn't parse closed group encryption key pair.") - } - let keyPair: ECKeyPair - do { - keyPair = try ECKeyPair(publicKeyData: proto.publicKey.removing05PrefixIfNeeded(), privateKeyData: proto.privateKey) - } catch { - return SNLog("Couldn't parse closed group encryption key pair.") - } - // Store it - Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: groupPublicKey, using: transaction) - SNLog("Received a new closed group encryption key pair.") - } - - private static func handleClosedGroupNameChanged(_ message: ClosedGroupControlMessage, using transaction: Any) { - - } - - private static func handleClosedGroupMembersAdded(_ message: ClosedGroupControlMessage, using transaction: Any) { - - } - - private static func handleClosedGroupMembersRemoved(_ message: ClosedGroupControlMessage, using transaction: Any) { - - } - - private static func handleClosedGroupMemberLeft(_ message: ClosedGroupControlMessage, using transaction: Any) { - - } }