From 0dd63229efdb62ad62905f0a0c79bd2b9922a795 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 22 Jan 2021 13:25:23 +1100 Subject: [PATCH] Implement sending logic for explicit closed group updates --- Session.xcodeproj/project.pbxproj | 12 +- .../MessageReceiver+Handling.swift | 25 ++- .../MessageSender+ClosedGroups.swift | 180 ++++++++++++++---- 3 files changed, 169 insertions(+), 48 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 3d0adad07..c34ebbfd4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5264,7 +5264,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5333,7 +5333,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5394,7 +5394,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5464,7 +5464,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6483,7 +6483,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6551,7 +6551,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 163; + CURRENT_PROJECT_VERSION = 164; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index e236e0d1d..3f2afc0dd 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -328,13 +328,13 @@ extension MessageReceiver { private static func handleClosedGroupMembersAdded(_ message: ClosedGroupControlMessage, using transaction: Any) { guard case let .membersAdded(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) + let members = Set(group.groupMemberIds).union(membersAsData.map { $0.toHexString() }) + 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 Set(members) != Set(group.groupMemberIds) else { return } + 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) @@ -344,11 +344,11 @@ extension MessageReceiver { private static func handleClosedGroupMembersRemoved(_ message: ClosedGroupControlMessage, using transaction: Any) { guard case let .membersRemoved(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 { + // Check that the admin wasn't removed + let members = Set(group.groupMemberIds).subtracting(membersAsData.map { $0.toHexString() }) + guard members.contains(group.groupAdminIds.first!) else { return SNLog("Ignoring invalid closed group update.") } // If the current user was removed: @@ -364,7 +364,7 @@ extension MessageReceiver { } // Generate and distribute a new encryption key pair if needed let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey()) - if isCurrentUserAdmin && !wasCurrentUserRemoved { + if isCurrentUserAdmin { do { try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: Set(members), using: transaction) } catch { @@ -372,10 +372,10 @@ extension MessageReceiver { } } // Update the group - let newGroupModel = TSGroupModel(title: group.groupName, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) + 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 Set(members) != Set(group.groupMemberIds) else { return } + guard 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) @@ -388,7 +388,8 @@ extension MessageReceiver { 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! ]) + let didAdminLeave = group.groupAdminIds.contains(message.sender!) + let members: Set = didAdminLeave ? [] : Set(group.groupMemberIds).subtracting([ message.sender! ]) // If the admin leaves the group is disbanded // Guard against self-sends guard message.sender != getUserHexEncodedPublicKey() else { return SNLog("Ignoring invalid closed group update.") @@ -436,6 +437,10 @@ extension MessageReceiver { update(groupID, thread, group) } + + + // MARK: - Deprecated + /// - Note: Deprecated. private static func handleClosedGroupUpdated(_ message: ClosedGroupControlMessage, using transaction: Any) { // Prepare diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift index 3faa1a43d..5b4ff40a4 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift @@ -45,7 +45,154 @@ extension MessageSender { // Return return when(fulfilled: promises).map2 { thread } } + + public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set, using transaction: Any) throws { + // Prepare + let transaction = transaction as! YapDatabaseReadWriteTransaction + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + SNLog("Can't distribute new encryption key pair for nonexistent closed group.") + throw Error.noThread + } + guard thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) else { + SNLog("Can't distribute new encryption key pair as a non-admin.") + throw Error.invalidClosedGroupUpdate + } + // Generate the new encryption key pair + let newKeyPair = Curve25519.generateKeyPair() + // Distribute it + let proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.builder(publicKey: newKeyPair.publicKey, + privateKey: newKeyPair.privateKey).build() + let plaintext = try proto.serializedData() + let wrappers = try targetMembers.compactMap { publicKey -> ClosedGroupControlMessage.KeyPairWrapper in + let ciphertext = try MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey) + return ClosedGroupControlMessage.KeyPairWrapper(publicKey: publicKey, encryptedKeyPair: ciphertext) + } + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .encryptionKeyPair(wrappers)) + let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { // FIXME: It'd be great if we could make this a durable operation + // Store it * after * having sent out the message to the group + SNMessagingKitConfiguration.shared.storage.write { transaction in + Storage.shared.addClosedGroupEncryptionKeyPair(newKeyPair, for: groupPublicKey, using: transaction) + } + } + } + public static func setName(to name: String, for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + // Get the group, check preconditions & prepare + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + SNLog("Can't leave nonexistent closed group.") + throw Error.noThread + } + guard !name.isEmpty else { + SNLog("Can't set closed group name to an empty value.") + throw Error.invalidClosedGroupUpdate + } + let group = thread.groupModel + // Send the update to the group + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .nameChange(name: name)) + MessageSender.send(closedGroupControlMessage, in: thread, using: transaction) + // 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 + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + + public static func addMembers(_ newMembers: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + // Get the group, check preconditions & prepare + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + SNLog("Can't leave nonexistent closed group.") + throw Error.noThread + } + guard !newMembers.isEmpty else { + SNLog("Invalid closed group update.") + throw Error.invalidClosedGroupUpdate + } + let group = thread.groupModel + // Send the update to the group + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersAdded(members: newMembers.map { Data(hex: $0) })) + MessageSender.send(closedGroupControlMessage, in: thread, using: transaction) + // Update the group + let members = [String](Set(group.groupMemberIds).union(newMembers)) + 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 + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + + public static func removeMembers(_ membersToRemove: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + // Get the group, check preconditions & prepare + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + SNLog("Can't leave nonexistent closed group.") + throw Error.noThread + } + guard !membersToRemove.isEmpty else { + SNLog("Invalid closed group update.") + throw Error.invalidClosedGroupUpdate + } + let group = thread.groupModel + // Send the update to the group + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersRemoved(members: membersToRemove.map { Data(hex: $0) })) + MessageSender.send(closedGroupControlMessage, in: thread, using: transaction) + // Update the group + let members = [String](Set(group.groupMemberIds).subtracting(membersToRemove)) + 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 + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + + @objc(v2_leaveClosedGroupWithPublicKey:using:error:) + public static func v2_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + // Get the group, check preconditions & prepare + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let threadID = TSGroupThread.threadId(fromGroupId: groupID) + guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + SNLog("Can't leave nonexistent closed group.") + throw Error.noThread + } + let group = thread.groupModel + let userPublicKey = getUserHexEncodedPublicKey() + let isCurrentUserAdmin = group.groupAdminIds.contains(userPublicKey) + let members: Set = isCurrentUserAdmin ? [] : Set(group.groupMemberIds).subtracting([ userPublicKey ]) // If the admin leaves the group is disbanded + let admins: Set = isCurrentUserAdmin ? [] : Set(group.groupAdminIds) + // Send the update to the group + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .memberLeft) + let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { + SNMessagingKitConfiguration.shared.storage.write { transaction in + // Remove the group from the database and unsubscribe from PNs + Storage.shared.removeAllClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) + Storage.shared.removeClosedGroupPublicKey(groupPublicKey, using: transaction) + let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) + } + } + // Update the group + let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: [String](admins)) + thread.setGroupModel(newGroupModel, with: transaction) + // Notify the user + let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate, customMessage: updateInfo) + infoMessage.save(with: transaction) + } + + + + // MARK: - Deprecated + + /// - Note: Deprecated. public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) throws { // Prepare let userPublicKey = getUserHexEncodedPublicKey() @@ -116,6 +263,7 @@ extension MessageSender { infoMessage.save(with: transaction) } + /// - Note: Deprecated. @objc(leaveClosedGroupWithPublicKey:using:error:) public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) @@ -135,36 +283,4 @@ extension MessageSender { } return try update(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction) } - - public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set, using transaction: Any) throws { - // Prepare - let transaction = transaction as! YapDatabaseReadWriteTransaction - let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - let threadID = TSGroupThread.threadId(fromGroupId: groupID) - guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { - SNLog("Can't distribute new encryption key pair for nonexistent closed group.") - throw Error.noThread - } - guard thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) else { - SNLog("Can't distribute new encryption key pair as a non-admin.") - throw Error.invalidClosedGroupUpdate - } - // Generate the new encryption key pair - let newKeyPair = Curve25519.generateKeyPair() - // Distribute it - let proto = try SNProtoDataMessageClosedGroupControlMessageKeyPair.builder(publicKey: newKeyPair.publicKey, - privateKey: newKeyPair.privateKey).build() - let plaintext = try proto.serializedData() - let wrappers = try targetMembers.compactMap { publicKey -> ClosedGroupControlMessage.KeyPairWrapper in - let ciphertext = try MessageSender.encryptWithSessionProtocol(plaintext, for: publicKey) - return ClosedGroupControlMessage.KeyPairWrapper(publicKey: publicKey, encryptedKeyPair: ciphertext) - } - let closedGroupControlMessage = ClosedGroupControlMessage(kind: .encryptionKeyPair(wrappers)) - let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { // FIXME: It'd be great if we could make this a durable operation - // Store it * after * having sent out the message to the group - SNMessagingKitConfiguration.shared.storage.write { transaction in - Storage.shared.addClosedGroupEncryptionKeyPair(newKeyPair, for: groupPublicKey, using: transaction) - } - } - } }