From bbd3ecd54a8de9a4a1940af435bfe615bdcc611f Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 23 Nov 2020 16:05:39 +1100 Subject: [PATCH] Delete ClosedGroupsProtocol --- Signal.xcodeproj/project.pbxproj | 4 - .../Database/Storage/Storage+Jobs.swift | 4 + .../MessageSenderDelegate.swift | 207 +++++++++++++ .../To Do/ClosedGroupsProtocol.swift | 271 ------------------ 4 files changed, 211 insertions(+), 275 deletions(-) delete mode 100644 SignalUtilitiesKit/To Do/ClosedGroupsProtocol.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 66780ac69..480ed9c80 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -342,7 +342,6 @@ C33FDC2B255A581F00E217F9 /* OWSReadReceiptManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA71255A57FA00E217F9 /* OWSReadReceiptManager.m */; }; C33FDC2C255A581F00E217F9 /* OWSFailedAttachmentDownloadsJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA72255A57FA00E217F9 /* OWSFailedAttachmentDownloadsJob.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDC2D255A581F00E217F9 /* ECKeyPair+Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */; }; - C33FDC2E255A581F00E217F9 /* ClosedGroupsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */; }; C33FDC2F255A581F00E217F9 /* OWSSyncManagerProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA75255A57FB00E217F9 /* OWSSyncManagerProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDC33255A581F00E217F9 /* TSGroupThread.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA79255A57FB00E217F9 /* TSGroupThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; C33FDC34255A581F00E217F9 /* NSRegularExpression+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */; }; @@ -1443,7 +1442,6 @@ C33FDA71255A57FA00E217F9 /* OWSReadReceiptManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSReadReceiptManager.m; sourceTree = ""; }; C33FDA72255A57FA00E217F9 /* OWSFailedAttachmentDownloadsJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFailedAttachmentDownloadsJob.h; sourceTree = ""; }; C33FDA73255A57FA00E217F9 /* ECKeyPair+Hexadecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ECKeyPair+Hexadecimal.swift"; sourceTree = ""; }; - C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosedGroupsProtocol.swift; sourceTree = ""; }; C33FDA75255A57FB00E217F9 /* OWSSyncManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSyncManagerProtocol.h; sourceTree = ""; }; C33FDA79255A57FB00E217F9 /* TSGroupThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGroupThread.h; sourceTree = ""; }; C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SSK.swift"; sourceTree = ""; }; @@ -3271,7 +3269,6 @@ isa = PBXGroup; children = ( C33FDB19255A580900E217F9 /* GroupUtilities.swift */, - C33FDA74255A57FB00E217F9 /* ClosedGroupsProtocol.swift */, C33FDAFF255A580600E217F9 /* DisplayNameUtilities.swift */, C33FDBF4255A581B00E217F9 /* DisplayNameUtilities2.swift */, C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */, @@ -4978,7 +4975,6 @@ B8D8F16B256615DE0092EF10 /* Storage+VolumeSamples.swift in Sources */, C33FDC41255A581F00E217F9 /* TypingIndicators.swift in Sources */, C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */, - C33FDC2E255A581F00E217F9 /* ClosedGroupsProtocol.swift in Sources */, B84072962565E9F50037CB17 /* TSOutgoingMessage+Conversion.swift in Sources */, C38EF247255B6D67007E1867 /* NSAttributedString+OWS.m in Sources */, C33FDC99255A582000E217F9 /* PublicChatManager.swift in Sources */, diff --git a/SignalUtilitiesKit/Database/Storage/Storage+Jobs.swift b/SignalUtilitiesKit/Database/Storage/Storage+Jobs.swift index 98649f318..12a223fc6 100644 --- a/SignalUtilitiesKit/Database/Storage/Storage+Jobs.swift +++ b/SignalUtilitiesKit/Database/Storage/Storage+Jobs.swift @@ -23,4 +23,8 @@ extension Storage { } return result } + + public func getAttachmentUploadJob(for attachmentID: String) -> AttachmentUploadJob? { + return nil // TODO: Implement + } } diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSenderDelegate.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSenderDelegate.swift index c08bcbfe1..3452f3d68 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSenderDelegate.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSenderDelegate.swift @@ -1,6 +1,21 @@ +import PromiseKit public final class MessageSenderDelegate : SessionMessagingKit.MessageSenderDelegate, SharedSenderKeysDelegate { + public enum Error : LocalizedError { + case noThread + case noPrivateKey + case invalidUpdate + + public var errorDescription: String? { + switch self { + case .noThread: return "Couldn't find a thread associated with the given group public key." + case .noPrivateKey: return "Couldn't find a private key associated with the given group public key." + case .invalidUpdate: return "Invalid group update." + } + } + } + public static let shared = MessageSenderDelegate() public func handleSuccessfulMessageSend(_ message: Message, using transaction: Any) { @@ -15,6 +30,198 @@ public final class MessageSenderDelegate : SessionMessagingKit.MessageSenderDele // TODO: Implement } + public static func createClosedGroup(name: String, members: Set, transaction: YapDatabaseReadWriteTransaction) -> Promise { + // Prepare + var members = members + let userPublicKey = getUserHexEncodedPublicKey() + // Generate a key pair for the group + let groupKeyPair = Curve25519.generateKeyPair() + let groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + // Ensure the current user is included in the member list + members.insert(userPublicKey) + let membersAsData = members.map { Data(hex: $0) } + // Create ratchets for all members + let senderKeys: [ClosedGroupSenderKey] = members.map { publicKey in + let ratchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) + } + // Create the group + let admins = [ userPublicKey ] + let adminsAsData = admins.map { Data(hex: $0) } + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + let group = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) + thread.usesSharedSenderKeys = true + thread.save(with: transaction) + // Send a closed group update message to all members using established channels + var promises: [Promise] = [] + for member in members { + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: groupKeyPair.privateKey, senderKeys: senderKeys, members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + let promise = MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction) + promises.append(promise) + } + // Add the group to the user's set of public keys to poll for + Storage.shared.setClosedGroupPrivateKey(groupKeyPair.privateKey.toHexString(), for: groupPublicKey, using: transaction) + // Notify the PN server + promises.append(PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey)) + // Notify the user + let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate) + infoMessage.save(with: transaction) + // Return + return when(fulfilled: promises).map2 { thread } + } + + /// - Note: The returned promise is only relevant for group leaving. + public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) -> Promise { + let (promise, seal) = Promise.pending() + let userPublicKey = getUserHexEncodedPublicKey() + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + SNLog("Can't update nonexistent closed group.") + return Promise(error: Error.noThread) + } + let group = thread.groupModel + let oldMembers = Set(group.groupMemberIds) + let newMembers = members.subtracting(oldMembers) + let membersAsData = members.map { Data(hex: $0) } + let admins = group.groupAdminIds + let adminsAsData = admins.map { Data(hex: $0) } + guard let groupPrivateKey = Storage.shared.getClosedGroupPrivateKey(for: groupPublicKey) else { + SNLog("Couldn't get private key for closed group.") + return Promise(error: Error.noPrivateKey) + } + let wasAnyUserRemoved = Set(members).intersection(oldMembers) != oldMembers + let removedMembers = oldMembers.subtracting(members) + let isUserLeaving = removedMembers.contains(userPublicKey) + var newSenderKeys: [ClosedGroupSenderKey] = [] + if wasAnyUserRemoved { + if isUserLeaving && removedMembers.count != 1 { + SNLog("Can't remove self and others simultaneously.") + return Promise(error: Error.invalidUpdate) + } + // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) + let promises: [Promise] = oldMembers.map { member in + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], + members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + return MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction) + } + when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } + let _ = promise.done { + Storage.writeSync { transaction in + let allOldRatchets = Storage.shared.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = ClosedGroupRatchetCollectionType.old + Storage.shared.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } + // Delete all ratchets (it's important that this happens * after * sending out the update) + Storage.shared.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if isUserLeaving { + Storage.shared.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) + // Notify the PN server + let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) + } else { + // Send closed group update messages to any new members using established channels + for member in newMembers { + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [], members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, in: thread, using: transaction) + } + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + let userRatchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) + let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) + for member in members { + guard member != userPublicKey else { continue } + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, in: thread, using: transaction) + } + } + } + } + } else if !newMembers.isEmpty { + seal.fulfill(()) + // Generate ratchets for any new members + newSenderKeys = newMembers.map { publicKey in + let ratchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) + return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: newSenderKeys, + members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, in: thread, using: transaction) + // Send closed group update messages to the new members using established channels + var allSenderKeys = Storage.shared.getAllClosedGroupSenderKeys(for: groupPublicKey) + allSenderKeys.formUnion(newSenderKeys) + for member in newMembers { + let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) + thread.save(with: transaction) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, + groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, in: thread, using: transaction) + } + } else { + seal.fulfill(()) + let allSenderKeys = Storage.shared.getAllClosedGroupSenderKeys(for: groupPublicKey) + let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, + senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) + let closedGroupUpdate = ClosedGroupUpdate() + closedGroupUpdate.kind = closedGroupUpdateKind + MessageSender.send(closedGroupUpdate, in: thread, using: transaction) + } + // Update the group + let newGroupModel = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: 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) + // Return + return promise + } + + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. + @objc(leaveGroupWithPublicKey:transaction:) + public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { + return AnyPromise.from(leave(groupPublicKey, using: transaction)) + } + + /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. + public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + let userPublicKey = getUserHexEncodedPublicKey() + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) + guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { + SNLog("Can't leave nonexistent closed group.") + return Promise(error: Error.noThread) + } + let group = thread.groupModel + var newMembers = Set(group.groupMemberIds) + newMembers.remove(userPublicKey) + return update(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction) + } + public func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: Any) { SNLog("Requesting sender key for group public key: \(groupPublicKey), sender public key: \(senderPublicKey).") let transaction = transaction as! YapDatabaseReadWriteTransaction diff --git a/SignalUtilitiesKit/To Do/ClosedGroupsProtocol.swift b/SignalUtilitiesKit/To Do/ClosedGroupsProtocol.swift deleted file mode 100644 index 04acd3ead..000000000 --- a/SignalUtilitiesKit/To Do/ClosedGroupsProtocol.swift +++ /dev/null @@ -1,271 +0,0 @@ -import PromiseKit - -// A few notes about making changes in this file: -// -// • Don't use a database transaction if you can avoid it. -// • If you do need to use a database transaction, use a read transaction if possible. -// • For write transactions, consider making it the caller's responsibility to manage the database transaction (this helps avoid unnecessary transactions). -// • Think carefully about adding a function; there might already be one for what you need. -// • Document the expected cases in which a function will be used. -// • Express those cases in tests. - -/// See [the documentation](https://github.com/loki-project/session-protocol-docs/wiki/Medium-Size-Groups) for more information. -@objc(LKClosedGroupsProtocol) -public final class ClosedGroupsProtocol : NSObject { - public static let isSharedSenderKeysEnabled = true - public static let groupSizeLimit = 20 - public static let maxNameSize = 64 - - public enum Error : LocalizedError { - case noThread - case noPrivateKey - case invalidUpdate - - public var errorDescription: String? { - switch self { - case .noThread: return "Couldn't find a thread associated with the given group public key." - case .noPrivateKey: return "Couldn't find a private key associated with the given group public key." - case .invalidUpdate: return "Invalid group update." - } - } - } - - // MARK: - Sending - - /// - Note: It's recommended to batch fetch the device links for the given set of members before invoking this, to avoid the message sending pipeline - /// making a request for each member. - public static func createClosedGroup(name: String, members: Set, transaction: YapDatabaseReadWriteTransaction) -> Promise { - // Prepare - var members = members - let userPublicKey = getUserHexEncodedPublicKey() - // Generate a key pair for the group - let groupKeyPair = Curve25519.generateKeyPair() - let groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix - // Ensure the current user is included in the member list - members.insert(userPublicKey) - let membersAsData = members.map { Data(hex: $0) } - // Create ratchets for all members - let senderKeys: [ClosedGroupSenderKey] = members.map { publicKey in - let ratchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) - return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) - } - // Create the group - let admins = [ userPublicKey ] - let adminsAsData = admins.map { Data(hex: $0) } - let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - let group = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) - let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) - thread.usesSharedSenderKeys = true - thread.save(with: transaction) - // Establish sessions if needed - establishSessionsIfNeeded(with: [String](members), using: transaction) - // Send a closed group update message to all members using established channels - var promises: [Promise] = [] - for member in members { - guard member != userPublicKey else { continue } - let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, - groupPrivateKey: groupKeyPair.privateKey, senderKeys: senderKeys, members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - let promise = MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction) - promises.append(promise) - } - // Add the group to the user's set of public keys to poll for - Storage.shared.setClosedGroupPrivateKey(groupKeyPair.privateKey.toHexString(), for: groupPublicKey, using: transaction) - // Notify the PN server - promises.append(PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: userPublicKey)) - // Notify the user - let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate) - infoMessage.save(with: transaction) - // Return - return when(fulfilled: promises).map2 { thread } - } - - /// - Note: The returned promise is only relevant for group leaving. - public static func update(_ groupPublicKey: String, with members: Set, name: String, transaction: YapDatabaseReadWriteTransaction) -> Promise { - let (promise, seal) = Promise.pending() - let userPublicKey = getUserHexEncodedPublicKey() - let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { - SNLog("Can't update nonexistent closed group.") - return Promise(error: Error.noThread) - } - let group = thread.groupModel - let oldMembers = Set(group.groupMemberIds) - let newMembers = members.subtracting(oldMembers) - let membersAsData = members.map { Data(hex: $0) } - let admins = group.groupAdminIds - let adminsAsData = admins.map { Data(hex: $0) } - guard let groupPrivateKey = Storage.shared.getClosedGroupPrivateKey(for: groupPublicKey) else { - SNLog("Couldn't get private key for closed group.") - return Promise(error: Error.noPrivateKey) - } - let wasAnyUserRemoved = Set(members).intersection(oldMembers) != oldMembers - let removedMembers = oldMembers.subtracting(members) - let isUserLeaving = removedMembers.contains(userPublicKey) - var newSenderKeys: [ClosedGroupSenderKey] = [] - if wasAnyUserRemoved { - if isUserLeaving && removedMembers.count != 1 { - SNLog("Can't remove self and others simultaneously.") - return Promise(error: Error.invalidUpdate) - } - // Establish sessions if needed - establishSessionsIfNeeded(with: [String](members), using: transaction) - // Send the update to the existing members using established channels (don't include new ratchets as everyone should regenerate new ratchets individually) - let promises: [Promise] = oldMembers.map { member in - let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], - members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - return MessageSender.sendNonDurably(closedGroupUpdate, in: thread, using: transaction) - } - when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } - let _ = promise.done { - Storage.writeSync { transaction in - let allOldRatchets = Storage.shared.getAllClosedGroupRatchets(for: groupPublicKey) - for (senderPublicKey, oldRatchet) in allOldRatchets { - let collection = ClosedGroupRatchetCollectionType.old - Storage.shared.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) - } - // Delete all ratchets (it's important that this happens * after * sending out the update) - Storage.shared.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) - // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and - // send it out to all members (minus the removed ones) using established channels. - if isUserLeaving { - Storage.shared.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) - // Notify the PN server - let _ = PushNotificationAPI.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) - } else { - // Send closed group update messages to any new members using established channels - for member in newMembers { - let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, - groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [], members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - } - // Send out the user's new ratchet to all members (minus the removed ones) using established channels - let userRatchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: userPublicKey, using: transaction) - let userSenderKey = ClosedGroupSenderKey(chainKey: Data(hex: userRatchet.chainKey), keyIndex: userRatchet.keyIndex, publicKey: Data(hex: userPublicKey)) - for member in members { - guard member != userPublicKey else { continue } - let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.senderKey(groupPublicKey: Data(hex: groupPublicKey), senderKey: userSenderKey) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - } - } - } - } - } else if !newMembers.isEmpty { - seal.fulfill(()) - // Generate ratchets for any new members - newSenderKeys = newMembers.map { publicKey in - let ratchet = SharedSenderKeys.generateRatchet(for: groupPublicKey, senderPublicKey: publicKey, using: transaction) - return ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) - } - // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: newSenderKeys, - members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - // Establish sessions if needed - establishSessionsIfNeeded(with: [String](newMembers), using: transaction) - // Send closed group update messages to the new members using established channels - var allSenderKeys = Storage.shared.getAllClosedGroupSenderKeys(for: groupPublicKey) - allSenderKeys.formUnion(newSenderKeys) - for member in newMembers { - let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.new(groupPublicKey: Data(hex: groupPublicKey), name: name, - groupPrivateKey: Data(hex: groupPrivateKey), senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - } - } else { - seal.fulfill(()) - let allSenderKeys = Storage.shared.getAllClosedGroupSenderKeys(for: groupPublicKey) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, - senderKeys: [ClosedGroupSenderKey](allSenderKeys), members: membersAsData, admins: adminsAsData) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - } - // Update the group - let newGroupModel = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: groupID, groupType: .closedGroup, adminIds: 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) - // Return - return promise - } - - /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. - @objc(leaveGroupWithPublicKey:transaction:) - public static func objc_leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { - return AnyPromise.from(leave(groupPublicKey, using: transaction)) - } - - /// The returned promise is fulfilled when the group update message has been sent. It doesn't wait for the user's new ratchet to be distributed. - public static func leave(_ groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { - let userPublicKey = getUserHexEncodedPublicKey() - let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) - guard let thread = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) else { - SNLog("Can't leave nonexistent closed group.") - return Promise(error: Error.noThread) - } - let group = thread.groupModel - var newMembers = Set(group.groupMemberIds) - newMembers.remove(userPublicKey) - return update(groupPublicKey, with: newMembers, name: group.groupName!, transaction: transaction) - } - - public static func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { - SNLog("Requesting sender key for group public key: \(groupPublicKey), sender public key: \(senderPublicKey).") - // Establish session if needed - SessionManagementProtocol.sendSessionRequestIfNeeded(to: senderPublicKey, using: transaction) - // Send the request - let thread = TSContactThread.getOrCreateThread(withContactId: senderPublicKey, transaction: transaction) - thread.save(with: transaction) - let closedGroupUpdateKind = ClosedGroupUpdate.Kind.senderKeyRequest(groupPublicKey: Data(hex: groupPublicKey)) - let closedGroupUpdate = ClosedGroupUpdate() - closedGroupUpdate.kind = closedGroupUpdateKind - MessageSender.send(closedGroupUpdate, in: thread, using: transaction) - } - - // MARK: - General - - @objc(establishSessionsIfNeededWithClosedGroupMembers:transaction:) - public static func establishSessionsIfNeeded(with closedGroupMembers: [String], using transaction: YapDatabaseReadWriteTransaction) { - closedGroupMembers.forEach { publicKey in - SessionManagementProtocol.sendSessionRequestIfNeeded(to: publicKey, using: transaction) - } - } - - @objc(shouldIgnoreClosedGroupMessage:inThread:wrappedIn:) - public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SNProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SNProtoEnvelope) -> Bool { - guard thread.groupModel.groupType == .closedGroup else { return true } - let publicKey = envelope.source! // Set during UD decryption - return !thread.isUserMember(inGroup: publicKey) - } - - /// - Note: Deprecated. - @objc(shouldIgnoreClosedGroupUpdateMessage:inThread:wrappedIn:) - public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SNProtoDataMessage, in thread: TSGroupThread, wrappedIn envelope: SNProtoEnvelope) -> Bool { - guard thread.groupModel.groupType == .closedGroup else { return true } - let publicKey = envelope.source! // Set during UD decryption - return !thread.isUserAdmin(inGroup: publicKey) - } -}