diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 0963b11c0..2c2d75243 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 214A023A26A3029700C24066 /* EmailKeyManagerApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214A023926A3029700C24066 /* EmailKeyManagerApi.swift */; }; 215002A32690B1DD00980DDD /* client_configuraion_with_unknown_flag.json in Resources */ = {isa = PBXBuildFile; fileRef = 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */; }; 215897E8267A553300423694 /* FilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215897E7267A553200423694 /* FilesManager.swift */; }; + 21750D7D26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */; }; + 21750D7F26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */; }; 2196A2202684B9BE001B9E00 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2196A21F2684B9BE001B9E00 /* URLExtension.swift */; }; 21B15C9426AA00A400D8522B /* ClientConfigurationServiceResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B15C9326AA00A400D8522B /* ClientConfigurationServiceResults.swift */; }; 21C7DEFC26669A3700C44800 /* CalendarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */; }; @@ -392,6 +394,8 @@ 214A023926A3029700C24066 /* EmailKeyManagerApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailKeyManagerApi.swift; sourceTree = ""; }; 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_with_unknown_flag.json; sourceTree = ""; }; 215897E7267A553200423694 /* FilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesManager.swift; sourceTree = ""; }; + 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupEKMKeyViewController.swift; sourceTree = ""; }; + 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupCreatePassphraseAbstractViewController.swift; sourceTree = ""; }; 2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; 21B15C9326AA00A400D8522B /* ClientConfigurationServiceResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigurationServiceResults.swift; sourceTree = ""; }; 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExtension.swift; sourceTree = ""; }; @@ -1041,6 +1045,7 @@ 9F1B49E02624E19D00420472 /* Realm Models */ = { isa = PBXGroup; children = ( + 9FC7EAB2266A404D00F3BF5D /* PassPhraseObject.swift */, D2F41372243CC7990066AFB5 /* UserObject.swift */, D27B911C24EFE806002DF0A1 /* ContactObject.swift */, D2E26F6924F25AB800612AF1 /* KeyAlgoObject.swift */, @@ -1588,7 +1593,9 @@ isa = PBXGroup; children = ( 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */, + 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */, 9FA405C6265AEBA40084D133 /* SetupGenerateKeyViewController.swift */, + 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */, C192421E1EC48B6900C3D251 /* SetupBackupsViewController.swift */, 9F268890237DC55600428A94 /* SetupManuallyImportKeyViewController.swift */, 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */, @@ -1628,7 +1635,6 @@ D212D36324C1AC4800035991 /* KeyId.swift */, D212D35C24C1AACF00035991 /* PrvKeyInfo.swift */, D2E26F6B24F25B1F00612AF1 /* KeyAlgo.swift */, - 9FC7EAB2266A404D00F3BF5D /* PassPhraseObject.swift */, 2133D51726A1E45300CC686F /* DecryptedPrivateKey.swift */, ); path = Models; @@ -2565,6 +2571,7 @@ 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */, 9FF0673325520DE400FCC9E6 /* GmailService+send.swift in Sources */, D20D3C892520B67C00D4AA9A /* BackupOptionsViewController.swift in Sources */, + 21750D7D26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift in Sources */, 211392A5266511E6009202EC /* PubLookup.swift in Sources */, D20D3C672520AB1000D4AA9A /* BackupViewController.swift in Sources */, D2E26F6324F1698100612AF1 /* ContactsListViewController.swift in Sources */, @@ -2617,6 +2624,7 @@ 9F0C3C102316DD5B00299985 /* GoogleUserService.swift in Sources */, D24F4C2223E2359B00C5EEE4 /* BootstrapViewController.swift in Sources */, D211CE7623FC36BC00D1CE38 /* UIColorExtension.swift in Sources */, + 21750D7F26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift in Sources */, 9FC7EBAA266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */, 9FA9C83C264C2D75005A9670 /* MessageService.swift in Sources */, D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */, diff --git a/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift b/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift index 187be7eec..7a4eb67c0 100644 --- a/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift +++ b/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift @@ -9,7 +9,7 @@ import FlowCryptUI protocol PassPhraseSaveable { - var shouldSaveLocally: Bool { get set } + var shouldStorePassPhrase: Bool { get set } var passPhraseIndexes: [IndexPath] { get } var saveLocallyNode: CellNode { get } var saveInMemoryNode: CellNode { get } @@ -26,11 +26,11 @@ extension PassPhraseSaveable where Self: TableNodeViewController { } var saveLocallyNode: CellNode { - CheckBoxTextNode(input: .passPhraseLocally(isSelected: self.shouldSaveLocally)) + CheckBoxTextNode(input: .passPhraseLocally(isSelected: self.shouldStorePassPhrase)) } var saveInMemoryNode: CellNode { - CheckBoxTextNode(input: .passPhraseMemory(isSelected: !self.shouldSaveLocally)) + CheckBoxTextNode(input: .passPhraseMemory(isSelected: !self.shouldStorePassPhrase)) } func showPassPhraseErrorAlert() { diff --git a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift index 0365c2524..b12ead124 100644 --- a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift @@ -34,7 +34,7 @@ final class SetupBackupsViewController: TableNodeViewController, PassPhraseSavea private var passPhrase: String? - var shouldSaveLocally = true { + var shouldStorePassPhrase = true { didSet { handleSelectedPassPhraseOption() } @@ -150,7 +150,7 @@ extension SetupBackupsViewController { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) } .forEach { - passPhraseService.savePassPhrase(with: $0, inStorage: shouldSaveLocally) + passPhraseService.savePassPhrase(with: $0, inStorage: shouldStorePassPhrase) } // save keys @@ -256,9 +256,9 @@ extension SetupBackupsViewController: ASTableDelegate, ASTableDataSource { switch part { case .saveLocally: - shouldSaveLocally = true + shouldStorePassPhrase = true case .saveInMemory: - shouldSaveLocally = false + shouldStorePassPhrase = false default: break } diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift new file mode 100644 index 000000000..cb49f7f70 --- /dev/null +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -0,0 +1,278 @@ +// +// SetupCreatePassphraseAbstractViewController.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 13.08.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptCommon +import FlowCryptUI +import Promises + +/** + * Controller which decalres a base logic for passphrase setup + * - Has not to have an instance! + */ + +class SetupCreatePassphraseAbstractViewController: TableNodeViewController, PassPhraseSaveable { + enum Parts: Int, CaseIterable { + case title, description, passPhrase, divider, saveLocally, saveInMemory, action, subtitle + } + + var parts: [Parts] { + Parts.allCases + } + + let decorator: SetupViewDecorator + let core: Core + let router: GlobalRouterType + let user: UserId + let storage: DataServiceType + let keyStorage: KeyStorageType + let passPhraseService: PassPhraseServiceType + + var shouldStorePassPhrase = true { + didSet { + handleSelectedPassPhraseOption() + } + } + + var passPhraseIndexes: [IndexPath] { + [Parts.saveLocally, Parts.saveInMemory] + .map { IndexPath(row: $0.rawValue, section: 0) } + } + + private var passPhrase: String? + + private lazy var logger = Logger.nested(in: Self.self, with: .setup) + + init( + user: UserId, + core: Core = .shared, + router: GlobalRouterType = GlobalRouter(), + decorator: SetupViewDecorator = SetupViewDecorator(), + storage: DataServiceType = DataService.shared, + keyStorage: KeyStorageType = KeyDataStorage(), + passPhraseService: PassPhraseServiceType = PassPhraseService() + ) { + self.user = user + self.core = core + self.router = router + self.decorator = decorator + self.storage = storage + self.keyStorage = keyStorage + self.passPhraseService = passPhraseService + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + func setupAccount(with passphrase: String) { + fatalError("This method has to be overriden") + } +} + +// MARK: - UI + +extension SetupCreatePassphraseAbstractViewController { + func setupUI() { + node.delegate = self + node.dataSource = self + + title = decorator.sceneTitle(for: .createKey) + observeKeyboardNotifications() + } + + // TODO: - Ticket? - Unify this logic for all controllers + // swiftlint:disable discarded_notification_center_observer + private func observeKeyboardNotifications() { + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillShowNotification, + object: nil, + queue: .main + ) { [weak self] notification in + guard let self = self else { return } + self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) + } + + NotificationCenter.default.addObserver( + forName: UIResponder.keyboardWillHideNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.adjustForKeyboard(height: 0) + } + } + + private func adjustForKeyboard(height: CGFloat) { + let insets = UIEdgeInsets(top: 0, left: 0, bottom: height + 5, right: 0) + node.contentInset = insets + node.scrollToRow(at: IndexPath(item: Parts.passPhrase.rawValue, section: 0), at: .middle, animated: true) + } +} + +// MARK: - Setup + +extension SetupCreatePassphraseAbstractViewController { + + func validateAndConfirmNewPassPhraseOrReject(passPhrase: String) -> Promise { + Promise { [weak self] in + guard let self = self else { throw AppErr.nilSelf } + + let strength = try self.core.zxcvbnStrengthBar(passPhrase: passPhrase) + + guard strength.word.pass else { + throw CreateKeyError.weakPassPhrase(strength) + } + + let confirmPassPhrase = try awaitPromise(self.awaitUserPassPhraseEntry()) + + guard confirmPassPhrase != nil else { + throw CreateKeyError.conformingPassPhraseError + } + + guard confirmPassPhrase == passPhrase else { + throw CreateKeyError.doesntMatch + } + } + } + + private func awaitUserPassPhraseEntry() -> Promise { + Promise(on: .main) { [weak self] resolve, _ in + guard let self = self else { throw AppErr.nilSelf } + let alert = UIAlertController( + title: "Pass Phrase", + message: "Confirm Pass Phrase", + preferredStyle: .alert + ) + + alert.addTextField { textField in + textField.isSecureTextEntry = true + textField.accessibilityLabel = "textField" + } + + alert.addAction(UIAlertAction(title: "cancel".localized, style: .default) { _ in + resolve(nil) + }) + + alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { [weak alert] _ in + resolve(alert?.textFields?[0].text) + }) + + self.present(alert, animated: true, completion: nil) + } + } +} + +extension SetupCreatePassphraseAbstractViewController { + func moveToMainFlow() { + router.proceed() + } + + private func showChoosingOptions() { + showToast("Not implemented yet") + } + + private func handleButtonAction() { + view.endEditing(true) + guard let passPhrase = passPhrase, passPhrase.isNotEmpty else { + showAlert(message: "setup_wrong_pass_phrase_retry".localized) + return + } + logger.logInfo("Setup account with \(passPhrase)") + setupAccount(with: passPhrase) + } +} + +// MARK: - ASTableDelegate, ASTableDataSource + +extension SetupCreatePassphraseAbstractViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { + parts.count + } + + func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + return { [weak self] in + guard let self = self else { return ASCellNode() } + let part = self.parts[indexPath.row] + switch part { + case .title: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.decorator.title(for: .setup), + insets: self.decorator.insets.titleInset, + backgroundColor: .backgroundColor + ) + ) + case .description: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.decorator.subtitle(for: .choosingPassPhrase), + insets: self.decorator.insets.subTitleInset, + backgroundColor: .backgroundColor + ) + ) + case .passPhrase: + return TextFieldCellNode(input: .passPhraseTextFieldStyle) { [weak self] action in + guard case let .didEndEditing(value) = action else { return } + self?.passPhrase = value + } + .onShouldReturn { [weak self] _ in + self?.view.endEditing(true) + self?.handleButtonAction() + return true + } + .then { + $0.becomeFirstResponder() + } + case .action: + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .setPassPhrase), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in + self?.handleButtonAction() + } + case .subtitle: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.decorator.passPhraseLostDescription, + insets: .side(8), + backgroundColor: .backgroundColor + ) + ) + case .divider: + return DividerCellNode(inset: self.decorator.insets.dividerInsets) + case .saveLocally: + return self.saveLocallyNode + case .saveInMemory: + return self.saveInMemoryNode + } + } + } + + func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { + let part = parts[indexPath.row] + + switch part { + case .description: + showChoosingOptions() + case .saveLocally: + shouldStorePassPhrase = true + case .saveInMemory: + shouldStorePassPhrase = false + default: + break + } + } +} diff --git a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift new file mode 100644 index 000000000..5dea07992 --- /dev/null +++ b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift @@ -0,0 +1,123 @@ +// +// SetupEKMKeyViewController.swift +// FlowCrypt +// +// Created by Yevhen Kyivskyi on 13.08.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptCommon +import FlowCryptUI +import Promises + +enum CreatePassphraseWithExistingKeyError: Error { + // No private key was found + case noPrivateKey +} + +/** + * Controller which is responsible for setting up a keys received from EKM + * - User is sent here from **SetupInitialViewController** in case he has keys on EKM + * - Here user can enter a pass phrase (will be saved in memory) + * - After passphrase is set up, user will be redirected to **main flow** (inbox view) + */ + +final class SetupEKMKeyViewController: SetupCreatePassphraseAbstractViewController { + + override var parts: [SetupCreatePassphraseAbstractViewController.Parts] { + SetupCreatePassphraseAbstractViewController.Parts.ekmKeysSetup + } + private let keys: [CoreRes.ParseKeys] + + private lazy var logger = Logger.nested(in: Self.self, with: .setup) + + init( + user: UserId, + keys: [CoreRes.ParseKeys] = [], + core: Core = .shared, + router: GlobalRouterType = GlobalRouter(), + decorator: SetupViewDecorator = SetupViewDecorator(), + storage: DataServiceType = DataService.shared, + keyStorage: KeyStorageType = KeyDataStorage(), + passPhraseService: PassPhraseServiceType = PassPhraseService() + ) { + self.keys = keys + super.init( + user: user, + core: core, + router: router, + decorator: decorator, + storage: storage, + keyStorage: keyStorage, + passPhraseService: passPhraseService + ) + self.shouldStorePassPhrase = false + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + override func setupAccount(with passphrase: String) { + setupAccountWithKeysFetchedFromEkm(with: passphrase) + } +} + +// MARK: - Setup + +extension SetupEKMKeyViewController { + + private func setupAccountWithKeysFetchedFromEkm(with passPhrase: String) { + Promise { [weak self] in + guard let self = self else { return } + self.showSpinner() + + try awaitPromise(self.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase)) + + var allFingerprints: [String] = [] + try self.keys.forEach { key in + try key.keyDetails.forEach { keyDetail in + guard let privateKey = keyDetail.private else { + throw CreatePassphraseWithExistingKeyError.noPrivateKey + } + let encryptedPrv = try self.core.encryptKey( + armoredPrv: privateKey, + passphrase: passPhrase + ) + let parsedKey = try self.core.parseKeys(armoredOrBinary: encryptedPrv.encryptedKey.data()) + self.keyStorage.addKeys(keyDetails: parsedKey.keyDetails, source: .ekm, for: self.user.email) + allFingerprints.append(contentsOf: parsedKey.keyDetails.flatMap { $0.fingerprints }) + } + } + let passPhrase = PassPhrase(value: passPhrase, fingerprints: allFingerprints.unique()) + self.passPhraseService.savePassPhrase(with: passPhrase, inStorage: self.shouldStorePassPhrase) + } + .then(on: .main) { [weak self] in + self?.hideSpinner() + self?.moveToMainFlow() + } + .catch(on: .main) { [weak self] error in + guard let self = self else { return } + self.hideSpinner() + + let isErrorHandled = self.handleCommon(error: error) + + if !isErrorHandled { + self.showAlert(error: error, message: "Could not finish setup, please try again") + } + } + } +} + +extension SetupCreatePassphraseAbstractViewController.Parts { + static var ekmKeysSetup: [SetupCreatePassphraseAbstractViewController.Parts] { + return [.title, .description, .passPhrase, .divider, .action, .subtitle] + } +} diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index b5db10e1e..50b5472f7 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -30,34 +30,10 @@ enum CreateKeyError: Error { * - After key is generated, user will be redirected to **main flow** (inbox view) */ -final class SetupGenerateKeyViewController: TableNodeViewController, PassPhraseSaveable { - enum Parts: Int, CaseIterable { - case title, description, passPhrase, divider, saveLocally, saveInMemory, action, subtitle - } +final class SetupGenerateKeyViewController: SetupCreatePassphraseAbstractViewController { - private let parts = Parts.allCases - private let decorator: SetupViewDecorator - private let core: Core - private let router: GlobalRouterType - private let user: UserId private let backupService: BackupServiceType - private let storage: DataServiceType - private let keyStorage: KeyStorageType private let attester: AttesterApiType - let passPhraseService: PassPhraseServiceType - - var shouldSaveLocally = true { - didSet { - handleSelectedPassPhraseOption() - } - } - - var passPhraseIndexes: [IndexPath] { - [Parts.saveLocally, Parts.saveInMemory] - .map { IndexPath(row: $0.rawValue, section: 0) } - } - - private var passPhrase: String? private lazy var logger = Logger.nested(in: Self.self, with: .setup) @@ -72,17 +48,17 @@ final class SetupGenerateKeyViewController: TableNodeViewController, PassPhraseS attester: AttesterApiType = AttesterApi(), passPhraseService: PassPhraseServiceType = PassPhraseService() ) { - self.user = user - self.core = core - self.router = router - self.decorator = decorator self.backupService = backupService - self.storage = storage self.attester = attester - self.keyStorage = keyStorage - self.passPhraseService = passPhraseService - - super.init(node: TableNode()) + super.init( + user: user, + core: core, + router: router, + decorator: decorator, + storage: storage, + keyStorage: keyStorage, + passPhraseService: passPhraseService + ) } @available(*, unavailable) @@ -94,44 +70,9 @@ final class SetupGenerateKeyViewController: TableNodeViewController, PassPhraseS super.viewDidLoad() setupUI() } -} - -// MARK: - UI - -extension SetupGenerateKeyViewController { - private func setupUI() { - node.delegate = self - node.dataSource = self - title = decorator.sceneTitle(for: .createKey) - observeKeyboardNotifications() - } - - // TODO: - Ticket? - Unify this logic for all controllers - // swiftlint:disable discarded_notification_center_observer - private func observeKeyboardNotifications() { - NotificationCenter.default.addObserver( - forName: UIResponder.keyboardWillShowNotification, - object: nil, - queue: .main - ) { [weak self] notification in - guard let self = self else { return } - self.adjustForKeyboard(height: self.keyboardHeight(from: notification)) - } - - NotificationCenter.default.addObserver( - forName: UIResponder.keyboardWillHideNotification, - object: nil, - queue: .main - ) { [weak self] _ in - self?.adjustForKeyboard(height: 0) - } - } - - private func adjustForKeyboard(height: CGFloat) { - let insets = UIEdgeInsets(top: 0, left: 0, bottom: height + 5, right: 0) - node.contentInset = insets - node.scrollToRow(at: IndexPath(item: Parts.passPhrase.rawValue, section: 0), at: .middle, animated: true) + override func setupAccount(with passphrase: String) { + setupAccountWithGeneratedKey(with: passphrase) } } @@ -154,7 +95,7 @@ extension SetupGenerateKeyViewController { let passPhrase = PassPhrase(value: passPhrase, fingerprints: encryptedPrv.key.fingerprints) self.keyStorage.addKeys(keyDetails: [encryptedPrv.key], source: .generated, for: self.user.email) - self.passPhraseService.savePassPhrase(with: passPhrase, inStorage: self.shouldSaveLocally) + self.passPhraseService.savePassPhrase(with: passPhrase, inStorage: self.shouldStorePassPhrase) let updateKey = self.attester.updateKey( email: userId.email, @@ -197,154 +138,4 @@ extension SetupGenerateKeyViewController { } return UserId(email: email, name: name) } - - private func validateAndConfirmNewPassPhraseOrReject(passPhrase: String) -> Promise { - Promise { [weak self] in - guard let self = self else { throw AppErr.nilSelf } - - let strength = try self.core.zxcvbnStrengthBar(passPhrase: passPhrase) - - guard strength.word.pass else { - throw CreateKeyError.weakPassPhrase(strength) - } - - let confirmPassPhrase = try awaitPromise(self.awaitUserPassPhraseEntry()) - - guard confirmPassPhrase != nil else { - throw CreateKeyError.conformingPassPhraseError - } - - guard confirmPassPhrase == passPhrase else { - throw CreateKeyError.doesntMatch - } - } - } - - private func awaitUserPassPhraseEntry() -> Promise { - Promise(on: .main) { [weak self] resolve, _ in - guard let self = self else { throw AppErr.nilSelf } - let alert = UIAlertController( - title: "Pass Phrase", - message: "Confirm Pass Phrase", - preferredStyle: .alert - ) - - alert.addTextField { textField in - textField.isSecureTextEntry = true - textField.accessibilityLabel = "textField" - } - - alert.addAction(UIAlertAction(title: "cancel".localized, style: .default) { _ in - resolve(nil) - }) - - alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { [weak alert] _ in - resolve(alert?.textFields?[0].text) - }) - - self.present(alert, animated: true, completion: nil) - } - } -} - -extension SetupGenerateKeyViewController { - private func moveToMainFlow() { - router.proceed() - } - - private func showChoosingOptions() { - showToast("Not implemented yet") - } - - private func handleButtonAction() { - view.endEditing(true) - guard let passPhrase = passPhrase, passPhrase.isNotEmpty else { - showAlert(message: "setup_wrong_pass_phrase_retry".localized) - return - } - logger.logInfo("Setup account with \(passPhrase)") - setupAccountWithGeneratedKey(with: passPhrase) - } -} - -// MARK: - ASTableDelegate, ASTableDataSource - -extension SetupGenerateKeyViewController: ASTableDelegate, ASTableDataSource { - func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - parts.count - } - - func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - return { [weak self] in - guard let self = self, let part = Parts(rawValue: indexPath.row) else { return ASCellNode() } - switch part { - case .title: - return SetupTitleNode( - SetupTitleNode.Input( - title: self.decorator.title(for: .setup), - insets: self.decorator.insets.titleInset, - backgroundColor: .backgroundColor - ) - ) - case .description: - return SetupTitleNode( - SetupTitleNode.Input( - title: self.decorator.subtitle(for: .choosingPassPhrase), - insets: self.decorator.insets.subTitleInset, - backgroundColor: .backgroundColor - ) - ) - case .passPhrase: - return TextFieldCellNode(input: .passPhraseTextFieldStyle) { [weak self] action in - guard case let .didEndEditing(value) = action else { return } - self?.passPhrase = value - } - .onShouldReturn { [weak self] _ in - self?.view.endEditing(true) - self?.handleButtonAction() - return true - } - .then { - $0.becomeFirstResponder() - } - case .action: - let input = ButtonCellNode.Input( - title: self.decorator.buttonTitle(for: .setPassPhrase), - insets: self.decorator.insets.buttonInsets - ) - return ButtonCellNode(input: input) { [weak self] in - self?.handleButtonAction() - } - case .subtitle: - return SetupTitleNode( - SetupTitleNode.Input( - title: self.decorator.passPhraseLostDescription, - insets: .side(8), - backgroundColor: .backgroundColor - ) - ) - case .divider: - return DividerCellNode(inset: self.decorator.insets.dividerInsets) - case .saveLocally: - return self.saveLocallyNode - case .saveInMemory: - return self.saveInMemoryNode - } - } - } - - func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - guard let part = Parts(rawValue: indexPath.row) else { return } - - switch part { - case .description: - showChoosingOptions() - case .saveLocally: - shouldSaveLocally = true - case .saveInMemory: - shouldSaveLocally = false - default: - break - } - } } diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index 73cfab5a4..beb337c44 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -158,16 +158,10 @@ extension SetupInitialViewController { private func fetchKeysFromEKM() { emailKeyManagerApi.getPrivateKeys() - .then { [weak self] result in - guard let urlString = self?.emailKeyManagerApi.getPrivateKeysUrlString() else { - fatalError("Private keys URL can not be nil at this point") - } - + .then(on: .main) { [weak self] result in switch result { - case .success(decryptedResponse: let response): - self?.showToast( - "organisational_rules_ekm_private_keys_message".localizeWithArguments(response.privateKeys.count, urlString) - ) + case .success(keys: let keys): + self?.proceedToSetupWithEKMKeys(keys: keys) case .noKeys: self?.showRetryAlert( message: "organisational_rules_ekm_empty_private_keys_error".localized, @@ -183,9 +177,6 @@ extension SetupInitialViewController { self?.router.signOut() }) } - // todo - this is temporary, until we finish EKM integration - // instead we should use the keys from EKM for setup - self?.state = .searchingKeyBackupsInInbox } .catch { [weak self] error in if case .noPrivateKeysUrlString = error as? EmailKeyManagerApiError { @@ -329,6 +320,10 @@ extension SetupInitialViewController { let viewController = SetupGenerateKeyViewController(user: user) navigationController?.pushViewController(viewController, animated: true) } + private func proceedToSetupWithEKMKeys(keys: [CoreRes.ParseKeys]) { + let viewController = SetupEKMKeyViewController(user: user, keys: keys) + navigationController?.pushViewController(viewController, animated: true) + } private func proceedToSetupWith(keys: [KeyDetails]) { logger.logInfo("Finish searching for backups in inbox") diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift index 51ad8ad12..a7cf6ae39 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift @@ -34,7 +34,7 @@ final class SetupManuallyEnterPassPhraseViewController: TableNodeViewController, private var passPhrase: String? - var shouldSaveLocally = true { + var shouldStorePassPhrase = true { didSet { handleSelectedPassPhraseOption() } @@ -197,9 +197,9 @@ extension SetupManuallyEnterPassPhraseViewController: ASTableDelegate, ASTableDa switch part { case .saveLocally: - shouldSaveLocally = true + shouldStorePassPhrase = true case .saveInMemory: - shouldSaveLocally = false + shouldStorePassPhrase = false default: break } @@ -249,7 +249,7 @@ extension SetupManuallyEnterPassPhraseViewController { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) } .forEach { - passPhraseService.updatePassPhrase(with: $0, inStorage: shouldSaveLocally) + passPhraseService.updatePassPhrase(with: $0, inStorage: shouldStorePassPhrase) } newKeysToAdd @@ -257,7 +257,7 @@ extension SetupManuallyEnterPassPhraseViewController { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) } .forEach { - passPhraseService.savePassPhrase(with: $0, inStorage: shouldSaveLocally) + passPhraseService.savePassPhrase(with: $0, inStorage: shouldStorePassPhrase) } hideSpinner() diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 6d1240c2f..ed1b64410 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -46,6 +46,11 @@ final class Core: KeyDecrypter, CoreComposeMessageType { return try r.json.decodeJson(as: CoreRes.DecryptKey.self) } + func encryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.EncryptKey { + let r = try call("encryptKey", jsonDict: ["armored": armoredPrv, "passphrase": passphrase], data: nil) + return try r.json.decodeJson(as: CoreRes.EncryptKey.self) + } + func parseDecryptMsg(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?, isEmail: Bool) throws -> CoreRes.ParseDecryptMsg { let json: [String : Any?]? = [ "keys": try keys.map { try $0.toJsonEncodedDict() }, diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 0c8793bd4..ef42117cc 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -40,6 +40,10 @@ struct CoreRes { let decryptedKey: String? } + struct EncryptKey: Decodable { + let encryptedKey: String + } + struct GenerateKey: Decodable { let key: KeyDetails } diff --git a/FlowCrypt/Functionality/Services/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Services/EmailKeyManagerApi.swift index b8e0014c9..d29e3c9b5 100644 --- a/FlowCrypt/Functionality/Services/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Services/EmailKeyManagerApi.swift @@ -20,7 +20,7 @@ enum EmailKeyManagerApiError: Error { } enum EmailKeyManagerApiResult { - case success(decryptedResponse: DecryptedPrivateKeysResponse) + case success(keys: [CoreRes.ParseKeys]) case noKeys case keysAreNotDecrypted } @@ -82,21 +82,22 @@ class EmailKeyManagerApi: EmailKeyManagerApiType { let decryptedPrivateKeysResponse = try JSONDecoder().decode(DecryptedPrivateKeysResponse.self, from: response.data) if decryptedPrivateKeysResponse.privateKeys.isEmpty { - resolve(.success(decryptedResponse: .empty)) + resolve(.noKeys) } let privateKeys = decryptedPrivateKeysResponse.privateKeys .map { $0.decryptedPrivateKey.data() } let parsedPrivateKeys = privateKeys - .map { try? self.core.parseKeys(armoredOrBinary: $0) } + .compactMap { try? self.core.parseKeys(armoredOrBinary: $0) } let areKeysDecrypted = parsedPrivateKeys - .compactMap { $0?.keyDetails.map { $0.isFullyDecrypted } } + .compactMap { $0.keyDetails.map { $0.isFullyDecrypted } } .flatMap { $0 } if areKeysDecrypted.contains(false) { resolve(.keysAreNotDecrypted) } - resolve(.success(decryptedResponse: decryptedPrivateKeysResponse)) + + resolve(.success(keys: parsedPrivateKeys)) } } } diff --git a/FlowCrypt/Models/Realm Models/KeyInfo.swift b/FlowCrypt/Models/Realm Models/KeyInfo.swift index 44e013fdf..a0d65c50c 100644 --- a/FlowCrypt/Models/Realm Models/KeyInfo.swift +++ b/FlowCrypt/Models/Realm Models/KeyInfo.swift @@ -8,6 +8,7 @@ enum KeySource: String { case backup case generated case imported + case ekm } enum KeyInfoError: Error { diff --git a/FlowCrypt/Core/Models/PassPhraseObject.swift b/FlowCrypt/Models/Realm Models/PassPhraseObject.swift similarity index 100% rename from FlowCrypt/Core/Models/PassPhraseObject.swift rename to FlowCrypt/Models/Realm Models/PassPhraseObject.swift