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`
This commit is contained in:
Morgan Pretty 2022-02-25 17:48:09 +11:00
parent dbead5e3c8
commit cc2a077a6c
12 changed files with 106 additions and 50 deletions

View file

@ -24,7 +24,8 @@ abstract_target 'GlobalDependencies' do
# Dependencies to be included only in all extensions/frameworks # Dependencies to be included only in all extensions/frameworks
abstract_target 'FrameworkAndExtensionDependencies' do 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' pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version'
target 'SessionNotificationServiceExtension' target 'SessionNotificationServiceExtension'

View file

@ -123,7 +123,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- AFNetworking - AFNetworking
- CryptoSwift - 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`) - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
- Nimble - Nimble
- NVActivityIndicatorView - NVActivityIndicatorView
@ -156,7 +156,8 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
Curve25519Kit: Curve25519Kit:
:git: https://github.com/signalapp/Curve25519Kit.git :branch: session
:git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git
Mantle: Mantle:
:branch: signal-master :branch: signal-master
:git: https://github.com/signalapp/Mantle :git: https://github.com/signalapp/Mantle
@ -174,8 +175,8 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS: CHECKOUT OPTIONS:
Curve25519Kit: Curve25519Kit:
:commit: 4fc1c10e98fff2534b5379a9bb587430fdb8e577 :commit: a23049232dc6c18928cdacfbcef287dad954c5c6
:git: https://github.com/signalapp/Curve25519Kit.git :git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git
Mantle: Mantle:
:commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4
:git: https://github.com/signalapp/Mantle :git: https://github.com/signalapp/Mantle
@ -213,6 +214,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 42874150fd08761ee6907c5bacf22b95ae849d8c PODFILE CHECKSUM: b3b9b5446a109dbcdb5381176ebe431f7762558d
COCOAPODS: 1.11.2 COCOAPODS: 1.11.2

View file

@ -3,6 +3,34 @@ import Sodium
extension Storage { 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. /// Returns the ID of the thread.
public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
let transaction = transaction as! YapDatabaseReadWriteTransaction let transaction = transaction as! YapDatabaseReadWriteTransaction

View file

@ -115,7 +115,7 @@ public final class OpenGroupAPI: NSObject {
// TODO: Limit? // TODO: Limit?
// queryParameters: [ .limit: 256 ] // 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 /// 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 /// `OpenGroupManager.handleInbox` method to ensure things are processed correctly
@available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") @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<NoBody>( let request: Request = Request<NoBody>(
server: server, server: server,
endpoint: .inbox endpoint: .inbox
) )
return send(request, using: dependencies) 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 /// **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 /// 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 /// 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") @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<NoBody>( let request: Request = Request<NoBody>(
server: server, server: server,
endpoint: .inboxSince(id: id) endpoint: .inboxSince(id: id)
) )
return send(request, using: dependencies) 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 /// Delivers a direct message to a user via their blinded Session ID

View file

@ -319,6 +319,8 @@ public final class OpenGroupManager: NSObject {
isBackgroundPoll: Bool, isBackgroundPoll: Bool,
using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() 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 { guard let serverPublicKey: String = dependencies.storage.getOpenGroupPublicKey(for: server) else {
SNLog("Couldn't receive inbox message.") SNLog("Couldn't receive inbox message.")
return return

View file

@ -15,6 +15,8 @@ public protocol SodiumType {
func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes?
func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> 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 { public protocol AeadXChaCha20Poly1305IetfType {

View file

@ -124,13 +124,13 @@ extension OpenGroupAPI {
) )
case .inbox, .inboxSince: 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.") SNLog("Open group polling failed due to invalid data.")
return return
} }
OpenGroupManager.handleInbox( OpenGroupManager.handleInbox(
responseBody, (responseBody ?? []),
on: server, on: server,
isBackgroundPoll: isBackgroundPoll isBackgroundPoll: isBackgroundPoll
) )

View file

@ -80,6 +80,9 @@ public protocol SessionMessagingKitStorageProtocol {
// MARK: - Message Handling // MARK: - Message Handling
func getAllMessageRequestThreads() -> [String: TSContactThread]
func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread]
func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64] func getReceivedMessageTimestamps(using transaction: Any) -> [UInt64]
func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any) func addReceivedMessageTimestamp(_ timestamp: UInt64, using transaction: Any)
/// Returns the ID of the thread. /// Returns the ID of the thread.

View file

@ -1,5 +1,6 @@
import Clibsodium import Clibsodium
import Sodium import Sodium
import Curve25519Kit
extension Sign { extension Sign {
@ -232,6 +233,33 @@ extension Sodium {
return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32) 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 { extension GenericHash {

View file

@ -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. /// 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<AESGCM.EncryptionResult> { static func encrypt(_ payload: String, for destination: Destination, with version: Version) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending() let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
do { do {
guard let data = payload.data(using: .utf8) else { guard let payloadAsData: Data = payload.data(using: .utf8) else { throw Error.invalidRequestInfo }
throw Error.invalidRequestInfo
}
let result = try encrypt(data, for: destination) let data: Data
seal.fulfill(result)
}
catch (let error) {
seal.reject(error)
}
}
return promise switch version {
} case .v2, .v3:
// Wrapping is only needed for snode requests
static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
// Wrapping isn't needed for file server or open group onion requests
switch destination { switch destination {
case .snode: case .snode: data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) case .server: data = payloadAsData
let data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
let result = try encrypt(data, for: destination)
seal.fulfill(result)
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) { catch (let error) {
seal.reject(error) seal.reject(error)

View file

@ -245,7 +245,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
/// Builds an onion around `payload` and returns the result. /// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: String, targetedAt destination: Destination) -> Promise<OnionBuildingResult> { private static func buildOnion(around payload: String, targetedAt destination: Destination, version: Version) -> Promise<OnionBuildingResult> {
var guardSnode: Snode! var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult! var encryptionResult: AESGCM.EncryptionResult!
@ -254,7 +254,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in
guardSnode = path.first! guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first // Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination).then2 { r -> Promise<AESGCM.EncryptionResult> in return encrypt(payload, for: destination, with: version).then2 { r -> Promise<AESGCM.EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order) // Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r encryptionResult = r
@ -328,7 +328,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending() let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending()
var guardSnode: Snode? var guardSnode: Snode?
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` 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 guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2" let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"
let finalEncryptionResult = intermediate.finalEncryptionResult let finalEncryptionResult = intermediate.finalEncryptionResult

View file

@ -131,8 +131,15 @@ public final class SnodeAPI : NSObject {
// MARK: Internal API // MARK: Internal API
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
if Features.useOnionRequests { if Features.useOnionRequests {
// TODO: Ensure this should use the v3 request? return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey)
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, using: .v3, associatedWith: publicKey).map2 { $0 as Any } .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 { } else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1" let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in