diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 025415be39..c3c4a890f7 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -241,6 +241,7 @@ "authentication_qr_login_failure_invalid_qr" = "QR code is invalid."; "authentication_qr_login_failure_request_denied" = "The request was denied on the other device."; "authentication_qr_login_failure_request_timed_out" = "The linking wasn’t completed in the required time."; +"authentication_qr_login_failure_e2ee_security_error" = "A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s);"; "authentication_qr_login_failure_retry" = "Try again"; // MARK: Password Validation diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 7671c5c732..adee5b0c75 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -771,6 +771,10 @@ public class VectorL10n: NSObject { public static var authenticationQrLoginDisplayTitle: String { return VectorL10n.tr("Vector", "authentication_qr_login_display_title") } + /// A security issue was encountered setting up secure messaging. One of the following may be compromised: Your homeserver; Your internet connection(s); Your device(s); + public static var authenticationQrLoginFailureE2eeSecurityError: String { + return VectorL10n.tr("Vector", "authentication_qr_login_failure_e2ee_security_error") + } /// QR code is invalid. public static var authenticationQrLoginFailureInvalidQr: String { return VectorL10n.tr("Vector", "authentication_qr_login_failure_invalid_qr") diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift index b6bae67577..d7b82b6a15 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Models/QRLoginCode.swift @@ -91,6 +91,7 @@ struct QRLoginRendezvousPayload: Codable { case success case declined case verified + case e2eeSecurityError = "e2ee_security_error" } // swiftformat:disable:next redundantBackticks diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift index 0dc3f78d57..17467666af 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/MatrixSDK/QRLoginService.swift @@ -289,13 +289,34 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { return } + // check that device key from verifier matches the one received from the homeserver + guard let verifyingDeviceInfo = session.crypto.device(withDeviceId: verifiyingDeviceId, ofUser: session.myUserId), + verifyingDeviceInfo.fingerprint == verifyingDeviceKey else { + MXLog.error("[QRLoginService] Received invalid verifying device info") + + // try informing the other party of a potential E2EE issue + if let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .e2eeSecurityError)) { + _ = await rendezvousService.send(data: requestData) + } + + await teardownRendezvous(state: .failed(error: .e2eeSecurityError)) + return + } + MXLog.debug("[QRLoginService] Received cross-signing details \(responsePayload)") if let masterKeyFromVerifyingDevice = responsePayload.masterKey, let localMasterKey = session.crypto.crossSigningKeys(forUser: session.myUserId).masterKeys?.keys { + // if master key was received from verifier then check that it matches the one from the homeserver guard masterKeyFromVerifyingDevice == localMasterKey else { MXLog.error("[QRLoginService] Received invalid master key from verifying device") - await teardownRendezvous(state: .failed(error: .rendezvousFailed)) + + // try informing the other party of a potential E2EE issue + if let requestData = try? JSONEncoder().encode(QRLoginRendezvousPayload(type: .loginFinish, outcome: .e2eeSecurityError)) { + _ = await rendezvousService.send(data: requestData) + } + + await teardownRendezvous(state: .failed(error: .e2eeSecurityError)) return } @@ -311,18 +332,13 @@ class QRLoginService: NSObject, QRLoginServiceProtocol { guard mskVerificationResult == true else { MXLog.error("[QRLoginService] Failed marking the master key as trusted") - await teardownRendezvous(state: .failed(error: .rendezvousFailed)) + await teardownRendezvous(state: .failed(error: .e2eeSecurityError)) 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 - } - + + // we only mark the verifying device as trusted if the device key matches and the master key matches (or the + // master key was not sent) MXLog.debug("[QRLoginService] Locally marking the existing device as verified \(verifyingDeviceInfo)") await withCheckedContinuation { (continuation: CheckedContinuation) in session.crypto.setDeviceVerification(.verified, forDevice: verifiyingDeviceId, ofUser: session.myUserId) { diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift index 823a4983c2..b7c4968be1 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Common/Service/QRLoginServiceProtocol.swift @@ -34,6 +34,7 @@ enum QRLoginServiceError: Error, Equatable { case requestDenied case requestTimedOut case rendezvousFailed + case e2eeSecurityError } // MARK: - QRLoginServiceState diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Confirm/View/AuthenticationQRLoginConfirmScreen.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Confirm/View/AuthenticationQRLoginConfirmScreen.swift index b0ddb09067..37d00bdf55 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Confirm/View/AuthenticationQRLoginConfirmScreen.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Confirm/View/AuthenticationQRLoginConfirmScreen.swift @@ -84,16 +84,17 @@ struct AuthenticationQRLoginConfirmScreen: View { /// The screen's footer. var footerContent: some View { VStack(spacing: 16) { - Text(VectorL10n.authenticationQrLoginConfirmAlert) - .padding(10) - .multilineTextAlignment(.center) - .font(theme.fonts.body) - .foregroundColor(theme.colors.alert) - .shapedBorder(color: theme.colors.alert, borderWidth: 1, shape: RoundedRectangle(cornerRadius: 8)) - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 12) - .accessibilityIdentifier("alertText") - +// These are only applicable to reciprocating a login via QR which isn't yet implemented: +// +// Text(VectorL10n.authenticationQrLoginConfirmAlert) +// .padding(10) +// .multilineTextAlignment(.center) +// .font(theme.fonts.body) +// .foregroundColor(theme.colors.alert) +// .shapedBorder(color: theme.colors.alert, borderWidth: 1, shape: RoundedRectangle(cornerRadius: 8)) +// .fixedSize(horizontal: false, vertical: true) +// .padding(.bottom, 12) +// .accessibilityIdentifier("alertText") // Button(action: confirm) { // Text(VectorL10n.confirm) // } diff --git a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift index 0e363e5492..f3c3e7113c 100644 --- a/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift +++ b/RiotSwiftUI/Modules/Authentication/QRLogin/Failure/AuthenticationQRLoginFailureViewModel.swift @@ -61,6 +61,9 @@ class AuthenticationQRLoginFailureViewModel: AuthenticationQRLoginFailureViewMod case .requestTimedOut: self.state.failureText = VectorL10n.authenticationQrLoginFailureRequestTimedOut self.state.retryButtonVisible = true + case .e2eeSecurityError: + self.state.failureText = VectorL10n.authenticationQrLoginFailureE2eeSecurityError + self.state.retryButtonVisible = true default: break }