diff --git a/Riot/Categories/MXSession+Riot.m b/Riot/Categories/MXSession+Riot.m index 97a74e5b2..aff1ec0e1 100644 --- a/Riot/Categories/MXSession+Riot.m +++ b/Riot/Categories/MXSession+Riot.m @@ -59,7 +59,7 @@ success:(void (^)(BOOL canEnableE2E))success failure:(void (^)(NSError *error))failure; { - if ([self vc_homeserverConfiguration].isE2EEByDefaultEnabled) + if ([self vc_homeserverConfiguration].encryption.isE2EEByDefaultEnabled) { return [self canEnableE2EByDefaultInNewRoomWithUsers:userIds success:success failure:failure]; } diff --git a/Riot/Categories/UIViewController.swift b/Riot/Categories/UIViewController.swift index 6846d8a64..1b2628c75 100644 --- a/Riot/Categories/UIViewController.swift +++ b/Riot/Categories/UIViewController.swift @@ -137,4 +137,18 @@ extension UIViewController { // set split view display mode button as left bar button item self.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem } + + /// Set the view controller to be displayed in fullscreen modal presentation style on any iOS version. + /// + /// - Parameters: + /// - isFullScreen: whether view controller should be displayed full screen + /// - Returns: the view controller + @discardableResult + func vc_setModalFullScreen(_ isFullScreen: Bool) -> UIViewController { + if #available(iOS 13.0, *) { + self.modalPresentationStyle = isFullScreen ? .fullScreen : .automatic + } + + return self + } } diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift index 581133ce8..994e4fb2c 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfiguration.swift @@ -22,14 +22,14 @@ final class HomeserverConfiguration: NSObject { // Note: Use an object per configuration subject when there is multiple properties related let jitsi: HomeserverJitsiConfiguration - let isE2EEByDefaultEnabled: Bool + let encryption: HomeserverEncryptionConfiguration let tileServer: HomeserverTileServerConfiguration init(jitsi: HomeserverJitsiConfiguration, - isE2EEByDefaultEnabled: Bool, + encryption: HomeserverEncryptionConfiguration, tileServer: HomeserverTileServerConfiguration) { self.jitsi = jitsi - self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled + self.encryption = encryption self.tileServer = tileServer } } diff --git a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift index ca87788b6..5b9224806 100644 --- a/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift +++ b/Riot/Model/HomeserverConfiguration/HomeserverConfigurationBuilder.swift @@ -28,10 +28,6 @@ final class HomeserverConfigurationBuilder: NSObject { /// Create an `HomeserverConfiguration` from an HS Well-Known when possible otherwise it takes hardcoded values from BuildSettings by default. func build(from wellKnown: MXWellKnown?) -> HomeserverConfiguration { - - let isE2EEByDefaultEnabled: Bool - let jitsiPreferredDomain: String - var vectorWellKnownEncryptionConfiguration: VectorWellKnownEncryptionConfiguration? var vectorWellKnownJitsiConfiguration: VectorWellKnownJitsiConfiguration? @@ -39,12 +35,26 @@ final class HomeserverConfigurationBuilder: NSObject { vectorWellKnownEncryptionConfiguration = self.getEncryptionConfiguration(from: vectorWellKnown) vectorWellKnownJitsiConfiguration = self.getJitsiConfiguration(from: vectorWellKnown) } - + // Encryption configuration // Enable E2EE by default when there is no value - isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true + let isE2EEByDefaultEnabled = vectorWellKnownEncryptionConfiguration?.isE2EEByDefaultEnabled ?? true + // Disable mandatory secure backup when there is no value + let isSecureBackupRequired = vectorWellKnownEncryptionConfiguration?.isSecureBackupRequired ?? false + // Defaults to all secure backup methods available when there is no value + let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod] + if let backupSetupMethods = vectorWellKnownEncryptionConfiguration?.secureBackupSetupMethods { + secureBackupSetupMethods = backupSetupMethods.isEmpty ? VectorWellKnownBackupSetupMethod.allCases : backupSetupMethods + } else { + secureBackupSetupMethods = VectorWellKnownBackupSetupMethod.allCases + } + + let encryptionConfiguration = HomeserverEncryptionConfiguration(isE2EEByDefaultEnabled: isE2EEByDefaultEnabled, + isSecureBackupRequired: isSecureBackupRequired, + secureBackupSetupMethods: secureBackupSetupMethods) // Jitsi configuration + let jitsiPreferredDomain: String let jitsiServerURL: URL let hardcodedJitsiServerURL: URL = BuildSettings.jitsiServerUrl @@ -77,7 +87,7 @@ final class HomeserverConfigurationBuilder: NSObject { serverURL: jitsiServerURL) return HomeserverConfiguration(jitsi: jitsiConfiguration, - isE2EEByDefaultEnabled: isE2EEByDefaultEnabled, + encryption: encryptionConfiguration, tileServer: tileServerConfiguration) } diff --git a/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift b/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift new file mode 100644 index 000000000..19b9aaee1 --- /dev/null +++ b/Riot/Model/HomeserverConfiguration/HomeserverEncryptionConfiguration.swift @@ -0,0 +1,35 @@ +// +// 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 Foundation + +/// `HomeserverEncryptionConfiguration` gives encryption configuration used by homeserver +@objcMembers +final class HomeserverEncryptionConfiguration: NSObject { + let isE2EEByDefaultEnabled: Bool + let isSecureBackupRequired: Bool + let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod] + + init(isE2EEByDefaultEnabled: Bool, + isSecureBackupRequired: Bool, + secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]) { + self.isE2EEByDefaultEnabled = isE2EEByDefaultEnabled + self.isSecureBackupRequired = isSecureBackupRequired + self.secureBackupSetupMethods = secureBackupSetupMethods + + super.init() + } +} diff --git a/Riot/Model/WellKnown/VectorWellKnown.swift b/Riot/Model/WellKnown/VectorWellKnown.swift index dd548d6b2..8d5669b21 100644 --- a/Riot/Model/WellKnown/VectorWellKnown.swift +++ b/Riot/Model/WellKnown/VectorWellKnown.swift @@ -41,14 +41,29 @@ extension VectorWellKnown: Decodable { } // MARK: - Encryption - -struct VectorWellKnownEncryptionConfiguration: Decodable { - +struct VectorWellKnownEncryptionConfiguration { /// Indicate if E2EE is enabled by default let isE2EEByDefaultEnabled: Bool? - + /// Check if secure backup (SSSS) is mandatory. + let isSecureBackupRequired: Bool? + /// Methods to use to setup secure backup (SSSS). + let secureBackupSetupMethods: [VectorWellKnownBackupSetupMethod]? +} + +extension VectorWellKnownEncryptionConfiguration: Decodable { + /// JSON keys associated to `VectorWellKnownEncryptionConfiguration` enum CodingKeys: String, CodingKey { case isE2EEByDefaultEnabled = "default" + case isSecureBackupRequired = "secure_backup_required" + case secureBackupSetupMethods = "secure_backup_setup_methods" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + isE2EEByDefaultEnabled = try? container.decode(Bool.self, forKey: .isE2EEByDefaultEnabled) + isSecureBackupRequired = try? container.decode(Bool.self, forKey: .isSecureBackupRequired) + let secureBackupSetupMethodsKeys = try? container.decode([String].self, forKey: .secureBackupSetupMethods) + secureBackupSetupMethods = secureBackupSetupMethodsKeys?.compactMap { VectorWellKnownBackupSetupMethod(key: $0) } } } diff --git a/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift b/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift new file mode 100644 index 000000000..a5a241c91 --- /dev/null +++ b/Riot/Model/WellKnown/VectorWellKnownBackupSetupMethod.swift @@ -0,0 +1,39 @@ +// +// 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 Foundation + +/// Methods to use to setup secure backup (SSSS). +@objc enum VectorWellKnownBackupSetupMethod: Int, CaseIterable { + case passphrase = 0 + case key + + private enum Constants { + static let setupMethodPassphrase: String = "passphrase" + static let setupMethodKey: String = "key" + } + + init?(key: String) { + switch key { + case Constants.setupMethodPassphrase: + self = .passphrase + case Constants.setupMethodKey: + self = .key + default: + return nil + } + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 85180036a..d65e7ece5 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -85,7 +85,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification"; -@interface LegacyAppDelegate () +@interface LegacyAppDelegate () { /** Reachability observer @@ -129,6 +129,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni */ KeyVerificationCoordinatorBridgePresenter *keyVerificationCoordinatorBridgePresenter; + /** + Currently displayed secure backup setup + */ + SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter; + /** Account picker used in case of multiple account. */ @@ -2447,6 +2452,15 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni // wait for another session state change to check room list data is ready return; } + + if (mainSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired + && mainSession.vc_canSetupSecureBackup) + { + // This only happens at the first login + // Or when migrating an existing user + MXLogDebug(@"[AppDelegate] handleAppState: Force SSSS setup"); + [self presentSecureBackupSetupForSession:mainSession]; + } void (^finishAppLaunch)(void) = ^{ [self hideLaunchAnimation]; @@ -4316,6 +4330,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (!keyVerificationCoordinatorBridgePresenter.isPresenting) { keyVerificationCoordinatorBridgePresenter = [[KeyVerificationCoordinatorBridgePresenter alloc] initWithSession:mxSession]; + keyVerificationCoordinatorBridgePresenter.cancellable = !mxSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired; keyVerificationCoordinatorBridgePresenter.delegate = self; [keyVerificationCoordinatorBridgePresenter presentCompleteSecurityFrom:self.presentedViewController isNewSignIn:NO animated:YES]; @@ -4696,4 +4711,37 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [self openSpaceWithId:spaceId]; } +#pragma mark - Mandatory SSSS setup + +- (void)presentSecureBackupSetupForSession:(MXSession*)mxSession +{ + MXLogDebug(@"[AppDelegate][Mandatory SSSS] presentSecureBackupSetupForSession"); + + if (!secureBackupSetupCoordinatorBridgePresenter.isPresenting) + { + secureBackupSetupCoordinatorBridgePresenter = [[SecureBackupSetupCoordinatorBridgePresenter alloc] initWithSession:mxSession allowOverwrite:false]; + secureBackupSetupCoordinatorBridgePresenter.delegate = self; + + [secureBackupSetupCoordinatorBridgePresenter presentFrom:self.masterTabBarController animated:NO cancellable:NO]; + } + else + { + MXLogDebug(@"[AppDelegate][Mandatory SSSS] presentSecureBackupSetupForSession: Controller already presented") + } +} + +#pragma mark - SecureBackupSetupCoordinatorBridgePresenterDelegate + +- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + secureBackupSetupCoordinatorBridgePresenter = nil; +} + +- (void)secureBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + secureBackupSetupCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Authentication/AuthenticationCoordinator.swift b/Riot/Modules/Authentication/AuthenticationCoordinator.swift index e452d8529..3bcae3348 100644 --- a/Riot/Modules/Authentication/AuthenticationCoordinator.swift +++ b/Riot/Modules/Authentication/AuthenticationCoordinator.swift @@ -127,7 +127,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc } let isNewSignIn = true - let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn)) + let cancellable = !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired + let keyVerificationCoordinator = KeyVerificationCoordinator(session: session, flow: .completeSecurity(isNewSignIn), cancellable: cancellable) keyVerificationCoordinator.delegate = self let presentable = keyVerificationCoordinator.toPresentable() @@ -176,7 +177,7 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc // TODO: This is still not sure we want to disable the automatic cross-signing bootstrap // if the admin disabled e2e by default. // Do like riot-web for the moment - if session.vc_homeserverConfiguration().isE2EEByDefaultEnabled { + if session.vc_homeserverConfiguration().encryption.isE2EEByDefaultEnabled { // Bootstrap cross-signing on user's account // We do it for both registration and new login as long as cross-signing does not exist yet if let password = self.password, !password.isEmpty { diff --git a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift index 33094e7c1..3a518e846 100644 --- a/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift +++ b/Riot/Modules/CreateRoom/EnterNewRoomDetails/EnterNewRoomDetailsViewModel.swift @@ -51,7 +51,7 @@ final class EnterNewRoomDetailsViewModel: EnterNewRoomDetailsViewModelType { init(session: MXSession, parentSpace: MXSpace?) { self.session = session self.parentSpace = parentSpace - roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted + roomCreationParameters.isEncrypted = session.vc_homeserverConfiguration().encryption.isE2EEByDefaultEnabled && RiotSettings.shared.roomCreationScreenRoomIsEncrypted roomCreationParameters.joinRule = RiotSettings.shared.roomCreationScreenRoomIsPublic ? .public : .private viewState = .loaded } diff --git a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift index 3fce9171d..c14c56272 100644 --- a/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift +++ b/Riot/Modules/KeyBackup/Setup/KeyBackupSetupCoordinator.swift @@ -92,7 +92,7 @@ final class KeyBackupSetupCoordinator: KeyBackupSetupCoordinatorType { self.createKeyBackupUsingSecureBackup(privateKey: privateKey, completion: completion) } - let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: .passphraseOrKey, recoveryGoal: recoveryGoal, navigationRouter: self.navigationRouter) + let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: .passphraseOrKey, recoveryGoal: recoveryGoal, navigationRouter: self.navigationRouter, cancellable: true) coordinator.delegate = self coordinator.start() self.add(childCoordinator: coordinator) diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift index 19dbc98f8..d7b31c693 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinator.swift @@ -29,6 +29,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { private let session: MXSession private let verificationFlow: KeyVerificationFlow private let verificationKind: KeyVerificationKind + private let cancellable: Bool private weak var completeSecurityCoordinator: KeyVerificationSelfVerifyWaitCoordinatorType? private var otherUserId: String { @@ -86,7 +87,8 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { /// - session: The MXSession. /// - flow: The wanted key verification flow. /// - navigationRouter: Existing NavigationRouter from which present the flow (optional). - init(session: MXSession, flow: KeyVerificationFlow, navigationRouter: NavigationRouterType? = nil) { + /// - cancellable: Whether key verification process can be cancelled. + init(session: MXSession, flow: KeyVerificationFlow, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) { self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController()) self.session = session @@ -113,6 +115,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } self.verificationKind = verificationKind + self.cancellable = cancellable } // MARK: - Public methods @@ -155,7 +158,9 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + return self.navigationRouter + .toPresentable() + .vc_setModalFullScreen(!self.cancellable) } // MARK: - Private methods @@ -177,7 +182,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } private func createCompleteSecurityCoordinator(isNewSignIn: Bool) -> KeyVerificationSelfVerifyWaitCoordinatorType { - let coordinator = KeyVerificationSelfVerifyWaitCoordinator(session: self.session, isNewSignIn: isNewSignIn) + let coordinator = KeyVerificationSelfVerifyWaitCoordinator(session: self.session, isNewSignIn: isNewSignIn, cancellable: self.cancellable) coordinator.delegate = self coordinator.start() @@ -185,7 +190,7 @@ final class KeyVerificationCoordinator: KeyVerificationCoordinatorType { } private func showSecretsRecovery(with recoveryMode: SecretsRecoveryMode) { - let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: recoveryMode, recoveryGoal: .verifyDevice, navigationRouter: self.navigationRouter) + let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: recoveryMode, recoveryGoal: .verifyDevice, navigationRouter: self.navigationRouter, cancellable: self.cancellable) coordinator.delegate = self coordinator.start() diff --git a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift index 5d8ead04c..f07ea75a2 100644 --- a/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift +++ b/Riot/Modules/KeyVerification/Common/KeyVerificationCoordinatorBridgePresenter.swift @@ -38,6 +38,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { // MARK: Public weak var delegate: KeyVerificationCoordinatorBridgePresenterDelegate? + var cancellable: Bool = true var isPresenting: Bool { return self.coordinator != nil @@ -61,7 +62,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present from \(viewController)") - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: otherUserId, deviceId: otherDeviceId)) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: otherUserId, deviceId: otherDeviceId), cancellable: self.cancellable) self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated) } @@ -69,7 +70,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present from \(viewController)") - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyUser(roomMember)) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyUser(roomMember), cancellable: self.cancellable) self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated) } @@ -77,7 +78,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present incoming verification from \(viewController)") - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingSASTransaction(incomingTransaction)) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingSASTransaction(incomingTransaction), cancellable: self.cancellable) self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated) } @@ -85,7 +86,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present incoming key verification request from \(viewController)") - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingRequest(incomingKeyVerificationRequest)) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .incomingRequest(incomingKeyVerificationRequest), cancellable: self.cancellable) self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated) } @@ -93,7 +94,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { MXLog.debug("[KeyVerificationCoordinatorBridgePresenter] Present complete security from \(viewController)") - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn)) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), cancellable: self.cancellable) self.present(coordinator: keyVerificationCoordinator, from: viewController, animated: animated) } @@ -103,7 +104,7 @@ final class KeyVerificationCoordinatorBridgePresenter: NSObject { let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .completeSecurity(isNewSignIn), navigationRouter: navigationRouter, cancellable: self.cancellable) keyVerificationCoordinator.delegate = self keyVerificationCoordinator.start() // Will trigger view controller push diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift index 3b6e2824a..94b3a3eca 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitCoordinator.swift @@ -28,6 +28,7 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW private let session: MXSession private var keyVerificationSelfVerifyWaitViewModel: KeyVerificationSelfVerifyWaitViewModelType private let keyVerificationSelfVerifyWaitViewController: KeyVerificationSelfVerifyWaitViewController + private let cancellable: Bool // MARK: Public @@ -38,13 +39,14 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW // MARK: - Setup - init(session: MXSession, isNewSignIn: Bool) { + init(session: MXSession, isNewSignIn: Bool, cancellable: Bool) { self.session = session let keyVerificationSelfVerifyWaitViewModel = KeyVerificationSelfVerifyWaitViewModel(session: self.session, isNewSignIn: isNewSignIn) - let keyVerificationSelfVerifyWaitViewController = KeyVerificationSelfVerifyWaitViewController.instantiate(with: keyVerificationSelfVerifyWaitViewModel) + let keyVerificationSelfVerifyWaitViewController = KeyVerificationSelfVerifyWaitViewController.instantiate(with: keyVerificationSelfVerifyWaitViewModel, cancellable: cancellable) self.keyVerificationSelfVerifyWaitViewModel = keyVerificationSelfVerifyWaitViewModel self.keyVerificationSelfVerifyWaitViewController = keyVerificationSelfVerifyWaitViewController + self.cancellable = cancellable } // MARK: - Public methods @@ -55,6 +57,7 @@ final class KeyVerificationSelfVerifyWaitCoordinator: KeyVerificationSelfVerifyW func toPresentable() -> UIViewController { return self.keyVerificationSelfVerifyWaitViewController + .vc_setModalFullScreen(!self.cancellable) } } diff --git a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift index dd988c45e..f7969bf22 100644 --- a/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift +++ b/Riot/Modules/KeyVerification/Device/SelfVerifyWait/KeyVerificationSelfVerifyWaitViewController.swift @@ -47,6 +47,7 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { // MARK: Private private var viewModel: KeyVerificationSelfVerifyWaitViewModelType! + private var cancellable: Bool! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! @@ -55,9 +56,10 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: KeyVerificationSelfVerifyWaitViewModelType) -> KeyVerificationSelfVerifyWaitViewController { + class func instantiate(with viewModel: KeyVerificationSelfVerifyWaitViewModelType, cancellable: Bool) -> KeyVerificationSelfVerifyWaitViewController { let viewController = StoryboardScene.KeyVerificationSelfVerifyWaitViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -112,15 +114,17 @@ final class KeyVerificationSelfVerifyWaitViewController: UIViewController { } private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.skip, style: .plain) { [weak self] in - self?.cancelButtonAction() + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.skip, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.vc_removeBackTitle() + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem + self.cancelBarButtonItem = cancelBarButtonItem } - self.vc_removeBackTitle() - - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - self.cancelBarButtonItem = cancelBarButtonItem - self.title = VectorL10n.deviceVerificationSelfVerifyWaitTitle self.informationLabel.text = VectorL10n.deviceVerificationSelfVerifyWaitInformation(AppInfo.current.displayName) diff --git a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift index 55565fb9d..b56748a40 100644 --- a/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift +++ b/Riot/Modules/KeyVerification/User/UserVerificationCoordinator.swift @@ -118,7 +118,7 @@ final class UserVerificationCoordinator: NSObject, UserVerificationCoordinatorTy private func presentDeviceVerification(for deviceId: String) { - let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: self.userId, deviceId: deviceId), navigationRouter: self.navigationRouter) + let keyVerificationCoordinator = KeyVerificationCoordinator(session: self.session, flow: .verifyDevice(userId: self.userId, deviceId: deviceId), navigationRouter: self.navigationRouter, cancellable: true) keyVerificationCoordinator.delegate = self keyVerificationCoordinator.start() diff --git a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift index c9be632dd..efb42a35a 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyCoordinator.swift @@ -24,6 +24,7 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator private let secretsRecoveryWithKeyViewController: SecretsRecoveryWithKeyViewController private let secretsRecoveryWithKeyViewModel: SecretsRecoveryWithKeyViewModel + private let cancellable: Bool // MARK: Public @@ -33,12 +34,13 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator // MARK: - Setup - init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal) { + init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal, cancellable: Bool) { let secretsRecoveryWithKeyViewModel = SecretsRecoveryWithKeyViewModel(recoveryService: recoveryService, recoveryGoal: recoveryGoal) - let secretsRecoveryWithKeyViewController = SecretsRecoveryWithKeyViewController.instantiate(with: secretsRecoveryWithKeyViewModel) + let secretsRecoveryWithKeyViewController = SecretsRecoveryWithKeyViewController.instantiate(with: secretsRecoveryWithKeyViewModel, cancellable: cancellable) self.secretsRecoveryWithKeyViewController = secretsRecoveryWithKeyViewController self.secretsRecoveryWithKeyViewModel = secretsRecoveryWithKeyViewModel + self.cancellable = cancellable } // MARK: - Public @@ -49,6 +51,7 @@ final class SecretsRecoveryWithKeyCoordinator: SecretsRecoveryWithKeyCoordinator func toPresentable() -> UIViewController { return self.secretsRecoveryWithKeyViewController + .vc_setModalFullScreen(!self.cancellable) } } diff --git a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift index 23b48397e..a746656a6 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift @@ -43,6 +43,7 @@ final class SecretsRecoveryWithKeyViewController: UIViewController { private var viewModel: SecretsRecoveryWithKeyViewModelType! private var keyboardAvoider: KeyboardAvoider? + private var cancellable: Bool! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! @@ -52,9 +53,10 @@ final class SecretsRecoveryWithKeyViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: SecretsRecoveryWithKeyViewModelType) -> SecretsRecoveryWithKeyViewController { + class func instantiate(with viewModel: SecretsRecoveryWithKeyViewModelType, cancellable: Bool) -> SecretsRecoveryWithKeyViewController { let viewController = StoryboardScene.SecretsRecoveryWithKeyViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -86,11 +88,13 @@ final class SecretsRecoveryWithKeyViewController: UIViewController { // MARK: - Private private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + self.navigationItem.rightBarButtonItem = cancelBarButtonItem } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - + self.title = VectorL10n.secretsRecoveryWithKeyTitle self.scrollView.keyboardDismissMode = .interactive diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift index bb2390050..02133fcbc 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseCoordinator.swift @@ -24,6 +24,7 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr private let secretsRecoveryWithPassphraseViewController: SecretsRecoveryWithPassphraseViewController private var secretsRecoveryWithPassphraseViewModel: SecretsRecoveryWithPassphraseViewModelType + private let cancellable: Bool // MARK: Public @@ -33,11 +34,12 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr // MARK: - Setup - init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal) { + init(recoveryService: MXRecoveryService, recoveryGoal: SecretsRecoveryGoal, cancellable: Bool) { let secretsRecoveryWithPassphraseViewModel = SecretsRecoveryWithPassphraseViewModel(recoveryService: recoveryService, recoveryGoal: recoveryGoal) - let secretsRecoveryWithPassphraseViewController = SecretsRecoveryWithPassphraseViewController.instantiate(with: secretsRecoveryWithPassphraseViewModel) + let secretsRecoveryWithPassphraseViewController = SecretsRecoveryWithPassphraseViewController.instantiate(with: secretsRecoveryWithPassphraseViewModel, cancellable: cancellable) self.secretsRecoveryWithPassphraseViewController = secretsRecoveryWithPassphraseViewController self.secretsRecoveryWithPassphraseViewModel = secretsRecoveryWithPassphraseViewModel + self.cancellable = cancellable } // MARK: - Public @@ -48,6 +50,7 @@ final class SecretsRecoveryWithPassphraseCoordinator: SecretsRecoveryWithPassphr func toPresentable() -> UIViewController { return self.secretsRecoveryWithPassphraseViewController + .vc_setModalFullScreen(!self.cancellable) } } diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift index 0578f2b37..a29e304ae 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift @@ -44,6 +44,7 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { private var viewModel: SecretsRecoveryWithPassphraseViewModelType! private var keyboardAvoider: KeyboardAvoider? + private var cancellable: Bool! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! @@ -52,9 +53,10 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: SecretsRecoveryWithPassphraseViewModelType) -> SecretsRecoveryWithPassphraseViewController { + class func instantiate(with viewModel: SecretsRecoveryWithPassphraseViewModelType, cancellable: Bool) -> SecretsRecoveryWithPassphraseViewController { let viewController = StoryboardScene.SecretsRecoveryWithPassphraseViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -86,11 +88,13 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { // MARK: - Private private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.viewModel.process(viewAction: .cancel) + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.viewModel.process(viewAction: .cancel) + } + self.navigationItem.rightBarButtonItem = cancelBarButtonItem } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - + self.title = VectorL10n.secretsRecoveryWithPassphraseTitle self.scrollView.keyboardDismissMode = .interactive diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift index fdca1dc7d..d36ee995e 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinator.swift @@ -26,6 +26,7 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { private let navigationRouter: NavigationRouterType private let recoveryMode: SecretsRecoveryMode private let recoveryGoal: SecretsRecoveryGoal + private let cancellable: Bool // MARK: Public @@ -35,10 +36,11 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { // MARK: - Setup - init(session: MXSession, recoveryMode: SecretsRecoveryMode, recoveryGoal: SecretsRecoveryGoal, navigationRouter: NavigationRouterType? = nil) { + init(session: MXSession, recoveryMode: SecretsRecoveryMode, recoveryGoal: SecretsRecoveryGoal, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) { self.session = session self.recoveryMode = recoveryMode self.recoveryGoal = recoveryGoal + self.cancellable = cancellable if let navigationRouter = navigationRouter { self.navigationRouter = navigationRouter @@ -76,19 +78,21 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + return self.navigationRouter + .toPresentable() + .vc_setModalFullScreen(!self.cancellable) } // MARK: - Private private func createRecoverFromKeyCoordinator() -> SecretsRecoveryWithKeyCoordinator { - let coordinator = SecretsRecoveryWithKeyCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal) + let coordinator = SecretsRecoveryWithKeyCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal, cancellable: self.cancellable) coordinator.delegate = self return coordinator } private func createRecoverFromPassphraseCoordinator() -> SecretsRecoveryWithPassphraseCoordinator { - let coordinator = SecretsRecoveryWithPassphraseCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal) + let coordinator = SecretsRecoveryWithPassphraseCoordinator(recoveryService: self.session.crypto.recoveryService, recoveryGoal: self.recoveryGoal, cancellable: self.cancellable) coordinator.delegate = self return coordinator } @@ -115,7 +119,7 @@ final class SecretsRecoveryCoordinator: SecretsRecoveryCoordinatorType { } private func showSecureBackupSetup(checkKeyBackup: Bool) { - let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter) + let coordinator = SecureBackupSetupCoordinator(session: self.session, checkKeyBackup: checkKeyBackup, navigationRouter: self.navigationRouter, cancellable: self.cancellable) coordinator.delegate = self coordinator.start() diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift index a5a1ea31a..3011cfe3d 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift @@ -74,7 +74,7 @@ final class SecretsRecoveryCoordinatorBridgePresenter: NSObject { func present(from viewController: UIViewController, animated: Bool) { - let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: self.recoveryMode, recoveryGoal: self.recoveryGoal.goal) + let coordinator = SecretsRecoveryCoordinator(session: self.session, recoveryMode: self.recoveryMode, recoveryGoal: self.recoveryGoal.goal, cancellable: true) coordinator.delegate = self let presentable = coordinator.toPresentable() diff --git a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift index f08f88869..58ebf79ba 100644 --- a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift +++ b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyCoordinator.swift @@ -27,6 +27,7 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat private var secretsSetupRecoveryKeyViewModel: SecretsSetupRecoveryKeyViewModelType private let secretsSetupRecoveryKeyViewController: SecretsSetupRecoveryKeyViewController + private let cancellable: Bool // MARK: Public @@ -40,11 +41,13 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat init(recoveryService: MXRecoveryService, passphrase: String?, passphraseOnly: Bool, - allowOverwrite: Bool = false) { + allowOverwrite: Bool = false, + cancellable: Bool) { let secretsSetupRecoveryKeyViewModel = SecretsSetupRecoveryKeyViewModel(recoveryService: recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite) - let secretsSetupRecoveryKeyViewController = SecretsSetupRecoveryKeyViewController.instantiate(with: secretsSetupRecoveryKeyViewModel) + let secretsSetupRecoveryKeyViewController = SecretsSetupRecoveryKeyViewController.instantiate(with: secretsSetupRecoveryKeyViewModel, cancellable: cancellable) self.secretsSetupRecoveryKeyViewModel = secretsSetupRecoveryKeyViewModel self.secretsSetupRecoveryKeyViewController = secretsSetupRecoveryKeyViewController + self.cancellable = cancellable } // MARK: - Public methods @@ -55,6 +58,7 @@ final class SecretsSetupRecoveryKeyCoordinator: SecretsSetupRecoveryKeyCoordinat func toPresentable() -> UIViewController { return self.secretsSetupRecoveryKeyViewController + .vc_setModalFullScreen(!self.cancellable) } } diff --git a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift index e6be0650d..ea623fdfe 100644 --- a/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift +++ b/Riot/Modules/Secrets/Setup/RecoveryKey/SecretsSetupRecoveryKeyViewController.swift @@ -34,6 +34,7 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController { private var viewModel: SecretsSetupRecoveryKeyViewModelType! private var isPassphraseOnly: Bool = true + private var cancellable: Bool! private var theme: Theme! private var errorPresenter: MXKErrorPresentation! private var activityPresenter: ActivityIndicatorPresenter! @@ -43,9 +44,10 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: SecretsSetupRecoveryKeyViewModelType) -> SecretsSetupRecoveryKeyViewController { + class func instantiate(with viewModel: SecretsSetupRecoveryKeyViewModelType, cancellable: Bool) -> SecretsSetupRecoveryKeyViewController { let viewController = StoryboardScene.SecretsSetupRecoveryKeyViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -108,12 +110,14 @@ final class SecretsSetupRecoveryKeyViewController: UIViewController { } private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem } - - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - + self.vc_removeBackTitle() self.title = VectorL10n.secretsSetupRecoveryKeyTitle diff --git a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift index 2f67abb74..a8154facc 100644 --- a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift +++ b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseCoordinator.swift @@ -27,6 +27,7 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp private var secretsSetupRecoveryPassphraseViewModel: SecretsSetupRecoveryPassphraseViewModelType private let secretsSetupRecoveryPassphraseViewController: SecretsSetupRecoveryPassphraseViewController + private let cancellable: Bool // MARK: Public @@ -37,12 +38,13 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp // MARK: - Setup - init(passphraseInput: SecretsSetupRecoveryPassphraseInput) { + init(passphraseInput: SecretsSetupRecoveryPassphraseInput, cancellable: Bool) { let secretsSetupRecoveryPassphraseViewModel = SecretsSetupRecoveryPassphraseViewModel(passphraseInput: passphraseInput) - let secretsSetupRecoveryPassphraseViewController = SecretsSetupRecoveryPassphraseViewController.instantiate(with: secretsSetupRecoveryPassphraseViewModel) + let secretsSetupRecoveryPassphraseViewController = SecretsSetupRecoveryPassphraseViewController.instantiate(with: secretsSetupRecoveryPassphraseViewModel, cancellable: cancellable) self.secretsSetupRecoveryPassphraseViewModel = secretsSetupRecoveryPassphraseViewModel self.secretsSetupRecoveryPassphraseViewController = secretsSetupRecoveryPassphraseViewController + self.cancellable = cancellable } // MARK: - Public methods @@ -53,6 +55,7 @@ final class SecretsSetupRecoveryPassphraseCoordinator: SecretsSetupRecoveryPassp func toPresentable() -> UIViewController { return self.secretsSetupRecoveryPassphraseViewController + .vc_setModalFullScreen(!self.cancellable) } } diff --git a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift index 0cb5bd2d2..5ac8c91ea 100644 --- a/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift +++ b/Riot/Modules/Secrets/Setup/RecoveryPassphrase/SecretsSetupRecoveryPassphraseViewController.swift @@ -53,6 +53,7 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController { // MARK: Private private var viewModel: SecretsSetupRecoveryPassphraseViewModelType! + private var cancellable: Bool! private var theme: Theme! private var keyboardAvoider: KeyboardAvoider? private var errorPresenter: MXKErrorPresentation! @@ -64,9 +65,10 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: SecretsSetupRecoveryPassphraseViewModelType) -> SecretsSetupRecoveryPassphraseViewController { + class func instantiate(with viewModel: SecretsSetupRecoveryPassphraseViewModelType, cancellable: Bool) -> SecretsSetupRecoveryPassphraseViewController { let viewController = StoryboardScene.SecretsSetupRecoveryPassphraseViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -119,12 +121,14 @@ final class SecretsSetupRecoveryPassphraseViewController: UIViewController { // MARK: - Private private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem - self.vc_removeBackTitle() self.title = VectorL10n.secretsSetupRecoveryPassphraseTitle diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift index 346f0c535..4af46f86f 100644 --- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift +++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewController.swift @@ -39,6 +39,7 @@ final class SecureBackupSetupIntroViewController: UIViewController { // MARK: Private private var viewModel: SecureBackupSetupIntroViewModelType! + private var cancellable: Bool! private var theme: Theme! private var activityIndicatorPresenter: ActivityIndicatorPresenter! @@ -50,9 +51,10 @@ final class SecureBackupSetupIntroViewController: UIViewController { // MARK: - Setup - class func instantiate(with viewModel: SecureBackupSetupIntroViewModelType) -> SecureBackupSetupIntroViewController { + class func instantiate(with viewModel: SecureBackupSetupIntroViewModelType, cancellable: Bool) -> SecureBackupSetupIntroViewController { let viewController = StoryboardScene.SecureBackupSetupIntroViewController.initialScene.instantiate() viewController.viewModel = viewModel + viewController.cancellable = cancellable viewController.theme = ThemeService.shared().theme return viewController } @@ -86,13 +88,15 @@ final class SecureBackupSetupIntroViewController: UIViewController { // MARK: - Private private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - guard let self = self else { - return + if self.cancellable { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + guard let self = self else { + return + } + self.delegate?.secureBackupSetupIntroViewControllerDidCancel(self, showSkipAlert: true) } - self.delegate?.secureBackupSetupIntroViewControllerDidCancel(self, showSkipAlert: true) + self.navigationItem.rightBarButtonItem = cancelBarButtonItem } - self.navigationItem.rightBarButtonItem = cancelBarButtonItem self.title = VectorL10n.secureKeyBackupSetupIntroTitle @@ -119,6 +123,21 @@ final class SecureBackupSetupIntroViewController: UIViewController { } self.delegate?.secureBackupSetupIntroViewControllerDidTapUsePassphrase(self) } + + setupBackupMethods() + } + + private func setupBackupMethods() { + let secureBackupSetupMethods = self.viewModel.homeserverEncryptionConfiguration.secureBackupSetupMethods + + // Hide setup methods that are not listed + if !secureBackupSetupMethods.contains(.key) { + self.secureKeyCell.isHidden = true + } + + if !secureBackupSetupMethods.contains(.passphrase) { + self.securePassphraseCell.isHidden = true + } } private func renderLoading() { diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift index 3a10148d9..e85653ee7 100644 --- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift +++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModel.swift @@ -23,11 +23,13 @@ final class SecureBackupSetupIntroViewModel: SecureBackupSetupIntroViewModelType // TODO: Make these properties private let keyBackup: MXKeyBackup? let checkKeyBackup: Bool + let homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration // MARK: - Setup - init(keyBackup: MXKeyBackup?, checkKeyBackup: Bool) { + init(keyBackup: MXKeyBackup?, checkKeyBackup: Bool, homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration) { self.keyBackup = keyBackup self.checkKeyBackup = checkKeyBackup - } + self.homeserverEncryptionConfiguration = homeserverEncryptionConfiguration + } } diff --git a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift index b7d08f5d5..3e301fb56 100644 --- a/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift +++ b/Riot/Modules/SecureBackup/Setup/Intro/SecureBackupSetupIntroViewModelType.swift @@ -22,4 +22,5 @@ protocol SecureBackupSetupIntroViewModelType { // TODO: Hide these properties from interface and use same behavior as other view models var keyBackup: MXKeyBackup? { get } var checkKeyBackup: Bool { get } + var homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration { get } } diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift index 543616377..3385063ce 100644 --- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift +++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinator.swift @@ -30,7 +30,14 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { private let recoveryService: MXRecoveryService private let keyBackup: MXKeyBackup? private let checkKeyBackup: Bool + private let homeserverEncryptionConfiguration: HomeserverEncryptionConfiguration private let allowOverwrite: Bool + private let cancellable: Bool + + private var isBackupSetupMethodKeySupported: Bool { + let homeserverEncryptionConfiguration = self.session.vc_homeserverConfiguration().encryption + return homeserverEncryptionConfiguration.secureBackupSetupMethods.contains(.key) + } // MARK: Public @@ -46,12 +53,15 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { /// - session: The MXSession. /// - checkKeyBackup: Indicate false to ignore existing key backup. /// - navigationRouter: Use existing navigation router to plug this flow or let nil to use new one. - init(session: MXSession, checkKeyBackup: Bool = true, allowOverwrite: Bool = false, navigationRouter: NavigationRouterType? = nil) { + /// - cancellable: Whether secure backup can be cancelled + init(session: MXSession, checkKeyBackup: Bool = true, allowOverwrite: Bool = false, navigationRouter: NavigationRouterType? = nil, cancellable: Bool) { self.session = session self.recoveryService = session.crypto.recoveryService self.keyBackup = session.crypto.backup self.checkKeyBackup = checkKeyBackup + self.homeserverEncryptionConfiguration = session.vc_homeserverConfiguration().encryption self.allowOverwrite = allowOverwrite + self.cancellable = cancellable if let navigationRouter = navigationRouter { self.navigationRouter = navigationRouter @@ -73,21 +83,25 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { } func toPresentable() -> UIViewController { - return self.navigationRouter.toPresentable() + return self.navigationRouter + .toPresentable() + .vc_setModalFullScreen(!self.cancellable) } // MARK: - Private methods private func createIntro() -> SecureBackupSetupIntroViewController { // TODO: Use a coordinator - let viewModel = SecureBackupSetupIntroViewModel(keyBackup: self.keyBackup, checkKeyBackup: self.checkKeyBackup) - let introViewController = SecureBackupSetupIntroViewController.instantiate(with: viewModel) + let viewModel = SecureBackupSetupIntroViewModel(keyBackup: self.keyBackup, + checkKeyBackup: self.checkKeyBackup, + homeserverEncryptionConfiguration: self.homeserverEncryptionConfiguration) + let introViewController = SecureBackupSetupIntroViewController.instantiate(with: viewModel, cancellable: self.cancellable) introViewController.delegate = self return introViewController } private func showSetupKey(passphraseOnly: Bool, passphrase: String? = nil) { - let coordinator = SecretsSetupRecoveryKeyCoordinator(recoveryService: self.recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite) + let coordinator = SecretsSetupRecoveryKeyCoordinator(recoveryService: self.recoveryService, passphrase: passphrase, passphraseOnly: passphraseOnly, allowOverwrite: allowOverwrite, cancellable: self.cancellable) coordinator.delegate = self coordinator.start() @@ -98,7 +112,7 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { } private func showSetupPassphrase() { - let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .new) + let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .new, cancellable: self.cancellable) coordinator.delegate = self coordinator.start() @@ -109,7 +123,7 @@ final class SecureBackupSetupCoordinator: SecureBackupSetupCoordinatorType { } private func showSetupPassphraseConfirmation(with passphrase: String) { - let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .confirm(passphrase)) + let coordinator = SecretsSetupRecoveryPassphraseCoordinator(passphraseInput: .confirm(passphrase), cancellable: self.cancellable) coordinator.delegate = self coordinator.start() @@ -203,7 +217,9 @@ extension SecureBackupSetupCoordinator: SecretsSetupRecoveryPassphraseCoordinato } func secretsSetupRecoveryPassphraseCoordinator(_ coordinator: SecretsSetupRecoveryPassphraseCoordinatorType, didConfirmPassphrase passphrase: String) { - self.showSetupKey(passphraseOnly: false, passphrase: passphrase) + + // Do not present recovery key export screen if secure backup setup key method is not supported + self.showSetupKey(passphraseOnly: !self.isBackupSetupMethodKeySupported, passphrase: passphrase) } func secretsSetupRecoveryPassphraseCoordinatorDidCancel(_ coordinator: SecretsSetupRecoveryPassphraseCoordinatorType) { diff --git a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift index 3db379e8f..60128a8a8 100644 --- a/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift +++ b/Riot/Modules/SecureBackup/Setup/SecureBackupSetupCoordinatorBridgePresenter.swift @@ -37,9 +37,12 @@ final class SecureBackupSetupCoordinatorBridgePresenter: NSObject { private var coordinator: SecureBackupSetupCoordinator? // MARK: Public - weak var delegate: SecureBackupSetupCoordinatorBridgePresenterDelegate? - + + var isPresenting: Bool { + return self.coordinator != nil + } + // MARK: - Setup init(session: MXSession, allowOverwrite: Bool) { @@ -54,9 +57,13 @@ final class SecureBackupSetupCoordinatorBridgePresenter: NSObject { // func present(from viewController: UIViewController, animated: Bool) { // self.present(from: viewController, animated: animated) // } - + func present(from viewController: UIViewController, animated: Bool) { - let secureBackupSetupCoordinator = SecureBackupSetupCoordinator(session: self.session, allowOverwrite: self.allowOverwrite) + self.present(from: viewController, animated: animated, cancellable: true) + } + + func present(from viewController: UIViewController, animated: Bool, cancellable: Bool) { + let secureBackupSetupCoordinator = SecureBackupSetupCoordinator(session: self.session, allowOverwrite: self.allowOverwrite, cancellable: cancellable) secureBackupSetupCoordinator.delegate = self viewController.present(secureBackupSetupCoordinator.toPresentable(), animated: animated, completion: nil) secureBackupSetupCoordinator.start() diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 280413786..3e47ba057 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -287,6 +287,8 @@ TableViewSectionsDelegate> - (void)updateSections { NSMutableArray *sections = [NSMutableArray array]; + + BOOL isSecureBackupRequired = self.mainSession.vc_homeserverConfiguration.encryption.isSecureBackupRequired; // Pin code section @@ -341,14 +343,17 @@ TableViewSectionsDelegate> } // Secure backup - - Section *secureBackupSection = [Section sectionWithTag:SECTION_SECURE_BACKUP]; - secureBackupSection.headerTitle = [VectorL10n securitySettingsSecureBackup]; - secureBackupSection.footerTitle = VectorL10n.securitySettingsSecureBackupDescription; - - [secureBackupSection addRowsWithCount:self->secureBackupSection.numberOfRows]; - - [sections addObject:secureBackupSection]; + + if (!isSecureBackupRequired) + { + Section *secureBackupSection = [Section sectionWithTag:SECTION_SECURE_BACKUP]; + secureBackupSection.headerTitle = [VectorL10n securitySettingsSecureBackup]; + secureBackupSection.footerTitle = VectorL10n.securitySettingsSecureBackupDescription; + + [secureBackupSection addRowsWithCount:self->secureBackupSection.numberOfRows]; + + [sections addObject:secureBackupSection]; + } // Cross-Signing @@ -359,24 +364,24 @@ TableViewSectionsDelegate> [sections addObject:crossSigningSection]; - // Cryptograhpy + // Cryptography - Section *cryptograhpySection = [Section sectionWithTag:SECTION_CRYPTOGRAPHY]; - cryptograhpySection.headerTitle = [VectorL10n securitySettingsCryptography]; + Section *cryptographySection = [Section sectionWithTag:SECTION_CRYPTOGRAPHY]; + cryptographySection.headerTitle = [VectorL10n securitySettingsCryptography]; if (RiotSettings.shared.settingsSecurityScreenShowCryptographyInfo) { - [cryptograhpySection addRowWithTag:CRYPTOGRAPHY_INFO]; + [cryptographySection addRowWithTag:CRYPTOGRAPHY_INFO]; } - if (RiotSettings.shared.settingsSecurityScreenShowCryptographyExport) + if (RiotSettings.shared.settingsSecurityScreenShowCryptographyExport && !isSecureBackupRequired) { - [cryptograhpySection addRowWithTag:CRYPTOGRAPHY_EXPORT]; + [cryptographySection addRowWithTag:CRYPTOGRAPHY_EXPORT]; } - if (cryptograhpySection.rows.count) + if (cryptographySection.rows.count) { - [sections addObject:cryptograhpySection]; + [sections addObject:cryptographySection]; } #ifdef CROSS_SIGNING_AND_BACKUP_DEV diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index bc306b757..435f7ebcb 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -952,6 +952,15 @@ } self.reviewSessionAlertHasBeenDisplayed = YES; + + // Force verification if required by the HS configuration + if (session.vc_homeserverConfiguration.encryption.isSecureBackupRequired) + { + NSLog(@"[MasterTabBarController] presentVerifyCurrentSessionAlertIfNeededWithSession: Force verification of the device"); + [[AppDelegate theDelegate] presentCompleteSecurityForSession:session]; + return; + } + [self presentVerifyCurrentSessionAlertWithSession:session]; } diff --git a/RiotTests/HomeserverConfigurationTests.swift b/RiotTests/HomeserverConfigurationTests.swift index f50e4540f..f861a97ff 100644 --- a/RiotTests/HomeserverConfigurationTests.swift +++ b/RiotTests/HomeserverConfigurationTests.swift @@ -38,7 +38,10 @@ class HomeserverConfigurationTests: XCTestCase { let expectedE2EEEByDefaultEnabled = true let expectedDeprecatedE2EEEByDefaultEnabled = false let expectedMapStyleURLString = "https://your.tileserver.org/style.json" - + let expectedSecureBackupRequired = true + let secureBackupSetupMethods = ["passphrase"] + let expectedSecureBackupSetupMethods: [VectorWellKnownBackupSetupMethod] = [.passphrase] + let wellKnownDictionary: [String: Any] = [ "m.homeserver": [ "base_url": "https://your.homeserver.org" @@ -56,7 +59,9 @@ class HomeserverConfigurationTests: XCTestCase { "preferredDomain" : expectedDeprecatedJitsiServer ], "io.element.e2ee" : [ - "default" : expectedE2EEEByDefaultEnabled + "default" : expectedE2EEEByDefaultEnabled, + "secure_backup_required": expectedSecureBackupRequired, + "secure_backup_setup_methods": secureBackupSetupMethods ], "io.element.jitsi" : [ "preferredDomain" : expectedJitsiServer @@ -70,8 +75,34 @@ class HomeserverConfigurationTests: XCTestCase { XCTAssertEqual(homeserverConfiguration.jitsi.serverDomain, expectedJitsiServer) XCTAssertEqual(homeserverConfiguration.jitsi.serverURL.absoluteString, expectedJitsiServerStringURL) - XCTAssertEqual(homeserverConfiguration.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) - + XCTAssertEqual(homeserverConfiguration.encryption.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + XCTAssertEqual(homeserverConfiguration.encryption.isSecureBackupRequired, expectedSecureBackupRequired) + XCTAssertEqual(homeserverConfiguration.encryption.secureBackupSetupMethods, expectedSecureBackupSetupMethods) XCTAssertEqual(homeserverConfiguration.tileServer.mapStyleURL.absoluteString, expectedMapStyleURLString) } + + func testHomeserverEncryptionConfigurationDefaults() { + + let expectedE2EEEByDefaultEnabled = true + let expectedSecureBackupRequired = false + let expectedSecureBackupSetupMethods: [VectorWellKnownBackupSetupMethod] = [.passphrase, .key] + + let wellKnownDictionary: [String: Any] = [ + "m.homeserver": [ + "base_url": "https://your.homeserver.org" + ], + "m.identity_server": [ + "base_url": "https://your.identity-server.org" + ] + ] + + let wellKnown = MXWellKnown(fromJSON: wellKnownDictionary) + + let homeserverConfigurationBuilder = HomeserverConfigurationBuilder() + let homeserverConfiguration = homeserverConfigurationBuilder.build(from: wellKnown) + + XCTAssertEqual(homeserverConfiguration.encryption.isE2EEByDefaultEnabled, expectedE2EEEByDefaultEnabled) + XCTAssertEqual(homeserverConfiguration.encryption.isSecureBackupRequired, expectedSecureBackupRequired) + XCTAssertEqual(homeserverConfiguration.encryption.secureBackupSetupMethods, expectedSecureBackupSetupMethods) + } } diff --git a/changelog.d/5745.change b/changelog.d/5745.change new file mode 100644 index 000000000..e97d3d92b --- /dev/null +++ b/changelog.d/5745.change @@ -0,0 +1 @@ +Secure Backup: Add support for mandatory backup/verification