mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 23:18:27 +02:00
* commit '56d9e1f6a55a93dc71149ae429eaa615a98de0d5': (79 commits) finish version++ version++ Translated using Weblate (Hungarian) Translated using Weblate (Italian) Translated using Weblate (Ukrainian) Translated using Weblate (Hungarian) Translated using Weblate (Slovak) Translated using Weblate (Swedish) Translated using Weblate (Indonesian) Translated using Weblate (Albanian) Translated using Weblate (Estonian) Translated using Weblate (Estonian) updated the submodule updated SDK Update the SDK. (#7819) Prepare for new sprint finish version++ version++ fix Changelog. ... # Conflicts: # Config/AppVersion.xcconfig # Podfile # Podfile.lock # Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved # Riot/Managers/Settings/RiotSettings.swift # Riot/Modules/Analytics/Analytics.swift # Riot/Modules/Analytics/DecryptionFailure.swift # Riot/Modules/Analytics/PHGPostHogConfiguration.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewAction.swift # Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift # Riot/Modules/Room/Views/Title/Preview/PreviewRoomTitleView.m # Riot/Modules/Settings/SettingsViewController.m # Riot/Utils/EventFormatter.m # Riot/Utils/Tools.m # RiotNSE/target.yml # fastlane/Fastfile # project.yml
466 lines
18 KiB
Swift
466 lines
18 KiB
Swift
//
|
|
// Copyright 2022 New Vector Ltd
|
|
//
|
|
// 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 AVFoundation
|
|
import Combine
|
|
import Foundation
|
|
import MatrixSDK
|
|
import SwiftUI
|
|
import ZXingObjC
|
|
|
|
// MARK: - QRLoginService
|
|
|
|
// n.b MSC3886/MSC3903/MSC3906 that this is based on are now closed.
|
|
// However, we want to keep this implementation around for some time.
|
|
// TODO: define an end-of-life date for this implementation.
|
|
class QRLoginService: NSObject, QRLoginServiceProtocol {
|
|
private let client: AuthenticationRestClient
|
|
private let sessionCreator: SessionCreatorProtocol
|
|
private var isCameraReady = false
|
|
private lazy var zxCapture = ZXCapture()
|
|
|
|
private let cameraAccessManager = CameraAccessManager()
|
|
|
|
private var rendezvousService: RendezvousService?
|
|
|
|
init(client: AuthenticationRestClient,
|
|
mode: QRLoginServiceMode,
|
|
state: QRLoginServiceState = .initial) {
|
|
self.client = client
|
|
sessionCreator = SessionCreator()
|
|
self.mode = mode
|
|
self.state = state
|
|
super.init()
|
|
}
|
|
|
|
// MARK: QRLoginServiceProtocol
|
|
|
|
let mode: QRLoginServiceMode
|
|
|
|
var state: QRLoginServiceState {
|
|
didSet {
|
|
if state != oldValue {
|
|
callbacks.send(.didUpdateState)
|
|
}
|
|
}
|
|
}
|
|
|
|
let callbacks = PassthroughSubject<QRLoginServiceCallback, Never>()
|
|
|
|
func isServiceAvailable() async throws -> Bool {
|
|
switch mode {
|
|
case .authenticated:
|
|
guard BuildSettings.qrLoginEnabledFromAuthenticated else {
|
|
return false
|
|
}
|
|
case .notAuthenticated:
|
|
guard BuildSettings.qrLoginEnabledFromNotAuthenticated else {
|
|
return false
|
|
}
|
|
}
|
|
return try await client.supportedMatrixVersions().supportsQRLogin
|
|
}
|
|
|
|
func canDisplayQR() -> Bool {
|
|
BuildSettings.qrLoginEnableDisplayingQRs
|
|
}
|
|
|
|
func generateQRCode() async throws -> QRLoginCode {
|
|
fatalError("Not implemented")
|
|
}
|
|
|
|
func scannerView() -> AnyView {
|
|
let frame = UIScreen.main.bounds
|
|
let view = UIView(frame: frame)
|
|
zxCapture.layer.frame = frame
|
|
view.layer.addSublayer(zxCapture.layer)
|
|
return AnyView(ViewWrapper(view: view))
|
|
}
|
|
|
|
func startScanning() {
|
|
Task { @MainActor in
|
|
if cameraAccessManager.isCameraAvailable {
|
|
let granted = await cameraAccessManager.requestCameraAccessIfNeeded()
|
|
if granted {
|
|
state = .scanningQR
|
|
zxCapture.delegate = self
|
|
zxCapture.camera = zxCapture.back()
|
|
zxCapture.start()
|
|
} else {
|
|
state = .failed(error: .noCameraAccess)
|
|
}
|
|
} else {
|
|
state = .failed(error: .noCameraAvailable)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopScanning(destroy: Bool) {
|
|
if (zxCapture.delegate != nil) {
|
|
// Setting the zxCapture to nil without checking makes it start
|
|
// scanning and implicitly requesting camera access
|
|
zxCapture.delegate = nil
|
|
}
|
|
|
|
guard zxCapture.running else {
|
|
return
|
|
}
|
|
|
|
if destroy {
|
|
zxCapture.hard_stop()
|
|
} else {
|
|
zxCapture.stop()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
func processScannedQR(_ data: Data) {
|
|
guard let code = try? JSONDecoder().decode(QRLoginCode.self, from: data) else {
|
|
state = .failed(error: .invalidQR)
|
|
return
|
|
}
|
|
|
|
Task {
|
|
await processQRLoginCode(code)
|
|
}
|
|
}
|
|
|
|
func confirmCode() {
|
|
switch state {
|
|
case .waitingForConfirmation:
|
|
// TODO: implement
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
|
|
func restart() {
|
|
state = .initial
|
|
|
|
Task {
|
|
await declineRendezvous()
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
stopScanning(destroy: false)
|
|
state = .initial
|
|
|
|
Task {
|
|
await declineRendezvous()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
stopScanning(destroy: true)
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
@MainActor
|
|
private func processQRLoginCode(_ code: QRLoginCode) async {
|
|
MXLog.debug("[QRLoginService] processQRLoginCode: \(code)")
|
|
|
|
// we check these first so that we can show a more specific error message
|
|
guard code.rendezvous.transport?.type == "org.matrix.msc3886.http.v1",
|
|
let algorithm = RendezvousChannelAlgorithm(rawValue: code.rendezvous.algorithm) else {
|
|
MXLog.error("[QRLoginService] Unsupported algorithm or transport")
|
|
state = .failed(error: .deviceNotSupported)
|
|
return
|
|
}
|
|
|
|
guard let flow = code.flow != nil ? RendezvousFlow(rawValue: code.flow!) : .SETUP_ADDITIONAL_DEVICE_V1 else {
|
|
MXLog.error("[QRLoginService] Unsupported flow")
|
|
state = .failed(error: .deviceNotSupported)
|
|
return
|
|
}
|
|
|
|
// so, this is of an expected algorithm so any bad data can be considered an invalid QR code
|
|
guard code.intent == QRLoginRendezvousPayload.Intent.loginReciprocate.rawValue,
|
|
let uri = code.rendezvous.transport?.uri,
|
|
let rendezvousURL = URL(string: uri),
|
|
let key = code.rendezvous.key else {
|
|
MXLog.error("[QRLoginService] QR code invalid")
|
|
state = .failed(error: .invalidQR)
|
|
return
|
|
}
|
|
|
|
state = .connectingToDevice
|
|
|
|
let transport = RendezvousTransport(baseURL: BuildSettings.rendezvousServerBaseURL,
|
|
rendezvousURL: rendezvousURL)
|
|
let rendezvousService = RendezvousService(transport: transport, algorithm: algorithm)
|
|
self.rendezvousService = rendezvousService
|
|
|
|
MXLog.debug("[QRLoginService] Joining the rendezvous at \(rendezvousURL)")
|
|
guard case .success(let validationCode) = await rendezvousService.joinRendezvous(withPublicKey: key) else {
|
|
MXLog.error("[QRLoginService] Failed joining rendezvous")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
state = .waitingForConfirmation(validationCode)
|
|
|
|
MXLog.debug("[QRLoginService] Waiting for available protocols")
|
|
guard case let .success(data) = await rendezvousService.receive(),
|
|
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data) else {
|
|
MXLog.error("[QRLoginService] Failed receiving available protocols")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Received available protocols \(responsePayload)")
|
|
guard let protocols = responsePayload.protocols,
|
|
protocols.contains(.loginToken) else {
|
|
MXLog.error("[QRLoginService] Unexpected protocols, cannot continue")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Request login with `login_token`")
|
|
let protocolPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
|
? QRLoginRendezvousPayload(type: .loginProgress, protocol: .loginToken)
|
|
: QRLoginRendezvousPayload(type: .loginProtocol, protocol: .loginToken)
|
|
guard let requestData = try? JSONEncoder().encode(protocolPayload),
|
|
case .success = await rendezvousService.send(data: requestData) else {
|
|
MXLog.error("[QRLoginService] Failed sending continue with `login_token` request")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Waiting for the login token")
|
|
// bwi: #6018 add error handling for request timeout and provide more information about the error to the user
|
|
let result = await rendezvousService.receive()
|
|
guard case let .success(data) = result,
|
|
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data),
|
|
let login_token = responsePayload.loginToken,
|
|
let homeserver = responsePayload.homeserver,
|
|
let homeserverURL = URL(string: homeserver) else {
|
|
// Handle possible errors
|
|
MXLog.error("[QRLoginService] Invalid login details")
|
|
guard case let .failure(rendezvousError) = result else {
|
|
// Unkown error, display general error information
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
switch rendezvousError {
|
|
case .transportError(let transportError):
|
|
// Check for timeout
|
|
if transportError == .rendezvousCancelled {
|
|
await teardownRendezvous(state: .failed(error: .requestTimedOut))
|
|
} else {
|
|
// Display general error information for all other errors
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
}
|
|
break
|
|
default:
|
|
// Display general error information for all other errors
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
break
|
|
}
|
|
return
|
|
}
|
|
MXLog.debug("[QRLoginService] Received login token \(responsePayload)")
|
|
|
|
state = .waitingForRemoteSignIn
|
|
|
|
// Use a custom rest client linked to the existing device's homeserver
|
|
let authenticationRestClient = MXRestClient(homeServer: homeserverURL, unrecognizedCertificateHandler: nil)
|
|
|
|
MXLog.debug("[QRLoginService] Logging in with the login token")
|
|
guard let credentials = try? await authenticationRestClient.login(parameters: LoginTokenParameters(token: login_token)) else {
|
|
MXLog.error("[QRLoginService] Failed logging in with the login token")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Got acess token")
|
|
|
|
let session = await createSession(credentials: credentials, client: client)
|
|
|
|
MXLog.debug("[QRLoginService] Session created, sending device details")
|
|
let successPayload = flow == .SETUP_ADDITIONAL_DEVICE_V1
|
|
? QRLoginRendezvousPayload(type: .loginProgress, outcome: .success, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
|
: QRLoginRendezvousPayload(type: .loginSuccess, deviceId: session.myDeviceId, deviceKey: session.crypto.deviceEd25519Key)
|
|
|
|
guard let requestData = try? JSONEncoder().encode(successPayload),
|
|
case .success = await rendezvousService.send(data: requestData) else {
|
|
MXLog.error("[QRLoginService] Failed sending session details")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
// explicitly download keys for ourself rather than racing with initial sync which might not complete in time
|
|
MXLog.debug("[QRLoginService] Downloading device list for self")
|
|
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
session.crypto.downloadKeys([session.myUserId], forceDownload: false) { _, _ in
|
|
MXLog.debug("[QRLoginService] Device list downloaded for self")
|
|
continuation.resume(returning: ())
|
|
} failure: { _ in
|
|
MXLog.error("[QRLoginService] Failed to download the device list for self")
|
|
continuation.resume(returning: ())
|
|
}
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Wait for cross-signing details")
|
|
guard case let .success(data) = await rendezvousService.receive(),
|
|
let responsePayload = try? JSONDecoder().decode(QRLoginRendezvousPayload.self, from: data),
|
|
flow == .SETUP_ADDITIONAL_DEVICE_V1 && responsePayload.outcome == .verified || responsePayload.type == .loginVerified,
|
|
let verifiyingDeviceId = responsePayload.verifyingDeviceId,
|
|
let verifyingDeviceKey = responsePayload.verifyingDeviceKey else {
|
|
MXLog.error("[QRLoginService] Received invalid cross-signing details")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Received cross-signing details \(responsePayload)")
|
|
|
|
if let masterKeyFromVerifyingDevice = responsePayload.masterKey,
|
|
let localMasterKey = session.crypto.crossSigning.crossSigningKeys(forUser: session.myUserId)?.masterKeys?.keys {
|
|
guard masterKeyFromVerifyingDevice == localMasterKey else {
|
|
MXLog.error("[QRLoginService] Received invalid master key from verifying device")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Marking the received master key as trusted")
|
|
let mskVerificationResult = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
|
|
session.crypto.setUserVerification(true, forUser: session.myUserId) {
|
|
MXLog.debug("[QRLoginService] Successfully marked the received master key as trusted")
|
|
continuation.resume(returning: true)
|
|
} failure: { error in
|
|
continuation.resume(returning: false)
|
|
}
|
|
}
|
|
|
|
guard mskVerificationResult == true else {
|
|
MXLog.error("[QRLoginService] Failed marking the master key as trusted")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
}
|
|
|
|
guard let verifyingDeviceInfo = session.crypto.device(withDeviceId: verifiyingDeviceId, ofUser: session.myUserId),
|
|
verifyingDeviceInfo.fingerprint == verifyingDeviceKey else {
|
|
MXLog.error("[QRLoginService] Received invalid verifying device info")
|
|
await teardownRendezvous(state: .failed(error: .rendezvousFailed))
|
|
return
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Locally marking the existing device as verified \(verifyingDeviceInfo)")
|
|
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
session.crypto.setDeviceVerification(.verified, forDevice: verifiyingDeviceId, ofUser: session.myUserId) {
|
|
MXLog.debug("[QRLoginService] Marked the existing device as verified")
|
|
continuation.resume(returning: ())
|
|
} failure: { _ in
|
|
MXLog.error("[QRLoginService] Failed marking the existing device as verified")
|
|
continuation.resume(returning: ())
|
|
}
|
|
}
|
|
|
|
MXLog.debug("[QRLoginService] Login flow finished, returning session")
|
|
state = .completed(session: session, securityCompleted: true)
|
|
}
|
|
|
|
private func createSession(credentials: MXCredentials, client: AuthenticationRestClient) async -> MXSession {
|
|
let session = await sessionCreator.createSession(credentials: credentials, client: client, removeOtherAccounts: false)
|
|
|
|
if session.state == .storeDataReady {
|
|
return session
|
|
}
|
|
|
|
await withCheckedContinuation { continuation in
|
|
NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: session, queue: nil) { notification in
|
|
guard let session = notification.object as? MXSession else {
|
|
fatalError()
|
|
}
|
|
|
|
if session.state == .storeDataReady {
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
return session
|
|
}
|
|
|
|
private func declineRendezvous() async {
|
|
guard let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .declined)) else {
|
|
return
|
|
}
|
|
|
|
_ = await rendezvousService?.send(data: requestData)
|
|
|
|
await teardownRendezvous()
|
|
}
|
|
|
|
@MainActor
|
|
private func teardownRendezvous(state: QRLoginServiceState? = nil) async {
|
|
// Stop listening for changes, try deleting the resource
|
|
_ = await rendezvousService?.tearDown()
|
|
|
|
// Try setting the new state, if necessary
|
|
if let state = state {
|
|
switch self.state {
|
|
case .completed:
|
|
return
|
|
case .initial:
|
|
return
|
|
default:
|
|
self.state = state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ZXCaptureDelegate
|
|
|
|
extension QRLoginService: ZXCaptureDelegate {
|
|
func captureCameraIsReady(_ capture: ZXCapture!) {
|
|
isCameraReady = true
|
|
}
|
|
|
|
func captureResult(_ capture: ZXCapture!, result: ZXResult!) {
|
|
guard isCameraReady,
|
|
let result = result,
|
|
result.barcodeFormat == kBarcodeFormatQRCode else {
|
|
return
|
|
}
|
|
|
|
stopScanning(destroy: false)
|
|
|
|
if let bytes = result.resultMetadata.object(forKey: kResultMetadataTypeByteSegments.rawValue) as? NSArray,
|
|
let byteArray = bytes.firstObject as? ZXByteArray {
|
|
let data = Data(bytes: UnsafeRawPointer(byteArray.array), count: Int(byteArray.length))
|
|
|
|
callbacks.send(.didScanQR(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ViewWrapper
|
|
|
|
private struct ViewWrapper: UIViewRepresentable {
|
|
var view: UIView
|
|
|
|
func makeUIView(context: Context) -> some UIView {
|
|
view
|
|
}
|
|
|
|
func updateUIView(_ uiView: UIViewType, context: Context) { }
|
|
}
|