diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 558f037bb..71e60e900 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -251,6 +251,7 @@ B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A62398B23E00211ABE /* QRCodeVC.swift */; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B6A904218B4C90007C4606 /* TypingIndicatorInteraction.swift */; }; + B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; }; @@ -310,6 +311,7 @@ C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; }; C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; }; + C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */; }; C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; @@ -1234,6 +1236,7 @@ B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = ""; }; B886B4A82398BA1500211ABE /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; + B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIV2.swift; sourceTree = ""; }; B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; @@ -1306,6 +1309,7 @@ C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = ""; }; C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = ""; }; C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = ""; }; + C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = ""; }; C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = ""; }; C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuWindow.swift; sourceTree = ""; }; @@ -2495,6 +2499,26 @@ path = Meta; sourceTree = ""; }; + C3227FF4260AAD58006EA627 /* V2 */ = { + isa = PBXGroup; + children = ( + B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */, + C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */, + ); + path = V2; + sourceTree = ""; + }; + C3228005260AAD7E006EA627 /* V1 */ = { + isa = PBXGroup; + children = ( + C3A721352558BDF90043A11F /* OpenGroupAPI.swift */, + C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */, + C3A721342558BDF90043A11F /* OpenGroupMessage.swift */, + C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */, + ); + path = V1; + sourceTree = ""; + }; C328252E25CA54F70062D0A7 /* Context Menu */ = { isa = PBXGroup; children = ( @@ -3158,11 +3182,9 @@ C3A721332558BDDF0043A11F /* Open Groups */ = { isa = PBXGroup; children = ( + C3228005260AAD7E006EA627 /* V1 */, + C3227FF4260AAD58006EA627 /* V2 */, C3A721372558BDFA0043A11F /* OpenGroup.swift */, - C3A721352558BDF90043A11F /* OpenGroupAPI.swift */, - C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */, - C3A721342558BDF90043A11F /* OpenGroupMessage.swift */, - C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */, C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */, ); path = "Open Groups"; @@ -4733,6 +4755,7 @@ C32C5BDD256DC88D003C73A2 /* OWSReadReceiptManager.m in Sources */, C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */, C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */, + C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */, B8B32021258B1A650020074B /* Contact.swift in Sources */, C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, @@ -4801,6 +4824,7 @@ C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */, C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */, C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */, + B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */, C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */, C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/V1/OpenGroupAPI.swift similarity index 100% rename from SessionMessagingKit/Open Groups/OpenGroupAPI.swift rename to SessionMessagingKit/Open Groups/V1/OpenGroupAPI.swift diff --git a/SessionMessagingKit/Open Groups/OpenGroupInfo.swift b/SessionMessagingKit/Open Groups/V1/OpenGroupInfo.swift similarity index 100% rename from SessionMessagingKit/Open Groups/OpenGroupInfo.swift rename to SessionMessagingKit/Open Groups/V1/OpenGroupInfo.swift diff --git a/SessionMessagingKit/Open Groups/OpenGroupMessage+Conversion.swift b/SessionMessagingKit/Open Groups/V1/OpenGroupMessage+Conversion.swift similarity index 100% rename from SessionMessagingKit/Open Groups/OpenGroupMessage+Conversion.swift rename to SessionMessagingKit/Open Groups/V1/OpenGroupMessage+Conversion.swift diff --git a/SessionMessagingKit/Open Groups/OpenGroupMessage.swift b/SessionMessagingKit/Open Groups/V1/OpenGroupMessage.swift similarity index 100% rename from SessionMessagingKit/Open Groups/OpenGroupMessage.swift rename to SessionMessagingKit/Open Groups/V1/OpenGroupMessage.swift diff --git a/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift new file mode 100644 index 000000000..9364031fa --- /dev/null +++ b/SessionMessagingKit/Open Groups/V2/OpenGroupAPIV2.swift @@ -0,0 +1,210 @@ +import PromiseKit +import SessionSnodeKit + +// TODO: Auth token & public key storage + +public enum OpenGroupAPIV2 { + + // MARK: Error + public enum Error : LocalizedError { + case generic + case parsingFailed + case decryptionFailed + case signingFailed + case invalidURL + case noPublicKey + + public var errorDescription: String? { + switch self { + case .generic: return "An error occurred." + case .parsingFailed: return "Invalid response." + case .decryptionFailed: return "Couldn't decrypt response." + case .signingFailed: return "Couldn't sign message." + case .invalidURL: return "Invalid URL." + case .noPublicKey: return "Couldn't find server public key." + } + } + } + + // MARK: Request + private struct Request { + let verb: HTTP.Verb + let room: String + let server: String + let endpoint: String + let queryParameters: [String:String] + let parameters: JSON + let isAuthRequired: Bool + /// Always `true` under normal circumstances. You might want to disable + /// this when running over Lokinet. + let useOnionRouting: Bool + + init(verb: HTTP.Verb, room: String, server: String, endpoint: String, queryParameters: [String:String] = [:], + parameters: JSON = [:], isAuthRequired: Bool = true, useOnionRouting: Bool = true) { + self.verb = verb + self.room = room + self.server = server + self.endpoint = endpoint + self.queryParameters = queryParameters + self.parameters = parameters + self.isAuthRequired = isAuthRequired + self.useOnionRouting = useOnionRouting + } + } + + // MARK: Convenience + private static func send(_ request: Request) -> Promise { + let tsRequest: TSRequest + switch request.verb { + case .get: + var rawURL = "\(request.server)/\(request.endpoint)" + if !request.queryParameters.isEmpty { + let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&") + rawURL += "?\(queryString)" + } + guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } + tsRequest = TSRequest(url: url) + case .post, .put, .delete: + let rawURL = "\(request.server)/\(request.endpoint)" + guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } + tsRequest = TSRequest(url: url, method: request.verb.rawValue, parameters: request.parameters) + } + tsRequest.setValue(request.room, forKey: "Room") + if request.useOnionRouting { + guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { return Promise(error: Error.noPublicKey) } + return getAuthToken(for: request.server).then(on: DispatchQueue.global(qos: .default)) { authToken -> Promise in + tsRequest.setValue(authToken, forKey: "Authorization") + return OnionRequestAPI.sendOnionRequest(tsRequest, to: request.server, using: publicKey) + } + } else { + preconditionFailure("It's currently not allowed to send non onion routed requests.") + } + } + + // MARK: Authorization + private static func getAuthToken(for server: String) -> Promise { + return Promise.value("") // TODO: Implement + } + + public static func requestNewAuthToken(for room: String, on server: String) -> Promise { + SNLog("Requesting auth token for server: \(server).") + guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) } + let queryParameters = [ "public_key" : getUserHexEncodedPublicKey() ] + let request = Request(verb: .get, room: room, server: server, endpoint: "auth_token_challenge", queryParameters: queryParameters, isAuthRequired: false) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let base64EncodedCiphertext = json["ciphertext"] as? String, let base64EncodedEphemeralPublicKey = json["ephemeral_public_key"] as? String, + let ciphertext = Data(base64Encoded: base64EncodedCiphertext), let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else { + throw Error.parsingFailed + } + let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) + guard let tokenAsData = try? AESGCM.decrypt(ciphertext, with: symmetricKey) else { throw Error.decryptionFailed } + return tokenAsData.toHexString() + } + } + + public static func claimAuthToken(for room: String, on server: String) -> Promise { + guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) } + let parameters = [ "public_key" : userKeyPair.publicKey.toHexString() ] + let request = Request(verb: .post, room: room, server: server, endpoint: "claim_auth_token", parameters: parameters) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in } + } + + /// Should be called when leaving a group. + public static func deleteAuthToken(for room: String, on server: String) -> Promise { + let request = Request(verb: .delete, room: room, server: server, endpoint: "auth_token") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in } + } + + // MARK: File Storage + public static func upload(_ file: Data, to room: String, on server: String) -> Promise { + let base64EncodedFile = file.base64EncodedString() + let parameters = [ "file" : base64EncodedFile ] + let request = Request(verb: .post, room: room, server: server, endpoint: "files", parameters: parameters) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let fileID = json["result"] as? String else { throw Error.parsingFailed } + return fileID + } + } + + public static func download(_ file: String, from room: String, on server: String) -> Promise { + let request = Request(verb: .get, room: room, server: server, endpoint: "files/\(file)") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed } + return file + } + } + + // MARK: Message Sending & Receiving + public static func send(_ message: OpenGroupMessageV2, to room: String, on server: String) -> Promise { + guard let signedMessage = message.sign() else { return Promise(error: Error.signingFailed) } + guard let json = signedMessage.toJSON() else { return Promise(error: Error.parsingFailed) } + let request = Request(verb: .post, room: room, server: server, endpoint: "messages", parameters: json) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let message = OpenGroupMessageV2.fromJSON(json) else { throw Error.parsingFailed } + return message + } + } + + public static func getMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { + // TODO: From server ID & limit + let queryParameters: [String:String] = [:] + let request = Request(verb: .get, room: room, server: server, endpoint: "messages", queryParameters: queryParameters) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let rawMessages = json["messages"] as? [[String:Any]] else { throw Error.parsingFailed } + let messages: [OpenGroupMessageV2] = rawMessages.compactMap { json in + // TODO: Signature validation + guard let message = OpenGroupMessageV2.fromJSON(json) else { + SNLog("Couldn't parse open group message from JSON: \(json).") + return nil + } + return message + } + return messages + } + } + + // MARK: Message Deletion + public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise { + let request = Request(verb: .delete, room: room, server: server, endpoint: "messages/\(serverID)") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in } + } + + public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Int64]> { + // TODO: From server ID & limit + let queryParameters: [String:String] = [:] + let request = Request(verb: .get, room: room, server: server, endpoint: "deleted_messages", queryParameters: queryParameters) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let ids = json["ids"] as? [Int64] else { throw Error.parsingFailed } + return ids + } + } + + // MARK: Moderation + public static func getModerators(for room: String, on server: String) -> Promise<[String]> { + let request = Request(verb: .get, room: room, server: server, endpoint: "moderators") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let moderators = json["moderators"] as? [String] else { throw Error.parsingFailed } + return moderators + } + } + + public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise { + let parameters = [ "public_key" : publicKey ] + let request = Request(verb: .post, room: room, server: server, endpoint: "block_list", parameters: parameters) + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in } + } + + public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise { + let request = Request(verb: .delete, room: room, server: server, endpoint: "block_list/\(publicKey)") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { _ in } + } + + // MARK: General + public static func getMemberCount(for room: String, on server: String) -> Promise { + let request = Request(verb: .get, room: room, server: server, endpoint: "member_count") + return send(request).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let memberCount = json["member_count"] as? UInt else { throw Error.parsingFailed } + return memberCount + } + } +} diff --git a/SessionMessagingKit/Open Groups/V2/OpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/V2/OpenGroupMessageV2.swift new file mode 100644 index 000000000..dcf07efde --- /dev/null +++ b/SessionMessagingKit/Open Groups/V2/OpenGroupMessageV2.swift @@ -0,0 +1,33 @@ + +public struct OpenGroupMessageV2 { + public let serverID: Int64? + /// The serialized protobuf in base64 encoding. + public let base64EncodedData: String + /// When sending a message, the sender signs the serialized protobuf with their private key so that + /// a receiving user can verify that the message wasn't tampered with. + public let base64EncodedSignature: String? + + public func sign() -> OpenGroupMessageV2? { + let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()! + let data = Data(base64Encoded: base64EncodedData)! + guard let signature = try? Ed25519.sign(data, with: userKeyPair) else { + SNLog("Failed to sign open group message.") + return nil + } + return OpenGroupMessageV2(serverID: serverID, base64EncodedData: base64EncodedData, base64EncodedSignature: signature.base64EncodedString()) + } + + public func toJSON() -> JSON? { + var result: JSON = [ "data" : base64EncodedData ] + if let serverID = serverID { result["server_id"] = serverID } + if let base64EncodedSignature = base64EncodedSignature { result["signature"] = base64EncodedSignature } + return result + } + + public static func fromJSON(_ json: JSON) -> OpenGroupMessageV2? { + guard let base64EncodedData = json["data"] as? String else { return nil } + let serverID = json["server_id"] as? Int64 + let base64EncodedSignature = json["signature"] as? String + return OpenGroupMessageV2(serverID: serverID, base64EncodedData: base64EncodedData, base64EncodedSignature: base64EncodedSignature) + } +} diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 9b1c6167f..420b548fc 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -374,12 +374,16 @@ public enum OnionRequestAPI { let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } do { let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey) + // The old open group server and file server implementations put the status code in the JSON under the "status" + // key, whereas the new implementations put it under the "status_code" key guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON, - let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) } + let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) } if statusCode == 406 { // Clock out of sync SNLog("The user's clock is out of sync with the service node network.") seal.reject(SnodeAPI.Error.clockOutOfSync) } else if let bodyAsString = json["body"] as? String { + // This clause is only used by the old open group and file server implementations. The new implementations will + // always go to the next clause. let body: JSON if !isJSONRequired { body = [ "result" : bodyAsString ] diff --git a/SessionUtilitiesKit/Crypto/AESGCM.swift b/SessionUtilitiesKit/Crypto/AESGCM.swift index 8cb40809f..e20c8987d 100644 --- a/SessionUtilitiesKit/Crypto/AESGCM.swift +++ b/SessionUtilitiesKit/Crypto/AESGCM.swift @@ -19,6 +19,20 @@ public enum AESGCM { } } + /// - Note: Sync. Don't call from the main thread. + public static func generateSymmetricKey(x25519PublicKey: Data, x25519PrivateKey: Data) throws -> Data { + if Thread.isMainThread { + #if DEBUG + preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") + #endif + } + guard let sharedSecret = try? Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: x25519PrivateKey) else { + throw Error.sharedSecretGenerationFailed + } + let salt = "LOKI" + return try Data(HMAC(key: salt.bytes, variant: .sha256).authenticate(sharedSecret.bytes)) + } + /// - Note: Sync. Don't call from the main thread. public static func decrypt(_ ivAndCiphertext: Data, with symmetricKey: Data) throws -> Data { if Thread.isMainThread { @@ -56,11 +70,7 @@ public enum AESGCM { } let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey) let ephemeralKeyPair = Curve25519.generateKeyPair() - guard let ephemeralSharedSecret = try? Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey) else { - throw Error.sharedSecretGenerationFailed - } - let salt = "LOKI" - let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes) + let symmetricKey = try generateSymmetricKey(x25519PublicKey: x25519PublicKey, x25519PrivateKey: ephemeralKeyPair.privateKey) let ciphertext = try encrypt(plaintext, with: Data(symmetricKey)) return EncryptionResult(ciphertext: ciphertext, symmetricKey: Data(symmetricKey), ephemeralPublicKey: ephemeralKeyPair.publicKey) }