diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index 45b683caf..4ba5ebbb3 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -860,7 +860,7 @@ CGFloat kIconViewLength = 24; if (gThread.isClosedGroup) { NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:gThread.groupModel.groupId]; [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - [SNMessageSender leaveClosedGroupWithPublicKey:groupPublicKey using:transaction error:nil]; + [[SNMessageSender leaveClosedGroupWithPublicKey:groupPublicKey using:transaction] retainUntilComplete]; }]; } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index c1a8e3341..05131e7fa 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -377,7 +377,6 @@ extension MessageReceiver { } private static func handleNewClosedGroup(_ message: ClosedGroupControlMessage, using transaction: Any) { - // Prepare guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData) = message.kind else { return } let groupPublicKey = publicKeyAsData.toHexString() let members = membersAsData.map { $0.toHexString() } @@ -426,8 +425,8 @@ extension MessageReceiver { guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { return SNLog("Ignoring closed group encryption key pair for nonexistent group.") } - guard thread.groupModel.groupMemberIds.contains(message.sender!) else { - return SNLog("Ignoring closed group encryption key pair from non-member.") + 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 } @@ -507,6 +506,10 @@ extension MessageReceiver { guard members.contains(group.groupAdminIds.first!) else { return SNLog("Ignoring invalid closed group update.") } + // Check that the message was sent by the group admin + guard group.groupAdminIds.contains(message.sender!) 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 @@ -518,16 +521,6 @@ extension MessageReceiver { 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 - // NOTE: If we're the admin we can be sure at this point that we weren't removed - let isCurrentUserAdmin = group.groupAdminIds.contains(getUserHexEncodedPublicKey()) - if isCurrentUserAdmin { - 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: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) thread.setGroupModel(newGroupModel, with: transaction) @@ -547,25 +540,17 @@ extension MessageReceiver { performIfValid(for: message, using: transaction) { groupID, thread, group in 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 - let userPublicKey = getUserHexEncodedPublicKey() - let isCurrentUserAdmin = group.groupAdminIds.contains(userPublicKey) // If a regular member left: - // • Distribute a new encryption key pair if we're the admin of the group + // • Mark them as a zombie (to be removed by the admin later) // If the admin left: - // • Don't distribute a new encryption key pair // • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded if didAdminLeave { // 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) - } else if isCurrentUserAdmin { - // Generate and distribute a new encryption key pair if needed - do { - try MessageSender.generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction) - } catch { - SNLog("Couldn't distribute new encryption key pair.") - } + let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey()) + } else { + // TODO: Mark as zombie } // Update the group let newGroupModel = TSGroupModel(title: group.groupName, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: group.groupAdminIds) @@ -579,6 +564,7 @@ extension MessageReceiver { } private static func handleClosedGroupEncryptionKeyPairRequest(_ message: ClosedGroupControlMessage, using transaction: Any) { + /* guard case .encryptionKeyPairRequest = message.kind else { return } let transaction = transaction as! YapDatabaseReadWriteTransaction guard let groupPublicKey = message.groupPublicKey else { return } @@ -590,6 +576,7 @@ extension MessageReceiver { } MessageSender.sendLatestEncryptionKeyPair(to: publicKey, for: groupPublicKey, using: transaction) } + */ } private static func performIfValid(for message: ClosedGroupControlMessage, using transaction: Any, _ update: (Data, TSGroupThread, TSGroupModel) -> Void) { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift index 71ed19990..b1e37fd69 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+ClosedGroups.swift @@ -29,6 +29,8 @@ extension MessageSender { let closedGroupControlMessageKind = ClosedGroupControlMessage.Kind.new(publicKey: Data(hex: groupPublicKey), name: name, encryptionKeyPair: encryptionKeyPair, members: membersAsData, admins: adminsAsData) let closedGroupControlMessage = ClosedGroupControlMessage(kind: closedGroupControlMessageKind) + // Sending this non-durably is okay because we show a loader to the user. If they close the app while the + // loader is still showing, it's within expectation that the group creation might be incomplete. let promise = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction) promises.append(promise) } @@ -45,34 +47,34 @@ extension MessageSender { return when(fulfilled: promises).map2 { thread } } - public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set, using transaction: Any) throws { + public static func generateAndSendNewEncryptionKeyPair(for groupPublicKey: String, to targetMembers: Set, using transaction: Any) -> Promise { // 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 + return Promise(error: Error.noThread) } guard thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) else { SNLog("Can't distribute new encryption key pair as a non-admin.") - throw Error.invalidClosedGroupUpdate + return Promise(error: Error.invalidClosedGroupUpdate) } // Generate the new encryption key pair let newKeyPair = Curve25519.generateKeyPair() // Distribute it - let proto = try SNProtoKeyPair.builder(publicKey: newKeyPair.publicKey, + let proto = try! SNProtoKeyPair.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) + let plaintext = try! proto.serializedData() + let wrappers = 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(publicKey: nil, wrappers: wrappers)) var distributingKeyPairs = distributingClosedGroupEncryptionKeyPairs[groupPublicKey] ?? [] distributingKeyPairs.append(newKeyPair) distributingClosedGroupEncryptionKeyPairs[groupPublicKey] = distributingKeyPairs - let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { // FIXME: It'd be great if we could make this a durable operation + return MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { // 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) @@ -82,39 +84,42 @@ extension MessageSender { distributingKeyPairs.remove(at: index) } distributingClosedGroupEncryptionKeyPairs[groupPublicKey] = distributingKeyPairs - } + }.map { _ in } } - public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) throws { + public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) -> Promise { // 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 update nonexistent closed group.") - throw Error.noThread + return Promise(error: Error.noThread) } let group = thread.groupModel + var promises: [Promise] = [] // Update name if needed - if name != group.groupName { try setName(to: name, for: groupPublicKey, using: transaction) } + if name != group.groupName { promises.append(setName(to: name, for: groupPublicKey, using: transaction)) } // Add members if needed let addedMembers = members.subtracting(group.groupMemberIds) - if !addedMembers.isEmpty { try addMembers(addedMembers, to: groupPublicKey, using: transaction) } + if !addedMembers.isEmpty { promises.append(addMembers(addedMembers, to: groupPublicKey, using: transaction)) } // Remove members if needed let removedMembers = Set(group.groupMemberIds).subtracting(members) - if !removedMembers.isEmpty { try removeMembers(removedMembers, to: groupPublicKey, using: transaction) } + if !removedMembers.isEmpty { promises.append(removeMembers(removedMembers, to: groupPublicKey, using: transaction)) } + // Return + return when(fulfilled: promises).map2 { _ in } } - public static func setName(to name: String, for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + public static func setName(to name: String, for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { // 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 change name for nonexistent closed group.") - throw Error.noThread + return Promise(error: Error.noThread) } guard !name.isEmpty else { SNLog("Can't set closed group name to an empty value.") - throw Error.invalidClosedGroupUpdate + return Promise(error: Error.invalidClosedGroupUpdate) } let group = thread.groupModel // Send the update to the group @@ -127,19 +132,21 @@ extension MessageSender { let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo) infoMessage.save(with: transaction) + // Return + return Promise.value(()) } - public static func addMembers(_ newMembers: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + public static func addMembers(_ newMembers: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { // 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 add members to nonexistent closed group.") - throw Error.noThread + return Promise(error: Error.noThread) } guard !newMembers.isEmpty else { SNLog("Invalid closed group update.") - throw Error.invalidClosedGroupUpdate + return Promise(error: Error.invalidClosedGroupUpdate) } let group = thread.groupModel let members = [String](Set(group.groupMemberIds).union(newMembers)) @@ -147,7 +154,7 @@ extension MessageSender { let adminsAsData = group.groupAdminIds.map { Data(hex: $0) } guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { SNLog("Couldn't find encryption key pair for closed group: \(groupPublicKey).") - throw Error.noKeyPair + return Promise(error: Error.noKeyPair) } // Send the update to the group let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersAdded(members: newMembers.map { Data(hex: $0) })) @@ -168,37 +175,38 @@ extension MessageSender { let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo) infoMessage.save(with: transaction) + // Return + return Promise.value(()) } - public static func removeMembers(_ membersToRemove: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + public static func removeMembers(_ membersToRemove: Set, to groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { // Get the group, check preconditions & prepare let userPublicKey = getUserHexEncodedPublicKey() let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) let threadID = TSGroupThread.threadId(fromGroupId: groupID) guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { SNLog("Can't remove members from nonexistent closed group.") - throw Error.noThread + return Promise(error: Error.noThread) } guard !membersToRemove.isEmpty else { SNLog("Invalid closed group update.") - throw Error.invalidClosedGroupUpdate + return Promise(error: Error.invalidClosedGroupUpdate) } guard !membersToRemove.contains(userPublicKey) else { SNLog("Invalid closed group update.") - throw Error.invalidClosedGroupUpdate + return Promise(error: Error.invalidClosedGroupUpdate) } let group = thread.groupModel - let members = Set(group.groupMemberIds).subtracting(membersToRemove) - let isCurrentUserAdmin = group.groupAdminIds.contains(userPublicKey) - // Send the update to the group and generate + distribute a new encryption key pair if needed - let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersRemoved(members: membersToRemove.map { Data(hex: $0) })) - if isCurrentUserAdmin { - let _ = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).done { - try generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction) - } - } else { - MessageSender.send(closedGroupControlMessage, in: thread, using: transaction) + guard group.groupAdminIds.contains(userPublicKey) else { + SNLog("Only an admin can remove members from a group.") + return Promise(error: Error.invalidClosedGroupUpdate) } + let members = Set(group.groupMemberIds).subtracting(membersToRemove) + // Send the update to the group and generate + distribute a new encryption key pair + let closedGroupControlMessage = ClosedGroupControlMessage(kind: .membersRemoved(members: membersToRemove.map { Data(hex: $0) })) + let promise = MessageSender.sendNonDurably(closedGroupControlMessage, in: thread, using: transaction).map { + generateAndSendNewEncryptionKeyPair(for: groupPublicKey, to: members, using: transaction) + }.map { _ in } // 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) @@ -206,16 +214,22 @@ extension MessageSender { let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo) infoMessage.save(with: transaction) + // Return + return promise } - @objc(leaveClosedGroupWithPublicKey:using:error:) - public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { + @objc(leaveClosedGroupWithPublicKey:using:) + public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { + return AnyPromise.from(leave(groupPublicKey, using: transaction)) + } + + public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { // 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 + return Promise(error: Error.noThread) } let group = thread.groupModel let userPublicKey = getUserHexEncodedPublicKey() @@ -224,14 +238,14 @@ extension MessageSender { 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 { + let promise = 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) } - } + }.map { _ in } // 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) @@ -239,8 +253,11 @@ extension MessageSender { let updateInfo = group.getInfoStringAboutUpdate(to: newGroupModel) let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .groupUpdate, customMessage: updateInfo) infoMessage.save(with: transaction) + // Return + return promise } + /* public static func requestEncryptionKeyPair(for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws { #if DEBUG preconditionFailure("Shouldn't currently be in use.") @@ -258,15 +275,16 @@ extension MessageSender { let closedGroupControlMessage = ClosedGroupControlMessage(kind: .encryptionKeyPairRequest) MessageSender.send(closedGroupControlMessage, in: thread, using: transaction) } + */ public static func sendLatestEncryptionKeyPair(to publicKey: String, for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { // Check that the user in question is part of the closed group let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) let threadID = TSGroupThread.threadId(fromGroupId: groupID) - guard let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { - return SNLog("Couldn't find thread .") + guard let groupThread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { + return SNLog("Couldn't send key pair for nonexistent closed group.") } - let group = thread.groupModel + let group = groupThread.groupModel guard group.groupMemberIds.contains(publicKey) else { return SNLog("Refusing to send latest encryption key pair to non-member.") }