From bb60e8f85db3dff912738f5bee228e8e60fd7541 Mon Sep 17 00:00:00 2001 From: Frank Rotermund Date: Wed, 24 Jul 2024 15:54:45 +0200 Subject: [PATCH] feat: add server selection protection with jwt (MESSENGER-6162) --- Config/BWIBuildSettings.swift | 4 +- .../BuM-Beta/BWIBuildSettings+BuM-Beta.swift | 1 + Config/BuM/BWIBuildSettings+BuM.swift | 1 + Riot/target-bum-beta.yml | 2 + Riot/target-bum-open.yml | 3 +- Riot/target-bwi-mdm.yml | 3 +- Riot/target-messenger.yml | 4 +- RiotNSE/target.yml | 1 + ...enticationServerSelectionCoordinator.swift | 17 -- .../AuthenticationServerSelectionScreen.swift | 48 ++++-- bwi/TokenVerification/TokenVerificator.swift | 145 ++++++++++++++++++ project.yml | 3 + publickeys/jwt-test.key.pub | 9 ++ publickeys/jwt-test2.key.pub | 9 ++ 14 files changed, 219 insertions(+), 31 deletions(-) create mode 100644 bwi/TokenVerification/TokenVerificator.swift create mode 100644 publickeys/jwt-test.key.pub create mode 100644 publickeys/jwt-test2.key.pub diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index eac6a5d0b..934d8c5e7 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -173,7 +173,7 @@ class BWIBuildSettings: NSObject { var bwiHashes = [ "a3f65e35a7476799afe8d80282fb3c45b39dab06d1d8c70dc98e45ab7d8e93a9", "2fda1a831655c22a5e6096d7cfbff4429fbf27891141e191b46adbf168142a11", "4f8cbb3fef885f7284d0477d797d7007f0e1ba76221834132752f4d645796e28", - "24c2ec541e61e8e68944b96dc45ed5df12f6bdbda283cb0b3a522742aa970256", + /* "24c2ec541e61e8e68944b96dc45ed5df12f6bdbda283cb0b3a522742aa970256", remove internal test server that is handled by token verification*/ "1be0b314a6c915d4475290522baef5b642db1b6d68937792b8e0eb5b7b0d6666", "3deb73db8cafcd1d5a59e25e251c35816162e1f6ee67b5d7d011da0e8d6ef931", "42e57985d61202c2c7dd87d898cef9bdce020877a4c7a8c7cd699f6a28f58c0c", @@ -225,6 +225,8 @@ class BWIBuildSettings: NSObject { "c58c1892ba63b2a482a2ad72d563d523eff08759e6026b8630d64d41b48e7ae0", "db0c9012e0886da4cbbaf4fae3d4c8d345a95fcc004c0fa8132b5f718963750d" ] + // bwi #6162 login protection with jwt tokens + var bwiEnableTokenizedLoginProtection = false // use a different badge color if the user was mentioned in a room var showMentionsInRoom = true diff --git a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift index 0382075bd..559dd34fb 100644 --- a/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift +++ b/Config/BuM-Beta/BWIBuildSettings+BuM-Beta.swift @@ -43,5 +43,6 @@ extension BWIBuildSettings { itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351" avoidServerSelectionOnAppConfig = true enableFeatureWYSIWYGByDefault = true + bwiEnableTokenizedLoginProtection = true } } diff --git a/Config/BuM/BWIBuildSettings+BuM.swift b/Config/BuM/BWIBuildSettings+BuM.swift index 96afec0aa..63ce262b0 100644 --- a/Config/BuM/BWIBuildSettings+BuM.swift +++ b/Config/BuM/BWIBuildSettings+BuM.swift @@ -31,6 +31,7 @@ extension BWIBuildSettings { itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351" avoidServerSelectionOnAppConfig = true enableFeatureWYSIWYGByDefault = true + bwiEnableTokenizedLoginProtection = true } } diff --git a/Riot/target-bum-beta.yml b/Riot/target-bum-beta.yml index 205564fb6..066ba790d 100644 --- a/Riot/target-bum-beta.yml +++ b/Riot/target-bum-beta.yml @@ -50,6 +50,7 @@ targets: - package: WysiwygComposer - package: DeviceKit - package: DTCoreText + - package: SwiftJWT configFiles: Debug: Debug.xcconfig @@ -107,3 +108,4 @@ targets: - path: Assets/de.lproj/Localizable.strings - path: Assets/de.lproj/Vector.strings - path: Assets/de.lproj/Bwi.strings + - path: ../publickeys diff --git a/Riot/target-bum-open.yml b/Riot/target-bum-open.yml index 79c6edf97..db8362c76 100644 --- a/Riot/target-bum-open.yml +++ b/Riot/target-bum-open.yml @@ -36,7 +36,8 @@ targets: - package: WysiwygComposer - package: DeviceKit - package: DTCoreText - + - package: SwiftJWT + configFiles: Debug: Debug.xcconfig Release: Release.xcconfig diff --git a/Riot/target-bwi-mdm.yml b/Riot/target-bwi-mdm.yml index 5cd3cf24b..81fb058c0 100644 --- a/Riot/target-bwi-mdm.yml +++ b/Riot/target-bwi-mdm.yml @@ -36,7 +36,8 @@ targets: - package: WysiwygComposer - package: DeviceKit - package: DTCoreText - + - package: SwiftJWT + configFiles: Debug: Debug.xcconfig Release: Release.xcconfig diff --git a/Riot/target-messenger.yml b/Riot/target-messenger.yml index 3ace8c54d..cd78a43b6 100644 --- a/Riot/target-messenger.yml +++ b/Riot/target-messenger.yml @@ -36,7 +36,8 @@ targets: - package: WysiwygComposer - package: DeviceKit - package: DTCoreText - + - package: SwiftJWT + configFiles: Debug: Debug.xcconfig Release: Release.xcconfig @@ -93,3 +94,4 @@ targets: - path: Assets/de.lproj/Localizable.strings - path: Assets/de.lproj/Vector.strings - path: Assets/de.lproj/Bwi.strings + - path: ../publickeys diff --git a/RiotNSE/target.yml b/RiotNSE/target.yml index 18670109d..ae459e641 100644 --- a/RiotNSE/target.yml +++ b/RiotNSE/target.yml @@ -35,6 +35,7 @@ targets: - package: AnalyticsEvents - package: DeviceKit - package: DTCoreText + - package: SwiftJWT configFiles: Debug: Debug.xcconfig diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift index ae6f27428..13308c262 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/Coordinator/AuthenticationServerSelectionCoordinator.swift @@ -122,23 +122,6 @@ final class AuthenticationServerSelectionCoordinator: Coordinator, Presentable { let homeserverAddress = HomeserverAddress.sanitized(homeserverAddress) - if BWIBuildSettings.shared.bwiEnableLoginProtection { - let protectionService = LoginProtectionService() - protectionService.hashes = BWIBuildSettings.shared.bwiHashes - - guard protectionService.isValid(homeserverAddress) else { - stopLoading() - let primaryButtonCompletion: (() -> Void)? = { () in - if let url = URL(string: BWIBuildSettings.shared.bumAdvertizementURLString) { - UIApplication.shared.vc_open(url, completionHandler: nil) - } - } - - authenticationServerSelectionViewModel.displayInfo(BWIL10n.bwiLoginProtectionInfoMessage(AppInfo.current.displayName, AppInfo.current.displayName), buttonTitle: BWIL10n.bwiLoginProtectionInfoButton, completion: primaryButtonCompletion) - return - } - } - Task { do { try await authenticationService.startFlow(parameters.flow, for: homeserverAddress) diff --git a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift index e1cc77e7d..3dd599406 100644 --- a/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/ServerSelection/View/AuthenticationServerSelectionScreen.swift @@ -212,23 +212,51 @@ struct AuthenticationServerSelectionScreen: View { private func submit() { guard !viewModel.viewState.hasValidationError else { return } - if isHomeserverAddressValid(viewModel.homeserverAddress) { - viewModel.send(viewAction: .confirm) - } else { - isInvalidServerAlert = true - showAlert = true + // bwi #6162 homeserver validation is async now, due to server calls for token validation + Task { + let verified = await isHomeserverAddressValid(viewModel.homeserverAddress) + if verified { + viewModel.send(viewAction: .confirm) + } else { + isInvalidServerAlert = true + showAlert = true + } } + } - private func isHomeserverAddressValid(_ homeserverAddress: String) -> Bool { - if BWIBuildSettings.shared.bwiEnableLoginProtection { + private func isHomeserverAddressValid(_ homeserverAddress: String) async -> Bool { + + // bwi #6162 a homeserveraddress is valid when there is either + // a) no homeserver protection (bwm) + // b) tokenized protection and there is a valid token + // c) hashed protection and there is a valid hash (this will be disabled soon) + // d) b) && c) can be combined for now + if !BWIBuildSettings.shared.bwiEnableTokenizedLoginProtection && !BWIBuildSettings.shared.bwiEnableLoginProtection { + return true + } + + var validHomeserver = false + + if BWIBuildSettings.shared.bwiEnableTokenizedLoginProtection { + + let tokenVerificator = ServerTokenVerificator() + + let token = await tokenVerificator.fetchToken(baseURL: homeserverAddress) + + if let token = token { + validHomeserver = tokenVerificator.verifyToken(baseURL: homeserverAddress, token: token) + } + } + + if BWIBuildSettings.shared.bwiEnableLoginProtection && !validHomeserver { let protectionService = LoginProtectionService() protectionService.hashes = BWIBuildSettings.shared.bwiHashes - return protectionService.isValid(homeserverAddress) - } else { - return true + validHomeserver = protectionService.isValid(homeserverAddress) } + + return validHomeserver } /// bwi: jump directly into the iOS settings app to allow camera access diff --git a/bwi/TokenVerification/TokenVerificator.swift b/bwi/TokenVerification/TokenVerificator.swift new file mode 100644 index 000000000..0af2be050 --- /dev/null +++ b/bwi/TokenVerification/TokenVerificator.swift @@ -0,0 +1,145 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/* + let publicKey: Data = publicKeyStr.data(using: .utf8)! + + struct MyClaims: Claims { + let version: Int + let hostname: String + } + + let signedJWT = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJob3N0bmFtZSI6ImJ3aS5kZSJ9.bondtDczLlOHlHLLZj1C9tn60LqBpIhFNaUy6nYL6CVVwWGIv8EIxYWMTx-MP9OSjj-aeMcy0tmDqSz6nbdbgvUJvkB6r-ByH7fTsVj6OtEEs8mWnqHBOFBwTy9tv5vSTfjFX7PBSko2OK3HQrZkFSkfr-xZoOIc_PxblUnec2hClxVq7ImJnIAW1HCh85DMz2c-MiEHd7wQwBcgwWKWmAY9X6uS25WWhQcPH9i0-QMEQNjXGJp-_wM10KJuuOMDx7QdmcX78QgcOyP-G64cA36NL4-6Aby5EnJUDX-uzFbM_ZERgPVmjfzHoZarFCHSK6-fTBg_MQuDF-O2OOdM6Q" + + let jwtVerifier = JWTVerifier.rs256(publicKey: publicKey) + + + + do { + let verified = JWT.verify(signedJWT, using: jwtVerifier) + let newJWT = try JWT(jwtString: signedJWT, verifier: jwtVerifier) + print(newJWT.claims) + } catch let error { + print(error.localizedDescription) + } + */ + +import SwiftJWT + +/* + + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "iat": 1516239022 + */ + +struct ServerTokenClaims: Claims { + let issuer: String + let sub: String + let exp: Int + let iat: Int + let jti: String + let version: Int +} + +// verifies if a selected homeserver is valid to be used with bundesmessenger +struct ServerTokenVerificator { + + // takes a token and the selected server url, returns true if token is valid for at least one public key in the subfolder publickeys. Additionally the token needs to be valid (inside the valid timestamp) and contain a matching homeserver + func verifyToken( baseURL: String, token: String ) -> Bool { + let publicKeys = publicKeys(folder: Bundle.main.resourcePath! + "/publickeys" ) + + let homeServerURL = baseURL.replacingOccurrences(of: "https://", with: "") + + for key in publicKeys { + guard let keyData = key.data(using: .utf8) else { + continue + } + + // only one public key needs to be fine + let jwtVerifier = JWTVerifier.ps512(publicKey: keyData) + do { + let verified = JWT.verify(token, using: jwtVerifier) + let verifiedJWT = try JWT(jwtString: token, verifier: jwtVerifier) + let validated = verifiedJWT.validateClaims() + let matchingHomeServer = verifiedJWT.claims.sub == homeServerURL + if verified && (validated == .success) && matchingHomeServer { + return true + } + } catch let error { + // counts like an unverified Token + } + } + + return true + } + + // fetch the current token from the new endpoint, + func fetchToken( baseURL: String ) async -> String? { + let path = "/_bum/client/v1/verify" + + guard let url = URL(string: baseURL + path) else { + return nil + } + + do { + let config = URLSessionConfiguration.default + config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData + let session : URLSession = URLSession(configuration: config) + + let (data, response) = try await session.data(from: url) + + // the token may have additional endlines + if let str = String(data: data, encoding: .utf8) { + return str.trimmingCharacters(in: .whitespacesAndNewlines) + } + } catch { + return nil + } + + return nil + } + + // reads the current public key folder into a string array of public keys + private func publicKeys(folder: String) -> [String] { + var keys: [String] = [] + + let fm = FileManager.default + + do { + let items = try fm.contentsOfDirectory(atPath: folder) + + for item in items { + let fileUrl = URL(fileURLWithPath: folder + "/" + item) + + do { + let key = try String(contentsOf: fileUrl, encoding: .utf8) + keys.append(key) + } catch { + // invalid files do not count as public keys and are just not inserted into the array + } + } + } catch { + // should not happen but in this case there are no public keys == no verification + } + + return keys + } +} diff --git a/project.yml b/project.yml index 848056d28..7b4390525 100644 --- a/project.yml +++ b/project.yml @@ -74,3 +74,6 @@ packages: DTCoreText: url: https://github.com/Cocoanetics/DTCoreText version: 1.6.26 + SwiftJWT: + url: https://github.com/Kitura/Swift-JWT + version: 4.0.0 diff --git a/publickeys/jwt-test.key.pub b/publickeys/jwt-test.key.pub new file mode 100644 index 000000000..4ff9becd5 --- /dev/null +++ b/publickeys/jwt-test.key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4UILd45kW1+bkNy/Id4k +tPPZRH52JICTpssW8Qe4pdVJvg422iAEhyOgLWbUDKlOrCGJrzTbS59jGf2AzUPj +t1uvCWDUjHXXjqY90SyW1IGQI+R7ANe7CQqdGnA6gKx1eKpfRL1reY+Sc4w6E6Qd +8J4xNmdIX73na1IvEVA+NOm8l4HA3TINsGn+p7yyx5ZghiY0Y3F0uWWxygTehJlp +YGVHejZcXqdTeGSNjTUIvL7zljin47SxUr7HIRnDyEgeihdt5r9FT042Yr+igAHw +4ywixNgPfRc+euJExbJ3TFqPQhf/J/BdKZSqUrq1b0J/Y9RgY6fvzd0LXGcR1SIi +4QIDAQAB +-----END PUBLIC KEY----- diff --git a/publickeys/jwt-test2.key.pub b/publickeys/jwt-test2.key.pub new file mode 100644 index 000000000..f2c288993 --- /dev/null +++ b/publickeys/jwt-test2.key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypUe1MYcVbjaeYQ6EPXD +SSTinbfpdg2pPSQhNZQpjyRqZ06D1nOnN2ZITqO88rHc4UQ0/QGyLnMHEw3HGLPB +UeaBEBCX67Vc4eH6In7HjnKPYbnin8lYdukyZNoFtvtdM/QmUTa6d34H4LJNIEnB +cjfrLOCtK6K1q+a5TugwsgNO9CiHqOasQuFxLfeP7ToAz6q/sbE5gweL4V3mWRqF +HAbEwYSnF6pdx+Z4sRmy0XmHzsJh0IBNfeqasROIMGiABH7cAs7J4Whj+GjXxJgd +Ac7Fpa21i0Trzh1jlQW+MAaCI4ni8HhjlLIURpEq0lerVkpMFXT451AVB9l8Pxp2 +pQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file