diff --git a/CHANGES.rst b/CHANGES.rst index eb030c58b..399f93983 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,12 +4,14 @@ Changes in 0.11.6 (2020-xx-xx) Improvements: * PushNotificationService: Move all notification related code to a new class (PR #3100). * Cross-signing: Bootstrap cross-sign on registration (and login if applicable). This action is now invisible to the user (#3292). + * Cross-signing: Setup cross-signing for existing users (#3299). * Authentication: Redirect the webview (SSO) javascript logs to iOS native logs. * Timeline: Hide encrypted history (pre-invite) (#3239). * Complete security: Add recovery from 4S (#3304). * Key backup: Connect/restore backup created with SSSS (#3124). * E2E by default: Disable it if the HS admin disabled it (#3305). * Key backup: Add secure backup creation flow (#3344). + * Add AuthenticatedSessionViewControllerFactory to set up a authenticated flow for a given CS API request. * Set up SSSS from banners (#3293). Bug fix: diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index fb86d3110..2cc2accf3 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 32F6B96D2270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9672270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift */; }; 32F6B96E2270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F6B9682270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift */; }; 32FDC1CD2386CD390084717A /* RiotSettingIntegrationProvisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */; }; + 32FEFA9924A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */; }; 3AF393339D2D566CE14AC200 /* Pods_RiotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 129EB7E27E7E4AC3F5F098F5 /* Pods_RiotTests.framework */; }; 405FD41D306133A48D9B5AA1 /* Pods_RiotPods_Riot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACF09217ADF1D7E7A35BC02 /* Pods_RiotPods_Riot.framework */; }; 670966FEFE120D865FD8A5B6 /* Pods_RiotPods_SiriIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51187E952D5CECF6D6F5A28E /* Pods_RiotPods_SiriIntents.framework */; }; @@ -951,6 +952,7 @@ 32F6B9672270623100BBA352 /* KeyVerificationDataLoadingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyVerificationDataLoadingViewModel.swift; sourceTree = ""; }; 32F6B9682270623100BBA352 /* KeyVerificationDataLoadingViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyVerificationDataLoadingViewModelType.swift; sourceTree = ""; }; 32FDC1CC2386CD390084717A /* RiotSettingIntegrationProvisioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiotSettingIntegrationProvisioning.swift; sourceTree = ""; }; + 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedSessionViewControllerFactory.swift; sourceTree = ""; }; 3942DD65EBEB7AE647C6392A /* Pods-RiotPods-SiriIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RiotPods-SiriIntents.debug.xcconfig"; path = "Target Support Files/Pods-RiotPods-SiriIntents/Pods-RiotPods-SiriIntents.debug.xcconfig"; sourceTree = ""; }; 3D78489021AC9E6400B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; 3D78489121AC9E6500B98A7D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -2200,6 +2202,14 @@ path = Modal; sourceTree = ""; }; + 32FEFA9724A52861005237F6 /* AuthenticatedSession */ = { + isa = PBXGroup; + children = ( + 32FEFA9824A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift */, + ); + path = AuthenticatedSession; + sourceTree = ""; + }; 4220F60B660591FD80AF3428 /* Pods */ = { isa = PBXGroup; children = ( @@ -3021,6 +3031,7 @@ B1B5567620EE6C4C00210D55 /* Modules */ = { isa = PBXGroup; children = ( + 32FEFA9724A52861005237F6 /* AuthenticatedSession */, B1B556EA20EE6C4C00210D55 /* Main */, B1B556CA20EE6C4C00210D55 /* TabBar */, B1B556F920EE6C4C00210D55 /* Authentication */, @@ -5464,6 +5475,7 @@ 3232AB4F2256558300AD6A5C /* TemplateScreenViewController.swift in Sources */, B1B558FC20EF768F00210D55 /* RoomIncomingTextMsgWithPaginationTitleBubbleCell.m in Sources */, B1B5572920EE6C4D00210D55 /* RoomFilesViewController.m in Sources */, + 32FEFA9924A528FD005237F6 /* AuthenticatedSessionViewControllerFactory.swift in Sources */, B1BEE74B23E093260003A4CB /* UserVerificationSessionStatusViewAction.swift in Sources */, B1098C1021ED07E4000DDA48 /* Presentable.swift in Sources */, B1BEE73923DF44A60003A4CB /* UserVerificationSessionsStatusViewController.swift in Sources */, diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index b0458f718..5ea747b71 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -605,6 +605,8 @@ "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; +// AuthenticatedSessionViewControllerFactory +"authenticated_session_flow_not_supported" = "This app does not support the authentication mechanism on your homeserver."; // Identity server settings "identity_server_settings_title" = "Identity Server"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index f43fdcea6..5ebdbfd12 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -310,6 +310,10 @@ internal enum VectorL10n { internal static var authUsernameInUse: String { return VectorL10n.tr("Vector", "auth_username_in_use") } + /// This app does not support the authentication mechanism on your homeserver. + internal static var authenticatedSessionFlowNotSupported: String { + return VectorL10n.tr("Vector", "authenticated_session_flow_not_supported") + } /// Back internal static var back: String { return VectorL10n.tr("Vector", "back") diff --git a/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift new file mode 100644 index 000000000..56b4dc4fe --- /dev/null +++ b/Riot/Modules/AuthenticatedSession/AuthenticatedSessionViewControllerFactory.swift @@ -0,0 +1,223 @@ +/* + Copyright 2020 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 + +enum AuthenticatedSessionViewControllerFactoryError: Int, Error, CustomNSError { + case flowNotSupported = 0 + + // MARK: - CustomNSError + + static let errorDomain = "AuthenticatedSessionViewControllerFactoryErrorDomain" + + var errorCode: Int { return self.rawValue } + + var errorUserInfo: [String: Any] { + let userInfo: [String: Any] + + switch self { + case .flowNotSupported: + userInfo = [NSLocalizedDescriptionKey: VectorL10n.authenticatedSessionFlowNotSupported] + } + return userInfo + } +} + +/// This class creates view controllers that can handle an authentication flow for given requests. +@objcMembers +final class AuthenticatedSessionViewControllerFactory: NSObject { + + // MARK: - Constants + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + + + // MARK: Public + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } + + + // MARK: - Public methods + + /// Create a view controller to handle an authentication flow for a given request. + /// + /// - Parameters: + /// - path: the request path. + /// - httpMethod: the request http method. + /// - title: the title to use in the view controller. + /// - message: the information to display in the view controller. + /// - onViewController: the block called when the view controller is ready. The caller must display it. + /// - onAuthenticated: the block called when the user finished to enter their credentials. + /// - onCancelled: the block called when the user cancelled the authentication. + /// - onFailure: the blocked called on error. + func viewController(forPath path: String, + httpMethod: String, + title: String?, + message: String?, + onViewController: @escaping (UIViewController) -> Void, + onAuthenticated: @escaping ([String: Any]) -> Void, + onCancelled: @escaping () -> Void, + onFailure: @escaping (Error) -> Void) -> MXHTTPOperation { + + // Get the authentication flow required for this API + return session.matrixRestClient.authSessionForRequest(withMethod: httpMethod, path: path, parameters: [:], success: { [weak self] (authenticationSession) in + guard let self = self else { + return + } + + guard let authenticationSession = authenticationSession, let flows = authenticationSession.flows else { + onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) + return + } + + // Return the corresponding VC + if self.hasPasswordFlow(inFlows: flows) { + let authViewController = self.createPasswordViewController(title: title, + message: message, + authenticationSession: authenticationSession, + onAuthenticated: onAuthenticated, + onCancelled: onCancelled, + onFailure: onFailure) + onViewController(authViewController) + } else { + // Flow not supported yet + onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) + } + + }, failure: { (error) in + guard let error = error else { + return + } + + onFailure(error) + }) + } + + /// Check if we support the authentication flow for a given request. + /// + /// - Parameters: + /// - path: the request path. + /// - httpMethod: the request http method. + /// - onCancelled: the block called when the user cancelled the authentication. + /// - onFailure: the blocked called on error. + func hasSupport(forPath path: String, + httpMethod: String, + success: @escaping (Bool) -> Void, + failure: @escaping (Error) -> Void) -> MXHTTPOperation { + + // Get the authentication flow required for this API + return session.matrixRestClient.authSessionForRequest(withMethod: httpMethod, path: path, parameters: [:], success: { [weak self] (authenticationSession) in + guard let self = self else { + return + } + + guard let authenticationSession = authenticationSession, let flows = authenticationSession.flows else { + success(false) + return + } + + // Return the corresponding VC + if self.hasPasswordFlow(inFlows: flows) { + success(true) + } else { + // Flow not supported yet + success(false) + } + + }, failure: { (error) in + guard let error = error else { + return + } + + failure(error) + }) + } + + + // MARK: - Private methods + + // MARK: - Password flow + + private func hasPasswordFlow(inFlows flows: [MXLoginFlow]) -> Bool { + for flow in flows { + if flow.type == kMXLoginFlowTypePassword || flow.stages.contains(kMXLoginFlowTypePassword) { + return true + } + } + + return false + } + + private func createPasswordViewController( + title: String?, + message: String?, + authenticationSession: MXAuthenticationSession, + onAuthenticated: @escaping ([String: Any]) -> Void, + onCancelled: @escaping () -> Void, + onFailure: @escaping (Error) -> Void) -> UIViewController { + + // Use a simple UIAlertController as before + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + alertController.addTextField { (textField) in + textField.isSecureTextEntry = true + textField.placeholder = VectorL10n.authPasswordPlaceholder + textField.keyboardType = .default + } + + alertController.addAction(UIAlertAction(title: Bundle.mxk_localizedString(forKey: "cancel"), style: .cancel, handler: { _ in + onCancelled() + })) + + alertController.addAction(UIAlertAction(title: Bundle.mxk_localizedString(forKey: "ok"), style: .default, handler: { _ in + + guard let password = alertController.textFields?.first?.text else { + // Should not happen + return + } + + guard let authParams = self.createAuthParams(password: password, authenticationSession: authenticationSession) else { + onFailure(AuthenticatedSessionViewControllerFactoryError.flowNotSupported) + return + } + + onAuthenticated(authParams) + })) + + return alertController + } + + private func createAuthParams(password: String, + authenticationSession: MXAuthenticationSession) -> [String: Any]? { + guard let userId = self.session.myUserId, let session = authenticationSession.session else { + return nil + } + + return [ + "type": kMXLoginFlowTypePassword, + "session": session, + "user": userId, + "password": password + ] + } +} diff --git a/Riot/Modules/Authentication/AuthenticationViewController.m b/Riot/Modules/Authentication/AuthenticationViewController.m index cb58d39bc..cc5bf71c6 100644 --- a/Riot/Modules/Authentication/AuthenticationViewController.m +++ b/Riot/Modules/Authentication/AuthenticationViewController.m @@ -1169,7 +1169,7 @@ { NSLog(@"[AuthenticationVC] sessionStateDidChange: Bootstrap with password"); - [session.crypto.crossSigning bootstrapWithPassword:self.authInputsView.password success:^{ + [session.crypto.crossSigning setupWithPassword:self.authInputsView.password success:^{ NSLog(@"[AuthenticationVC] sessionStateDidChange: Bootstrap succeeded"); [self dismiss]; } failure:^(NSError * _Nonnull error) { diff --git a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift index d06423983..c8c97a5c4 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithKey/SecretsRecoveryWithKeyViewController.swift @@ -99,7 +99,7 @@ final class SecretsRecoveryWithKeyViewController: UIViewController { let informationText: String switch self.viewModel.recoveryGoal { - case .default, .keyBackup: + case .default, .keyBackup, .restoreSecureBackup: informationText = VectorL10n.secretsRecoveryWithKeyInformationDefault case .verifyDevice: informationText = VectorL10n.secretsRecoveryWithKeyInformationVerifyDevice diff --git a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift index 3532ab409..efa5265c0 100644 --- a/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift +++ b/Riot/Modules/Secrets/Recover/RecoverWithPassphrase/SecretsRecoveryWithPassphraseViewController.swift @@ -102,7 +102,7 @@ final class SecretsRecoveryWithPassphraseViewController: UIViewController { let informationText: String switch self.viewModel.recoveryGoal { - case .default, .keyBackup: + case .default, .keyBackup, .restoreSecureBackup: informationText = VectorL10n.secretsRecoveryWithPassphraseInformationDefault case .verifyDevice: informationText = VectorL10n.secretsRecoveryWithPassphraseInformationVerifyDevice diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift index c14f053f4..0ddff770c 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryCoordinatorBridgePresenter.swift @@ -32,7 +32,7 @@ final class SecretsRecoveryCoordinatorBridgePresenter: NSObject { private let session: MXSession private let recoveryMode: SecretsRecoveryMode - private let recoveryGoal: SecretsRecoveryGoal + let recoveryGoal: SecretsRecoveryGoal private var coordinator: SecretsRecoveryCoordinator? diff --git a/Riot/Modules/Secrets/Recover/SecretsRecoveryGoal.swift b/Riot/Modules/Secrets/Recover/SecretsRecoveryGoal.swift index 61158bb42..0017d2b75 100644 --- a/Riot/Modules/Secrets/Recover/SecretsRecoveryGoal.swift +++ b/Riot/Modules/Secrets/Recover/SecretsRecoveryGoal.swift @@ -21,4 +21,5 @@ enum SecretsRecoveryGoal: Int { case `default` case keyBackup case verifyDevice + case restoreSecureBackup } diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 582be5281..54ee656e9 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -29,11 +29,17 @@ #import "Riot-Swift.h" +// Dev flag for demoing what could be the settings +// There is still a lot of TODO behind +//#define NEW_CROSS_SIGNING_FLOW enum { SECTION_CRYPTO_SESSIONS, SECTION_CROSSSIGNING, +#ifdef NEW_CROSS_SIGNING_FLOW + SECTION_SECURE_BACKUP, +#endif SECTION_CRYPTOGRAPHY, SECTION_KEYBACKUP, SECTION_ADVANCED, @@ -46,6 +52,22 @@ enum { CROSSSIGNING_SECOND_ACTION, // Reset }; +enum { + SECURE_BACKUP_DESCRIPTION, + // TODO: We can display the state of 4S both locally and on the server. Then, provide actions according to all combinations. + // - Does the 4S contains all the 4 keys server side? + // - Advice the user to do a recovery if there is less keys locally + // - Advice them to do a recovery if local keys are obsolete -> We cannot know now + // - Advice them to fix a secure backup if there is 4S but no key backup + // - Warm them if there is no 4S and they do not have all 3 signing keys locally. They will set up a not complete secure backup + SECURE_BACKUP_INFO, + SECURE_BACKUP_SETUP, + SECURE_BACKUP_RESTORE, + SECURE_BACKUP_DELETE, + SECURE_BACKUP_MANAGE_MANUALLY, // TODO: What to do with that? +}; + + enum { CRYPTOGRAPHY_INFO, CRYPTOGRAPHY_EXPORT, // TODO: To move to SECTION_KEYBACKUP @@ -64,7 +86,8 @@ SettingsKeyBackupTableViewSectionDelegate, KeyBackupSetupCoordinatorBridgePresenterDelegate, KeyBackupRecoverCoordinatorBridgePresenterDelegate, UIDocumentInteractionControllerDelegate, -SecretsRecoveryCoordinatorBridgePresenterDelegate> +SecretsRecoveryCoordinatorBridgePresenterDelegate, +SecureBackupSetupCoordinatorBridgePresenterDelegate> { // Current alert (if any). UIAlertController *currentAlert; @@ -72,6 +95,9 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> // Devices NSMutableArray *devicesArray; + // SECURE_BACKUP_* rows to display + NSArray *secureBackupSectionState; + // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; @@ -94,6 +120,8 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> @property (nonatomic) BOOL isLoadingDevices; @property (nonatomic, strong) MXKeyBackupVersion *currentkeyBackupVersion; +@property (nonatomic, strong) SecureBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter; +@property (nonatomic, strong) AuthenticatedSessionViewControllerFactory *authenticatedSessionViewControllerFactory; @end @@ -132,7 +160,8 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> [self.tableView registerClass:MXKTableViewCellWithLabelAndSwitch.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndSwitch defaultReuseIdentifier]]; [self.tableView registerNib:MXKTableViewCellWithTextView.nib forCellReuseIdentifier:[MXKTableViewCellWithTextView defaultReuseIdentifier]]; - + [self.tableView registerNib:MXKTableViewCellWithButton.nib forCellReuseIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; + // Enable self sizing cells self.tableView.rowHeight = UITableViewAutomaticDimension; self.tableView.estimatedRowHeight = 50; @@ -396,6 +425,8 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> - (void)reloadData { + [self refreshSecureBackupSectionData]; + // Trigger a full table reloadData [self.tableView reloadData]; } @@ -455,6 +486,13 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> break; case MXCrossSigningStateCanCrossSign: crossSigningInformation = [NSBundle mxk_localizedStringForKey:@"security_settings_crosssigning_info_ok"]; + +#ifdef NEW_CROSS_SIGNING_FLOW + if (![self.mainSession.crypto.recoveryService hasSecretLocally:MXSecretId.crossSigningMaster]) + { + crossSigningInformation = [crossSigningInformation stringByAppendingString:@"\n\n⚠️ The MSK is missing. Verify this device again or use the Secure Backup below to synchronise your keys accross your devices"]; + } +#endif break; } @@ -524,12 +562,88 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> [buttonCell.mxkButton setTitle:btnTitle forState:UIControlStateNormal]; [buttonCell.mxkButton setTitle:btnTitle forState:UIControlStateHighlighted]; - [buttonCell.mxkButton addTarget:self action:@selector(bootstrapCrossSigning:) forControlEvents:UIControlEventTouchUpInside]; + [buttonCell.mxkButton addTarget:self action:@selector(setupCrossSigning:) forControlEvents:UIControlEventTouchUpInside]; } -- (void)bootstrapCrossSigning:(UITapGestureRecognizer *)recognizer +- (void)setupCrossSigning:(id)sender { +#ifdef NEW_CROSS_SIGNING_FLOW + __block UIViewController *viewController; + [self startActivityIndicator]; + + // Get credentials to set up cross-signing + NSString *path = [NSString stringWithFormat:@"%@/keys/device_signing/upload", kMXAPIPrefixPathUnstable]; + _authenticatedSessionViewControllerFactory = [[AuthenticatedSessionViewControllerFactory alloc] initWithSession:self.mainSession]; + [_authenticatedSessionViewControllerFactory viewControllerForPath:path + httpMethod:@"POST" + title:@"Set up cross-signing" // TODO + message:@"Confirm your identity by entering your account password" // TODO + onViewController:^(UIViewController * _Nonnull theViewController) + { + viewController = theViewController; + [self presentViewController:viewController animated:YES completion:nil]; + + } onAuthenticated:^(NSDictionary * _Nonnull authParams) { + + [viewController dismissViewControllerAnimated:NO completion:nil]; + viewController = nil; + + MXCrossSigning *crossSigning = self.mainSession.crypto.crossSigning; + if (crossSigning) + { + [crossSigning setupWithAuthParams:authParams success:^{ + [self stopActivityIndicator]; + [self reloadData]; + } failure:^(NSError * _Nonnull error) { + [self stopActivityIndicator]; + [self reloadData]; + + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } + + } onCancelled:^{ + [self stopActivityIndicator]; + + [viewController dismissViewControllerAnimated:NO completion:nil]; + viewController = nil; + } onFailure:^(NSError * _Nonnull error) { + + [self stopActivityIndicator]; + [[AppDelegate theDelegate] showErrorAsAlert:error]; + + [viewController dismissViewControllerAnimated:NO completion:nil]; + viewController = nil; + }]; + +#else [self displayComingSoon]; +#endif +} + +- (void)resetCrossSigning:(id)sender +{ + [currentAlert dismissViewControllerAnimated:NO completion:nil]; + + // Double confirmation + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Are you sure?" // TODO + message:@"Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from." // TODO + preferredStyle:UIAlertControllerStyleAlert]; + + [alertController addAction:[UIAlertAction actionWithTitle:@"Reset" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) + { + // Setup and reset are the same thing + [self setupCrossSigning:nil]; + }]]; + + [alertController addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:UIAlertActionStyleCancel + handler:nil]]; + + [self presentViewController:alertController animated:YES completion:nil]; + currentAlert = alertController; } - (void)setUpcrossSigningButtonCellForReset:(MXKTableViewCellWithButton*)buttonCell @@ -543,11 +657,6 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> [buttonCell.mxkButton addTarget:self action:@selector(resetCrossSigning:) forControlEvents:UIControlEventTouchUpInside]; } -- (void)resetCrossSigning:(UITapGestureRecognizer *)recognizer -{ - [self displayComingSoon]; -} - - (void)setUpcrossSigningButtonCellForCompletingSecurity:(MXKTableViewCellWithButton*)buttonCell { NSString *btnTitle = [NSBundle mxk_localizedStringForKey:@"security_settings_crosssigning_complete_security"]; @@ -563,6 +672,218 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> } +#pragma mark - SSSS + +- (void)refreshSecureBackupSectionData +{ + // TODO + MXRecoveryService *recoveryService = self.mainSession.crypto.recoveryService; + if (recoveryService.hasRecovery) + { + secureBackupSectionState = @[ + @(SECURE_BACKUP_INFO), + @(SECURE_BACKUP_RESTORE), + @(SECURE_BACKUP_DELETE), + @(SECURE_BACKUP_DESCRIPTION), + //@(SECURE_BACKUP_MANAGE_MANUALLY), + ]; + } + else + { + if (self.canSetupSecureBackup) + { + secureBackupSectionState = @[ + @(SECURE_BACKUP_INFO), + @(SECURE_BACKUP_SETUP), // TODO: Check we have all keys locally (at least MSK, SSK & SSK) + @(SECURE_BACKUP_DESCRIPTION), + //@(SECURE_BACKUP_MANAGE_MANUALLY), + ]; + } + else + { + secureBackupSectionState = @[ + @(SECURE_BACKUP_INFO), + @(SECURE_BACKUP_DESCRIPTION), + //@(SECURE_BACKUP_MANAGE_MANUALLY), + ]; + } + } +} + +- (NSUInteger)secureBackupSectionEnumForRow:(NSUInteger)row +{ + if (row < secureBackupSectionState.count) + { + return secureBackupSectionState[row].unsignedIntegerValue; + } + + return SECURE_BACKUP_DESCRIPTION; +} + +- (NSUInteger)numberOfRowsInSecureBackupSection +{ + return secureBackupSectionState.count; +} + +- (NSString*)secureBackupInformation +{ + NSString *secureBackupInformation; + + MXRecoveryService *recoveryService = self.mainSession.crypto.recoveryService; + + if (recoveryService.hasRecovery) + { + NSMutableString *mutableString = [@"Your account has a Secure Backup.\n" mutableCopy]; + + // Check all keys that should be in the SSSSS + // TODO: Check obsoletes ones but need spec update + + BOOL hasWarning = NO; + NSString *keyState = [self informationForSecret:MXSecretId.crossSigningMaster secretName:@"Cross-signing" hasWarning:&hasWarning]; + if (keyState) + { + [mutableString appendString:keyState]; + } + + keyState = [self informationForSecret:MXSecretId.crossSigningSelfSigning secretName:@"Self signing" hasWarning:&hasWarning]; + if (keyState) + { + [mutableString appendString:keyState]; + } + + keyState = [self informationForSecret:MXSecretId.crossSigningUserSigning secretName:@"User signing" hasWarning:&hasWarning]; + if (keyState) + { + [mutableString appendString:keyState]; + } + + keyState = [self informationForSecret:MXSecretId.keyBackup secretName:@"Message Backup" hasWarning:&hasWarning]; + if (keyState) + { + [mutableString appendString:keyState]; + } + else + { + if (self.mainSession.crypto.backup.keyBackupVersion) + { + [mutableString appendString:@"\n\n⚠️ The key of your current Message backup is not in the Secure Backup. Restore it first (see below)."]; + } + else + { + [mutableString appendString:@"\n\n⚠️ Consider create a Message Backup (see below)."]; + } + } + + if (!hasWarning) + { + [mutableString appendFormat:@"\n\nIf you are facing an issue, synchronise your Secure Backup."]; + } + + secureBackupInformation = mutableString; + } + else + { + if (self.canSetupSecureBackup) + { + secureBackupInformation = [NSString stringWithFormat:@"No Secure Backup. Create one.\n-----\nKeys to back up: %@", recoveryService.secretsStoredLocally]; + } + else + { + secureBackupInformation = [NSString stringWithFormat:@"No Secure Backup. Set up cross-signing first (see above)"]; + } + } + + return secureBackupInformation; +} + +- (nullable NSString*)informationForSecret:(NSString*)secretId secretName:(NSString*)secretName hasWarning:(BOOL*)hasWarning +{ + NSString *information; + + MXRecoveryService *recoveryService = self.mainSession.crypto.recoveryService; + + if ([recoveryService hasSecretWithSecretId:secretId]) + { + if ([recoveryService hasSecretLocally:secretId]) + { + information = [NSString stringWithFormat:@"\n ✅ %@ is in the backup", secretName]; + } + else + { + information = [NSString stringWithFormat:@"\n ⚠️ %@ is in the backup but not locally. Tap Synchronise", secretName]; + *hasWarning |= YES; + } + } + else + { + if ([recoveryService hasSecretLocally:secretId]) + { + information = [NSString stringWithFormat:@"\n ⚠️ %@ is not in the backup. Tap Synchronise", secretName]; + *hasWarning |= YES; + } + } + + return information; +} + +- (BOOL)canSetupSecureBackup +{ + // Accept to create a setup only if we have the 3 cross-signing keys + // This is the path to have a sane state + // TODO: What about missing MSK that was not gossiped before? + + MXRecoveryService *recoveryService = self.mainSession.crypto.recoveryService; + + NSArray *crossSigningServiceSecrets = @[ + MXSecretId.crossSigningMaster, + MXSecretId.crossSigningSelfSigning, + MXSecretId.crossSigningUserSigning]; + + return ([recoveryService.secretsStoredLocally mx_intersectArray:crossSigningServiceSecrets].count + == crossSigningServiceSecrets.count); +} + +- (void)setupSecureBackup +{ +#ifdef NEW_CROSS_SIGNING_FLOW + SecureKeyBackupSetupCoordinatorBridgePresenter *secureBackupSetupCoordinatorBridgePresenter = [[SecureKeyBackupSetupCoordinatorBridgePresenter alloc] initWithSession:self.mainSession]; + secureBackupSetupCoordinatorBridgePresenter.delegate = self; + + [secureBackupSetupCoordinatorBridgePresenter presentFrom:self animated:YES]; + + self.secureBackupSetupCoordinatorBridgePresenter = secureBackupSetupCoordinatorBridgePresenter; +#else + [self displayComingSoon]; +#endif +} + +- (void)restoreFromSecureBackup +{ + secretsRecoveryCoordinatorBridgePresenter = [[SecretsRecoveryCoordinatorBridgePresenter alloc] initWithSession:self.mainSession recoveryGoal:SecretsRecoveryGoalRestoreSecureBackup]; + + [secretsRecoveryCoordinatorBridgePresenter presentFrom:self animated:true]; + secretsRecoveryCoordinatorBridgePresenter.delegate = self; +} + +- (void)deleteSecureBackup +{ + MXRecoveryService *recoveryService = self.mainSession.crypto.recoveryService; + if (recoveryService) + { + [self startActivityIndicator]; + [recoveryService deleteRecoveryWithSuccess:^{ + [self stopActivityIndicator]; + [self reloadData]; + } failure:^(NSError * _Nonnull error) { + [self stopActivityIndicator]; + [self reloadData]; + + [[AppDelegate theDelegate] showErrorAsAlert:error]; + }]; + } +} + + #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender @@ -596,6 +917,11 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> count = devicesArray.count + 1; } break; +#ifdef NEW_CROSS_SIGNING_FLOW + case SECTION_SECURE_BACKUP: + count = [self numberOfRowsInSecureBackupSection]; + break; +#endif case SECTION_KEYBACKUP: count = keyBackupSection.numberOfRows; break; @@ -723,6 +1049,45 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> return textViewCell; } +- (MXKTableViewCellWithButton *)buttonCellForTableView:(UITableView*)tableView atIndexPath:(NSIndexPath *)indexPath +{ + MXKTableViewCellWithButton *cell = [self.tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier] forIndexPath:indexPath]; + + if (!cell) + { + cell = [[MXKTableViewCellWithButton alloc] init]; + } + else + { + // Fix https://github.com/vector-im/riot-ios/issues/1354 + cell.mxkButton.titleLabel.text = nil; + cell.mxkButton.enabled = YES; + } + + cell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; + [cell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; + + return cell; +} + +- (MXKTableViewCellWithButton *)buttonCellWithTitle:(NSString*)title + action:(SEL)action + forTableView:(UITableView*)tableView + atIndexPath:(NSIndexPath *)indexPath +{ + MXKTableViewCellWithButton *cell = [self buttonCellForTableView:tableView atIndexPath:indexPath]; + + + [cell.mxkButton setTitle:title forState:UIControlStateNormal]; + [cell.mxkButton setTitle:title forState:UIControlStateHighlighted]; + + [cell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; + [cell.mxkButton addTarget:self action:action forControlEvents:UIControlEventTouchUpInside]; + cell.mxkButton.accessibilityIdentifier = nil; + + return cell; +} + - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger section = indexPath.section; @@ -762,6 +1127,79 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> } } } +#ifdef NEW_CROSS_SIGNING_FLOW + else if (section == SECTION_SECURE_BACKUP) + { + switch ([self secureBackupSectionEnumForRow:row]) + { + case SECURE_BACKUP_DESCRIPTION: + { + // TODO + cell = [self descriptionCellForTableView:tableView + //withText:@"Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server."]; + //withText:@"Back up your encryption keys with your account data in case you lose access to your logins. Your keys will be secured with a unique Recovery Key."]; + withText:@"Back up your encryption keys with your account data in case you lose access to your logins. Your keys are secured with a Recovery Key or a Secret Phrase."]; + break; + } + case SECURE_BACKUP_INFO: + { + // TODO + cell = [self descriptionCellForTableView:tableView + withText:self.secureBackupInformation]; + break; + } + case SECURE_BACKUP_SETUP: + { + // TODO: Button or cell? +// MXKTableViewCellWithTextView *textCell = [self textViewCellForTableView:tableView atIndexPath:indexPath]; +// textCell.mxkTextView.text = @"Set up Secure Backup"; // TODO +// textCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; +// +// cell = textCell; + + MXKTableViewCellWithButton *buttonCell = [self buttonCellWithTitle:@"Set up Secure Backup" // TODO + action:@selector(setupSecureBackup) + forTableView:tableView + atIndexPath:indexPath]; + + cell = buttonCell; + break; + } + case SECURE_BACKUP_RESTORE: + { + MXKTableViewCellWithButton *buttonCell = [self buttonCellWithTitle:@"Synchronise (Restore and/or Back up)" // TODO + action:@selector(restoreFromSecureBackup) + forTableView:tableView + atIndexPath:indexPath]; + + cell = buttonCell; + break; + } + case SECURE_BACKUP_DELETE: + { + MXKTableViewCellWithButton *buttonCell = [self buttonCellWithTitle:@"Delete Secure Backup" // TODO + action:@selector(deleteSecureBackup) + forTableView:tableView + atIndexPath:indexPath]; + buttonCell.mxkButton.tintColor = ThemeService.shared.theme.warningColor; + + cell = buttonCell; + break; + } + + case SECURE_BACKUP_MANAGE_MANUALLY: + { + MXKTableViewCellWithTextView *textCell = [self textViewCellForTableView:tableView atIndexPath:indexPath]; + textCell.mxkTextView.text = @"Advanced: Manually manage keys"; // TODO + textCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + + cell = textCell; + break; + } + } + + } +#endif else if (section == SECTION_KEYBACKUP) { cell = [keyBackupSection cellForRowAtRow:row]; @@ -798,27 +1236,10 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> } case CRYPTOGRAPHY_EXPORT: { - MXKTableViewCellWithButton *exportKeysBtnCell = [tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; - if (!exportKeysBtnCell) - { - exportKeysBtnCell = [[MXKTableViewCellWithButton alloc] init]; - } - else - { - exportKeysBtnCell.mxkButton.titleLabel.text = nil; - exportKeysBtnCell.mxkButton.enabled = YES; - } - - NSString *btnTitle = NSLocalizedStringFromTable(@"security_settings_export_keys_manually", @"Vector", nil); - [exportKeysBtnCell.mxkButton setTitle:btnTitle forState:UIControlStateNormal]; - [exportKeysBtnCell.mxkButton setTitle:btnTitle forState:UIControlStateHighlighted]; - [exportKeysBtnCell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; - exportKeysBtnCell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; - - [exportKeysBtnCell.mxkButton removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside]; - [exportKeysBtnCell.mxkButton addTarget:self action:@selector(exportEncryptionKeys:) forControlEvents:UIControlEventTouchUpInside]; - exportKeysBtnCell.mxkButton.accessibilityIdentifier = nil; - + MXKTableViewCellWithButton *exportKeysBtnCell = [self buttonCellWithTitle:NSLocalizedStringFromTable(@"security_settings_export_keys_manually", @"Vector", nil) + action:@selector(exportEncryptionKeys:) + forTableView:tableView + atIndexPath:indexPath]; cell = exportKeysBtnCell; break; } @@ -860,6 +1281,10 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> { case SECTION_CRYPTO_SESSIONS: return NSLocalizedStringFromTable(@"security_settings_crypto_sessions", @"Vector", nil); +#ifdef NEW_CROSS_SIGNING_FLOW + case SECTION_SECURE_BACKUP: + return @"SECURE BACKUP"; // TODO +#endif case SECTION_KEYBACKUP: return NSLocalizedStringFromTable(@"security_settings_backup", @"Vector", nil); case SECTION_CROSSSIGNING: @@ -1099,23 +1524,8 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> - (MXKTableViewCellWithButton *)settingsKeyBackupTableViewSection:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection buttonCellForRow:(NSInteger)buttonCellForRow { - MXKTableViewCellWithButton *cell = [self.tableView dequeueReusableCellWithIdentifier:[MXKTableViewCellWithButton defaultReuseIdentifier]]; - - if (!cell) - { - cell = [[MXKTableViewCellWithButton alloc] init]; - } - else - { - // Fix https://github.com/vector-im/riot-ios/issues/1354 - cell.mxkButton.titleLabel.text = nil; - cell.mxkButton.enabled = YES; - } - - cell.mxkButton.titleLabel.font = [UIFont systemFontOfSize:17]; - [cell.mxkButton setTintColor:ThemeService.shared.theme.tintColor]; - - return cell; + return [self buttonCellForTableView:self.tableView + atIndexPath:[NSIndexPath indexPathForRow:buttonCellForRow inSection:SECTION_KEYBACKUP]] ; } - (void)settingsKeyBackupTableViewSectionShowKeyBackupSetup:(SettingsKeyBackupTableViewSection *)settingsKeyBackupTableViewSection @@ -1262,15 +1672,38 @@ SecretsRecoveryCoordinatorBridgePresenterDelegate> { UIViewController *presentedViewController = [coordinatorBridgePresenter toPresentable]; - if ([presentedViewController isKindOfClass:UINavigationController.class]) + if (coordinatorBridgePresenter.recoveryGoal == SecretsRecoveryGoalKeyBackup) { - UINavigationController *navigationController = (UINavigationController*)self.presentedViewController; - [self pushKeyBackupRecover:self.currentkeyBackupVersion fromNavigationController:navigationController]; + // Go to the true key backup recovery screen + if ([presentedViewController isKindOfClass:UINavigationController.class]) + { + UINavigationController *navigationController = (UINavigationController*)self.presentedViewController; + [self pushKeyBackupRecover:self.currentkeyBackupVersion fromNavigationController:navigationController]; + } + else + { + [self showKeyBackupRecover:self.currentkeyBackupVersion fromViewController:presentedViewController]; + } } else { - [self showKeyBackupRecover:self.currentkeyBackupVersion fromViewController:presentedViewController]; + [secretsRecoveryCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + secretsRecoveryCoordinatorBridgePresenter = nil; } } +#pragma mark - SecureKeyBackupSetupCoordinatorBridgePresenterDelegate + +- (void)secureKeyBackupSetupCoordinatorBridgePresenterDelegateDidComplete:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.secureBackupSetupCoordinatorBridgePresenter = nil; +} + +- (void)secureKeyBackupSetupCoordinatorBridgePresenterDelegateDidCancel:(SecureBackupSetupCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + [self.secureBackupSetupCoordinatorBridgePresenter dismissWithAnimated:YES completion:nil]; + self.secureBackupSetupCoordinatorBridgePresenter = nil; +} + @end