mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 15:09:31 +02:00
feat: add server selection protection with jwt (MESSENGER-6162)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -43,5 +43,6 @@ extension BWIBuildSettings {
|
||||
itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351"
|
||||
avoidServerSelectionOnAppConfig = true
|
||||
enableFeatureWYSIWYGByDefault = true
|
||||
bwiEnableTokenizedLoginProtection = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ extension BWIBuildSettings {
|
||||
itunesAppLink = "https://apps.apple.com/de/app/bundesmessenger/id1616866351"
|
||||
avoidServerSelectionOnAppConfig = true
|
||||
enableFeatureWYSIWYGByDefault = true
|
||||
bwiEnableTokenizedLoginProtection = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,8 @@ targets:
|
||||
- package: WysiwygComposer
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
||||
- package: SwiftJWT
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
Release: Release.xcconfig
|
||||
|
||||
@@ -36,7 +36,8 @@ targets:
|
||||
- package: WysiwygComposer
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
|
||||
- package: SwiftJWT
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
Release: Release.xcconfig
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ targets:
|
||||
- package: AnalyticsEvents
|
||||
- package: DeviceKit
|
||||
- package: DTCoreText
|
||||
- package: SwiftJWT
|
||||
|
||||
configFiles:
|
||||
Debug: Debug.xcconfig
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
145
bwi/TokenVerification/TokenVerificator.swift
Normal file
145
bwi/TokenVerification/TokenVerificator.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
9
publickeys/jwt-test.key.pub
Normal file
9
publickeys/jwt-test.key.pub
Normal 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-----
|
||||
9
publickeys/jwt-test2.key.pub
Normal file
9
publickeys/jwt-test2.key.pub
Normal 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-----
|
||||
Reference in New Issue
Block a user