From 0672f56f104271ba315111d8756ef1ef3113d9e3 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 18 Jan 2024 21:50:59 -0300 Subject: [PATCH] Add Client Certificate --- .../Client/RocketChatClient.swift | 13 +- .../URLSessionCertificateHandling.swift | 95 ++++++++++ .../Database/Database.swift | 6 +- .../Default.xcdatamodel/contents | 2 + .../Database/Models/Server.swift | 8 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 10 ++ ios/Shared/RocketChat/ClientSSL.swift | 28 +++ ios/Shared/RocketChat/MMKV.swift | 72 ++++---- ios/Watch/WatchConnection.swift | 165 ++++++++++-------- ios/Watch/WatchMessage.swift | 38 ++-- 10 files changed, 309 insertions(+), 128 deletions(-) create mode 100644 ios/RocketChat Watch App/Client/URLSessionCertificateHandling.swift create mode 100644 ios/Shared/RocketChat/ClientSSL.swift diff --git a/ios/RocketChat Watch App/Client/RocketChatClient.swift b/ios/RocketChat Watch App/Client/RocketChatClient.swift index c29c01ce2..0c1368e71 100644 --- a/ios/RocketChat Watch App/Client/RocketChatClient.swift +++ b/ios/RocketChat Watch App/Client/RocketChatClient.swift @@ -11,9 +11,18 @@ protocol RocketChatClientProtocol { func sendRead(rid: String) -> AnyPublisher } -final class RocketChatClient { +final class RocketChatClient: NSObject { private let server: Server + private lazy var session = URLSession( + configuration: .default, + delegate: URLSesionClientCertificateHandling( + certificate: server.certificate, + password: server.password + ), + delegateQueue: nil + ) + init(server: Server) { self.server = server } @@ -32,7 +41,7 @@ final class RocketChatClient { urlRequest.httpMethod = request.method.rawValue urlRequest.httpBody = request.body - return URLSession.shared.dataTaskPublisher(for: urlRequest) + return session.dataTaskPublisher(for: urlRequest) .tryMap { (data, response) in guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { throw RocketChatError.unauthorized diff --git a/ios/RocketChat Watch App/Client/URLSessionCertificateHandling.swift b/ios/RocketChat Watch App/Client/URLSessionCertificateHandling.swift new file mode 100644 index 000000000..b00cc2237 --- /dev/null +++ b/ios/RocketChat Watch App/Client/URLSessionCertificateHandling.swift @@ -0,0 +1,95 @@ +// https://medium.com/@hamidptb/implementing-mtls-on-ios-using-urlsession-and-cloudflare-890b76aca66c + +import Foundation + +final class URLSesionClientCertificateHandling: NSObject, URLSessionDelegate { + private let certificate: Data? + private let password: String? + + init(certificate: Data?, password: String?) { + self.certificate = certificate + self.password = password + } + + public func urlSession( + _: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate else { + completionHandler(.performDefaultHandling, nil) + return + } + + guard let credential = Credentials.urlCredential(certificate: certificate, password: password) else { + completionHandler(.performDefaultHandling, nil) + return + } + + challenge.sender?.use(credential, for: challenge) + completionHandler(.useCredential, credential) + } +} + +fileprivate typealias UserCertificate = (data: Data, password: String) + +fileprivate final class Credentials { + static func urlCredential(certificate: Data?, password: String?) -> URLCredential? { + guard let certificate, let password else { return nil } + + let p12Contents = PKCS12(pkcs12Data: certificate, password: password) + + guard let identity = p12Contents.identity else { + return nil + } + + return URLCredential(identity: identity, certificates: nil, persistence: .none) + } +} + +fileprivate struct PKCS12 { + let label: String? + let keyID: NSData? + let trust: SecTrust? + let certChain: [SecTrust]? + let identity: SecIdentity? + + public init(pkcs12Data: Data, password: String) { + let importPasswordOption: NSDictionary + = [kSecImportExportPassphrase as NSString: password] + var items: CFArray? + let secError: OSStatus + = SecPKCS12Import(pkcs12Data as NSData, + importPasswordOption, &items) + guard secError == errSecSuccess else { + if secError == errSecAuthFailed { + NSLog("Incorrect password?") + } + fatalError("Error trying to import PKCS12 data") + } + guard let theItemsCFArray = items else { fatalError() } + let theItemsNSArray: NSArray = theItemsCFArray as NSArray + guard let dictArray + = theItemsNSArray as? [[String: AnyObject]] + else { + fatalError() + } + + label = dictArray.element(for: kSecImportItemLabel) + keyID = dictArray.element(for: kSecImportItemKeyID) + trust = dictArray.element(for: kSecImportItemTrust) + certChain = dictArray.element(for: kSecImportItemCertChain) + identity = dictArray.element(for: kSecImportItemIdentity) + } +} + +fileprivate extension Array where Element == [String: AnyObject] { + func element(for key: CFString) -> T? { + for dictElement in self { + if let value = dictElement[key as String] as? T { + return value + } + } + return nil + } +} diff --git a/ios/RocketChat Watch App/Database/Database.swift b/ios/RocketChat Watch App/Database/Database.swift index 82e87273c..4ec290d73 100644 --- a/ios/RocketChat Watch App/Database/Database.swift +++ b/ios/RocketChat Watch App/Database/Database.swift @@ -74,6 +74,8 @@ final class DefaultDatabase: ServersDatabase { server.iconURL = updatedServer.iconURL server.useRealName = updatedServer.useRealName server.loggedUser = user(from: updatedServer.loggedUser) + server.certificate = updatedServer.clientSSL?.certificate + server.password = updatedServer.clientSSL?.password } else { Server( context: viewContext, @@ -81,7 +83,9 @@ final class DefaultDatabase: ServersDatabase { name: updatedServer.name, url: updatedServer.url, useRealName: updatedServer.useRealName, - loggedUser: user(from: updatedServer.loggedUser) + loggedUser: user(from: updatedServer.loggedUser), + certificate: updatedServer.clientSSL?.certificate, + password: updatedServer.clientSSL?.password ) } diff --git a/ios/RocketChat Watch App/Database/Default.xcdatamodeld/Default.xcdatamodel/contents b/ios/RocketChat Watch App/Database/Default.xcdatamodeld/Default.xcdatamodel/contents index 5e10ac99e..1c673558d 100644 --- a/ios/RocketChat Watch App/Database/Default.xcdatamodeld/Default.xcdatamodel/contents +++ b/ios/RocketChat Watch App/Database/Default.xcdatamodeld/Default.xcdatamodel/contents @@ -8,8 +8,10 @@ + + diff --git a/ios/RocketChat Watch App/Database/Models/Server.swift b/ios/RocketChat Watch App/Database/Models/Server.swift index d15f518ce..3295d334e 100644 --- a/ios/RocketChat Watch App/Database/Models/Server.swift +++ b/ios/RocketChat Watch App/Database/Models/Server.swift @@ -13,6 +13,8 @@ public final class Server: NSManagedObject { @NSManaged public var url: URL @NSManaged public var useRealName: Bool @NSManaged public var loggedUser: LoggedUser + @NSManaged public var certificate: Data? + @NSManaged public var password: String? @available(*, unavailable) init() { @@ -36,7 +38,9 @@ public final class Server: NSManagedObject { updatedSince: Date? = nil, url: URL, useRealName: Bool, - loggedUser: LoggedUser + loggedUser: LoggedUser, + certificate: Data? = nil, + password: String? = nil ) { let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)! super.init(entity: entity, insertInto: context) @@ -46,6 +50,8 @@ public final class Server: NSManagedObject { self.url = url self.useRealName = useRealName self.loggedUser = loggedUser + self.certificate = certificate + self.password = password } } diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 13f27e1dc..683884fb3 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -112,6 +112,9 @@ 1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71682B59B6E100477BA2 /* MessageSender.swift */; }; 1E9A716F2B59CBCA00477BA2 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */; }; 1E9A71712B59CC1300477BA2 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71702B59CC1300477BA2 /* Attachment.swift */; }; + 1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */; }; + 1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */; }; + 1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */; }; 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB375882B55DBFB00AEC3D7 /* Server.swift */; }; 1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; }; 1EC6ACB722CB9FC300A41C61 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6ACB522CB9FC300A41C61 /* MainInterface.storyboard */; }; @@ -398,6 +401,8 @@ 1E9A71682B59B6E100477BA2 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = ""; }; 1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; 1E9A71702B59CC1300477BA2 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSSL.swift; sourceTree = ""; }; + 1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionCertificateHandling.swift; sourceTree = ""; }; 1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; 1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 1EC6ACB022CB9FC300A41C61 /* ShareRocketChatRN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareRocketChatRN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -593,6 +598,7 @@ 1E29A2E92B585B070093C03C /* FailableDecodable.swift */, 1E29A2EA2B585B070093C03C /* HTTP */, 1E29A2EE2B585B070093C03C /* RocketChatError.swift */, + 1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */, ); path = Client; sourceTree = ""; @@ -699,6 +705,7 @@ 1E470E822513A71E00E3DD1D /* RocketChat.swift */, 1EB8EF712510F1EE00F352B7 /* Storage.swift */, 1ED038A82B5090AD00C007D4 /* MMKV.swift */, + 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */, ); path = RocketChat; sourceTree = ""; @@ -1797,6 +1804,7 @@ 1E76CBD225152C730067298C /* Data+Extensions.swift in Sources */, 1E76CBD125152C710067298C /* Date+Extensions.swift in Sources */, 1E76CBD425152C790067298C /* Database.swift in Sources */, + 1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */, 1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, 1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1E76CBC325152A460067298C /* String+Extensions.swift in Sources */, @@ -1870,6 +1878,7 @@ 1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */, 1E29A3052B585B070093C03C /* Request.swift in Sources */, 1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */, + 1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */, 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */, 1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */, 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */, @@ -1955,6 +1964,7 @@ 7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */, 7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */, 7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */, + 1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */, 1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, 1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */, diff --git a/ios/Shared/RocketChat/ClientSSL.swift b/ios/Shared/RocketChat/ClientSSL.swift new file mode 100644 index 000000000..1009f8082 --- /dev/null +++ b/ios/Shared/RocketChat/ClientSSL.swift @@ -0,0 +1,28 @@ +import Foundation + +struct ClientSSL: Codable { + let path: String + let password: String +} + +extension MMKV { + func clientSSL(for url: URL) -> ClientSSL? { + guard let host = url.host else { + return nil + } + + guard let rawClientSSL = string(forKey: host) else { + return nil + } + + guard let data = rawClientSSL.data(using: .utf8) else { + return nil + } + + guard let clientSSL = try? JSONDecoder().decode(ClientSSL.self, from: data) else { + return nil + } + + return clientSSL + } +} diff --git a/ios/Shared/RocketChat/MMKV.swift b/ios/Shared/RocketChat/MMKV.swift index 05a8cafeb..057e9113f 100644 --- a/ios/Shared/RocketChat/MMKV.swift +++ b/ios/Shared/RocketChat/MMKV.swift @@ -1,40 +1,40 @@ import Foundation extension MMKV { - static func build() -> MMKV { - let password = SecureStorage().getSecureKey("com.MMKV.default".toHex()) - let groupDir = FileManager.default.groupDir() - - MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none) - - guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else { - fatalError("Could not initialize MMKV instance.") - } - - return mmkv - } - - func userToken(for userId: String) -> String { - guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else { - fatalError("userToken is nil for userId \(userId)") - } - - return userToken - } - - func userId(for server: String) -> String { - guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else { - fatalError("userId is nil for server \(server)") - } - - return userId - } - - func privateKey(for server: String) -> String { - guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else { - fatalError("privateKey is nil for server \(server)") - } - - return privateKey - } + static func build() -> MMKV { + let password = SecureStorage().getSecureKey("com.MMKV.default".toHex()) + let groupDir = FileManager.default.groupDir() + + MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none) + + guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else { + fatalError("Could not initialize MMKV instance.") + } + + return mmkv + } + + func userToken(for userId: String) -> String { + guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else { + fatalError("userToken is nil for userId \(userId)") + } + + return userToken + } + + func userId(for server: String) -> String { + guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else { + fatalError("userId is nil for server \(server)") + } + + return userId + } + + func privateKey(for server: String) -> String { + guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else { + fatalError("privateKey is nil for server \(server)") + } + + return privateKey + } } diff --git a/ios/Watch/WatchConnection.swift b/ios/Watch/WatchConnection.swift index 6d747852b..4a95cc58b 100644 --- a/ios/Watch/WatchConnection.swift +++ b/ios/Watch/WatchConnection.swift @@ -4,79 +4,100 @@ import WatchConnectivity @objc final class WatchConnection: NSObject { - private let database = WatermelonDB.Database(name: "default") - private let mmkv = MMKV.build() - private let session: WCSession - - @objc init(session: WCSession) { - self.session = session - super.init() - - if WCSession.isSupported() { - session.delegate = self - session.activate() - } - } - - private func getMessage() -> WatchMessage { - let serversQuery = database.query(raw: "select * from servers") as [DBServer] - - let servers = serversQuery.compactMap { item -> WatchMessage.Server? in - let userId = mmkv.userId(for: item.identifier) - let userToken = mmkv.userToken(for: userId) - - let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser] - - guard let user = usersQuery.first else { - return nil - } - - return WatchMessage.Server( - url: item.url, - name: item.name, - iconURL: item.iconURL, - useRealName: item.useRealName == 1 ? true : false, - loggedUser: .init( - id: userId, - token: userToken, - name: user.name, - username: user.username - ) - ) - } - - return WatchMessage(servers: servers) - } - - private func encodedMessage() -> [String: Any] { - do { - let data = try JSONEncoder().encode(getMessage()) - - guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { - fatalError("Could not serialize message: \(getMessage())") - } - - return dictionary - } catch { - fatalError("Could not encode message: \(getMessage())") - } - } + private let database = WatermelonDB.Database(name: "default") + private let mmkv = MMKV.build() + private let session: WCSession + + @objc init(session: WCSession) { + self.session = session + super.init() + + if WCSession.isSupported() { + session.delegate = self + session.activate() + } + } + + private func getClientSSL(from clientSSL: ClientSSL?) -> WatchMessage.Server.ClientSSL? { + guard let clientSSL else { + return nil + } + + guard FileManager.default.fileExists(atPath: clientSSL.path) else { + return nil + } + + guard let certificate = NSData(contentsOfFile: clientSSL.path) else { + return nil + } + + return .init( + certificate: Data(referencing: certificate), + password: clientSSL.password + ) + } + + private func getMessage() -> WatchMessage { + let serversQuery = database.query(raw: "select * from servers") as [DBServer] + + let servers = serversQuery.compactMap { item -> WatchMessage.Server? in + let userId = mmkv.userId(for: item.identifier) + let userToken = mmkv.userToken(for: userId) + let clientSSL = mmkv.clientSSL(for: item.url) + + let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser] + + guard let user = usersQuery.first else { + return nil + } + + return WatchMessage.Server( + url: item.url, + name: item.name, + iconURL: item.iconURL, + useRealName: item.useRealName == 1 ? true : false, + loggedUser: .init( + id: userId, + token: userToken, + name: user.name, + username: user.username + ), + clientSSL: getClientSSL(from: clientSSL) + ) + } + + return WatchMessage(servers: servers) + } + + private func encodedMessage() -> [String: Any] { + do { + let data = try JSONEncoder().encode(getMessage()) + + guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { + fatalError("Could not serialize message: \(getMessage())") + } + + return dictionary + } catch { + fatalError("Could not encode message: \(getMessage())") + } + } } extension WatchConnection: WCSessionDelegate { - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { - - } - - func sessionDidBecomeInactive(_ session: WCSession) { - session.activate() - } - - func sessionDidDeactivate(_ session: WCSession) { - session.activate() - } - - func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { - replyHandler(encodedMessage()) - } + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + + } + + func sessionDidBecomeInactive(_ session: WCSession) { + session.activate() + } + + func sessionDidDeactivate(_ session: WCSession) { + session.activate() + } + + func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { + replyHandler(encodedMessage()) + } } diff --git a/ios/Watch/WatchMessage.swift b/ios/Watch/WatchMessage.swift index 534eb606e..34cb07697 100644 --- a/ios/Watch/WatchMessage.swift +++ b/ios/Watch/WatchMessage.swift @@ -1,20 +1,26 @@ import Foundation struct WatchMessage: Codable { - let servers: [Server] - - struct Server: Codable { - let url: URL - let name: String - let iconURL: URL - let useRealName: Bool - let loggedUser: LoggedUser - - struct LoggedUser: Codable { - let id: String - let token: String - let name: String - let username: String - } - } + let servers: [Server] + + struct Server: Codable { + let url: URL + let name: String + let iconURL: URL + let useRealName: Bool + let loggedUser: LoggedUser + let clientSSL: ClientSSL? + + struct LoggedUser: Codable { + let id: String + let token: String + let name: String + let username: String + } + + struct ClientSSL: Codable { + let certificate: Data + let password: String + } + } }