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

This commit is contained in:
Frank Rotermund
2024-07-24 15:54:45 +02:00
parent 579e7ddd3c
commit bb60e8f85d
14 changed files with 219 additions and 31 deletions

View File

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

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

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

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,4 @@ targets:
- path: Assets/de.lproj/Localizable.strings
- path: Assets/de.lproj/Vector.strings
- path: Assets/de.lproj/Bwi.strings
- path: ../publickeys

View File

@@ -35,6 +35,7 @@ targets:
- package: AnalyticsEvents
- package: DeviceKit
- package: DTCoreText
- package: SwiftJWT
configFiles:
Debug: Debug.xcconfig

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

View File

@@ -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<MyClaims>.verify(signedJWT, using: jwtVerifier)
let newJWT = try JWT<MyClaims>(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<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 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
}
}

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