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:
parent
dbead5e3c8
commit
cc2a077a6c
3
Podfile
3
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'
|
||||
|
|
11
Podfile.lock
11
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
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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<NoBody>(
|
||||
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<NoBody>(
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,12 +14,24 @@ 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<AESGCM.EncryptionResult> {
|
||||
static func encrypt(_ payload: String, for destination: Destination, with version: Version) -> Promise<AESGCM.EncryptionResult> {
|
||||
let (promise, seal) = Promise<AESGCM.EncryptionResult>.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 data: Data
|
||||
|
||||
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 .v4:
|
||||
data = payloadAsData
|
||||
}
|
||||
|
||||
let result = try encrypt(data, for: destination)
|
||||
|
@ -33,34 +45,6 @@ internal extension OnionRequestAPI {
|
|||
return promise
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
case .server:
|
||||
let data = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||
let result = try encrypt(data, for: destination)
|
||||
seal.fulfill(result)
|
||||
}
|
||||
}
|
||||
catch (let error) {
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
private static func encrypt(_ payload: Data, for destination: Destination) throws -> AESGCM.EncryptionResult {
|
||||
switch destination {
|
||||
case .snode(let snode):
|
||||
|
|
|
@ -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<OnionBuildingResult> {
|
||||
private static func buildOnion(around payload: String, targetedAt destination: Destination, version: Version) -> Promise<OnionBuildingResult> {
|
||||
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<AESGCM.EncryptionResult> in
|
||||
guardSnode = path.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
|
||||
// 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
|
||||
|
|
|
@ -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<Any> in
|
||||
|
|
Loading…
Reference in New Issue