diff --git a/Podfile b/Podfile index 10ffca28a..3c34d96fc 100644 --- a/Podfile +++ b/Podfile @@ -79,6 +79,7 @@ target 'SessionSnodeKit' do pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end diff --git a/Podfile.lock b/Podfile.lock index 503cc3094..6c8c0c8fb 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -216,6 +216,6 @@ SPEC CHECKSUMS: YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 2fca3f32c171e1324c9e3809b96a32d4a929d05c +PODFILE CHECKSUM: 39a581f238201cd5bfb849a79ba3d503bca71260 COCOAPODS: 1.10.1 diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 0f96d8926..9b1c6167f 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -287,7 +287,7 @@ public enum OnionRequestAPI { // MARK: Public API /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise { + public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error } diff --git a/SessionSnodeKit/Snode.swift b/SessionSnodeKit/Snode.swift index 9b185edbb..ec6fbfce3 100644 --- a/SessionSnodeKit/Snode.swift +++ b/SessionSnodeKit/Snode.swift @@ -13,6 +13,7 @@ public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance public enum Method : String { case getSwarm = "get_snodes_for_pubkey" case getMessages = "retrieve" + case getSessionIDForONSName = "get_lns_mapping" case sendMessage = "store" } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 337197c7f..0987cca9a 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -1,5 +1,6 @@ import PromiseKit import SessionUtilitiesKit +import Sodium @objc(SNSnodeAPI) public final class SnodeAPI : NSObject { @@ -31,12 +32,20 @@ public final class SnodeAPI : NSObject { case generic case clockOutOfSync case snodePoolUpdatingFailed + // ONS + case decryptionFailed + case hashingFailed + case validationFailed public var errorDescription: String? { switch self { case .generic: return "An error occurred." case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time." case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool." + // ONS + case .decryptionFailed: return "Couldn't decrypt ONS name." + case .hashingFailed: return "Couldn't compute ONS name hash." + case .validationFailed: return "ONS name validation failed." } } } @@ -109,7 +118,7 @@ public final class SnodeAPI : NSObject { } // MARK: Internal API - internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { + internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { if useOnionRequests { return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any } } else { @@ -181,6 +190,64 @@ public final class SnodeAPI : NSObject { } // MARK: Public API + public static func getSessionID(for onsName: String) -> Promise { + let sodium = Sodium() + let validationCount = 3 + // The name must be lowercased + let onsName = onsName.lowercased() + // Hash the ONS name using BLAKE2b + let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) + guard let nameHash = sodium.genericHash.hash(message: nameAsData), + let base64EncodedNameHash = nameHash.toBase64() else { return Promise(error: Error.hashingFailed) } + // Ask 3 different snodes for the Session ID associated with the given name hash + let parameters: [String:Any] = [ "name_hash" : base64EncodedNameHash ] + let promises = (0...pending() + when(resolved: promises).done2 { results in + var sessionIDs: [String] = [] + for result in results { + switch result { + case .rejected(let error): return seal.reject(error) + case .fulfilled(let rawResponse): + guard let json = rawResponse as? JSON, let x0 = json["result"] as? JSON, + let x1 = x0["entries"] as? [JSON], let x2 = x1.first, + let hexEncodedEncryptedSessionID = x2["encrypted_value"] as? String else { return seal.reject(HTTP.Error.invalidJSON) } + let encryptedSessionID = [UInt8](Data(hex: hexEncodedEncryptedSessionID)) + let sessionIDByteCount = 33 + let isArgon2Based = (encryptedSessionID.count == sessionIDByteCount + sodium.secretBox.MacBytes) + if isArgon2Based { + // Handle old Argon2-based encryption used before HF16 + let salt = [UInt8](Data(repeating: 0, count: sodium.pwHash.SaltBytes)) + guard let key = sodium.pwHash.hash(outputLength: sodium.secretBox.KeyBytes, passwd: nameAsData, salt: salt, + opsLimit: sodium.pwHash.OpsLimitModerate, memLimit: sodium.pwHash.MemLimitModerate, alg: .Argon2ID13) else { return seal.reject(Error.hashingFailed) } + let nonce = [UInt8](Data(repeating: 0, count: sodium.secretBox.NonceBytes)) + guard let sessionIDAsData = sodium.secretBox.open(authenticatedCipherText: encryptedSessionID, secretKey: key, nonce: nonce) else { + return seal.reject(Error.decryptionFailed) + } + sessionIDs.append(sessionIDAsData.toHexString()) + } else { + // BLAKE2b-based encryption + // key = H(name, key=H(name)) + guard let key = sodium.genericHash.hash(message: nameAsData, key: nameHash) else { return seal.reject(Error.hashingFailed) } + guard let sessionIDAsData = sodium.aead.xchacha20poly1305ietf.decrypt(nonceAndAuthenticatedCipherText: encryptedSessionID, secretKey: key) else { + return seal.reject(Error.decryptionFailed) + } + sessionIDs.append(sessionIDAsData.toHexString()) + } + } + } + guard sessionIDs.count == validationCount && Set(sessionIDs).count == 1 else { return seal.reject(Error.validationFailed) } + seal.fulfill(sessionIDs.first!) + } + return promise + } + public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> { // shuffled() uses the system's default random generator, which is cryptographically secure return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }