Fixes #2470 - Allow verifiying a session through entering the recovery key

This commit is contained in:
Stefan Ceriu
2024-02-19 10:10:25 +02:00
committed by Stefan Ceriu
parent 5f2ba3a1df
commit 59cfb0ff1a
17 changed files with 73 additions and 32 deletions

View File

@@ -567,6 +567,7 @@
"screen_session_verification_compare_numbers_subtitle" = "Confirm that the numbers below match those shown on your other session.";
"screen_session_verification_compare_numbers_title" = "Compare numbers";
"screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.";
"screen_session_verification_enter_recovery_key" = "Enter recovery key";
"screen_session_verification_open_existing_session_subtitle" = "Prove its you in order to access your encrypted message history.";
"screen_session_verification_open_existing_session_title" = "Open an existing session";
"screen_session_verification_positive_button_canceled" = "Retry verification";

View File

@@ -208,7 +208,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
let verificationParameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
recoveryState: parameters.userSession.sessionSecurityStatePublisher.value.recoveryState)
let coordinator = SessionVerificationScreenCoordinator(parameters: verificationParameters)
coordinator.actions
@@ -216,6 +217,9 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .recoveryKey:
navigationStackCoordinator.setSheetCoordinator(nil)
handleAppRoute(.chatBackupSettings, animated: true)
case .done:
navigationStackCoordinator.setSheetCoordinator(nil)
}

View File

@@ -270,7 +270,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
break
case (.roomList, .showLogoutConfirmationScreen, .logoutConfirmationScreen):
presentSecureBackupConfirmationScreen()
presentSecureBackupLogoutConfirmationScreen()
case (.logoutConfirmationScreen, .dismissedLogoutConfirmationScreen, .roomList):
break
@@ -400,7 +400,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
return
}
presentSecureBackupConfirmationScreen()
presentSecureBackupLogoutConfirmationScreen()
}
// MARK: Session verification
@@ -410,7 +410,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
fatalError("The sessionVerificationController should aways be valid at this point")
}
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController,
recoveryState: userSession.sessionSecurityStatePublisher.value.recoveryState)
let coordinator = SessionVerificationScreenCoordinator(parameters: parameters)
@@ -419,6 +420,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .recoveryKey:
settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true)
case .done:
navigationSplitCoordinator.setSheetCoordinator(nil)
}
@@ -511,7 +514,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Secure backup confirmation
private func presentSecureBackupConfirmationScreen() {
private func presentSecureBackupLogoutConfirmationScreen() {
let coordinator = SecureBackupLogoutConfirmationScreenCoordinator(parameters: .init(secureBackupController: userSession.clientProxy.secureBackupController,
networkMonitor: ServiceLocator.shared.networkMonitor))

View File

@@ -1392,6 +1392,8 @@ public enum L10n {
public static var screenSessionVerificationCompareNumbersTitle: String { return L10n.tr("Localizable", "screen_session_verification_compare_numbers_title") }
/// Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.
public static var screenSessionVerificationCompleteSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_complete_subtitle") }
/// Enter recovery key
public static var screenSessionVerificationEnterRecoveryKey: String { return L10n.tr("Localizable", "screen_session_verification_enter_recovery_key") }
/// Prove its you in order to access your encrypted message history.
public static var screenSessionVerificationOpenExistingSessionSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_open_existing_session_subtitle") }
/// Open an existing session

View File

@@ -198,6 +198,7 @@ enum A11yIdentifiers {
let emojiWrapper = "session_verification-emojis"
let verificationComplete = "session_verification-verification_complete"
let close = "session_verification-close"
let enterRecoveryKey = "session_verification-enter_recovery_key"
}
struct SettingsScreen {

View File

@@ -75,7 +75,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
case (.unverifiedLastSession, .incomplete):
state.requiresExtraAccountSetup = true
if state.securityBannerMode != .dismissed {
state.securityBannerMode = .recoveryKeyConfirmation
state.securityBannerMode = .sessionVerification
}
case (.verified, .disabled):
state.requiresExtraAccountSetup = true

View File

@@ -18,11 +18,13 @@ import Combine
import SwiftUI
enum SessionVerificationScreenCoordinatorAction {
case recoveryKey
case done
}
struct SessionVerificationScreenCoordinatorParameters {
let sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol
let recoveryState: SecureBackupRecoveryState
}
final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
@@ -36,7 +38,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
}
init(parameters: SessionVerificationScreenCoordinatorParameters) {
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy)
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: parameters.sessionVerificationControllerProxy,
recoveryState: parameters.recoveryState)
}
// MARK: - Public
@@ -47,6 +50,8 @@ final class SessionVerificationScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .recoveryKey:
actionsSubject.send(.recoveryKey)
case .finished:
actionsSubject.send(.done)
}

View File

@@ -17,10 +17,13 @@
import Foundation
enum SessionVerificationScreenViewModelAction {
case recoveryKey
case finished
}
struct SessionVerificationScreenViewState: BindableState {
let showRecoveryOption: Bool
var verificationState: SessionVerificationScreenStateMachine.State = .initial
var title: String? {
@@ -83,6 +86,7 @@ struct SessionVerificationScreenViewState: BindableState {
}
enum SessionVerificationScreenViewAction {
case recoveryKey
case requestVerification
case startSasVerification
case restart

View File

@@ -31,12 +31,13 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
}
init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol,
initialState: SessionVerificationScreenViewState = SessionVerificationScreenViewState()) {
recoveryState: SecureBackupRecoveryState,
verificationState: SessionVerificationScreenStateMachine.State = .initial) {
self.sessionVerificationControllerProxy = sessionVerificationControllerProxy
stateMachine = SessionVerificationScreenStateMachine()
super.init(initialViewState: initialState)
super.init(initialViewState: .init(showRecoveryOption: recoveryState == .incomplete, verificationState: verificationState))
setupStateMachine()
@@ -70,6 +71,8 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess
override func process(viewAction: SessionVerificationScreenViewAction) {
switch viewAction {
case .recoveryKey:
actionsSubject.send(.recoveryKey)
case .requestVerification:
stateMachine.processEvent(.requestVerification)
case .startSasVerification:

View File

@@ -128,12 +128,21 @@ struct SessionVerificationScreen: View {
private var actionButtons: some View {
switch context.viewState.verificationState {
case .initial:
Button(L10n.actionStartVerification) {
context.send(viewAction: .requestVerification)
VStack(spacing: 32) {
Button(L10n.actionStartVerification) {
context.send(viewAction: .requestVerification)
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification)
if context.viewState.showRecoveryOption {
Button(L10n.screenSessionVerificationEnterRecoveryKey) {
context.send(viewAction: .recoveryKey)
}
.buttonStyle(.compound(.plain))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.enterRecoveryKey)
}
}
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.sessionVerificationScreen.requestVerification)
case .cancelled:
Button(L10n.actionRetry) {
context.send(viewAction: .restart)
@@ -235,7 +244,8 @@ struct SessionVerification_Previews: PreviewProvider, TestablePreview {
static func sessionVerificationScreen(state: SessionVerificationScreenStateMachine.State) -> some View {
let viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: SessionVerificationControllerProxyMock.configureMock(),
initialState: SessionVerificationScreenViewState(verificationState: state))
recoveryState: .incomplete,
verificationState: state)
return SessionVerificationScreen(context: viewModel.context)
}

View File

@@ -55,7 +55,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
state.securitySectionMode = .sessionVerification
case (.unverifiedLastSession, .incomplete):
state.showSecuritySectionBadge = true
state.securitySectionMode = .secureBackup
state.securitySectionMode = .sessionVerification
case (.verified, .disabled):
state.showSecuritySectionBadge = true
state.securitySectionMode = .secureBackup

View File

@@ -25,5 +25,5 @@ struct MockUserSession: UserSessionProtocol {
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
var sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never> = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).eraseToAnyPublisher()
var sessionSecurityStatePublisher = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher()
}

View File

@@ -51,22 +51,16 @@ class UserSession: UserSessionProtocol {
}
}
let sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never>
let sessionSecurityStateSubject = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .unknown, recoveryState: .unknown))
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> {
sessionSecurityStateSubject.asCurrentValuePublisher()
}
init(clientProxy: ClientProxyProtocol, mediaProvider: MediaProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol) {
self.clientProxy = clientProxy
self.mediaProvider = mediaProvider
self.voiceMessageMediaManager = voiceMessageMediaManager
sessionSecurityStatePublisher = Publishers.CombineLatest(sessionVerificationStateSubject, clientProxy.secureBackupController.recoveryState)
.map {
MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)")
return SessionSecurityState(verificationState: $0, recoveryState: $1)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
clientProxy.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
@@ -88,6 +82,18 @@ class UserSession: UserSessionProtocol {
break
}
}
Publishers.CombineLatest(sessionVerificationStateSubject, clientProxy.secureBackupController.recoveryState)
.map {
MXLog.info("Session security state changed, verificationState: \($0), recoveryState: \($1)")
return SessionSecurityState(verificationState: $0, recoveryState: $1)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.sessionSecurityStateSubject.send(value)
}
.store(in: &cancellables)
}
// MARK: - Private

View File

@@ -42,7 +42,7 @@ protocol UserSessionProtocol {
var mediaProvider: MediaProviderProtocol { get }
var voiceMessageMediaManager: VoiceMessageMediaManagerProtocol { get }
var sessionSecurityStatePublisher: AnyPublisher<SessionSecurityState, Never> { get }
var sessionSecurityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never> { get }
var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get }
var callbacks: PassthroughSubject<UserSessionCallback, Never> { get }

View File

@@ -552,7 +552,8 @@ class MockScreen: Identifiable {
return navigationStackCoordinator
case .sessionVerification:
var sessionVerificationControllerProxy = SessionVerificationControllerProxyMock.configureMock(requestDelay: .seconds(5))
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy)
let parameters = SessionVerificationScreenCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationControllerProxy,
recoveryState: .unknown)
return SessionVerificationScreenCoordinator(parameters: parameters)
case .userSessionScreen, .userSessionScreenReply, .userSessionScreenRTE:
let appSettings: AppSettings = ServiceLocator.shared.settings

View File

@@ -27,7 +27,8 @@ class SessionVerificationViewModelTests: XCTestCase {
override func setUpWithError() throws {
sessionVerificationController = SessionVerificationControllerProxyMock.configureMock()
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController)
viewModel = SessionVerificationScreenViewModel(sessionVerificationControllerProxy: sessionVerificationController,
recoveryState: .incomplete)
context = viewModel.context
}

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0c8f14034cf3b7ec84ea309e02ef139ae9ef6556137c1c0a4348286faac3a8f
size 98718
oid sha256:768819e156631bd2188b55376ce95e270a16828b0d70c3f2c3559a979e558d19
size 105136