From cc2a077a6c4b9f3efacc263a89f304dede6fa1da Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 25 Feb 2022 17:48:09 +1100 Subject: [PATCH] Started working on `MessageRequestResponse` handling for SOGS message requests Pointing Curve25519 to use a fork that exposes an XEd25519 conversion method Fixed an issue where I had broken all message sending due to the SnodeAPI casting Onion responses to `Any` --- Podfile | 3 +- Podfile.lock | 11 +++-- .../Database/Storage+Messaging.swift | 28 +++++++++++ .../Open Groups/OpenGroupAPI.swift | 12 ++--- .../Open Groups/OpenGroupManager.swift | 2 + .../Open Groups/Types/SodiumProtocols.swift | 2 + .../Pollers/OpenGroupPoller.swift | 4 +- SessionMessagingKit/Storage.swift | 3 ++ .../Utilities/Sodium+Utilities.swift | 28 +++++++++++ .../OnionRequestAPI+Encryption.swift | 46 ++++++------------- SessionSnodeKit/OnionRequestAPI.swift | 6 +-- SessionSnodeKit/SnodeAPI.swift | 11 ++++- 12 files changed, 106 insertions(+), 50 deletions(-) diff --git a/Podfile b/Podfile index b2f5d93ca..a6b96cb29 100644 --- a/Podfile +++ b/Podfile @@ -24,7 +24,8 @@ abstract_target 'GlobalDependencies' do # Dependencies to be included only in all extensions/frameworks abstract_target 'FrameworkAndExtensionDependencies' do - pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git' + # TODO: Swap this to use an oxen-io fork + pod 'Curve25519Kit', git: 'https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git', branch: 'session' pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version' target 'SessionNotificationServiceExtension' diff --git a/Podfile.lock b/Podfile.lock index 372f13c9d..2158f8257 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -123,7 +123,7 @@ PODS: DEPENDENCIES: - AFNetworking - CryptoSwift - - Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`) + - Curve25519Kit (from `https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git`, branch `session`) - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - Nimble - NVActivityIndicatorView @@ -156,7 +156,8 @@ SPEC REPOS: EXTERNAL SOURCES: Curve25519Kit: - :git: https://github.com/signalapp/Curve25519Kit.git + :branch: session + :git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git Mantle: :branch: signal-master :git: https://github.com/signalapp/Mantle @@ -174,8 +175,8 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Curve25519Kit: - :commit: 4fc1c10e98fff2534b5379a9bb587430fdb8e577 - :git: https://github.com/signalapp/Curve25519Kit.git + :commit: a23049232dc6c18928cdacfbcef287dad954c5c6 + :git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git Mantle: :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :git: https://github.com/signalapp/Mantle @@ -213,6 +214,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 42874150fd08761ee6907c5bacf22b95ae849d8c +PODFILE CHECKSUM: b3b9b5446a109dbcdb5381176ebe431f7762558d COCOAPODS: 1.11.2 diff --git a/SessionMessagingKit/Database/Storage+Messaging.swift b/SessionMessagingKit/Database/Storage+Messaging.swift index aabf74b7d..48421aa49 100644 --- a/SessionMessagingKit/Database/Storage+Messaging.swift +++ b/SessionMessagingKit/Database/Storage+Messaging.swift @@ -2,6 +2,34 @@ import PromiseKit import Sodium extension Storage { + + public func getAllMessageRequestThreads() -> [String: TSContactThread] { + var result: [String: TSContactThread] = [:] + + Storage.read { transaction in + result = self.getAllMessageRequestThreads(using: transaction) + } + + return result + } + + public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] { + var result = [String: TSContactThread]() + + // FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15' + let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue) + + transaction.enumerateKeysAndObjects( + inCollection: TSContactThread.collection(), + using: { threadID, object, _ in + guard let contactThread = object as? TSContactThread else { return } + result[threadID] = contactThread + }, + withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) } + ) + + return result + } /// Returns the ID of the thread. public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index d895780d9..ecd7d9de3 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -115,7 +115,7 @@ public final class OpenGroupAPI: NSObject { // TODO: Limit? // queryParameters: [ .limit: 256 ] ), - responseType: [DirectMessage].self + responseType: [DirectMessage]?.self // 'inboxSince' will return a `304` with an empty response if no messages ) ) @@ -507,30 +507,30 @@ public final class OpenGroupAPI: NSObject { /// method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the /// `OpenGroupManager.handleInbox` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") - public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> { + public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { let request: Request = Request( server: server, endpoint: .inbox ) return send(request, using: dependencies) - .decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) } - /// Polls for any DMs received since the given id + /// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages /// /// **Note:** This is the direct request to retrieve messages requests for a specific Open Group since a given messages so should be retrieved /// automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response /// of this method to the `OpenGroupManager.handleInbox` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") - public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage])> { + public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { let request: Request = Request( server: server, endpoint: .inboxSince(id: id) ) return send(request, using: dependencies) - .decoded(as: [DirectMessage].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) } /// Delivers a direct message to a user via their blinded Session ID diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 48ed6d831..ab669549b 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -319,6 +319,8 @@ public final class OpenGroupManager: NSObject { isBackgroundPoll: Bool, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() ) { + // Don't need to do anything if we have no messages (it's a valid case) + guard !messages.isEmpty else { return } guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: server) else { SNLog("Couldn't receive inbox message.") return diff --git a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift index 21799c354..7a23b1903 100644 --- a/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift +++ b/SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift @@ -15,6 +15,8 @@ public protocol SodiumType { func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? + + func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool } public protocol AeadXChaCha20Poly1305IetfType { diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 7aab7ae53..793200253 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -124,13 +124,13 @@ extension OpenGroupAPI { ) case .inbox, .inboxSince: - guard let responseData: BatchSubResponse<[DirectMessage]> = endpointResponse.data as? BatchSubResponse<[DirectMessage]>, let responseBody: [DirectMessage] = responseData.body else { + guard let responseData: BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? BatchSubResponse<[DirectMessage]?>, let responseBody: [DirectMessage]? = responseData.body else { SNLog("Open group polling failed due to invalid data.") return } OpenGroupManager.handleInbox( - responseBody, + (responseBody ?? []), on: server, isBackgroundPoll: isBackgroundPoll ) diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 414015fcd..7a86cbb23 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -80,6 +80,9 @@ public protocol SessionMessagingKitStorageProtocol { // MARK: - Message Handling + func getAllMessageRequestThreads() -> [String: TSContactThread] + func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] + func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) /// Returns the ID of the thread. diff --git a/SessionMessagingKit/Utilities/Sodium+Utilities.swift b/SessionMessagingKit/Utilities/Sodium+Utilities.swift index 43afd803b..0409c6623 100644 --- a/SessionMessagingKit/Utilities/Sodium+Utilities.swift +++ b/SessionMessagingKit/Utilities/Sodium+Utilities.swift @@ -1,5 +1,6 @@ import Clibsodium import Sodium +import Curve25519Kit extension Sign { @@ -232,6 +233,33 @@ extension Sodium { return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32) } + + /// This method should be used to check if a users standard sessionId matches a blinded one + public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String) -> Bool { + // Only support generating blinded keys for standard session ids + guard let sessionId: SessionId = SessionId(from: standardSessionId), sessionId.prefix == .standard else { return false } + guard let blindedId: SessionId = SessionId(from: blindedSessionId), blindedId.prefix == .blinded else { return false } + guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey) else { return false } + + /// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what + /// Signal's XEd25519 conversion always uses) + /// + /// Note: The below method is code we have exposed from the `curve25519_verify` method within the Curve25519 library + /// rather than custom code we have written + guard let xEd25519Key: Data = try? Ed25519.publicKey(from: Data(hex: sessionId.publicKey)) else { return false } + + /// Blind the positive public key + guard let pk1: Bytes = combineKeys(lhsKeyBytes: kBytes, rhsKeyBytes: xEd25519Key.bytes) else { return false } + + /// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2 + /// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000]) + let pk2: Bytes = (pk1[0..<31] + [(pk1[31] ^ 0b1000_0000)]) + + return ( + SessionId(.blinded, publicKey: pk1).publicKey == blindedId.publicKey || + SessionId(.blinded, publicKey: pk2).publicKey == blindedId.publicKey + ) + } } extension GenericHash { diff --git a/SessionSnodeKit/OnionRequestAPI+Encryption.swift b/SessionSnodeKit/OnionRequestAPI+Encryption.swift index deec2a30c..bcbb58a67 100644 --- a/SessionSnodeKit/OnionRequestAPI+Encryption.swift +++ b/SessionSnodeKit/OnionRequestAPI+Encryption.swift @@ -14,44 +14,28 @@ internal extension OnionRequestAPI { } /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. - static func encrypt(_ payload: String, for destination: Destination) -> Promise { + static func encrypt(_ payload: String, for destination: Destination, with version: Version) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { do { - guard let data = payload.data(using: .utf8) else { - throw Error.invalidRequestInfo - } + guard let payloadAsData: Data = payload.data(using: .utf8) else { throw Error.invalidRequestInfo } - let result = try encrypt(data, for: destination) - seal.fulfill(result) - } - catch (let error) { - seal.reject(error) - } - } - - return promise - } - - static func encrypt(_ payload: JSON, for destination: Destination) -> Promise { - let (promise, seal) = Promise.pending() - DispatchQueue.global(qos: .userInitiated).async { - do { - guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) } + let data: Data - // Wrapping isn't needed for file server or open group onion requests - switch destination { - case .snode: - let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) - let data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ]) - let result = try encrypt(data, for: destination) - seal.fulfill(result) + switch version { + case .v2, .v3: + // Wrapping is only needed for snode requests + switch destination { + case .snode: data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ]) + case .server: data = payloadAsData + } - case .server: - let data = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) - let result = try encrypt(data, for: destination) - seal.fulfill(result) + case .v4: + data = payloadAsData } + + let result = try encrypt(data, for: destination) + seal.fulfill(result) } catch (let error) { seal.reject(error) diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 016a373ca..2d7daa083 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -245,7 +245,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } /// Builds an onion around `payload` and returns the result. - private static func buildOnion(around payload: String, targetedAt destination: Destination) -> Promise { + private static func buildOnion(around payload: String, targetedAt destination: Destination, version: Version) -> Promise { var guardSnode: Snode! var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination var encryptionResult: AESGCM.EncryptionResult! @@ -254,7 +254,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { return getPath(excluding: snodeToExclude).then2 { path -> Promise in guardSnode = path.first! // Encrypt in reverse order, i.e. the destination first - return encrypt(payload, for: destination).then2 { r -> Promise in + return encrypt(payload, for: destination, with: version).then2 { r -> Promise in targetSnodeSymmetricKey = r.symmetricKey // Recursively encrypt the layers of the onion (again in reverse order) encryptionResult = r @@ -328,7 +328,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending() var guardSnode: Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` - buildOnion(around: payload, targetedAt: destination).done2 { intermediate in + buildOnion(around: payload, targetedAt: destination, version: version).done2 { intermediate in guardSnode = intermediate.guardSnode let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2" let finalEncryptionResult = intermediate.finalEncryptionResult diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index aec599f1a..becc23930 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -131,8 +131,15 @@ public final class SnodeAPI : NSObject { // MARK: Internal API internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { if Features.useOnionRequests { - // TODO: Ensure this should use the v3 request? - return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey).map2 { $0 as Any } + return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey) + .map2 { responseData in + guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else { + throw Error.generic + } + + // FIXME: Would be nice to change this to not send 'Any' + return responseJson as Any + } } else { let url = "\(snode.address):\(snode.port)/storage_rpc/v1" return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise in