Merge pull request #363 from oxen-io/ons

ONS Stage 1
pull/364/head
Niels Andriesse 4 years ago committed by GitHub
commit 5247306aa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -216,6 +216,6 @@ SPEC CHECKSUMS:
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 2fca3f32c171e1324c9e3809b96a32d4a929d05c
PODFILE CHECKSUM: 39a581f238201cd5bfb849a79ba3d503bca71260
COCOAPODS: 1.10.1

@ -124,17 +124,40 @@ final class NewPrivateChatVC : BaseVC, UIPageViewControllerDataSource, UIPageVie
startNewPrivateChatIfPossible(with: hexEncodedPublicKey)
}
fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) {
if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) {
let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: NSLocalizedString("Please check the Session ID and try again", comment: ""), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
presentAlert(alert)
fileprivate func startNewPrivateChatIfPossible(with onsNameOrPublicKey: String) {
if ECKeyPair.isValidHexEncodedPublicKey(candidate: onsNameOrPublicKey) {
startNewPrivateChat(with: onsNameOrPublicKey)
} else {
let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey)
presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
// This could be an ONS name
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
SnodeAPI.getSessionID(for: onsNameOrPublicKey).done { sessionID in
modalActivityIndicator.dismiss {
self?.startNewPrivateChat(with: sessionID)
}
}.catch { error in
modalActivityIndicator.dismiss {
var messageOrNil: String?
if let error = error as? SnodeAPI.Error {
switch error {
case .decryptionFailed, .hashingFailed, .validationFailed: messageOrNil = error.errorDescription
default: break
}
}
let message = messageOrNil ?? "Please check the Session ID or ONS name and try again"
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
self?.presentAlert(alert)
}
}
}
}
}
private func startNewPrivateChat(with sessionID: String) {
let thread = TSContactThread.getOrCreateThread(contactId: sessionID)
presentingViewController?.dismiss(animated: true, completion: nil)
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
}
}
private final class EnterPublicKeyVC : UIViewController {
@ -143,7 +166,11 @@ private final class EnterPublicKeyVC : UIViewController {
private var bottomConstraint: NSLayoutConstraint!
// MARK: Components
private let publicKeyTextView = TextView(placeholder: NSLocalizedString("vc_enter_public_key_text_field_hint", comment: ""))
private lazy var publicKeyTextView: TextView = {
let result = TextView(placeholder: "Enter Session ID or ONS name")
result.autocapitalizationType = .none
return result
}()
private lazy var copyButton: Button = {
let result = Button(style: .unimportant, size: .medium)
@ -292,8 +319,8 @@ private final class EnterPublicKeyVC : UIViewController {
}
@objc fileprivate func startNewPrivateChatIfPossible() {
let publicKey = publicKeyTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
newPrivateChatVC.startNewPrivateChatIfPossible(with: publicKey)
let text = publicKeyTextView.text?.trimmingCharacters(in: .whitespaces) ?? ""
newPrivateChatVC.startNewPrivateChatIfPossible(with: text)
}
}

@ -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<JSON> {
public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<JSON> in
guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error }

@ -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"
}

@ -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,71 @@ public final class SnodeAPI : NSObject {
}
// MARK: Public API
public static func getSessionID(for onsName: String) -> Promise<String> {
let sodium = Sodium()
let validationCount = 3
let sessionIDByteCount = 33
// 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..<validationCount).map { _ in
return getRandomSnode().then2 { snode in
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
invoke(.getSessionIDForONSName, on: snode, parameters: parameters)
}
}
}
let (promise, seal) = Promise<String>.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 hexEncodedEncryptedBlob = x2["encrypted_value"] as? String else { return seal.reject(HTTP.Error.invalidJSON) }
let encryptedBlob = [UInt8](Data(hex: hexEncodedEncryptedBlob))
let isArgon2Based = (encryptedBlob.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: encryptedBlob, secretKey: key, nonce: nonce) else {
return seal.reject(Error.decryptionFailed)
}
sessionIDs.append(sessionIDAsData.toHexString())
} else {
// BLAKE2b-based encryption
guard let key = sodium.genericHash.hash(message: nameAsData, key: nameHash) else { // key = H(name, key=H(name))
return seal.reject(Error.hashingFailed)
}
let nonceSize = sodium.aead.xchacha20poly1305ietf.NonceBytes
guard encryptedBlob.count >= (sessionIDByteCount + sodium.aead.xchacha20poly1305ietf.ABytes + nonceSize) else { // Should always be equal in practice
return seal.reject(Error.decryptionFailed)
}
let nonce = [UInt8](encryptedBlob[(encryptedBlob.endIndex - nonceSize) ..< encryptedBlob.endIndex])
let ciphertext = [UInt8](encryptedBlob[0 ..< (encryptedBlob.endIndex - nonceSize)])
guard let sessionIDAsData = sodium.aead.xchacha20poly1305ietf.decrypt(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) 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)) }

Loading…
Cancel
Save