Merge branch 'feature/6162_jwt_token' into 'develop'

feat: add server selection protection with jwt (MESSENGER-6162)

See merge request bwmessenger/bundesmessenger/bundesmessenger-ios!391
This commit is contained in:
Frank Rotermund
2024-07-26 04:53:27 +00:00
17 changed files with 210 additions and 57 deletions

View File

@@ -173,8 +173,10 @@ class BWIBuildSettings: NSObject {
var bwiHashes = [ "a3f65e35a7476799afe8d80282fb3c45b39dab06d1d8c70dc98e45ab7d8e93a9",
"2fda1a831655c22a5e6096d7cfbff4429fbf27891141e191b46adbf168142a11",
"4f8cbb3fef885f7284d0477d797d7007f0e1ba76221834132752f4d645796e28",
"24c2ec541e61e8e68944b96dc45ed5df12f6bdbda283cb0b3a522742aa970256",
"1be0b314a6c915d4475290522baef5b642db1b6d68937792b8e0eb5b7b0d6666",
/* "24c2ec541e61e8e68944b96dc45ed5df12f6bdbda283cb0b3a522742aa970256",
"1be0b314a6c915d4475290522baef5b642db1b6d68937792b8e0eb5b7b0d6666",
remove internal test server that is handled by token verification*/
"3deb73db8cafcd1d5a59e25e251c35816162e1f6ee67b5d7d011da0e8d6ef931",
"42e57985d61202c2c7dd87d898cef9bdce020877a4c7a8c7cd699f6a28f58c0c",
"e1c3c7cac12bd65bd48de79a2677187d2e768d2769377627534023588b8d7a33",
@@ -225,6 +227,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

View File

@@ -43,5 +43,6 @@ extension BWIBuildSettings {
itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351"
avoidServerSelectionOnAppConfig = true
enableFeatureWYSIWYGByDefault = true
bwiEnableTokenizedLoginProtection = true
}
}

View File

@@ -31,6 +31,7 @@ extension BWIBuildSettings {
itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351"
avoidServerSelectionOnAppConfig = true
enableFeatureWYSIWYGByDefault = true
bwiEnableTokenizedLoginProtection = true
}
}

View File

@@ -978,17 +978,7 @@
if (sender == _submitButton)
{
if (BWIBuildSettings.shared.bwiEnableLoginProtection) {
LoginProtectionService* service = [[LoginProtectionService alloc] init];
service.hashes = BWIBuildSettings.shared.bwiHashes;
if (![service isValid:self->mxRestClient.homeserver]) {
NSError *error = [[NSError alloc] initWithDomain:@"LoginProtectionError" code:0 userInfo:@{
NSLocalizedDescriptionKey: [BWIL10n bwiLoginProtectionErrorMessage:AppInfo.current.displayName]}];
[self onFailureDuringAuthRequest:error];
return;
}
}
// Disable user interaction to prevent multiple requests
self.userInteractionEnabled = NO;

View File

@@ -50,6 +50,7 @@ targets:
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig
@@ -107,3 +108,5 @@ targets:
- path: Assets/de.lproj/Localizable.strings
- path: Assets/de.lproj/Vector.strings
- path: Assets/de.lproj/Bwi.strings
- path: ../publickeys
type: folder

View File

@@ -36,7 +36,8 @@ targets:
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig
Release: Release.xcconfig

View File

@@ -36,7 +36,8 @@ targets:
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig
Release: Release.xcconfig

View File

@@ -36,7 +36,8 @@ targets:
- package: WysiwygComposer
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig
Release: Release.xcconfig
@@ -93,3 +94,5 @@ targets:
- path: Assets/de.lproj/Localizable.strings
- path: Assets/de.lproj/Vector.strings
- path: Assets/de.lproj/Bwi.strings
- path: ../publickeys
type: folder

View File

@@ -35,6 +35,7 @@ targets:
- package: AnalyticsEvents
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig
@@ -67,6 +68,7 @@ targets:
- path: ../Config/BuM/BWIBuildSettings+BuM.swift
- path: ../bwi/UserAgent/UserAgentService.swift
- path: ../bwi/LoginProtection/LoginProtectionService.swift
- path: ../bwi/TokenVerification/TokenVerificator.swift
- path: ../Config/MDMSettings.swift
- path: ../Riot/Utils/DataProtectionHelper.swift
- path: ../Config/CommonConfiguration.swift

View File

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

View File

@@ -212,23 +212,29 @@ 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 {
if BWIBuildSettings.shared.bwiEnableLoginProtection || BWIBuildSettings.shared.bwiEnableTokenizedLoginProtection {
let protectionService = LoginProtectionService()
protectionService.hashes = BWIBuildSettings.shared.bwiHashes
return protectionService.isValid(homeserverAddress)
} else {
return true
return await protectionService.isValid(homeserverAddress)
}
return true
}
/// bwi: jump directly into the iOS settings app to allow camera access

View File

@@ -76,12 +76,12 @@ extension UserDefaults
}
}
private func checkUrlSavety(_ serverUrl: String) -> Bool {
private func checkUrlSavety(_ serverUrl: String) async -> Bool {
if BWIBuildSettings.shared.bwiEnableLoginProtection {
let protectionService = LoginProtectionService()
protectionService.hashes = BWIBuildSettings.shared.bwiHashes
return protectionService.isValid(serverUrl)
return await protectionService.isValid(serverUrl)
} else {
return true
}
@@ -112,11 +112,13 @@ extension UserDefaults
func registerForAppConfig() {
NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: OperationQueue.main) { [self] (note) in
handleAppConfig()
Task {
await handleAppConfig()
}
}
}
func handleAppConfig() {
func handleAppConfig() async {
if let dict = UserDefaults.standard.dictionary(forKey: configKey) {
// only compute if serverURL has not changed (this may need to be changed on Adminportal integration
if !isSameConfig(dict: dict) {
@@ -125,22 +127,22 @@ extension UserDefaults
if let serverUrl = dict[serverUrlKey] as? String {
if serverUrl.count == 0 {
config.serverUrl = nil
} else if checkUrlSavety(serverUrl) {
} else if await checkUrlSavety(serverUrl) {
config.serverUrl = serverUrl
}
}
if let contentScannerUrl = dict[contentScannerKey] as? String {
if checkUrlSavety(contentScannerUrl) {
if await checkUrlSavety(contentScannerUrl) {
config.contentScannerUrl = contentScannerUrl
}
}
if let pusherUrl = dict[pusherUrlKey] as? String {
if checkUrlSavety(pusherUrl) {
if await checkUrlSavety(pusherUrl) {
config.pusherUrl = pusherUrl
}
}
if let permalinkUrl = dict[permalinkUrlKey] as? String {
if checkUrlSavety(permalinkUrl) {
if await checkUrlSavety(permalinkUrl) {
config.permalinkUrl = permalinkUrl
}
}

View File

@@ -21,15 +21,36 @@ import CryptoKit
@objcMembers class LoginProtectionService : NSObject {
var hashes: [String]?
@objc func isValid(_ urlString: String) -> Bool {
guard let hashes = hashes else {
return false
@objc func isValid(_ 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
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)
}
}
let string = self.normalizeLoginUrl(urlString)
let hashedString = self.hashedString(string)
if BWIBuildSettings.shared.bwiEnableLoginProtection && !validHomeserver {
if let hashes = hashes {
let string = self.normalizeLoginUrl(homeserverAddress)
let hashedString = self.hashedString(string)
validHomeserver = hashes.contains(hashedString)
}
}
return hashes.contains(hashedString)
return validHomeserver
}
private func normalizeLoginUrl(_ urlString: String) -> String {

View File

@@ -0,0 +1,114 @@
//
/*
* 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
import SwiftJWT
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<ServerTokenClaims>.verify(token, using: jwtVerifier)
let verifiedJWT = try JWT<ServerTokenClaims>(jwtString: token, verifier: jwtVerifier)
let validated = verifiedJWT.validateClaims()
let matchingHomeServer = verifiedJWT.claims.sub == homeServerURL
if verified && (validated == .success) && matchingHomeServer {
return true
}
} catch {
// counts like an unverified Token
}
}
// no public key was able to verify the token
return false
}
// 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, _) = 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
}
}

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAypUe1MYcVbjaeYQ6EPXD
SSTinbfpdg2pPSQhNZQpjyRqZ06D1nOnN2ZITqO88rHc4UQ0/QGyLnMHEw3HGLPB
UeaBEBCX67Vc4eH6In7HjnKPYbnin8lYdukyZNoFtvtdM/QmUTa6d34H4LJNIEnB
cjfrLOCtK6K1q+a5TugwsgNO9CiHqOasQuFxLfeP7ToAz6q/sbE5gweL4V3mWRqF
HAbEwYSnF6pdx+Z4sRmy0XmHzsJh0IBNfeqasROIMGiABH7cAs7J4Whj+GjXxJgd
Ac7Fpa21i0Trzh1jlQW+MAaCI4ni8HhjlLIURpEq0lerVkpMFXT451AVB9l8Pxp2
pQIDAQAB
-----END PUBLIC KEY-----