From a385c7c436a85cd8dc7ecb1b9049f1e2ea4137b7 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Thu, 30 Nov 2023 12:25:48 -0400 Subject: [PATCH 1/6] feat: refresh EKM keys for invalid keys on message screen --- FlowCrypt/App/AppDelegate.swift | 4 +- .../Compose/ComposeViewController.swift | 3 + .../ComposeViewController+ErrorHandling.swift | 16 ++++ .../InboxViewContainerController.swift | 4 +- .../Functionality/Services/EKMVcHelper.swift | 96 +++++++++---------- appium/tests/data/index.ts | 1 + .../CheckEKMRefreshAfterComposeError.spec.ts | 49 ++++++++++ .../setup/SetupOnlyRevokedKeyFromEKM.spec.ts | 3 +- 8 files changed, 123 insertions(+), 53 deletions(-) create mode 100644 appium/tests/specs/mock/composeEmail/CheckEKMRefreshAfterComposeError.spec.ts diff --git a/FlowCrypt/App/AppDelegate.swift b/FlowCrypt/App/AppDelegate.swift index 11800e4da..0ddccfcd8 100644 --- a/FlowCrypt/App/AppDelegate.swift +++ b/FlowCrypt/App/AppDelegate.swift @@ -64,7 +64,9 @@ extension AppDelegate: BlursTopView { removeBlurView() } if let topViewController = UIApplication.topViewController() { - ekmVcHelper?.refreshKeysFromEKMIfNeeded(in: topViewController) + Task { + await ekmVcHelper?.refreshKeysFromEKMIfNeeded(in: topViewController) + } } } } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 8cd9c0836..c973d00fb 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -87,6 +87,7 @@ final class ComposeViewController: TableNodeViewController { var selectedRecipientType: RecipientType? = .to var shouldShowAllRecipientTypes = false + var isKeyUpdatedFromEKM = false var popoverVC: ComposeRecipientPopupViewController! var sectionsList: [Section] = [] @@ -95,6 +96,7 @@ final class ComposeViewController: TableNodeViewController { var sendAsList: [SendAsModel] = [] let handleAction: ((ComposeMessageAction) -> Void)? + let ekmVcHelper: EKMVcHelper init( appContext: AppContextWithUser, @@ -141,6 +143,7 @@ final class ComposeViewController: TableNodeViewController { ) self.router = appContext.globalRouter self.clientConfiguration = clientConfiguration + self.ekmVcHelper = EKMVcHelper(appContext: appContext) let mailProvider = try appContext.getRequiredMailProvider() self.messageHelper = try messageHelper ?? MessageHelper( diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index c940beb01..b0dbc272f 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -71,6 +71,14 @@ extension ComposeViewController { MessageValidationError.expiredKeyRecipients, MessageValidationError.notUsableForEncryptionKeyRecipients: processSendMessageWithNoValidKeys(error: error) + case MessageValidationError.noUsableAccountKeys, + KeypairError.noAccountKeysAvailable: + if isKeyUpdatedFromEKM { + showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + isKeyUpdatedFromEKM = false // Set to false so that it will be fetched next time + } else { + refreshEKMAndProceed(error: error) + } case MessageValidationError.notUniquePassword, MessageValidationError.subjectContainsPassword, MessageValidationError.weakPassword: @@ -80,6 +88,14 @@ extension ComposeViewController { } } + private func refreshEKMAndProceed(error: Error) { + Task { + isKeyUpdatedFromEKM = true + await ekmVcHelper.refreshKeysFromEKMIfNeeded(in: self, forceRefresh: true) + handleSendTap() + } + } + private func processSendMessageWithNoValidKeys(error: Error) { let alert = UIAlertController( title: "compose_message_encryption".localized, diff --git a/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift b/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift index 7013e9f96..ce9069622 100644 --- a/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift +++ b/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift @@ -126,7 +126,9 @@ final class InboxViewContainerController: TableNodeViewController { viewModel: input ) navigationController?.setViewControllers([inboxViewController], animated: false) - ekmVcHelper.refreshKeysFromEKMIfNeeded(in: inboxViewController, forceRefresh: true) + Task { + await ekmVcHelper.refreshKeysFromEKMIfNeeded(in: inboxViewController, forceRefresh: true) + } } catch { showAlert(message: error.errorMessage) } diff --git a/FlowCrypt/Functionality/Services/EKMVcHelper.swift b/FlowCrypt/Functionality/Services/EKMVcHelper.swift index 6e9c16df2..f99681d08 100644 --- a/FlowCrypt/Functionality/Services/EKMVcHelper.swift +++ b/FlowCrypt/Functionality/Services/EKMVcHelper.swift @@ -9,7 +9,7 @@ import UIKit protocol EKMVcHelperType { - func refreshKeysFromEKMIfNeeded(in viewController: UIViewController, forceRefresh: Bool) + func refreshKeysFromEKMIfNeeded(in viewController: UIViewController, forceRefresh: Bool) async } final class EKMVcHelper: EKMVcHelperType { @@ -26,59 +26,57 @@ final class EKMVcHelper: EKMVcHelperType { self.alertsFactory = AlertsFactory(encryptedStorage: appContext.encryptedStorage) } - func refreshKeysFromEKMIfNeeded(in viewController: UIViewController, forceRefresh: Bool = false) { - Task { - do { - let lastUpdateTime = UserDefaults.standard.object(forKey: LAST_EKM_UPDATE_TIME_KEY) as? Date + func refreshKeysFromEKMIfNeeded(in viewController: UIViewController, forceRefresh: Bool = false) async { + do { + let lastUpdateTime = UserDefaults.standard.object(forKey: LAST_EKM_UPDATE_TIME_KEY) as? Date - // Only proceed if last update time is more than 8 hours ago or not set - // Or force refresh when forceRefresh is set - guard lastUpdateTime == nil || lastUpdateTime!.addingTimeInterval(8 * 60 * 60) < Date() || forceRefresh else { - return - } - - let configuration = try await appContext.clientConfigurationProvider.configuration - guard try configuration.checkUsesEKM() == .usesEKM else { - return - } - let passPhraseStorageMethod: PassPhraseStorageMethod = configuration.forbidStoringPassPhrase ? .memory : .persistent - let emailKeyManagerApi = EmailKeyManagerApi(clientConfiguration: configuration) - let idToken = try await IdTokenUtils.getIdToken(userEmail: appContext.user.email) - let fetchedKeys = try await emailKeyManagerApi.getPrivateKeys(idToken: idToken) - let localKeys = try appContext.encryptedStorage.getKeypairs(by: appContext.user.email) - - try removeLocalKeysIfNeeded(from: fetchedKeys, localKeys: localKeys) - - let keysToUpdate = try findKeysToUpdate(from: fetchedKeys, localKeys: localKeys) - // Set last update time - UserDefaults.standard.set(Date(), forKey: LAST_EKM_UPDATE_TIME_KEY) - guard keysToUpdate.isNotEmpty else { - return - } - guard let passPhrase = try await getPassphrase(in: viewController), passPhrase.isNotEmpty else { - return - } + // Only proceed if last update time is more than 8 hours ago or not set + // Or force refresh when forceRefresh is set + guard lastUpdateTime == nil || lastUpdateTime!.addingTimeInterval(8 * 60 * 60) < Date() || forceRefresh else { + return + } - for keyDetail in keysToUpdate { - try await saveKeyToLocal( - context: appContext, - keyDetail: keyDetail, - passPhrase: passPhrase, - passPhraseStorageMethod: passPhraseStorageMethod - ) - } + let configuration = try await appContext.clientConfigurationProvider.configuration + guard try configuration.checkUsesEKM() == .usesEKM else { + return + } + let passPhraseStorageMethod: PassPhraseStorageMethod = configuration.forbidStoringPassPhrase ? .memory : .persistent + let emailKeyManagerApi = EmailKeyManagerApi(clientConfiguration: configuration) + let idToken = try await IdTokenUtils.getIdToken(userEmail: appContext.user.email) + let fetchedKeys = try await emailKeyManagerApi.getPrivateKeys(idToken: idToken) + let localKeys = try appContext.encryptedStorage.getKeypairs(by: appContext.user.email) + + try removeLocalKeysIfNeeded(from: fetchedKeys, localKeys: localKeys) + + let keysToUpdate = try findKeysToUpdate(from: fetchedKeys, localKeys: localKeys) + // Set last update time + UserDefaults.standard.set(Date(), forKey: LAST_EKM_UPDATE_TIME_KEY) + guard keysToUpdate.isNotEmpty else { + return + } + guard let passPhrase = try await getPassphrase(in: viewController), passPhrase.isNotEmpty else { + return + } - await viewController.showToast("refresh_key_success".localized) - } catch { - // since this is an update function that happens on every startup - // it's ok if it's skipped sometimes - keys will be updated next time - if error is ApiError { - return - } - await viewController.showAlert( - message: "refresh_key_error".localizeWithArguments(error.errorMessage) + for keyDetail in keysToUpdate { + try await saveKeyToLocal( + context: appContext, + keyDetail: keyDetail, + passPhrase: passPhrase, + passPhraseStorageMethod: passPhraseStorageMethod ) } + + await viewController.showToast("refresh_key_success".localized) + } catch { + // since this is an update function that happens on every startup + // it's ok if it's skipped sometimes - keys will be updated next time + if error is ApiError { + return + } + await viewController.showAlert( + message: "refresh_key_error".localizeWithArguments(error.errorMessage) + ) } } diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 739afdae5..c68c4b57c 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -228,6 +228,7 @@ export const CommonData = { wrongPassPhrase: 'Error\n' + 'Could not compose message\n' + '\n' + 'This pass phrase did not match your signing private key.', notUsableEncryptionPublicKey: `Message Encryption\nOne or more of your recipients have sign-only public keys (marked in yellow).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.`, + noPrivateKey: 'Error\n' + 'Could not compose message\n\n' + 'Your account keys are not usable for encryption.', expiredPublicKey: `Message Encryption\nOne or more of your recipients have expired public keys (marked in orange).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.`, revokedPublicKey: `Message Encryption\nOne or more of your recipients have revoked public keys (marked in red).\n\nPlease ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.`, wrongPassPhraseOnLogin: 'Error\n' + 'Wrong pass phrase, please try again', diff --git a/appium/tests/specs/mock/composeEmail/CheckEKMRefreshAfterComposeError.spec.ts b/appium/tests/specs/mock/composeEmail/CheckEKMRefreshAfterComposeError.spec.ts new file mode 100644 index 000000000..f21a72a89 --- /dev/null +++ b/appium/tests/specs/mock/composeEmail/CheckEKMRefreshAfterComposeError.spec.ts @@ -0,0 +1,49 @@ +import { MockApi } from 'api-mocks/mock'; +import { MockApiConfig } from 'api-mocks/mock-config'; +import { MockUserList } from 'api-mocks/mock-data'; +import { ekmKeySamples } from 'api-mocks/apis/ekm/ekm-endpoints'; +import { MailFolderScreen, NewMessageScreen, SetupKeyScreen, SplashScreen } from '../../../screenobjects/all-screens'; +import { CommonData } from 'tests/data'; +import BaseScreen from 'tests/screenobjects/base.screen'; + +describe('COMPOSE EMAIL: ', () => { + it('check handling of invalid keys on message compose', async () => { + const mockApi = new MockApi(); + + const recipient = MockUserList.robot; + const subject = 'check revoked key after from ekm'; + const message = 'check revoked key after from ekm'; + const noPrivateKeyError = CommonData.errors.noPrivateKey; + + mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; + mockApi.ekmConfig = { + returnKeys: [ekmKeySamples.e2eRevokedKey.prv], + }; + mockApi.addGoogleAccount('e2e.enterprise.test@flowcrypt.com'); + mockApi.attesterConfig = { + servedPubkeys: { + [recipient.email]: recipient.pub!, + }, + }; + + await mockApi.withMockedApis(async () => { + await SplashScreen.mockLogin(); + await SetupKeyScreen.setPassPhrase(); + await MailFolderScreen.checkInboxScreen(); + await MailFolderScreen.clickCreateEmail(); + + // Stage1: Try to compose message with revoked encryption key + await NewMessageScreen.composeEmail(recipient.email, subject, message); + await NewMessageScreen.clickSendButton(); + await BaseScreen.checkModalMessage(noPrivateKeyError); + await BaseScreen.clickOkButtonOnError(); + + // Now update ekm to return valid key and check if message is sent correctly + mockApi.ekmConfig = { + returnKeys: [ekmKeySamples.e2e.prv], + }; + await NewMessageScreen.clickSendButton(); + await MailFolderScreen.checkInboxScreen(); + }); + }); +}); diff --git a/appium/tests/specs/mock/setup/SetupOnlyRevokedKeyFromEKM.spec.ts b/appium/tests/specs/mock/setup/SetupOnlyRevokedKeyFromEKM.spec.ts index 465cd8c3b..a3c0e9ace 100644 --- a/appium/tests/specs/mock/setup/SetupOnlyRevokedKeyFromEKM.spec.ts +++ b/appium/tests/specs/mock/setup/SetupOnlyRevokedKeyFromEKM.spec.ts @@ -17,8 +17,7 @@ describe('SETUP: ', () => { const emailText = CommonData.simpleEmail.message; // When private key is revoked key, there are no public keys to pick. So missing sender public key error occurs. - const noPrivateKeyError = - 'Error\n' + 'Could not compose message\n\n' + 'Your account keys are not usable for encryption.'; + const noPrivateKeyError = CommonData.errors.noPrivateKey; mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration; mockApi.ekmConfig = { From b2c526d77d7d74676294366335ac7746f2819e38 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Thu, 30 Nov 2023 14:12:27 -0400 Subject: [PATCH 2/6] temp: ts module --- appium/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/tsconfig.json b/appium/tsconfig.json index 532a8cc72..d655f3b1a 100644 --- a/appium/tsconfig.json +++ b/appium/tsconfig.json @@ -6,7 +6,7 @@ "compilerOptions": { "outDir": "./.tsbuild/", "lib": ["ES2022", "DOM"], - "module": "commonjs", + "module": "ESNext", "target": "ES2022", "types": [ "node", From 7fdee4b229e4ae54cc5a06b4d35e2a4ae013e358 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Thu, 30 Nov 2023 15:15:56 -0400 Subject: [PATCH 3/6] Revert "temp: ts module" This reverts commit b2c526d77d7d74676294366335ac7746f2819e38. --- appium/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appium/tsconfig.json b/appium/tsconfig.json index d655f3b1a..532a8cc72 100644 --- a/appium/tsconfig.json +++ b/appium/tsconfig.json @@ -6,7 +6,7 @@ "compilerOptions": { "outDir": "./.tsbuild/", "lib": ["ES2022", "DOM"], - "module": "ESNext", + "module": "commonjs", "target": "ES2022", "types": [ "node", From 7b02e4a92cbdb34abf34d0a498ccc896c8d72a41 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Fri, 1 Dec 2023 01:28:26 -0400 Subject: [PATCH 4/6] temp: remove cache restore --- .semaphore/semaphore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 365c7a62c..a4ff3c13d 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -78,7 +78,7 @@ blocks: commands: - checkout && cd ~/git/flowcrypt-ios/ - mv ~/appium-env ~/git/flowcrypt-ios/appium/.env - - sem-version node 18 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules + - sem-version node 18 && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules - cd appium - cache restore FlowCrypt-$SEMAPHORE_GIT_SHA.app epilogue: From 6fd5696fd9841950006b69e7755924091eca715f Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Fri, 1 Dec 2023 04:04:37 -0400 Subject: [PATCH 5/6] Revert "temp: remove cache restore" This reverts commit 7b02e4a92cbdb34abf34d0a498ccc896c8d72a41. --- .semaphore/semaphore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index a4ff3c13d..365c7a62c 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -78,7 +78,7 @@ blocks: commands: - checkout && cd ~/git/flowcrypt-ios/ - mv ~/appium-env ~/git/flowcrypt-ios/appium/.env - - sem-version node 18 && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules + - sem-version node 18 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules - cd appium - cache restore FlowCrypt-$SEMAPHORE_GIT_SHA.app epilogue: From 0c5f2202e65aa16604382f51177bc9abd3cebbf7 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Fri, 1 Dec 2023 06:42:34 -0400 Subject: [PATCH 6/6] fix: node version --- .semaphore/semaphore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 365c7a62c..040122156 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -78,7 +78,7 @@ blocks: commands: - checkout && cd ~/git/flowcrypt-ios/ - mv ~/appium-env ~/git/flowcrypt-ios/appium/.env - - sem-version node 18 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules + - sem-version node 20 && cache restore appium-npm && cd ./appium && npm i && cd .. && cache store appium-npm appium/node_modules - cd appium - cache restore FlowCrypt-$SEMAPHORE_GIT_SHA.app epilogue: