From 81c9dc891958521d1ba3364ea20a38858aff8d6f Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 6 Oct 2020 12:24:06 +1100 Subject: [PATCH 01/19] Fix SSK group leaving race condition --- .../Closed Groups/ClosedGroupsProtocol.swift | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift index b959d17c1..b2aa36302 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift @@ -118,11 +118,18 @@ public final class ClosedGroupsProtocol : NSObject { print("[Loki] Can't remove self and others simultaneously.") return Promise(error: Error.invalidUpdate) } - // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) - let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], - members: membersAsData, admins: adminsAsData) - let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) - SSKEnvironment.shared.messageSender.send(closedGroupUpdateMessage, success: { seal.fulfill(()) }, failure: { seal.reject($0) }) + // 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 closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.info(groupPublicKey: Data(hex: groupPublicKey), name: name, senderKeys: [], + members: membersAsData, admins: adminsAsData) + let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind) + return SSKEnvironment.shared.messageSender.sendPromise(message: closedGroupUpdateMessage) + } + when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } promise.done { try! Storage.writeSync { transaction in // Delete all ratchets (it's important that this happens * after * sending out the update) @@ -134,8 +141,6 @@ public final class ClosedGroupsProtocol : NSObject { // Notify the PN server LokiPushNotificationManager.performOperation(.unsubscribe, for: groupPublicKey, publicKey: userPublicKey) } else { - // Establish sessions if needed - establishSessionsIfNeeded(with: [String](members), using: transaction) // Send closed group update messages to any new members using established channels for member in newMembers { let thread = TSContactThread.getOrCreateThread(withContactId: member, transaction: transaction) @@ -203,13 +208,13 @@ public final class ClosedGroupsProtocol : NSObject { return promise } - /// The returned promise is fulfilled when the message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. + /// 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 message has been sent **to the group**. It doesn't wait for the user's new ratchet to be distributed. + /// 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 = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) From 3c0e5145657f9456837d62f03e1771da0e9d5765 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 6 Oct 2020 14:24:09 +1100 Subject: [PATCH 02/19] Show a loader while the group is updating --- .../View Controllers/EditClosedGroupVC.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift index 02548f496..075a146e4 100644 --- a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift +++ b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift @@ -241,13 +241,17 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega guard members != Set(thread.groupModel.groupMemberIds) || name != thread.groupModel.groupName else { return popToConversationVC(self) } - try! Storage.writeSync { [weak self] transaction in - ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) { - guard let self = self else { return } - popToConversationVC(self) - }.catch(on: DispatchQueue.main) { error in - guard let self = self else { return } - self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.") + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in + try! Storage.writeSync { [weak self] transaction in + ClosedGroupsProtocol.update(groupPublicKey, with: members, name: name, transaction: transaction).done(on: DispatchQueue.main) { + guard let self = self else { return } + self.dismiss(animated: true, completion: nil) // Dismiss the loader + popToConversationVC(self) + }.catch(on: DispatchQueue.main) { error in + guard let self = self else { return } + self.dismiss(animated: true, completion: nil) // Dismiss the loader + self.showError(title: "Couldn't Update Group", message: "Please check your internet connection and try again.") + } } } } From f95d3d171f5f808e828d1bdbd1d7430d487c9c79 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 7 Oct 2020 15:20:12 +1100 Subject: [PATCH 03/19] Retry PN server requests if needed --- .../LokiPushNotificationManager.swift | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift b/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift index faa163b52..253e63be7 100644 --- a/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift +++ b/SignalServiceKit/src/Loki/Push Notifications/LokiPushNotificationManager.swift @@ -10,6 +10,7 @@ public final class LokiPushNotificationManager : NSObject { private static let server = "https://live.apns.getsession.org" #endif internal static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + private static let maxRetryCount: UInt = 4 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 public enum ClosedGroupOperation: String { @@ -28,12 +29,14 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/unregister")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise: Promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't unregister from push notifications.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't unregister from push notifications.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + } } } promise.catch2 { error in @@ -68,16 +71,18 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/register")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise: Promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't register device token.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't register device token.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + } + userDefaults[.deviceToken] = hexEncodedToken + userDefaults[.lastDeviceTokenUpload] = now + userDefaults[.isUsingFullAPNs] = true } - userDefaults[.deviceToken] = hexEncodedToken - userDefaults[.lastDeviceTokenUpload] = now - userDefaults[.isUsingFullAPNs] = true } promise.catch2 { error in print("[Loki] Couldn't register device token.") @@ -103,14 +108,15 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/\(operation.rawValue)")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") + } } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") - } - return } promise.catch2 { error in print("[Loki] Couldn't subscribe/unsubscribe closed group: \(closedGroupPublicKey).") @@ -124,14 +130,15 @@ public final class LokiPushNotificationManager : NSObject { let url = URL(string: "\(server)/notify")! let request = TSRequest(url: url, method: "POST", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] - let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { - return print("[Loki] Couldn't notify PN server.") - } - guard json["code"] as? Int != 0 else { - return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").") + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { + OnionRequestAPI.sendOnionRequest(request, to: server, using: pnServerPublicKey).map2 { response in + guard let json = response["body"] as? JSON else { + return print("[Loki] Couldn't notify PN server.") + } + guard json["code"] as? Int != 0 else { + return print("[Loki] Couldn't notify PN server due to error: \(json["message"] as? String ?? "nil").") + } } - return } promise.catch2 { error in print("[Loki] Couldn't notify PN server.") From e262013147bdd9ecc1a288f35a99669427d04f57 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Thu, 8 Oct 2020 16:20:42 +1100 Subject: [PATCH 04/19] use onion routing for individual avatar downloads --- SignalMessaging/profiles/OWSProfileManager.m | 48 ++----------------- .../src/Loki/API/FileServerAPI.swift | 26 ++++++++++ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 58017d509..29636658d 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -1157,43 +1157,14 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId); - NSString *tempDirectory = OWSTemporaryDirectory(); - NSString *tempFilePath = [tempDirectory stringByAppendingPathComponent:fileName]; - NSString *profilePictureURL = userProfile.avatarUrlPath; - NSError *serializationError; - NSMutableURLRequest *request = - [self.avatarHTTPManager.requestSerializer requestWithMethod:@"GET" - URLString:profilePictureURL - parameters:nil - error:&serializationError]; - if (serializationError) { - OWSFailDebug(@"serializationError: %@", serializationError); - return; - } - - NSURLSession* session = [NSURLSession sharedSession]; - NSURLSessionTask* downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - + AnyPromise *promise = [LKFileServerAPI downloadProfilePicture:profilePictureURL]; + [promise.then(^(NSData *data) { @synchronized(self.currentAvatarDownloads) { [self.currentAvatarDownloads removeObject:userProfile.recipientId]; } - - if (error) { - OWSLogError(@"Dowload failed: %@", error); - return; - } - - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *tempFileUrl = [NSURL fileURLWithPath:tempFilePath]; - NSError *moveError; - if (![fileManager moveItemAtURL:location toURL:tempFileUrl error:&moveError]) { - OWSLogError(@"MoveItemAtURL for avatar failed: %@", moveError); - return; - } - - NSData *_Nullable encryptedData = (error ? nil : [NSData dataWithContentsOfFile:tempFilePath]); + NSData *_Nullable encryptedData = data; NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart]; UIImage *_Nullable image = nil; if (decryptedData) { @@ -1213,19 +1184,12 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); if (latestUserProfile.avatarUrlPath.length > 0) { [self downloadAvatarForUserProfile:latestUserProfile]; } - } else if (error) { - if ([response isKindOfClass:NSHTTPURLResponse.class] - && ((NSHTTPURLResponse *)response).statusCode == 403) { - OWSLogInfo(@"no avatar for: %@", userProfile.recipientId); - } else { - OWSLogError(@"avatar download for %@ failed with error: %@", userProfile.recipientId, error); - } } else if (!encryptedData) { OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId); } else if (!decryptedData) { OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId); } else if (!image) { - OWSLogError(@"avatar image for %@ could not be loaded with error: %@", userProfile.recipientId, error); + OWSLogError(@"avatar image for %@ could not be loaded.", userProfile.recipientId); } else { [self updateProfileAvatarCache:image filename:fileName]; @@ -1248,9 +1212,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSAssertDebug(backgroundTask); backgroundTask = nil; - }]; - - [downloadTask resume]; + }) retainUntilComplete]; }); } diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index fc2f69535..c8cc947c9 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -18,6 +18,7 @@ public final class FileServerAPI : DotNetAPI { public static let fileSizeORMultiplier: Double = 6 @objc public static let server = "https://file.getsession.org" + @objc public static let fileStaticServer = "https://file-static.lokinet.org" // MARK: Storage override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } @@ -52,6 +53,31 @@ public final class FileServerAPI : DotNetAPI { } } + @objc(downloadProfilePicture:) + public static func objc_downloadProfilePicture(_ downloadURL: String) -> AnyPromise { + return AnyPromise.from(downloadProfilePicture(downloadURL)) + } + + public static func downloadProfilePicture(_ downloadURL: String) -> Promise { + var error: NSError? + var url = downloadURL + if downloadURL.contains(fileStaticServer) { + url = downloadURL.replacingOccurrences(of: fileStaticServer, with: "\(server)/loki/v1") + } + let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) + if let error = error { + print("[Loki] Couldn't download profile picture due to error: \(error).") + return Promise(error: error) + } + return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey, isJSONRequired: false).map2 { json in + guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { + print("[Loki] Couldn't download profile picture.") + return Data() + } + return Data(dataArray) + } + } + // MARK: Open Group Server Public Key public static func getPublicKey(for openGroupServer: String) -> Promise { let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")! From b30cfd960c42acfeeb6e47d3ab0e7027dd49e200 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 9 Oct 2020 11:18:49 +1100 Subject: [PATCH 05/19] using onion routing for open group avatar downloads --- .../Loki/API/Open Groups/PublicChatAPI.swift | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index e18cd245a..58cb1dd4b 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -369,7 +369,7 @@ public final class PublicChatAPI : DotNetAPI { } } - static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo) { + static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo, token: String, serverPublicKey: String) { let storage = OWSPrimaryStorage.shared() let publicChatID = "\(server).\(channel)" try! Storage.writeSync { transaction in @@ -388,27 +388,26 @@ public final class PublicChatAPI : DotNetAPI { if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) if let profilePictureURL = info.profilePictureURL { - let configuration = URLSessionConfiguration.default - let manager = AFURLSessionManager.init(sessionConfiguration: configuration) - let url = URL(string: "\(server)\(profilePictureURL)")! - let request = URLRequest(url: url) - let task = manager.downloadTask(with: request, progress: nil, - destination: { (targetPath: URL, response: URLResponse) -> URL in - let tempFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth()).appendingPathComponent(UUID().uuidString) - return tempFilePath - }, - completionHandler: { (response: URLResponse, filePath: URL?, error: Error?) in - if let error = error { - print("[Loki] Couldn't download profile picture for public chat channel with ID: \(channel) on server: \(server).") - return - } - if let filePath = filePath, let avatarData = try? Data.init(contentsOf: filePath) { - let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil) - try! attachmentStream.write(avatarData) - groupThread.updateAvatar(with: attachmentStream) - } - }) - task.resume() + var error: NSError? + let url = "\(server)/loki/v1\(profilePictureURL)" + let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", + "Authorization" : "Bearer \(token)", + "Accept-Ranges" : "bytes"] + if let error = error { + print("[Loki] Couldn't download open group avatar due to error: \(error).") + return + } + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).map{ json in + guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { + print("[Loki] Couldn't download open group avatar.") + return + } + let avatarData = Data(dataArray) + let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil) + try! attachmentStream.write(avatarData) + groupThread.updateAvatar(with: attachmentStream) + } } } } @@ -444,7 +443,7 @@ public final class PublicChatAPI : DotNetAPI { storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction) } let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) - updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo) + updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo, token: token, serverPublicKey: serverPublicKey) return publicChatInfo } } From 4e5a1f9400f399ec05c8928681e55e4d777a2e2c Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Fri, 9 Oct 2020 11:26:12 +1100 Subject: [PATCH 06/19] remove accept-ranges headers --- SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index 58cb1dd4b..bc1cad0f6 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -391,9 +391,7 @@ public final class PublicChatAPI : DotNetAPI { var error: NSError? let url = "\(server)/loki/v1\(profilePictureURL)" let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", - "Authorization" : "Bearer \(token)", - "Accept-Ranges" : "bytes"] + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)"] if let error = error { print("[Loki] Couldn't download open group avatar due to error: \(error).") return From 6739bfc41ad3eb03e05f5658a78c369f6e1ae5a3 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 13 Oct 2020 13:55:26 +1100 Subject: [PATCH 07/19] download attachments with onion routing --- .../src/Loki/API/FileServerAPI.swift | 14 +- .../Attachments/OWSAttachmentDownloads.m | 136 +++--------------- 2 files changed, 29 insertions(+), 121 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index c8cc947c9..4ea2414dc 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -55,10 +55,16 @@ public final class FileServerAPI : DotNetAPI { @objc(downloadProfilePicture:) public static func objc_downloadProfilePicture(_ downloadURL: String) -> AnyPromise { - return AnyPromise.from(downloadProfilePicture(downloadURL)) + return AnyPromise.from(downloadAttachment(downloadURL)) } - public static func downloadProfilePicture(_ downloadURL: String) -> Promise { + // MARK: Attachment Download + @objc(downloadAttachment:) + public static func objc_downloadAttachment(_ downloadURL: String) -> AnyPromise { + return AnyPromise.from(downloadAttachment(downloadURL)) + } + + public static func downloadAttachment(_ downloadURL: String) -> Promise { var error: NSError? var url = downloadURL if downloadURL.contains(fileStaticServer) { @@ -66,12 +72,12 @@ public final class FileServerAPI : DotNetAPI { } let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) if let error = error { - print("[Loki] Couldn't download profile picture due to error: \(error).") + print("[Loki] Couldn't download attachment due to error: \(error).") return Promise(error: error) } return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey, isJSONRequired: false).map2 { json in guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { - print("[Loki] Couldn't download profile picture.") + print("[Loki] Couldn't download attachment.") return Data() } return Data(dataArray) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index fb850789b..de487cac6 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -513,7 +513,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); NSString *tempFilePath = [OWSTemporaryDirectoryAccessibleAfterFirstAuth() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; NSURL *tempFileURL = [NSURL fileURLWithPath:tempFilePath]; - __block NSURLSessionDownloadTask *task; void (^failureHandler)(NSError *) = ^(NSError *error) { OWSLogError(@"Failed to download attachment with error: %@", error.description); @@ -524,125 +523,28 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); failureHandlerParam(task, error); }; + + AnyPromise *promise = [LKFileServerAPI downloadAttachment:location]; + [promise.then(^(NSData *data) { + BOOL success = [data writeToFile:tempFilePath atomically:YES]; + if (success) { + successHandler(tempFilePath); + } - NSString *method = @"GET"; - NSError *serializationError = nil; - NSMutableURLRequest *request = [manager.requestSerializer requestWithMethod:method - URLString:location - parameters:nil - error:&serializationError]; - if (serializationError) { - return failureHandler(serializationError); - } - - task = [manager downloadTaskWithRequest:request - progress:^(NSProgress *progress) { - OWSAssertDebug(progress != nil); - - // Don't do anything until we've received at least one byte of data. - if (progress.completedUnitCount < 1) { - return; - } - - void (^abortDownload)(void) = ^{ - OWSFailDebug(@"Download aborted."); - [task cancel]; - }; - - if (progress.totalUnitCount > kMaxDownloadSize || progress.completedUnitCount > kMaxDownloadSize) { - // A malicious service might send a misleading content length header, - // so.... - // - // If the current downloaded bytes or the expected total byes - // exceed the max download size, abort the download. - OWSLogError(@"Attachment download exceed expected content length: %lld, %lld.", - (long long)progress.totalUnitCount, - (long long)progress.completedUnitCount); - abortDownload(); - return; - } - - job.progress = progress.fractionCompleted; - - [self fireProgressNotification:MAX(kAttachmentDownloadProgressTheta, progress.fractionCompleted) - attachmentId:attachmentPointer.uniqueId]; - - // We only need to check the content length header once. - if (hasCheckedContentLength) { - return; - } - - // Once we've received some bytes of the download, check the content length - // header for the download. - // - // If the task doesn't exist, or doesn't have a response, or is missing - // the expected headers, or has an invalid or oversize content length, etc., - // abort the download. - NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response; - if (![httpResponse isKindOfClass:[NSHTTPURLResponse class]]) { - OWSLogError(@"Attachment download has missing or invalid response."); - abortDownload(); - return; - } - - NSDictionary *headers = [httpResponse allHeaderFields]; - if (![headers isKindOfClass:[NSDictionary class]]) { - OWSLogError(@"Attachment download invalid headers."); - abortDownload(); - return; - } - - - NSString *contentLength = headers[@"Content-Length"]; - if (![contentLength isKindOfClass:[NSString class]]) { - OWSLogError(@"Attachment download missing or invalid content length."); - abortDownload(); - return; - } - - - if (contentLength.longLongValue > kMaxDownloadSize) { - OWSLogError(@"Attachment download content length exceeds max download size."); - abortDownload(); - return; - } - - // This response has a valid content length that is less - // than our max download size. Proceed with the download. - hasCheckedContentLength = YES; + NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath]; + if (!fileSize) { + OWSLogError(@"Could not determine attachment file size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); } - destination:^(NSURL *targetPath, NSURLResponse *response) { - return tempFileURL; + if (fileSize.unsignedIntegerValue > kMaxDownloadSize) { + OWSLogError(@"Attachment download length exceeds max size."); + NSError *error = OWSErrorWithCodeDescription( + OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); + return failureHandler(error); } - completionHandler:^(NSURLResponse *response, NSURL *_Nullable filePath, NSError *_Nullable error) { - if (error) { - failureHandler(error); - return; - } - if (![tempFileURL isEqual:filePath]) { - OWSLogError(@"Unexpected temp file path."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - - NSNumber *_Nullable fileSize = [OWSFileSystem fileSizeOfPath:tempFilePath]; - if (!fileSize) { - OWSLogError(@"Could not determine attachment file size."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - if (fileSize.unsignedIntegerValue > kMaxDownloadSize) { - OWSLogError(@"Attachment download length exceeds max size."); - NSError *error = OWSErrorWithCodeDescription( - OWSErrorCodeInvalidMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @"")); - return failureHandler(error); - } - successHandler(tempFilePath); - }]; - - [task resume]; + }) retainUntilComplete]; } - (void)fireProgressNotification:(CGFloat)progress attachmentId:(NSString *)attachmentId From 9cd52342e8a717286a429a7bf4d5b7920a033565 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 13 Oct 2020 14:25:12 +1100 Subject: [PATCH 08/19] clean --- .../src/Messages/Attachments/OWSAttachmentDownloads.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index de487cac6..8a118d070 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -500,12 +500,6 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); OWSAssertDebug(job); TSAttachmentPointer *attachmentPointer = job.attachmentPointer; - AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; - manager.requestSerializer = [AFHTTPRequestSerializer serializer]; - [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; - manager.responseSerializer = [AFHTTPResponseSerializer serializer]; - manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - // We want to avoid large downloads from a compromised or buggy service. const long kMaxDownloadSize = 10 * 1024 * 1024; __block BOOL hasCheckedContentLength = NO; From 76b6afe0e30ab8e2cb830295fbc02386f387c46b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Tue, 13 Oct 2020 14:59:10 +1100 Subject: [PATCH 09/19] show an indefinite loader for onion routing download --- .../Cells/ConversationMediaView.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift index 304ede4d0..7e11383f0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift @@ -169,10 +169,18 @@ public class ConversationMediaView: UIView { return } + let view: UIView backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05) - let progressView = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) - self.addSubview(progressView) - progressView.autoPinEdgesToSuperviewEdges() + if isOnionRouted { // Loki: Due to the way onion routing works we can't get upload progress for those attachments + let activityIndicatorView = UIActivityIndicatorView(style: .white) + activityIndicatorView.isHidden = false + activityIndicatorView.startAnimating() + view = activityIndicatorView + } else { + view = MediaDownloadView(attachmentId: attachmentId, radius: maxMessageWidth * 0.1) + } + addSubview(view) + view.autoPinEdgesToSuperviewEdges() } private func addUploadProgressIfNecessary(_ subview: UIView) -> Bool { From b2c7f702f4ece65be91f428d6b8fad2d953ee230 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Fri, 9 Oct 2020 16:08:22 +1100 Subject: [PATCH 10/19] Update version number --- Signal.xcodeproj/project.pbxproj | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 03bff2116..61cd2a1b2 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -4143,7 +4143,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4157,7 +4157,7 @@ INFOPLIST_FILE = SignalShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4205,7 +4205,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -4224,7 +4224,7 @@ INFOPLIST_FILE = SignalShareExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4259,7 +4259,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; @@ -4278,7 +4278,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -4329,7 +4329,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = SUQ8J2PCT7; @@ -4353,7 +4353,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; @@ -4391,7 +4391,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -4403,7 +4403,7 @@ INFOPLIST_FILE = LokiPushNotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; @@ -4454,7 +4454,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -4471,7 +4471,7 @@ INFOPLIST_FILE = LokiPushNotificationService/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.push-notification-service"; @@ -4655,7 +4655,7 @@ CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -4690,7 +4690,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -4722,7 +4722,7 @@ CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 125; + CURRENT_PROJECT_VERSION = 127; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -4757,7 +4757,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.6.0; + MARKETING_VERSION = 1.6.1; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; From 0ae8da838a1f8100348243b7746d2d10b188e807 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 14 Oct 2020 10:51:07 +1100 Subject: [PATCH 11/19] Minor refactoring --- .../Cells/ConversationMediaView.swift | 2 +- .../MediaGalleryViewController.swift | 2 +- SignalMessaging/profiles/OWSProfileManager.m | 3 +- .../src/Loki/API/FileServerAPI.swift | 30 +++++++------------ .../Loki/API/Open Groups/PublicChatAPI.swift | 15 +++++----- .../Attachments/OWSAttachmentDownloads.m | 3 +- 6 files changed, 23 insertions(+), 32 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift index 7e11383f0..5fa530c01 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationMediaView.swift @@ -169,8 +169,8 @@ public class ConversationMediaView: UIView { return } - let view: UIView backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05) + let view: UIView if isOnionRouted { // Loki: Due to the way onion routing works we can't get upload progress for those attachments let activityIndicatorView = UIActivityIndicatorView(style: .white) activityIndicatorView.isHidden = false diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index ccc9c253e..e0e35dbca 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -377,7 +377,7 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel } guard let initialDetailItem = galleryItem else { - owsFailDebug("unexpectedly failed to build initialDetailItem.") +// owsFailDebug("unexpectedly failed to build initialDetailItem.") return } diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m index 29636658d..006126dea 100644 --- a/SignalMessaging/profiles/OWSProfileManager.m +++ b/SignalMessaging/profiles/OWSProfileManager.m @@ -1158,8 +1158,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId); NSString *profilePictureURL = userProfile.avatarUrlPath; - AnyPromise *promise = [LKFileServerAPI downloadProfilePicture:profilePictureURL]; - [promise.then(^(NSData *data) { + [[LKFileServerAPI downloadAttachmentFrom:profilePictureURL].then(^(NSData *data) { @synchronized(self.currentAvatarDownloads) { [self.currentAvatarDownloads removeObject:userProfile.recipientId]; diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index 4ea2414dc..69ae71d67 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -18,7 +18,7 @@ public final class FileServerAPI : DotNetAPI { public static let fileSizeORMultiplier: Double = 6 @objc public static let server = "https://file.getsession.org" - @objc public static let fileStaticServer = "https://file-static.lokinet.org" + @objc public static let fileStorageBucketURL = "https://file-static.lokinet.org" // MARK: Storage override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } @@ -53,34 +53,26 @@ public final class FileServerAPI : DotNetAPI { } } - @objc(downloadProfilePicture:) - public static func objc_downloadProfilePicture(_ downloadURL: String) -> AnyPromise { - return AnyPromise.from(downloadAttachment(downloadURL)) + // MARK: Attachments + @objc(downloadAttachmentFrom:) + public static func objc_downloadAttachment(from url: String) -> AnyPromise { + return AnyPromise.from(downloadAttachment(from: url)) } - // MARK: Attachment Download - @objc(downloadAttachment:) - public static func objc_downloadAttachment(_ downloadURL: String) -> AnyPromise { - return AnyPromise.from(downloadAttachment(downloadURL)) - } - - public static func downloadAttachment(_ downloadURL: String) -> Promise { + public static func downloadAttachment(from url: String) -> Promise { var error: NSError? - var url = downloadURL - if downloadURL.contains(fileStaticServer) { - url = downloadURL.replacingOccurrences(of: fileStaticServer, with: "\(server)/loki/v1") - } + let url = url.replacingOccurrences(of: fileStorageBucketURL, with: "\(server)/loki/v1") let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) if let error = error { print("[Loki] Couldn't download attachment due to error: \(error).") return Promise(error: error) } return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey, isJSONRequired: false).map2 { json in - guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { - print("[Loki] Couldn't download attachment.") - return Data() + guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else { + print("[Loki] Couldn't parse attachment from: \(json).") + throw DotNetAPIError.parsingFailed } - return Data(dataArray) + return Data(data) } } diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index bc1cad0f6..d5d1b3be6 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -391,19 +391,20 @@ public final class PublicChatAPI : DotNetAPI { var error: NSError? let url = "\(server)/loki/v1\(profilePictureURL)" let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)"] + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] if let error = error { print("[Loki] Couldn't download open group avatar due to error: \(error).") return } - OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).map{ json in - guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { - print("[Loki] Couldn't download open group avatar.") + OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { json in + guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else { + print("[Loki] Couldn't parse open group profile picture from: \(json).") return } - let avatarData = Data(dataArray) - let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil) - try! attachmentStream.write(avatarData) + let profilePicture = Data(data) + let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(profilePicture.count), + sourceFilename: nil, caption: nil, albumMessageId: nil) + try! attachmentStream.write(profilePicture) groupThread.updateAvatar(with: attachmentStream) } } diff --git a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m index 8a118d070..f55716b41 100644 --- a/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m +++ b/SignalServiceKit/src/Messages/Attachments/OWSAttachmentDownloads.m @@ -518,8 +518,7 @@ typedef void (^AttachmentDownloadFailure)(NSError *error); failureHandlerParam(task, error); }; - AnyPromise *promise = [LKFileServerAPI downloadAttachment:location]; - [promise.then(^(NSData *data) { + [[LKFileServerAPI downloadAttachmentFrom:location].then(^(NSData *data) { BOOL success = [data writeToFile:tempFilePath atomically:YES]; if (success) { successHandler(tempFilePath); From 863b0944319e586e1726cf157521149873ed8fc0 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 14 Oct 2020 10:58:36 +1100 Subject: [PATCH 12/19] Re-enable debug assertion --- Signal/src/ViewControllers/MediaGalleryViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index e0e35dbca..ccc9c253e 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -377,7 +377,7 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel } guard let initialDetailItem = galleryItem else { -// owsFailDebug("unexpectedly failed to build initialDetailItem.") + owsFailDebug("unexpectedly failed to build initialDetailItem.") return } From bdd178b227968618ef2104f426ce4632d594caab Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 14 Oct 2020 14:07:18 +1100 Subject: [PATCH 13/19] onion routing attachment download for open groups --- .../src/Loki/API/FileServerAPI.swift | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index 4ea2414dc..f1c7b1959 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -66,24 +66,38 @@ public final class FileServerAPI : DotNetAPI { public static func downloadAttachment(_ downloadURL: String) -> Promise { var error: NSError? - var url = downloadURL - if downloadURL.contains(fileStaticServer) { - url = downloadURL.replacingOccurrences(of: fileStaticServer, with: "\(server)/loki/v1") + let url = URL(string: downloadURL)! + var host = "https://\(url.host!)" + var actualDownloadURL = downloadURL + if fileStaticServer.contains(host) { + actualDownloadURL = downloadURL.replacingOccurrences(of: fileStaticServer, with: "\(server)/loki/v1") + host = server + } else { + actualDownloadURL = downloadURL.replacingOccurrences(of: host, with: "\(host)/loki/v1") } - let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) + let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: actualDownloadURL, parameters: nil, error: &error) if let error = error { print("[Loki] Couldn't download attachment due to error: \(error).") return Promise(error: error) } - return OnionRequestAPI.sendOnionRequest(request, to: server, using: fileServerPublicKey, isJSONRequired: false).map2 { json in - guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { - print("[Loki] Couldn't download attachment.") - return Data() + return getServerPublicKey(for: host).then2 { serverPublicKey in + OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map2 { json in + guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { + print("[Loki] Couldn't download attachment.") + return Data() + } + return Data(dataArray) } - return Data(dataArray) } } + public static func getServerPublicKey(for host: String) -> Promise { + if server.contains(host) { + return Promise.value(fileServerPublicKey) + } + return PublicChatAPI.getOpenGroupServerPublicKey(for: host) + } + // MARK: Open Group Server Public Key public static func getPublicKey(for openGroupServer: String) -> Promise { let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")! From 24825a2eea613b3c8c09527f1ba6e160190b093c Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 14 Oct 2020 14:58:49 +1100 Subject: [PATCH 14/19] minor refactor --- .../Loki/API/Open Groups/PublicChatAPI.swift | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift index bc1cad0f6..ccd3a9ce3 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift @@ -369,7 +369,7 @@ public final class PublicChatAPI : DotNetAPI { } } - static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo, token: String, serverPublicKey: String) { + static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo) { let storage = OWSPrimaryStorage.shared() let publicChatID = "\(server).\(channel)" try! Storage.writeSync { transaction in @@ -388,20 +388,7 @@ public final class PublicChatAPI : DotNetAPI { if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) if let profilePictureURL = info.profilePictureURL { - var error: NSError? - let url = "\(server)/loki/v1\(profilePictureURL)" - let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: url, parameters: nil, error: &error) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)"] - if let error = error { - print("[Loki] Couldn't download open group avatar due to error: \(error).") - return - } - OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).map{ json in - guard let body = json["body"] as? JSON, let dataArray = body["data"] as? [UInt8] else { - print("[Loki] Couldn't download open group avatar.") - return - } - let avatarData = Data(dataArray) + FileServerAPI.downloadAttachment("\(server)\(profilePictureURL)").map { avatarData in let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(avatarData.count), sourceFilename: nil, caption: nil, albumMessageId: nil) try! attachmentStream.write(avatarData) groupThread.updateAvatar(with: attachmentStream) @@ -441,7 +428,7 @@ public final class PublicChatAPI : DotNetAPI { storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction) } let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) - updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo, token: token, serverPublicKey: serverPublicKey) + updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo) return publicChatInfo } } From 203959dbab455e8c100875ef545dadb3636ac77a Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 15 Oct 2020 13:04:38 +1100 Subject: [PATCH 15/19] Fix edit closed group screen add members button updating --- .../View Controllers/EditClosedGroupVC.swift | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift index 075a146e4..3ae6393eb 100644 --- a/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift +++ b/Signal/src/Loki/View Controllers/EditClosedGroupVC.swift @@ -22,6 +22,14 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega return result }() + private lazy var addMembersButton: Button = { + let result = Button(style: .prominentOutline, size: .large) + result.setTitle("Add Members", for: UIControl.State.normal) + result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside) + result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing) + return result + }() + @objc private lazy var tableView: UITableView = { let result = UITableView() result.dataSource = self @@ -56,13 +64,13 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega let backButton = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) backButton.tintColor = Colors.text navigationItem.backBarButtonItem = backButton - setUpViewHierarchy() - updateNavigationBarButtons() - name = thread.groupModel.groupName! func getDisplayName(for publicKey: String) -> String { return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey } members = GroupUtilities.getClosedGroupMembers(thread).sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } + setUpViewHierarchy() + updateNavigationBarButtons() + name = thread.groupModel.groupName! } private func setUpViewHierarchy() { @@ -88,11 +96,8 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega membersLabel.font = .systemFont(ofSize: Values.mediumFontSize) membersLabel.text = "Members" // Add members button - let addMembersButton = Button(style: .prominentOutline, size: .large) - addMembersButton.setTitle("Add Members", for: UIControl.State.normal) - addMembersButton.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside) - addMembersButton.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing) - if (Set(ContactUtilities.getAllContacts()).subtracting(members).isEmpty) { + let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty + if (!hasContactsToAdd) { addMembersButton.isUserInteractionEnabled = false let disabledColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) addMembersButton.layer.borderColor = disabledColor.cgColor @@ -222,6 +227,11 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega return UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) ?? publicKey } self.members = members.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } + let hasContactsToAdd = !Set(ContactUtilities.getAllContacts()).subtracting(self.members).isEmpty + self.addMembersButton.isUserInteractionEnabled = hasContactsToAdd + let color = hasContactsToAdd ? Colors.accent : Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + self.addMembersButton.layer.borderColor = color.cgColor + self.addMembersButton.setTitleColor(color, for: UIControl.State.normal) } navigationController!.pushViewController(userSelectionVC, animated: true, completion: nil) } From 11d8764db3b2ffd1f0282bc2a19dcd056930a5c2 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 15 Oct 2020 13:05:32 +1100 Subject: [PATCH 16/19] Avoid unnecessary write transaction --- .../ConversationView/ConversationViewController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 7a80995d0..6b183a0f8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -5398,13 +5398,13 @@ typedef enum : NSUInteger { } dispatch_async(dispatch_get_main_queue(), ^{ __block TSInteraction *targetInteraction; - [LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) { [self.thread enumerateInteractionsWithTransaction:transaction usingBlock:^(TSInteraction *interaction, YapDatabaseReadTransaction *t) { if (interaction.timestampForUI == timestamp.unsignedLongLongValue) { targetInteraction = interaction; } }]; - } error:nil]; + }]; if (targetInteraction == nil || targetInteraction.interactionType != OWSInteractionType_OutgoingMessage) { return; } NSString *hexEncodedPublicKey = targetInteraction.thread.contactIdentifier; if (hexEncodedPublicKey == nil) { return; } From a6a179d17517fde6db7dd8c33718b7c80f86e559 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 15 Oct 2020 13:06:57 +1100 Subject: [PATCH 17/19] Handle self sends more gracefully --- .../src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift index 706a5ab14..4fb7e8bbf 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupUtilities.swift @@ -10,6 +10,7 @@ public final class ClosedGroupUtilities : NSObject { @objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ]) @objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ]) @objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ]) + @objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ]) } @objc(encryptData:usingGroupPublicKey:transaction:error:) @@ -59,6 +60,7 @@ public final class ClosedGroupUtilities : NSObject { // 4. ) Parse the closed group ciphertext message let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData) let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() + guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend } // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction) From 20d7c5949be631aac473ba4909eed7ac7e71179a Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 15 Oct 2020 13:56:05 +1100 Subject: [PATCH 18/19] Fix group member removal race condition --- .../Closed Groups/ClosedGroupsProtocol.swift | 10 +++++ .../SharedSenderKeysImplementation.swift | 34 ++++++++++------ .../Closed Groups/Storage+ClosedGroups.swift | 40 ++++++++++++------- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift index b2aa36302..ef9d5dd3e 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/ClosedGroupsProtocol.swift @@ -132,6 +132,11 @@ public final class ClosedGroupsProtocol : NSObject { when(resolved: promises).done2 { _ in seal.fulfill(()) }.catch2 { seal.reject($0) } promise.done { try! Storage.writeSync { transaction in + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.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.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 @@ -363,6 +368,11 @@ public final class ClosedGroupsProtocol : NSObject { let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() let wasUserRemoved = !members.contains(userPublicKey) if Set(members).intersection(oldMembers) != Set(oldMembers) { + let allOldRatchets = Storage.getAllClosedGroupRatchets(for: groupPublicKey) + for (senderPublicKey, oldRatchet) in allOldRatchets { + let collection = Storage.ClosedGroupRatchetCollectionType.old + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: oldRatchet, in: collection, using: transaction) + } Storage.removeAllClosedGroupRatchets(for: groupPublicKey, using: transaction) if wasUserRemoved { Storage.removeClosedGroupPrivateKey(for: groupPublicKey, using: transaction) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift index b53b4725e..427e4dc6f 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift @@ -90,11 +90,12 @@ public final class SharedSenderKeysImplementation : NSObject { } /// - Note: Sync. Don't call from the main thread. - private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet { + private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet { #if DEBUG assert(!Thread.isMainThread) #endif - guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else { + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else { let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) print("[Loki] \(error.errorDescription!)") throw error @@ -122,7 +123,8 @@ public final class SharedSenderKeysImplementation : NSObject { } } let result = ClosedGroupRatchet(chainKey: current.chainKey, keyIndex: current.keyIndex, messageKeys: messageKeys) // Includes any skipped message keys - Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction) + let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current + Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction) return result } } @@ -161,17 +163,21 @@ public final class SharedSenderKeysImplementation : NSObject { return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction) } - public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction) throws -> Data { + public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data { let ratchet: ClosedGroupRatchet do { - ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction) + ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry) } catch { - // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more - // convenient because there's an easy way to get the sender public key from here. - if case RatchetingError.loadingFailed(_, _) = error { - ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + if !isRetry { + return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true) + } else { + // FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more + // convenient because there's an easy way to get the sender public key from here. + if case RatchetingError.loadingFailed(_, _) = error { + ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction) + } + throw error } - throw error } let iv = ivAndCiphertext[0.. String { - return "LokiClosedGroupRatchetCollection.\(groupPublicKey)" + internal static func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String { + switch collection { + case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)" + case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)" + } } - internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String) -> ClosedGroupRatchet? { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) var result: ClosedGroupRatchet? read { transaction in result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet @@ -15,26 +22,31 @@ public extension Storage { return result } - internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, using transaction: YapDatabaseReadWriteTransaction) { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) transaction.setObject(ratchet, forKey: senderPublicKey, inCollection: collection) } - internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String) -> Set { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) - var result: Set = [] + internal static func getAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) + var result: [(senderPublicKey: String, ratchet: ClosedGroupRatchet)] = [] read { transaction in transaction.enumerateRows(inCollection: collection) { key, object, _, _ in - guard let publicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } - let senderKey = ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: publicKey)) - result.insert(senderKey) + guard let senderPublicKey = key as? String, let ratchet = object as? ClosedGroupRatchet else { return } + result.append((senderPublicKey: senderPublicKey, ratchet: ratchet)) } } return result } - internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { - let collection = getClosedGroupRatchetCollection(for: groupPublicKey) + internal static func getAllClosedGroupSenderKeys(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> Set { + return Set(getAllClosedGroupRatchets(for: groupPublicKey, from: collection).map { senderPublicKey, ratchet in + ClosedGroupSenderKey(chainKey: Data(hex: ratchet.chainKey), keyIndex: ratchet.keyIndex, publicKey: Data(hex: senderPublicKey)) + }) + } + + internal static func removeAllClosedGroupRatchets(for groupPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current, using transaction: YapDatabaseReadWriteTransaction) { + let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey) transaction.removeAllObjects(inCollection: collection) } } From 15a6c2352a33a9bd674e990e8740c371023055c6 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Thu, 15 Oct 2020 13:58:12 +1100 Subject: [PATCH 19/19] Fix message key indexing issue --- .../SharedSenderKeysImplementation.swift | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift index 427e4dc6f..21724bf07 100644 --- a/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift +++ b/SignalServiceKit/src/Loki/Protocol/Closed Groups/SharedSenderKeysImplementation.swift @@ -40,11 +40,13 @@ public final class SharedSenderKeysImplementation : NSObject { public enum RatchetingError : LocalizedError { case loadingFailed(groupPublicKey: String, senderPublicKey: String) case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String) + case generic public var errorDescription: String? { switch self { case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)." case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)." + case .generic: return "An error occurred" } } } @@ -66,7 +68,8 @@ public final class SharedSenderKeysImplementation : NSObject { let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ]) let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ]) let nextKeyIndex = ratchet.keyIndex + 1 - return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: [ nextMessageKey.toHexString() ]) + let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ] + return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys) } /// - Note: Sync. Don't call from the main thread. @@ -110,19 +113,16 @@ public final class SharedSenderKeysImplementation : NSObject { return ratchet } else { var currentKeyIndex = ratchet.keyIndex - var current = ratchet - var messageKeys: [String] = [] + var result = ratchet while currentKeyIndex < targetKeyIndex { do { - current = try step(current) - messageKeys += current.messageKeys - currentKeyIndex = current.keyIndex + result = try step(result) + currentKeyIndex = result.keyIndex } catch { print("[Loki] Couldn't step ratchet due to error: \(error).") throw error } } - let result = ClosedGroupRatchet(chainKey: current.chainKey, keyIndex: current.keyIndex, messageKeys: messageKeys) // Includes any skipped message keys let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction) return result @@ -182,20 +182,31 @@ public final class SharedSenderKeysImplementation : NSObject { let iv = ivAndCiphertext[0.. 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order + lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16).. Bool {