diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 2870e5675..ddb1f1823 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -174,7 +174,10 @@ 75DCE6A02869EBC0003435F1 /* EmptyFolderCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DCE69F2869EBC0003435F1 /* EmptyFolderCellNode.swift */; }; 949ED9422303E3B400530579 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 949ED9412303E3B400530579 /* Colors.xcassets */; }; 95473C1B297E61DE006C8957 /* SequenceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95473C1A297E61DE006C8957 /* SequenceExtension.swift */; }; + 9547EF212A5F106E00A048FF /* PassPhraseAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */; }; 9547EF242A5FBA2B00A048FF /* MenuSeparatorCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */; }; + 958566B72A6126DE001C84D3 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; }; + 958566B92A612822001C84D3 /* ASButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 958566B82A612822001C84D3 /* ASButtonNode.swift */; }; 9F003D6125E1B4ED00EB38C0 /* TrashFolderProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D6025E1B4ED00EB38C0 /* TrashFolderProvider.swift */; }; 9F003D6D25EA8F3200EB38C0 /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D6C25EA8F3200EB38C0 /* SessionManager.swift */; }; 9F0C3C102316DD5B00299985 /* GoogleAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C0F2316DD5B00299985 /* GoogleAuthManager.swift */; }; @@ -242,7 +245,6 @@ 9F8839142721EB5000669B56 /* MessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8839132721EB5000669B56 /* MessageAction.swift */; }; 9F883916272709E200669B56 /* MessagesThreadApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F883915272709E200669B56 /* MessagesThreadApiClient.swift */; }; 9F88391927270A1A00669B56 /* MessagesThreadOperationsApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F88391827270A1A00669B56 /* MessagesThreadOperationsApiClient.swift */; }; - 9F92EE72236F165E009BE0D7 /* EncryptedStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */; }; 9F9361A52573CE260009912F /* MessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9361A42573CE260009912F /* MessageProvider.swift */; }; 9F9362062573D0C80009912F /* Gmail+MessagesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9362052573D0C80009912F /* Gmail+MessagesList.swift */; }; 9F9362192573D10E0009912F /* Imap+Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9362182573D10E0009912F /* Imap+Message.swift */; }; @@ -635,7 +637,9 @@ 75DCE69F2869EBC0003435F1 /* EmptyFolderCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyFolderCellNode.swift; sourceTree = ""; }; 949ED9412303E3B400530579 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 95473C1A297E61DE006C8957 /* SequenceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceExtension.swift; sourceTree = ""; }; + 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseAlertNode.swift; sourceTree = ""; }; 9547EF232A5FBA2B00A048FF /* MenuSeparatorCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSeparatorCellNode.swift; sourceTree = ""; }; + 958566B82A612822001C84D3 /* ASButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASButtonNode.swift; sourceTree = ""; }; 9F003D6025E1B4ED00EB38C0 /* TrashFolderProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashFolderProvider.swift; sourceTree = ""; }; 9F003D6C25EA8F3200EB38C0 /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 9F003D9D25EA910B00EB38C0 /* LocalStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageTests.swift; sourceTree = ""; }; @@ -2089,6 +2093,7 @@ D2FD0F682453245E00259FF0 /* Either.swift */, 750A6C3C28244A780048E1CC /* OptionalExtensions.swift */, 95473C1A297E61DE006C8957 /* SequenceExtension.swift */, + 958566B82A612822001C84D3 /* ASButtonNode.swift */, ); path = Extensions; sourceTree = ""; @@ -2242,6 +2247,7 @@ 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */, 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */, 51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */, + 9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */, ); path = Nodes; sourceTree = ""; @@ -2720,6 +2726,7 @@ 042B140227F596C70018BDC4 /* ComposeRecipientPopupViewController.swift in Sources */, 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 049E606327FDB9C70089EE2A /* ComposeViewController+Keyboard.swift in Sources */, + 958566B72A6126DE001C84D3 /* EncryptedStorage.swift in Sources */, 5A948DC5239EF2F4006284D7 /* LegalViewController.swift in Sources */, 51C0C63828D1E42A003C540E /* ComposeViewController+ErrorHandling.swift in Sources */, 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */, @@ -2824,7 +2831,6 @@ 040FDF1227EDFC5C00CB936A /* IdTokenUtils.swift in Sources */, 9F953E09238310D500AEB98B /* KeyMethods.swift in Sources */, 5137CB1427F8E0A900AEF895 /* EnterpriseServerApiHelper.swift in Sources */, - 9F92EE72236F165E009BE0D7 /* EncryptedStorage.swift in Sources */, 51433AC32902A6EB00E9D488 /* RSAMessage.swift in Sources */, 32DCA00224982EDA88D69C6E /* AppErr.swift in Sources */, 9F6EE17B2598F9FA0059BA51 /* Gmail+Backup.swift in Sources */, @@ -2876,6 +2882,7 @@ D27177502425659F00BDA9A9 /* TitleCellNode.swift in Sources */, 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */, D2CDC3D824047066002B045F /* RecipientEmailNode.swift in Sources */, + 9547EF212A5F106E00A048FF /* PassPhraseAlertNode.swift in Sources */, D2717753242568A600BDA9A9 /* NavigationBarItemsView.swift in Sources */, 51C56BE92901867D00610D12 /* ENSideMenuNavigationController.swift in Sources */, D211CE7023FC35AC00D1CE38 /* TableNode.swift in Sources */, @@ -2927,6 +2934,7 @@ 21C7DEFE26669CE100C44800 /* DateFormattingExtensions.swift in Sources */, 9FD5052927889D8200FAA82F /* UIAlertControllerExtensions.swift in Sources */, 9FBD69EC27775086002FC602 /* UIApplicationExtensions.swift in Sources */, + 958566B92A612822001C84D3 /* ASButtonNode.swift in Sources */, D2CDC3D42402D50A002B045F /* EncodableExtensions.swift in Sources */, 9FD505272785C2CD00FAA82F /* UIDeviceExtensions.swift in Sources */, 750A6C3D28244A780048E1CC /* OptionalExtensions.swift in Sources */, diff --git a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0df85a3e3..7fef2b7ab 100644 --- a/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "b1090ecd269dddd96bda0df24ca3f1aa78f33578", - "version" : "0.52.4" + "revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", + "version" : "0.34.1" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 0731e5783..c4b6d4c9e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -72,7 +72,7 @@ final class ComposeViewController: TableNodeViewController { var composedLatestDraft: ComposedDraft? var messagePasswordAlertController: UIAlertController? - lazy var alertsFactory = AlertsFactory() + let alertsFactory: AlertsFactory var didFinishSetup = false { didSet { @@ -132,6 +132,7 @@ final class ComposeViewController: TableNodeViewController { ) } + self.alertsFactory = AlertsFactory(encryptedStorage: appContext.encryptedStorage) self.filesManager = filesManager self.photosManager = photosManager self.pubLookup = PubLookup( diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index 8d29b569c..98f3f3f84 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -11,7 +11,8 @@ import UIKit // MARK: - Error handling extension ComposeViewController { func requestMissingPassPhraseWithModal(for signingKey: Keypair, isDraft: Bool = false) { - let alert = alertsFactory.makePassPhraseAlert( + alertsFactory.makePassPhraseAlert( + viewController: self, onCancel: { [weak self] in self?.navigationController?.popViewController(animated: true) }, @@ -26,8 +27,10 @@ extension ComposeViewController { ) if matched { + self.alertsFactory.passphraseCheckSucceed() self.handleMatchedPassphrase(isDraft: isDraft) } else { + self.alertsFactory.passphraseCheckFailed() self.handle(error: ComposeMessageError.passPhraseNoMatch) } } catch { @@ -36,7 +39,6 @@ extension ComposeViewController { } } ) - present(alert, animated: true, completion: nil) } private func handleMatchedPassphrase(isDraft: Bool) { diff --git a/FlowCrypt/Controllers/Threads/Models/AlertsFactory.swift b/FlowCrypt/Controllers/Threads/Models/AlertsFactory.swift index 040dce9d8..52031546a 100644 --- a/FlowCrypt/Controllers/Threads/Models/AlertsFactory.swift +++ b/FlowCrypt/Controllers/Threads/Models/AlertsFactory.swift @@ -6,66 +6,78 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // +import AsyncDisplayKit import FlowCryptCommon +import FlowCryptUI import UIKit class AlertsFactory { typealias PassPhraseCompletion = (String) -> Void typealias CancelCompletion = () -> Void + let encryptedStorage: EncryptedStorageType + + init(encryptedStorage: EncryptedStorageType) { + self.encryptedStorage = encryptedStorage + } + private var textFieldDelegate: UITextFieldDelegate? + func passphraseCheckFailed() { + guard var activeUser = try? encryptedStorage.activeUser else { + return + } + activeUser.failedPassPhraseAttempts = (activeUser.failedPassPhraseAttempts ?? 0) + 1 + activeUser.lastUnsuccessfulPassPhraseAttempt = Date() + try? encryptedStorage.saveActiveUser(with: activeUser) + } + + func passphraseCheckSucceed() { + guard var activeUser = try? encryptedStorage.activeUser else { + return + } + activeUser.failedPassPhraseAttempts = nil + activeUser.lastUnsuccessfulPassPhraseAttempt = nil + try? encryptedStorage.saveActiveUser(with: activeUser) + } + func makePassPhraseAlert( + viewController: UIViewController, title: String = "setup_enter_pass_phrase".localized, onCancel: @escaping CancelCompletion, onCompletion: @escaping PassPhraseCompletion - ) -> UIAlertController { - let alert = UIAlertController( + ) { + guard var activeUser = try? encryptedStorage.activeUser else { + return + } + let alertNode = PassPhraseAlertNode( + failedPassPhraseAttempts: activeUser.failedPassPhraseAttempts, + lastUnsuccessfulPassPhraseAttempt: activeUser.lastUnsuccessfulPassPhraseAttempt, title: title, - message: nil, - preferredStyle: .alert + message: nil ) - - textFieldDelegate = SubmitOnPasteTextFieldDelegate(onSubmit: { passPhrase in - alert.dismiss(animated: true, completion: { - onCompletion(passPhrase) - }) - }) - - alert.addTextField { [weak self] tf in - tf.isSecureTextEntry = true - tf.delegate = self?.textFieldDelegate - tf.accessibilityIdentifier = "aid-message-passphrase-textfield" - } - let saveAction = UIAlertAction( - title: "ok".localized, - style: .default - ) { _ in - guard let textField = alert.textFields?.first, - let passPhrase = textField.text, - passPhrase.isNotEmpty + alertNode.onOkay = { passPhrase in + guard let passPhrase, passPhrase.isNotEmpty else { - alert.dismiss(animated: true, completion: nil) return } - - alert.dismiss(animated: true) { + viewController.dismiss(animated: true) { onCompletion(passPhrase) } } - let cancelAction = UIAlertAction( - title: "cancel".localized, - style: .destructive - ) { _ in - alert.dismiss(animated: true) { - onCancel() - } + alertNode.onCancel = { + viewController.dismiss(animated: true) + onCancel() } + alertNode.resetFailedPassphraseAttempts = { + activeUser.failedPassPhraseAttempts = 0 + try? self.encryptedStorage.saveActiveUser(with: activeUser) + } + let alertViewController = ASDKViewController(node: alertNode) + alertViewController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext + alertViewController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve - alert.addAction(cancelAction) - alert.addAction(saveAction) - - return alert + viewController.present(alertViewController, animated: true, completion: nil) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 1c56bc41e..a65b18eea 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -12,7 +12,7 @@ import FlowCryptUI import UIKit final class ThreadDetailsViewController: TableNodeViewController { - private lazy var alertsFactory = AlertsFactory() + private let alertsFactory: AlertsFactory lazy var logger = Logger.nested(Self.self) struct Input { @@ -72,6 +72,7 @@ final class ThreadDetailsViewController: TableNodeViewController { messageProvider: try mailProvider.messageProvider, combinedPassPhraseStorage: appContext.combinedPassPhraseStorage ) + self.alertsFactory = AlertsFactory(encryptedStorage: appContext.encryptedStorage) self.threadOperationsApiClient = try mailProvider.threadOperationsApiClient self.messageActionsHelper = try await MessageActionsHelper( appContext: appContext @@ -374,7 +375,8 @@ final class ThreadDetailsViewController: TableNodeViewController { ? "setup_enter_pass_phrase".localized : "setup_wrong_pass_phrase_retry".localized - let alert = alertsFactory.makePassPhraseAlert( + alertsFactory.makePassPhraseAlert( + viewController: self, title: title, onCancel: { [weak self] in self?.navigationController?.popViewController(animated: true) @@ -383,8 +385,6 @@ final class ThreadDetailsViewController: TableNodeViewController { self?.handlePassPhraseEntry(passPhrase, indexPath: indexPath) } ) - - present(alert, animated: true, completion: nil) } private func handlePassPhraseEntry(_ passPhrase: String, indexPath: IndexPath) { @@ -409,8 +409,10 @@ final class ThreadDetailsViewController: TableNodeViewController { isUsingKeyManager: appContext.clientConfigurationProvider.configuration.isUsingKeyManager ) + alertsFactory.passphraseCheckSucceed() handle(processedMessage: processedMessage, at: indexPath) } else { + alertsFactory.passphraseCheckFailed() handleWrongPassPhrase(passPhrase, indexPath: indexPath) } } catch { diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index bcd6bc8ef..4a113be1e 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -50,6 +50,7 @@ final class EncryptedStorage: EncryptedStorageType { case version12 case version13 case version14 + case version15 var version: SchemaVersion { switch self { @@ -75,6 +76,8 @@ final class EncryptedStorage: EncryptedStorageType { return SchemaVersion(appVersion: "1.1.1", dbSchemaVersion: 13) case .version14: return SchemaVersion(appVersion: "1.2.3", dbSchemaVersion: 14) + case .version15: + return SchemaVersion(appVersion: "1.2.3", dbSchemaVersion: 15) } } } @@ -82,7 +85,7 @@ final class EncryptedStorage: EncryptedStorageType { private lazy var migrationLogger = Logger.nested(in: Self.self, with: .migration) private lazy var logger = Logger.nested(Self.self) - private let currentSchema: EncryptedStorageSchema = .version14 + private let currentSchema: EncryptedStorageSchema = .version15 private let supportedSchemas = EncryptedStorageSchema.allCases private let storageEncryptionKey: Data diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index ea3f1aeb8..e78240f1c 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -17,11 +17,12 @@ final class EKMVcHelper: EKMVcHelperType { private let appContext: AppContextWithUser private let keyMethods: KeyMethodsType - private lazy var alertsFactory = AlertsFactory() + private let alertsFactory: AlertsFactory init(appContext: AppContextWithUser) { self.appContext = appContext self.keyMethods = KeyMethods() + self.alertsFactory = AlertsFactory(encryptedStorage: appContext.encryptedStorage) } func refreshKeysFromEKMIfNeeded(in viewController: UIViewController) { @@ -146,7 +147,8 @@ final class EKMVcHelper: EKMVcHelperType { @MainActor private func requestPassPhraseWithModal(in viewController: UIViewController) async throws -> String { return try await withCheckedThrowingContinuation { continuation in - let alert = alertsFactory.makePassPhraseAlert( + alertsFactory.makePassPhraseAlert( + viewController: viewController, title: "refresh_key_alert_title".localized, onCancel: { return continuation.resume(returning: "") @@ -165,8 +167,10 @@ final class EKMVcHelper: EKMVcHelperType { passPhrase ) if matched { + self.alertsFactory.passphraseCheckSucceed() return continuation.resume(returning: passPhrase) } + self.alertsFactory.passphraseCheckFailed() // Pass phrase mismatch, display error alert and ask again try await viewController.showAsyncAlert(message: "refresh_key_invalid_pass_phrase".localized) let newPassPhrase = try await self.requestPassPhraseWithModal(in: viewController) @@ -177,7 +181,6 @@ final class EKMVcHelper: EKMVcHelperType { } } ) - viewController.present(alert, animated: true, completion: nil) } } diff --git a/FlowCrypt/Models/Common/User.swift b/FlowCrypt/Models/Common/User.swift index 0b7ce714f..c6efa710d 100644 --- a/FlowCrypt/Models/Common/User.swift +++ b/FlowCrypt/Models/Common/User.swift @@ -18,6 +18,9 @@ struct User: Codable, Equatable { } } + var lastUnsuccessfulPassPhraseAttempt: Date? + var failedPassPhraseAttempts: Int? + var imap: Session? var smtp: Session? @@ -68,6 +71,8 @@ extension User { self.isActive = userObject.isActive self.imap = userObject.imap.flatMap(Session.init) self.smtp = userObject.smtp.flatMap(Session.init) + self.lastUnsuccessfulPassPhraseAttempt = userObject.lastUnsuccessfulPassPhraseAttempt + self.failedPassPhraseAttempts = userObject.failedPassPhraseAttempts } } diff --git a/FlowCrypt/Models/Realm Models/UserRealmObject.swift b/FlowCrypt/Models/Realm Models/UserRealmObject.swift index 61f9690a7..498b0c364 100644 --- a/FlowCrypt/Models/Realm Models/UserRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/UserRealmObject.swift @@ -13,6 +13,8 @@ final class UserRealmObject: Object { @Persisted var isActive: Bool @Persisted var name: String @Persisted var imap: SessionRealmObject? + @Persisted var lastUnsuccessfulPassPhraseAttempt: Date? + @Persisted var failedPassPhraseAttempts: Int? @Persisted var smtp: SessionRealmObject? } @@ -35,5 +37,7 @@ extension UserRealmObject { self.name = user.name self.imap = user.imap.flatMap(SessionRealmObject.init) self.smtp = user.smtp.flatMap(SessionRealmObject.init) + self.lastUnsuccessfulPassPhraseAttempt = user.lastUnsuccessfulPassPhraseAttempt + self.failedPassPhraseAttempts = user.failedPassPhraseAttempts } } diff --git a/FlowCrypt/Resources/Localizable.stringsdict b/FlowCrypt/Resources/Localizable.stringsdict index 2e5de6e67..1714f0e86 100644 --- a/FlowCrypt/Resources/Localizable.stringsdict +++ b/FlowCrypt/Resources/Localizable.stringsdict @@ -2,6 +2,24 @@ + %@ attempt(s) + + NSStringLocalizedFormatKey + %#@attempts@ + attempts + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + No attempts + one + %d attempt + other + %d attempts + + Found %@ key backup(s) NSStringLocalizedFormatKey diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index ab13e39f2..a046b848c 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -71,6 +71,10 @@ "message_reply_all" = "Reply all"; "message_not_found_in_folder" = "Message not found in folder: "; +// Passphrase Anti BruteForce Protection +"passphrase_anti_brute_force_protection_hint" = "To protect you and your data, the next attempt will only be possible after the timer below finishes. Please wait until then before trying again."; +"passphrase_attempt_introduce" = "For your protection and data security, there are currently only %@ left.\nIf attempts are exceeded, there will be a 5-minute cooldown period before you may try again."; + // ERROR "error_fetch_folders" = "Could not fetch folders"; "error_move_trash" = "Unable to move message to Trash"; diff --git a/FlowCryptCommon/Extensions/ASButtonNode.swift b/FlowCryptCommon/Extensions/ASButtonNode.swift new file mode 100644 index 000000000..bcff40293 --- /dev/null +++ b/FlowCryptCommon/Extensions/ASButtonNode.swift @@ -0,0 +1,29 @@ +// +// ASButtonNode.swift +// FlowCryptCommon +// +// Created by Ioan Moldovan on 7/14/23 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public extension ASButtonNode { + private func imageWithColor(color: UIColor) -> UIImage? { + let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext() + + context?.setFillColor(color.cgColor) + context?.fill(rect) + + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return image + } + + func setBackgroundColor(_ color: UIColor, forState controlState: UIControl.State) { + setBackgroundImage(imageWithColor(color: color), for: controlState) + } +} diff --git a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift index 6f782157d..5327f57d2 100644 --- a/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextFieldCellNode.swift @@ -69,9 +69,8 @@ public class TextFieldCellNode: CellNode { public init(input: Input, action: TextFieldAction? = nil) { textField = TextFieldNode( - preferredHeight: input.height, - action: action, - accessibilityIdentifier: input.accessibilityIdentifier + accessibilityIdentifier: input.accessibilityIdentifier, + action: action ) self.input = input super.init() diff --git a/FlowCryptUI/Nodes/PassPhraseAlertNode.swift b/FlowCryptUI/Nodes/PassPhraseAlertNode.swift new file mode 100644 index 000000000..0b0ecb022 --- /dev/null +++ b/FlowCryptUI/Nodes/PassPhraseAlertNode.swift @@ -0,0 +1,292 @@ +// +// PassPhraseAlertNode.swift +// FlowCryptUI +// +// Created by Ioan Moldovan on 17/07/23 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public class PassPhraseAlertNode: ASDisplayNode { + + enum Constants { + static let antiBruteForceProtectionAttemptsMaxValue = 5 + static let blockingTimeInSeconds: Double = 5 * 60 + } + + private lazy var overlayNode = createOverlayNode() + private lazy var contentView = createContentView() + private lazy var separatorNode = createSeparatorNode() + private lazy var titleLabel = createTextNode(text: title, isBold: true, fontSize: 17, identifier: "aid-enter-passphrase-title-label") + private lazy var messageLabel = createTextNode(text: message ?? "", isBold: false, fontSize: 13) + private lazy var introductionLabel = createTextNode(text: "", isBold: false, fontSize: 13, identifier: "aid-anti-brute-force-introduce-label") + private lazy var passPhraseTextField = createPassPhraseTextField() + private lazy var cancelButton = createButtonNode(title: "Cancel", color: .red, identifier: "aid-cancel-button", action: #selector(cancelButtonTapped)) + private lazy var okayButton = createButtonNode(title: "Ok", color: .blue, identifier: "aid-ok-button", action: #selector(okayButtonTapped)) + private weak var alertTimer: Timer? + + private var introduction: String? { didSet { updateIntroduction() } } + + private let title: String + private let message: String? + private var failedPassPhraseAttempts: Int? + private let lastUnsuccessfulPassPhraseAttempt: Date? + private var previousState: Bool? + + public var onOkay: ((String?) -> Void)? + public var onCancel: (() -> Void)? + public var resetFailedPassphraseAttempts: (() -> Void)? + + // MARK: - Initialization + + public init( + failedPassPhraseAttempts: Int?, + lastUnsuccessfulPassPhraseAttempt: Date?, + title: String, + message: String? = nil + ) { + self.failedPassPhraseAttempts = failedPassPhraseAttempts + self.lastUnsuccessfulPassPhraseAttempt = lastUnsuccessfulPassPhraseAttempt + self.title = title + self.message = message + super.init() + setupNodes() + startTimer() + } + + deinit { + alertTimer?.invalidate() + alertTimer = nil + } + + private func setupNodes() { + contentView.addSubnode(titleLabel) + if message != nil { + contentView.addSubnode(messageLabel) + } + contentView.addSubnode(passPhraseTextField) + contentView.addSubnode(introductionLabel) + contentView.addSubnode(separatorNode) + contentView.addSubnode(cancelButton) + contentView.addSubnode(okayButton) + overlayNode.addSubnode(contentView) + addSubnode(overlayNode) + passPhraseTextField.becomeFirstResponder() + if failedPassPhraseAttempts ?? 0 > 0 { + updateRemainingAttemptsLabel() + } + } + + private func startTimer() { + guard alertTimer == nil else { return } + alertTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.monitorBruteForceProtection() + } + alertTimer?.fire() + } + + // Monitor the status of anti-brute-force protection and manage the display of alerts. + func monitorBruteForceProtection() { + let isPassphraseCheckDisabled = shouldDisablePassphraseCheck() + + // Check if the state has changed or if we need to update the timer. + // This condition ensures the brute force protection alert is only rendered or dismissed + // when the state actually changes or when an update to the timer value is required. + if isPassphraseCheckDisabled != previousState || (previousState ?? false && isPassphraseCheckDisabled) { + previousState = isPassphraseCheckDisabled + if isPassphraseCheckDisabled { + renderBruteForceProtectionAlert() + } else { + dismissBruteForceProtectionAlert() + } + } + } + + private func convertToMinuteSecondFormat(seconds: Int) -> String { + let minutes = seconds / 60 + let remainingSeconds = seconds % 60 + return String(format: "%02d:%02d", minutes, remainingSeconds) + } + + private func updateRemainingAttemptsLabel() { + let remainingAttempts = Constants.antiBruteForceProtectionAttemptsMaxValue - (failedPassPhraseAttempts ?? 0) + introduction = "passphrase_attempt_introduce".localizeWithArguments("%@ attempt(s)".localizePluralsWithArguments(remainingAttempts)) + } + + private func renderBruteForceProtectionAlert() { + guard let lastUnsuccessfulPassPhraseAttempt else { return } + let now = Date() + let remainingTimeInSeconds = lastUnsuccessfulPassPhraseAttempt.addingTimeInterval(Constants.blockingTimeInSeconds).timeIntervalSince(now) + + introduction = "passphrase_anti_brute_force_protection_hint".localized + + okayButton.isEnabled = false + let minuteSecondStr = convertToMinuteSecondFormat(seconds: Int(remainingTimeInSeconds)) + okayButton.setTitle(minuteSecondStr, with: .systemFont(ofSize: 15), with: .gray, for: .normal) + } + + private func dismissBruteForceProtectionAlert() { + if failedPassPhraseAttempts == 0 { + introduction = nil + } + okayButton.setTitle("Ok", with: UIFont.systemFont(ofSize: 15), with: .blue, for: .normal) + okayButton.isEnabled = true + } + + private func shouldDisablePassphraseCheck() -> Bool { + let now = Date() + // already passed anti-brute force 5 minute cooldown period + // reset last unsuccessful count + if let lastUnsuccessfulPassPhraseAttempt, now.timeIntervalSince(lastUnsuccessfulPassPhraseAttempt) >= Constants.blockingTimeInSeconds, failedPassPhraseAttempts != 0 { + resetFailedPassphraseAttempts?() + failedPassPhraseAttempts = 0 + } + return (failedPassPhraseAttempts ?? 0) >= Constants.antiBruteForceProtectionAttemptsMaxValue + } + + private func submitPassphrase(text: String?) { + let isPassphraseCheckDisabled = shouldDisablePassphraseCheck() + if !isPassphraseCheckDisabled { + onOkay?(text) + } + } + + private func createContentView() -> ASDisplayNode { + let node = ASDisplayNode() + node.backgroundColor = UIColor(hex: "F0F0F0") + node.clipsToBounds = true + node.cornerRadius = 13 + node.shadowColor = UIColor.black.cgColor + node.shadowRadius = 15 + node.shadowOpacity = 0.1 + node.shadowOffset = CGSize(width: 0, height: 2) + return node + } + + private func createOverlayNode() -> ASDisplayNode { + let node = ASDisplayNode() + node.backgroundColor = UIColor(white: 0, alpha: 0.4) // semi-transparent black + return node + } + + private func createSeparatorNode() -> ASDisplayNode { + let node = ASDisplayNode() + node.backgroundColor = UIColor.lightGray + node.style.height = ASDimension(unit: .points, value: 0.5) + return node + } + + private func createTextNode(text: String, isBold: Bool, fontSize: CGFloat, identifier: String? = nil) -> ASTextNode { + let node = ASTextNode() + let font = isBold ? UIFont.boldSystemFont(ofSize: fontSize) : UIFont.systemFont(ofSize: fontSize) + node.attributedText = NSAttributedString( + string: text, + attributes: [NSAttributedString.Key.font: font] + ) + node.accessibilityIdentifier = identifier + return node + } + + private func createButtonNode(title: String, color: UIColor, identifier: String, action: Selector) -> ASButtonNode { + let node = ASButtonNode() + node.setTitle(title, with: UIFont.systemFont(ofSize: 15), with: color, for: .normal) + node.style.flexGrow = 1 + node.style.preferredSize.height = 35 + node.addTarget(self, action: action, forControlEvents: .touchUpInside) + node.accessibilityIdentifier = identifier + node.setBackgroundColor(.lightGray, forState: .highlighted) + return node + } + + private func updateIntroduction() { + if let introduction, !introduction.isEmpty { + introductionLabel.attributedText = NSAttributedString(string: introduction) + introductionLabel.isHidden = false + } else { + introductionLabel.isHidden = true + } + setNeedsLayout() + contentView.setNeedsLayout() + } + + private func createPassPhraseTextField() -> TextFieldNode { + let node = TextFieldNode(accessibilityIdentifier: "aid-message-passphrase-textfield") { [weak self] action in + if case let .didPaste(_, value) = action { + self?.submitPassphrase(text: value) + } + } + node.shouldReturn = { textField in + self.submitPassphrase(text: textField.text) + return true + } + node.borderColor = .init(gray: 0.2, alpha: 1.0) + node.borderWidth = 0.1 + node.backgroundColor = .white + node.cornerRadius = 6 + node.style.width = ASDimension(unit: .fraction, value: 1.0) + node.style.preferredSize.height = 35 + node.textInsets = 5 + node.isSecureTextEntry = true + return node + } + + // MARK: - Layout + override public func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + let separatorInsetSpec = ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0), + child: separatorNode + ) + + let buttonStack = ASStackLayoutSpec( + direction: .horizontal, + spacing: 0, + justifyContent: .spaceBetween, + alignItems: .center, + children: [cancelButton, okayButton] + ) + buttonStack.style.flexGrow = 1.0 + + var contentChildren: [ASLayoutElement] = [passPhraseTextField] + if message != nil { + contentChildren.append(messageLabel) + } + if introduction != nil { + contentChildren.append(introductionLabel) + } + let verticalStack = ASStackLayoutSpec( + direction: .vertical, + spacing: 10, + justifyContent: .center, + alignItems: .stretch, + children: [titleLabel] + contentChildren + [separatorInsetSpec, buttonStack] + ) + + let contentLayout = ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 20, left: 20, bottom: 10, right: 20), + child: verticalStack + ) + + contentView.layoutSpecBlock = { _, _ in + return contentLayout + } + + let contentViewInset = ASInsetLayoutSpec(insets: UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10), child: contentView) + + let centerSpec = ASCenterLayoutSpec(centeringOptions: .XY, sizingOptions: [], child: contentViewInset) + + overlayNode.layoutSpecBlock = { _, _ in + return centerSpec + } + return ASWrapperLayoutSpec(layoutElement: overlayNode) + } + + // MARK: - Action Handlers + @objc private func cancelButtonTapped() { + onCancel?() + } + + @objc private func okayButtonTapped() { + submitPassphrase(text: passPhraseTextField.text) + } +} diff --git a/FlowCryptUI/Nodes/TextFieldNode.swift b/FlowCryptUI/Nodes/TextFieldNode.swift index 393e000ee..df46fa42b 100644 --- a/FlowCryptUI/Nodes/TextFieldNode.swift +++ b/FlowCryptUI/Nodes/TextFieldNode.swift @@ -136,7 +136,7 @@ public final class TextFieldNode: ASDisplayNode { private var onToolbarDoneAction: (() -> Void)? - public init(preferredHeight: CGFloat?, action: TextFieldAction? = nil, accessibilityIdentifier: String?) { + public init(accessibilityIdentifier: String?, action: TextFieldAction? = nil) { super.init() addSubnode(node) textFieldAction = action diff --git a/appium/tests/screenobjects/email.screen.ts b/appium/tests/screenobjects/email.screen.ts index d4a2717de..d1b9db56c 100644 --- a/appium/tests/screenobjects/email.screen.ts +++ b/appium/tests/screenobjects/email.screen.ts @@ -4,10 +4,9 @@ import ElementHelper from '../helpers/ElementHelper'; const SELECTORS = { BACK_BTN: '~aid-back-button', - ENTER_PASS_PHRASE_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField', - OK_BUTTON: '~Ok', - WRONG_PASS_PHRASE_MESSAGE: - '-ios class chain:**/XCUIElementTypeStaticText[`label == "Wrong pass phrase, please try again"`]', + ENTER_PASS_PHRASE_FIELD: '~aid-message-passphrase-textfield', + OK_BUTTON: '~aid-ok-button', + ENTR_PASSPHRASE_TITLE_LABEL: '~aid-enter-passphrase-title-label', ATTACHMENT_CELL: '~aid-attachment-cell-0', ATTACHMENT_TITLE: '~aid-attachment-title-label-0', REPLY_BUTTON: '~aid-reply-button', @@ -16,6 +15,7 @@ const SELECTORS = { RECIPIENTS_CC_LABEL: '~aid-cc-0-label', RECIPIENTS_BCC_LABEL: '~aid-bcc-0-label', MENU_BUTTON: '~aid-message-menu-button', + ANTI_BRUTE_FORCE_INTRODUCE_LABEL: '~aid-anti-brute-force-introduce-label', FORWARD_BUTTON: '~aid-forward-button', REPLY_ALL_BUTTON: '~aid-reply-all-button', HELP_BUTTON: '~aid-help-button', @@ -49,8 +49,8 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.OK_BUTTON); } - get wrongPassPhraseMessage() { - return $(SELECTORS.WRONG_PASS_PHRASE_MESSAGE); + get enterPassPhraseTitleLabel() { + return $(SELECTORS.ENTR_PASSPHRASE_TITLE_LABEL); } get attachmentCell() { @@ -81,6 +81,10 @@ class EmailScreen extends BaseScreen { return $(SELECTORS.RECIPIENTS_BCC_LABEL); } + get antiBruteForceIntroduceLabel() { + return $(SELECTORS.ANTI_BRUTE_FORCE_INTRODUCE_LABEL); + } + get menuButton() { return $(SELECTORS.MENU_BUTTON); } @@ -227,11 +231,18 @@ class EmailScreen extends BaseScreen { }; enterPassPhrase = async (text: string = CommonData.account.passPhrase) => { + await ElementHelper.waitElementVisible(await this.enterPassPhraseField); await (await this.enterPassPhraseField).setValue(text); }; - checkWrongPassPhraseErrorMessage = async () => { - await ElementHelper.waitElementVisible(await this.wrongPassPhraseMessage); + checkAntiBruteForceIntroduceLabel = async (expectedValue: string) => { + await ElementHelper.waitElementVisible(await this.antiBruteForceIntroduceLabel); + const introduceLabel = await (await this.antiBruteForceIntroduceLabel).getValue(); + expect(introduceLabel.includes(expectedValue)).toEqual(true); + }; + + checkPassPhraseModalTitle = async (value = 'Wrong pass phrase, please try again') => { + await ElementHelper.waitForText(await this.enterPassPhraseTitleLabel, value); }; checkAttachment = async (name: string) => { diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index a0b211700..f051b4699 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -22,7 +22,6 @@ const SELECTORS = { SEND_BUTTON: '~aid-compose-send', SEND_PLAIN_MESSAGE_BUTTON: '~aid-compose-send-plain', CONFIRM_DELETING: '~aid-confirm-button', - MESSAGE_PASSPHRASE_TEXTFIELD: '~aid-message-passphrase-textfield', MESSAGE_PASSWORD_TEXTFIELD: '~aid-message-password-textfield', ALERT: "-ios predicate string:type == 'XCUIElementTypeAlert'", RECIPIENT_POPUP_EMAIL_NODE: '~aid-recipient-popup-email-node', @@ -111,10 +110,6 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.ALERT); } - get passphraseTextField() { - return $(SELECTORS.MESSAGE_PASSPHRASE_TEXTFIELD); - } - get passwordTextField() { return $(SELECTORS.MESSAGE_PASSWORD_TEXTFIELD); } diff --git a/appium/tests/screenobjects/refresh-key.screen.ts b/appium/tests/screenobjects/refresh-key.screen.ts index 2f82486c9..16b9f3922 100644 --- a/appium/tests/screenobjects/refresh-key.screen.ts +++ b/appium/tests/screenobjects/refresh-key.screen.ts @@ -3,8 +3,9 @@ import ElementHelper from '../helpers/ElementHelper'; const SELECTORS = { ENTER_YOUR_PASS_PHRASE_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField', - OK_BUTTON: '~Ok', - CANCEL_BUTTON: '~Cancel', + OK_BUTTON: '~aid-ok-button', + SYSTEM_OK_BUTTON: '~Ok', + CANCEL_BUTTON: '~aid-cancel-button', }; class RefreshKeyScreen extends BaseScreen { @@ -20,6 +21,10 @@ class RefreshKeyScreen extends BaseScreen { return $(SELECTORS.OK_BUTTON); } + get systemOkButton() { + return $(SELECTORS.SYSTEM_OK_BUTTON); + } + get cancelButton() { return $(SELECTORS.CANCEL_BUTTON); } @@ -35,6 +40,10 @@ class RefreshKeyScreen extends BaseScreen { clickOkButton = async () => { await ElementHelper.waitAndClick(await this.okButton); }; + + clickSystemOkButton = async () => { + await ElementHelper.waitAndClick(await this.systemOkButton); + }; } export default new RefreshKeyScreen(); diff --git a/appium/tests/specs/mock/inbox/CheckEncryptedEmailAfterRestartApp.spec.ts b/appium/tests/specs/mock/inbox/CheckEncryptedEmailAfterRestartApp.spec.ts index b2305c076..ed4782f9c 100644 --- a/appium/tests/specs/mock/inbox/CheckEncryptedEmailAfterRestartApp.spec.ts +++ b/appium/tests/specs/mock/inbox/CheckEncryptedEmailAfterRestartApp.spec.ts @@ -45,7 +45,7 @@ describe('INBOX: ', () => { // try to see encrypted message with wrong pass phrase await EmailScreen.enterPassPhrase(wrongPassPhrase); await EmailScreen.clickOkButton(); - await EmailScreen.checkWrongPassPhraseErrorMessage(); + await EmailScreen.checkPassPhraseModalTitle(); // check email after setting correct pass phrase await EmailScreen.enterPassPhrase(correctPassPhrase); diff --git a/appium/tests/specs/mock/inbox/ReadAttachmentEmail.spec.ts b/appium/tests/specs/mock/inbox/ReadAttachmentEmail.spec.ts index f4c4de979..4259fc531 100644 --- a/appium/tests/specs/mock/inbox/ReadAttachmentEmail.spec.ts +++ b/appium/tests/specs/mock/inbox/ReadAttachmentEmail.spec.ts @@ -63,7 +63,7 @@ describe('INBOX: ', () => { // try to see encrypted message with wrong pass phrase await EmailScreen.enterPassPhrase(wrongPassPhrase); await EmailScreen.clickOkButton(); - await EmailScreen.checkWrongPassPhraseErrorMessage(); + await EmailScreen.checkPassPhraseModalTitle(); // check attachment after setting correct pass phrase await EmailScreen.enterPassPhrase(correctPassPhrase); diff --git a/appium/tests/specs/mock/setup/CheckAntiBruteForceProtection.spec.ts b/appium/tests/specs/mock/setup/CheckAntiBruteForceProtection.spec.ts new file mode 100644 index 000000000..a93118e25 --- /dev/null +++ b/appium/tests/specs/mock/setup/CheckAntiBruteForceProtection.spec.ts @@ -0,0 +1,45 @@ +import { MockApi } from 'api-mocks/mock'; +import { MockApiConfig } from 'api-mocks/mock-config'; +import { SplashScreen, SetupKeyScreen, MailFolderScreen, EmailScreen } from '../../../screenobjects/all-screens'; +import AppiumHelper from 'tests/helpers/AppiumHelper'; +import { CommonData } from 'tests/data'; + +describe('INBOX: ', () => { + it('check anti brute force protection', async () => { + const mockApi = new MockApi(); + const subject = 'Signed and encrypted message'; + + mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; + mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com', { + messages: [subject], + }); + + const processArgs = CommonData.mockProcessArgs; + const wrongPassPhrase = 'test1234'; + + await mockApi.withMockedApis(async () => { + // stage 1 - setup + await SplashScreen.mockLogin(); + await SetupKeyScreen.setPassPhrase(); + await MailFolderScreen.checkInboxScreen(); + + await AppiumHelper.restartApp(processArgs); + await MailFolderScreen.clickOnEmailBySubject(subject); + + for (const i of [4, 3, 2, 1, 0]) { + await EmailScreen.enterPassPhrase(wrongPassPhrase); + await EmailScreen.clickOkButton(); + if (i > 0) { + await EmailScreen.checkAntiBruteForceIntroduceLabel( + `For your protection and data security, there are currently only ${i} attempt${i > 1 ? 's' : ''}`, + ); + } + } + await EmailScreen.checkAntiBruteForceIntroduceLabel( + 'To protect you and your data, the next attempt will only be possible after the timer below finishes. Please wait until then before trying again.', + ); + expect(await (await EmailScreen.okButton).isEnabled()).toEqual(false); + }); + }); +}); diff --git a/appium/tests/specs/mock/setup/CheckRefreshKeyFromEkmWithPassPhrasePrompt.spec.ts b/appium/tests/specs/mock/setup/CheckRefreshKeyFromEkmWithPassPhrasePrompt.spec.ts index dbad424c9..db5f22693 100644 --- a/appium/tests/specs/mock/setup/CheckRefreshKeyFromEkmWithPassPhrasePrompt.spec.ts +++ b/appium/tests/specs/mock/setup/CheckRefreshKeyFromEkmWithPassPhrasePrompt.spec.ts @@ -36,7 +36,7 @@ describe('SETUP: ', () => { await RefreshKeyScreen.fillPassPhrase('wrong passphrase'); await RefreshKeyScreen.clickOkButton(); await BaseScreen.checkModalMessage(CommonData.refreshingKeysFromEkm.wrongPassPhrase); - await RefreshKeyScreen.clickOkButton(); + await RefreshKeyScreen.clickSystemOkButton(); await RefreshKeyScreen.cancelRefresh(); await KeysScreen.openScreenFromSideMenu(); await KeysScreen.checkKeysScreen([ekmKeySamples.key0]); diff --git a/appium/tests/specs/mock/setup/RespectsPassPhraseSessionLength.spec.ts b/appium/tests/specs/mock/setup/RespectsPassPhraseSessionLength.spec.ts index 753051e9d..e937ec672 100644 --- a/appium/tests/specs/mock/setup/RespectsPassPhraseSessionLength.spec.ts +++ b/appium/tests/specs/mock/setup/RespectsPassPhraseSessionLength.spec.ts @@ -3,7 +3,6 @@ import { MockApi } from 'api-mocks/mock'; import { MockApiConfig } from 'api-mocks/mock-config'; import { CommonData } from 'tests/data'; import AppiumHelper from 'tests/helpers/AppiumHelper'; -import BaseScreen from 'tests/screenobjects/base.screen'; import { EmailScreen, SplashScreen } from '../../../screenobjects/all-screens'; import MailFolderScreen from '../../../screenobjects/mail-folder.screen'; import SetupKeyScreen from '../../../screenobjects/setup-key.screen'; @@ -46,7 +45,7 @@ describe('SETUP: ', () => { await EmailScreen.clickBackButton(); await browser.pause(5000); await MailFolderScreen.clickOnEmailBySubject(testMessageSubject); - await BaseScreen.checkModalMessage('Please enter pass phrase'); + await EmailScreen.checkPassPhraseModalTitle('Please enter pass phrase'); }); }); });