diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 40c124992..26b3e8600 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -80,8 +80,7 @@ 9F228BA923C67587005D2CB6 /* UserCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F228BA823C67587005D2CB6 /* UserCredentials.swift */; }; 9F228BAA23C67729005D2CB6 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCAEFF16F5D91A35791730 /* DataExtensions.swift */; }; 9F23EA50237217140017DFED /* ComposeViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */; }; - 9F268891237DC55600428A94 /* ImportKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F268890237DC55600428A94 /* ImportKeyViewController.swift */; }; - 9F268894237DD98900428A94 /* EnterPassPhraseViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F268893237DD98900428A94 /* EnterPassPhraseViewDecorator.swift */; }; + 9F268891237DC55600428A94 /* SetupManuallyImportKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F268890237DC55600428A94 /* SetupManuallyImportKeyViewController.swift */; }; 9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */; }; 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB8D23298BCF00CF87EA /* Imap+folders.swift */; }; 9F31AB91232993F500CF87EA /* Imap+session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F31AB90232993F500CF87EA /* Imap+session.swift */; }; @@ -93,6 +92,11 @@ 9F3EF32B23B16ADE00FA0CEF /* CommonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C2723194E8500299985 /* CommonExtensions.swift */; }; 9F3EF32F23B172D300FA0CEF /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF32E23B172D300FA0CEF /* SearchViewController.swift */; }; 9F3EF33123B1785600FA0CEF /* MsgListViewConroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EF33023B1785600FA0CEF /* MsgListViewConroller.swift */; }; + 9F4163B8265ED61C00106194 /* SetupInitialViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */; }; + 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163E5266520B600106194 /* CommonNodesInputs.swift */; }; + 9F4164102665754A00106194 /* PromiseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F41640F2665754A00106194 /* PromiseExtensions.swift */; }; + 9F416421266575AE00106194 /* PromiseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F41640F2665754A00106194 /* PromiseExtensions.swift */; }; + 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F416427266575DC00106194 /* BackupServiceType.swift */; }; 9F41FA28253B75F4003B970D /* BackupSelectKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F41FA27253B75F4003B970D /* BackupSelectKeyViewController.swift */; }; 9F41FA2F253B7624003B970D /* BackupSelectKeyDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F41FA2E253B7624003B970D /* BackupSelectKeyDecorator.swift */; }; 9F4300CC2571045B00791CFB /* InboxViewControllerContainerDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4300CB2571045B00791CFB /* InboxViewControllerContainerDecorator.swift */; }; @@ -112,9 +116,15 @@ 9F6EE17B2598F9FA0059BA51 /* Gmail+Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6EE17A2598F9FA0059BA51 /* Gmail+Backup.swift */; }; 9F6EE18C25A8AF970059BA51 /* GeneralConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */; }; 9F716308234FC73E0031645E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9F71630A234FC73E0031645E /* Localizable.strings */; }; + 9F7920E32666D28400DA3D80 /* BackupServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4163EC266574CB00106194 /* BackupServiceMock.swift */; }; + 9F7920EE2666D32500DA3D80 /* BackupServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F416427266575DC00106194 /* BackupServiceType.swift */; }; + 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */; }; + 9F79228826696B0200DA3D80 /* PassPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79228726696B0200DA3D80 /* PassPhraseStorage.swift */; }; + 9F79229426696B9300DA3D80 /* KeyDataStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79229326696B9300DA3D80 /* KeyDataStorage.swift */; }; 9F7DE8C5232029BD00F10B3E /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA0C3D34A69851A238E87 /* Core.swift */; }; 9F7DE8C6232029D000F10B3E /* CoreTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCAC732B988D9704658812 /* CoreTypes.swift */; }; 9F7DE8C723202A0200F10B3E /* CoreHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCACB22E895C2500A99350 /* CoreHost.swift */; }; + 9F7E5137267AA51B00CE37C3 /* AlertsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7E5136267AA51B00CE37C3 /* AlertsFactory.swift */; }; 9F8220D526336626004B2009 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8220D426336626004B2009 /* Logger.swift */; }; 9F82779823737E0900E19C07 /* MessageViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82779723737E0900E19C07 /* MessageViewDecorator.swift */; }; 9F82D352256D74FA0069A702 /* InboxViewControllerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F82D351256D74FA0069A702 /* InboxViewControllerContainer.swift */; }; @@ -126,14 +136,16 @@ 9F953E09238310D500AEB98B /* KeyMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F953E08238310D500AEB98B /* KeyMethods.swift */; }; 9F9AAFFD2383E216000A00F1 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9AAFFC2383E216000A00F1 /* Document.swift */; }; 9F9ABC8723AC1EAA00D560E3 /* MessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */; }; + 9FA0157A26565B7800CBBA05 /* KeyMethodsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0157926565B7800CBBA05 /* KeyMethodsTest.swift */; }; + 9FA0158026565B9D00CBBA05 /* KeyMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F953E08238310D500AEB98B /* KeyMethods.swift */; }; 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA1988F253C841F008C9CF2 /* TableViewController.swift */; }; + 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA405C6265AEBA40084D133 /* SetupGenerateKeyViewController.swift */; }; 9FA9C83C264C2D75005A9670 /* MessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA9C83B264C2D75005A9670 /* MessageService.swift */; }; 9FB22CD625715CA10026EE64 /* BackupServiceErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CD525715CA10026EE64 /* BackupServiceErrorHandler.swift */; }; 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CDC25715CF50026EE64 /* GmailServiceError.swift */; }; 9FB22CE425715D3E0026EE64 /* GmailServiceErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CE325715D3E0026EE64 /* GmailServiceErrorHandler.swift */; }; 9FB22CF025715D960026EE64 /* BackupServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CEF25715D960026EE64 /* BackupServiceError.swift */; }; 9FB22CF725715DC50026EE64 /* KeyServiceErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22CF625715DC50026EE64 /* KeyServiceErrorHandler.swift */; }; - 9FB22D0425715DF00026EE64 /* KeyServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB22D0325715DF00026EE64 /* KeyServiceError.swift */; }; 9FBEAE5525D41BFF009E98D4 /* UserMailSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBEAE5425D41BFF009E98D4 /* UserMailSessionProvider.swift */; }; 9FBEAF3125DFB8E1009E98D4 /* DBMigrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBEAF3025DFB8E1009E98D4 /* DBMigrationService.swift */; }; 9FC411212595EA12001180A8 /* MessageSearchProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC411202595EA12001180A8 /* MessageSearchProvider.swift */; }; @@ -141,7 +153,18 @@ 9FC411352595EA94001180A8 /* Imap+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC411342595EA94001180A8 /* Imap+Search.swift */; }; 9FC4114C25961CEA001180A8 /* MailServiceProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4114B25961CEA001180A8 /* MailServiceProviderType.swift */; }; 9FC411902596229D001180A8 /* AppErr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA4B11D4531B3B04D01D1 /* AppErr.swift */; }; - 9FD364862381EFCB00657302 /* EnterPassPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD364852381EFCB00657302 /* EnterPassPhraseViewController.swift */; }; + 9FC7EAB3266A404D00F3BF5D /* PassPhrase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EAB2266A404D00F3BF5D /* PassPhrase.swift */; }; + 9FC7EB69266EB64F00F3BF5D /* PassPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F79228726696B0200DA3D80 /* PassPhraseStorage.swift */; }; + 9FC7EB6F266EB66200F3BF5D /* PassPhrase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EAB2266A404D00F3BF5D /* PassPhrase.swift */; }; + 9FC7EB76266EB67B00F3BF5D /* EncryptedStorageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */; }; + 9FC7EB7C266EB67D00F3BF5D /* EncryptedStorageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */; }; + 9FC7EBA3266EB95300F3BF5D /* PassPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */; }; + 9FC7EBAA266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */; }; + 9FC7EBB0266EBD4600F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */; }; + 9FC7EBC2266EBE0100F3BF5D /* EmailProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */; }; + 9FC7EBC9266EBE0F00F3BF5D /* LocalPassPhraseStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBC8266EBE0F00F3BF5D /* LocalPassPhraseStorageMock.swift */; }; + 9FC7EBD0266EBE1D00F3BF5D /* EncryptedPassPhraseStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBCF266EBE1D00F3BF5D /* EncryptedPassPhraseStorageMock.swift */; }; + 9FD364862381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */; }; 9FDF364D235A1CCD00614596 /* SignInTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF364C235A1CCD00614596 /* SignInTest.swift */; }; 9FDF3650235A1D3F00614596 /* UITestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF364F235A1D3F00614596 /* UITestHelper.swift */; }; 9FDF3652235A1EDE00614596 /* XCUIApplicationBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF3651235A1EDE00614596 /* XCUIApplicationBuilder.swift */; }; @@ -171,7 +194,7 @@ C132B9BE1EC2DBD800763715 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C132B9BC1EC2DBD800763715 /* LaunchScreen.storyboard */; }; C132B9CB1EC2DE6400763715 /* GeneralConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */; }; C132B9D91EC30E1D00763715 /* InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C132B9D81EC30E1D00763715 /* InboxViewController.swift */; }; - C192421F1EC48B6900C3D251 /* SetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C192421E1EC48B6900C3D251 /* SetupViewController.swift */; }; + C192421F1EC48B6900C3D251 /* SetupBackupsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C192421E1EC48B6900C3D251 /* SetupBackupsViewController.swift */; }; CB5986DFBB2B4FF4AD555821 /* Pods_FlowCryptUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3FA3FBD561C4CAD27852EB0B /* Pods_FlowCryptUITests.framework */; }; D204DBA223FB35700083B9D6 /* FlowCryptUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D204DBA023FB35700083B9D6 /* FlowCryptUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; D20D3C672520AB1000D4AA9A /* BackupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20D3C662520AB1000D4AA9A /* BackupViewController.swift */; }; @@ -443,8 +466,7 @@ 9F228BA823C67587005D2CB6 /* UserCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentials.swift; sourceTree = ""; }; 9F23EA4D237216FA0017DFED /* TextViewCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewCellNode.swift; sourceTree = ""; }; 9F23EA4F237217140017DFED /* ComposeViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewDecorator.swift; sourceTree = ""; }; - 9F268890237DC55600428A94 /* ImportKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportKeyViewController.swift; sourceTree = ""; }; - 9F268893237DD98900428A94 /* EnterPassPhraseViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPassPhraseViewDecorator.swift; sourceTree = ""; }; + 9F268890237DC55600428A94 /* SetupManuallyImportKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupManuallyImportKeyViewController.swift; sourceTree = ""; }; 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+retry.swift"; sourceTree = ""; }; 9F31AB8D23298BCF00CF87EA /* Imap+folders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+folders.swift"; sourceTree = ""; }; 9F31AB90232993F500CF87EA /* Imap+session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+session.swift"; sourceTree = ""; }; @@ -455,6 +477,11 @@ 9F3EF32923B15C9500FA0CEF /* ImapHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapHelperTest.swift; sourceTree = ""; }; 9F3EF32E23B172D300FA0CEF /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 9F3EF33023B1785600FA0CEF /* MsgListViewConroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgListViewConroller.swift; sourceTree = ""; }; + 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupInitialViewController.swift; sourceTree = ""; }; + 9F4163E5266520B600106194 /* CommonNodesInputs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonNodesInputs.swift; sourceTree = ""; }; + 9F4163EC266574CB00106194 /* BackupServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceMock.swift; sourceTree = ""; }; + 9F41640F2665754A00106194 /* PromiseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromiseExtensions.swift; sourceTree = ""; }; + 9F416427266575DC00106194 /* BackupServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceType.swift; sourceTree = ""; }; 9F41FA27253B75F4003B970D /* BackupSelectKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSelectKeyViewController.swift; sourceTree = ""; }; 9F41FA2E253B7624003B970D /* BackupSelectKeyDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSelectKeyDecorator.swift; sourceTree = ""; }; 9F4300CB2571045B00791CFB /* InboxViewControllerContainerDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewControllerContainerDecorator.swift; sourceTree = ""; }; @@ -486,6 +513,10 @@ 9F716309234FC73E0031645E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 9F71630B234FC7500031645E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 9F72E866263ECE2A0039CF81 /* Trace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trace.swift; sourceTree = ""; }; + 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPraseSaveable.swift; sourceTree = ""; }; + 9F79228726696B0200DA3D80 /* PassPhraseStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassPhraseStorage.swift; sourceTree = ""; }; + 9F79229326696B9300DA3D80 /* KeyDataStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyDataStorage.swift; sourceTree = ""; }; + 9F7E5136267AA51B00CE37C3 /* AlertsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertsFactory.swift; sourceTree = ""; }; 9F8220D426336626004B2009 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 9F8277952373732000E19C07 /* UIImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 9F82779723737E0900E19C07 /* MessageViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewDecorator.swift; sourceTree = ""; }; @@ -505,24 +536,32 @@ 9F95A3F623607C0900C80B64 /* SigninButtonNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigninButtonNode.swift; sourceTree = ""; }; 9F9AAFFC2383E216000A00F1 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; 9F9ABC8623AC1EAA00D560E3 /* MessageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContext.swift; sourceTree = ""; }; + 9FA0157926565B7800CBBA05 /* KeyMethodsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyMethodsTest.swift; sourceTree = ""; }; 9FA1988F253C841F008C9CF2 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; + 9FA405C6265AEBA40084D133 /* SetupGenerateKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupGenerateKeyViewController.swift; sourceTree = ""; }; 9FA9C83B264C2D75005A9670 /* MessageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageService.swift; sourceTree = ""; }; 9FB22CD525715CA10026EE64 /* BackupServiceErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceErrorHandler.swift; sourceTree = ""; }; 9FB22CDC25715CF50026EE64 /* GmailServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailServiceError.swift; sourceTree = ""; }; 9FB22CE325715D3E0026EE64 /* GmailServiceErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GmailServiceErrorHandler.swift; sourceTree = ""; }; 9FB22CEF25715D960026EE64 /* BackupServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupServiceError.swift; sourceTree = ""; }; 9FB22CF625715DC50026EE64 /* KeyServiceErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceErrorHandler.swift; sourceTree = ""; }; - 9FB22D0325715DF00026EE64 /* KeyServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceError.swift; sourceTree = ""; }; 9FBEAE5425D41BFF009E98D4 /* UserMailSessionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMailSessionProvider.swift; sourceTree = ""; }; 9FBEAF3025DFB8E1009E98D4 /* DBMigrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMigrationService.swift; sourceTree = ""; }; 9FC411202595EA12001180A8 /* MessageSearchProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSearchProvider.swift; sourceTree = ""; }; 9FC4112D2595EA8B001180A8 /* Gmail+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+Search.swift"; sourceTree = ""; }; 9FC411342595EA94001180A8 /* Imap+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+Search.swift"; sourceTree = ""; }; 9FC4114B25961CEA001180A8 /* MailServiceProviderType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailServiceProviderType.swift; sourceTree = ""; }; + 9FC7EAB2266A404D00F3BF5D /* PassPhrase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhrase.swift; sourceTree = ""; }; + 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorageProtocols.swift; sourceTree = ""; }; + 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseStorageTests.swift; sourceTree = ""; }; + 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryPassPhraseStorage.swift; sourceTree = ""; }; + 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailProviderMock.swift; sourceTree = ""; }; + 9FC7EBC8266EBE0F00F3BF5D /* LocalPassPhraseStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPassPhraseStorageMock.swift; sourceTree = ""; }; + 9FC7EBCF266EBE1D00F3BF5D /* EncryptedPassPhraseStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedPassPhraseStorageMock.swift; sourceTree = ""; }; 9FD22A19230FD781005067A6 /* NavigationBarItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarItemsView.swift; sourceTree = ""; }; 9FD22A1B230FE7D0005067A6 /* Then.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Then.swift; sourceTree = ""; }; 9FD22A1E230FEFC6005067A6 /* NavigationBarActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarActionButton.swift; sourceTree = ""; }; - 9FD364852381EFCB00657302 /* EnterPassPhraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPassPhraseViewController.swift; sourceTree = ""; }; + 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupManuallyEnterPassPhraseViewController.swift; sourceTree = ""; }; 9FDF3637235A0B3100614596 /* InfoCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCellNode.swift; sourceTree = ""; }; 9FDF3639235A0B3B00614596 /* HeaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderNode.swift; sourceTree = ""; }; 9FDF3641235A1B0100614596 /* FlowCryptUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlowCryptUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -568,7 +607,7 @@ C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralConstants.swift; sourceTree = ""; }; C132B9D81EC30E1D00763715 /* InboxViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxViewController.swift; sourceTree = ""; }; C132B9DF1EC333AA00763715 /* FlowCrypt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FlowCrypt-Bridging-Header.h"; sourceTree = ""; }; - C192421E1EC48B6900C3D251 /* SetupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupViewController.swift; sourceTree = ""; }; + C192421E1EC48B6900C3D251 /* SetupBackupsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupBackupsViewController.swift; sourceTree = ""; }; D204DB9E23FB35700083B9D6 /* FlowCryptUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FlowCryptUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D204DBA023FB35700083B9D6 /* FlowCryptUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FlowCryptUI.h; sourceTree = ""; }; D204DBA123FB35700083B9D6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -766,7 +805,9 @@ 21F836A62652A1B700B2448C /* Functionallity */ = { isa = PBXGroup; children = ( + 9F4164162665757700106194 /* PGP */, 21F836A72652A1CD00B2448C /* WKDURLs */, + 9F4163F3266574CF00106194 /* Services */, ); path = Functionallity; sourceTree = ""; @@ -966,15 +1007,6 @@ path = Resources; sourceTree = ""; }; - 9F268892237DC55E00428A94 /* ImportKey */ = { - isa = PBXGroup; - children = ( - D29A0000240C133E00C1387D /* Import Key */, - D29AFFFF240C133800C1387D /* Enter Pass Phrase */, - ); - path = ImportKey; - sourceTree = ""; - }; 9F2A939F23363D2400014A92 /* Main */ = { isa = PBXGroup; children = ( @@ -1008,11 +1040,37 @@ path = Menu; sourceTree = ""; }; + 9F4163F3266574CF00106194 /* Services */ = { + isa = PBXGroup; + children = ( + 9F4163FE2665750500106194 /* Backup Services */, + 9FC7EBB6266EBDF000F3BF5D /* PassPhraseStorageTests */, + ); + path = Services; + sourceTree = ""; + }; + 9F4163FE2665750500106194 /* Backup Services */ = { + isa = PBXGroup; + children = ( + 9F4163EC266574CB00106194 /* BackupServiceMock.swift */, + ); + path = "Backup Services"; + sourceTree = ""; + }; + 9F4164162665757700106194 /* PGP */ = { + isa = PBXGroup; + children = ( + 9FA0157926565B7800CBBA05 /* KeyMethodsTest.swift */, + ); + path = PGP; + sourceTree = ""; + }; 9F41FA1C25372C2D003B970D /* Backup Services */ = { isa = PBXGroup; children = ( D20D3C742520AB9A00D4AA9A /* BackupService.swift */, 9FB22CEF25715D960026EE64 /* BackupServiceError.swift */, + 9F416427266575DC00106194 /* BackupServiceType.swift */, ); path = "Backup Services"; sourceTree = ""; @@ -1076,6 +1134,7 @@ children = ( 9F92EE71236F165E009BE0D7 /* EncryptedStorage.swift */, 9F589F14238C8249007FD759 /* KeyChainService.swift */, + 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */, ); path = "Encrypted Storage"; sourceTree = ""; @@ -1137,8 +1196,10 @@ 9FB22CFD25715DDF0026EE64 /* Key Services */ = { isa = PBXGroup; children = ( + 9F79229326696B9300DA3D80 /* KeyDataStorage.swift */, + 9F79228726696B0200DA3D80 /* PassPhraseStorage.swift */, + 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */, D2891AC124C59EFA008918E3 /* KeyService.swift */, - 9FB22D0325715DF00026EE64 /* KeyServiceError.swift */, ); path = "Key Services"; sourceTree = ""; @@ -1189,10 +1250,22 @@ path = "Mail Provider"; sourceTree = ""; }; + 9FC7EBB6266EBDF000F3BF5D /* PassPhraseStorageTests */ = { + isa = PBXGroup; + children = ( + 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */, + 9FC7EBC8266EBE0F00F3BF5D /* LocalPassPhraseStorageMock.swift */, + 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */, + 9FC7EBCF266EBE1D00F3BF5D /* EncryptedPassPhraseStorageMock.swift */, + ); + path = PassPhraseStorageTests; + sourceTree = ""; + }; 9FD22A1D230FEEC8005067A6 /* Common UI */ = { isa = PBXGroup; children = ( 5A5C234923A0422C0015E705 /* View Controllers */, + 9F4163E5266520B600106194 /* CommonNodesInputs.swift */, ); path = "Common UI"; sourceTree = ""; @@ -1288,6 +1361,7 @@ A3A680EF22EEF0BF00905813 /* FlowCryptTests-Bridging-Header.h */, D2A9CA44242622F800E1D898 /* GeneralConstantsTest.swift */, 9F003D9D25EA910B00EB38C0 /* LocalStorageTests.swift */, + 9F41640F2665754A00106194 /* PromiseExtensions.swift */, ); path = FlowCryptTests; sourceTree = ""; @@ -1364,7 +1438,6 @@ D2FF6969243115FE007182F0 /* SignIn Other */, 32DCA8D5AF0A43354CC7F58B /* SignIn */, C192421D1EC48B5600C3D251 /* Setup */, - 9F268892237DC55E00428A94 /* ImportKey */, D29AFFF12409301300C1387D /* Bootstrap */, C132B9D71EC30E0B00763715 /* Inbox */, 04B4728F1ECE29F600B8266F /* Compose */, @@ -1403,8 +1476,13 @@ C192421D1EC48B5600C3D251 /* Setup */ = { isa = PBXGroup; children = ( - C192421E1EC48B6900C3D251 /* SetupViewController.swift */, + 9F4163B7265ED61C00106194 /* SetupInitialViewController.swift */, + 9FA405C6265AEBA40084D133 /* SetupGenerateKeyViewController.swift */, + C192421E1EC48B6900C3D251 /* SetupBackupsViewController.swift */, + 9F268890237DC55600428A94 /* SetupManuallyImportKeyViewController.swift */, + 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */, 9F17976C2368EEBD002BF770 /* SetupViewDecorator.swift */, + 9F7920F42667CEF100DA3D80 /* PassPraseSaveable.swift */, ); path = Setup; sourceTree = ""; @@ -1440,6 +1518,7 @@ D212D36324C1AC4800035991 /* KeyId.swift */, D212D35C24C1AACF00035991 /* PrvKeyInfo.swift */, D2E26F6B24F25B1F00612AF1 /* KeyAlgo.swift */, + 9FC7EAB2266A404D00F3BF5D /* PassPhrase.swift */, ); path = Models; sourceTree = ""; @@ -1538,15 +1617,6 @@ path = Views; sourceTree = ""; }; - D29A0000240C133E00C1387D /* Import Key */ = { - isa = PBXGroup; - children = ( - 9F268890237DC55600428A94 /* ImportKeyViewController.swift */, - 9F268893237DD98900428A94 /* EnterPassPhraseViewDecorator.swift */, - ); - path = "Import Key"; - sourceTree = ""; - }; D29A0001240C137700C1387D /* MessageList Extension */ = { isa = PBXGroup; children = ( @@ -1608,14 +1678,6 @@ path = Bootstrap; sourceTree = ""; }; - D29AFFFF240C133800C1387D /* Enter Pass Phrase */ = { - isa = PBXGroup; - children = ( - 9FD364852381EFCB00657302 /* EnterPassPhraseViewController.swift */, - ); - path = "Enter Pass Phrase"; - sourceTree = ""; - }; D2A1D3B823FD64AB00D626D6 /* FlowCryptUIApplication */ = { isa = PBXGroup; children = ( @@ -1726,6 +1788,7 @@ children = ( D952B71C1ED0CB2500E5C02B /* MessageViewController.swift */, 9F82779723737E0900E19C07 /* MessageViewDecorator.swift */, + 9F7E5136267AA51B00CE37C3 /* AlertsFactory.swift */, ); path = Msg; sourceTree = ""; @@ -2284,14 +2347,25 @@ buildActionMask = 2147483647; files = ( D2D27B7A248A874C007346FA /* BigIntExtension.swift in Sources */, + 9FC7EB7C266EB67D00F3BF5D /* EncryptedStorageProtocols.swift in Sources */, 9F3EF32B23B16ADE00FA0CEF /* CommonExtensions.swift in Sources */, + 9FC7EBC2266EBE0100F3BF5D /* EmailProviderMock.swift in Sources */, + 9FC7EBD0266EBE1D00F3BF5D /* EncryptedPassPhraseStorageMock.swift in Sources */, + 9FA0157A26565B7800CBBA05 /* KeyMethodsTest.swift in Sources */, A357699622EA2BC8009242C4 /* KeyInfo.swift in Sources */, 9FC411902596229D001180A8 /* AppErr.swift in Sources */, + 9FC7EB69266EB64F00F3BF5D /* PassPhraseStorage.swift in Sources */, D212D35E24C1AACF00035991 /* PrvKeyInfo.swift in Sources */, 9F1D5769263B540100477938 /* Logger.swift in Sources */, + 9FC7EBC9266EBE0F00F3BF5D /* LocalPassPhraseStorageMock.swift in Sources */, A3DAD5FE22E4574B00F2C4CD /* FlowCryptCoreTests.swift in Sources */, + 9FC7EBB0266EBD4600F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */, 21EA3B2326565B5D00691848 /* DomainRulesTests.swift in Sources */, + 9F4164102665754A00106194 /* PromiseExtensions.swift in Sources */, 21F836CC2652A38700B2448C /* ZBase32EncodingTests.swift in Sources */, + 9FC7EBA3266EB95300F3BF5D /* PassPhraseStorageTests.swift in Sources */, + 9F7920EE2666D32500DA3D80 /* BackupServiceType.swift in Sources */, + 9FC7EB6F266EB66200F3BF5D /* PassPhrase.swift in Sources */, D2A9CA45242622F800E1D898 /* GeneralConstantsTest.swift in Sources */, A3DAD60B22E458C300F2C4CD /* DataExtensions.swift in Sources */, 21EA3B532656611C00691848 /* OrganisationalRule.swift in Sources */, @@ -2307,11 +2381,13 @@ D212D36524C1AC4800035991 /* KeyId.swift in Sources */, 9F003DBC25EA92D000EB38C0 /* LogOutHandler.swift in Sources */, 21F836A02652A19A00B2448C /* WKDURLsConstructor.swift in Sources */, + 9F7920E32666D28400DA3D80 /* BackupServiceMock.swift in Sources */, 9F7DE8C5232029BD00F10B3E /* Core.swift in Sources */, 32DCAD6360C9EFF4FDD8EF6F /* DispatchTimeExtension.swift in Sources */, 9F7DE8C723202A0200F10B3E /* CoreHost.swift in Sources */, 9F3EF32823B15C8400FA0CEF /* ImapHelper.swift in Sources */, 9F003D9E25EA910B00EB38C0 /* LocalStorageTests.swift in Sources */, + 9FA0158026565B9D00CBBA05 /* KeyMethods.swift in Sources */, 32DCAF683D87EA6221F71335 /* SequenceExtensions.swift in Sources */, 9F589F11238C7DDC007FD759 /* User.swift in Sources */, 21F836D32652A46E00B2448C /* WKDURLsConstructorTests.swift in Sources */, @@ -2327,6 +2403,7 @@ D2A9CA3D242619EC00E1D898 /* SignInViewDecorator.swift in Sources */, 9F4300CC2571045B00791CFB /* InboxViewControllerContainerDecorator.swift in Sources */, 9FB22CE425715D3E0026EE64 /* GmailServiceErrorHandler.swift in Sources */, + 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */, 04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */, C132B9B41EC2DBD800763715 /* AppDelegate.swift in Sources */, 04B472961ECE29F600B8266F /* SideMenuNavigationController.swift in Sources */, @@ -2337,10 +2414,11 @@ C132B9CB1EC2DE6400763715 /* GeneralConstants.swift in Sources */, 5ADEDCBE23A4363700EC495E /* KeyDetailInfoViewController.swift in Sources */, D20D3C752520AB9A00D4AA9A /* BackupService.swift in Sources */, - C192421F1EC48B6900C3D251 /* SetupViewController.swift in Sources */, + C192421F1EC48B6900C3D251 /* SetupBackupsViewController.swift in Sources */, 9F0C3C2623194E0A00299985 /* FolderViewModel.swift in Sources */, 21CE25E62650070300ADFF4B /* WKDURLsConstructor.swift in Sources */, 9FC411212595EA12001180A8 /* MessageSearchProvider.swift in Sources */, + 9F4163B8265ED61C00106194 /* SetupInitialViewController.swift in Sources */, D27B911D24EFE806002DF0A1 /* ContactObject.swift in Sources */, 9FDF3654235A218E00614596 /* main.swift in Sources */, D2E26F6C24F25B1F00612AF1 /* KeyAlgo.swift in Sources */, @@ -2378,6 +2456,8 @@ 32DCA04CA0DAB79C39514782 /* CoreTypes.swift in Sources */, 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */, 5ADEDCB923A42B9400EC495E /* KeyDetailViewDecorator.swift in Sources */, + 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */, + 9F7E5137267AA51B00CE37C3 /* AlertsFactory.swift in Sources */, 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */, 9FF0673325520DE400FCC9E6 /* GmailService+send.swift in Sources */, D20D3C892520B67C00D4AA9A /* BackupOptionsViewController.swift in Sources */, @@ -2385,15 +2465,16 @@ D20D3C672520AB1000D4AA9A /* BackupViewController.swift in Sources */, D2E26F6324F1698100612AF1 /* ContactsListViewController.swift in Sources */, 9F53CB7B2555E1E300C0157A /* GmailService+folders.swift in Sources */, + 9F79228826696B0200DA3D80 /* PassPhraseStorage.swift in Sources */, D2E26F6624F169B400612AF1 /* ContactsListDecorator.swift in Sources */, D2FF6968243115F9007182F0 /* EmailProviderViewDecorator.swift in Sources */, 9F589F0D238C7A9B007FD759 /* LocalStorage.swift in Sources */, D2FC1C0624D82C9F003B949D /* ContactsService.swift in Sources */, - 9FB22D0425715DF00026EE64 /* KeyServiceError.swift in Sources */, - 9FD364862381EFCB00657302 /* EnterPassPhraseViewController.swift in Sources */, + 9FD364862381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift in Sources */, D212D35D24C1AACF00035991 /* PrvKeyInfo.swift in Sources */, 9F23EA50237217140017DFED /* ComposeViewDecorator.swift in Sources */, D21574B724376852006B094F /* ConnectionType.swift in Sources */, + 9F7920F52667CEF100DA3D80 /* PassPraseSaveable.swift in Sources */, 5ADEDCAF23A3EA9E00EC495E /* KeySettingsViewDecorator.swift in Sources */, 9F3EF33123B1785600FA0CEF /* MsgListViewConroller.swift in Sources */, 9F31ABA0232C071700CF87EA /* GlobalRouter.swift in Sources */, @@ -2413,6 +2494,7 @@ D274724124F97C5C006BA6EF /* CacheService.swift in Sources */, A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */, 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */, + 9F416421266575AE00106194 /* PromiseExtensions.swift in Sources */, D2891AC224C59EFA008918E3 /* KeyService.swift in Sources */, D269E02724103A20000495C3 /* ComposeViewControllerInput.swift in Sources */, 9F0C3C142316E69300299985 /* User.swift in Sources */, @@ -2422,7 +2504,6 @@ 9FE1B3A02565B0CE00D6D086 /* Message.swift in Sources */, D27177492424D73000BDA9A9 /* InboxViewDecorator.swift in Sources */, D2F41371243CC76F0066AFB5 /* SessionObject.swift in Sources */, - 9F268894237DD98900428A94 /* EnterPassPhraseViewDecorator.swift in Sources */, 9F9362192573D10E0009912F /* Imap+Message.swift in Sources */, 32DCA9C61ABB3234649B374E /* CoreHost.swift in Sources */, 9F41FA28253B75F4003B970D /* BackupSelectKeyViewController.swift in Sources */, @@ -2431,6 +2512,7 @@ 9F0C3C102316DD5B00299985 /* GoogleUserService.swift in Sources */, D24F4C2223E2359B00C5EEE4 /* BootstrapViewController.swift in Sources */, D211CE7623FC36BC00D1CE38 /* UIColorExtension.swift in Sources */, + 9FC7EBAA266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */, 9FA9C83C264C2D75005A9670 /* MessageService.swift in Sources */, D2FF6966243115EC007182F0 /* EmailProviderViewController.swift in Sources */, D2CDC3D22402D4DA002B045F /* UIViewControllerExtensions.swift in Sources */, @@ -2441,9 +2523,12 @@ D29AFFF92409767F00C1387D /* GoogleContactsResponse.swift in Sources */, 9F003D6125E1B4ED00EB38C0 /* TrashFolderProvider.swift in Sources */, 9FF0671025520D7100FCC9E6 /* MessageGateway.swift in Sources */, + 9FC7EAB3266A404D00F3BF5D /* PassPhrase.swift in Sources */, 9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */, + 9F79229426696B9300DA3D80 /* KeyDataStorage.swift in Sources */, 9F82D352256D74FA0069A702 /* InboxViewControllerContainer.swift in Sources */, D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */, + 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */, 9F6EE1552597399D0059BA51 /* BackupProvider.swift in Sources */, 9FEED1D2230DAD1E00700F8E /* InboxViewModel.swift in Sources */, 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */, @@ -2458,12 +2543,13 @@ 9F003DB625EA92BC00EB38C0 /* LogOutHandler.swift in Sources */, 9F0C3C122316DDA500299985 /* DataService.swift in Sources */, D20D3C6E2520AB3900D4AA9A /* BackupViewDecorator.swift in Sources */, - 9F268891237DC55600428A94 /* ImportKeyViewController.swift in Sources */, + 9F268891237DC55600428A94 /* SetupManuallyImportKeyViewController.swift in Sources */, D2E26F7024F266F300612AF1 /* ContactDetailViewController.swift in Sources */, 9FDF3656235A22DA00614596 /* AppReset.swift in Sources */, 5A39F43F239EE7D2001F4607 /* SegmentedViewController.swift in Sources */, 5A5C234B23A042520015E705 /* WebViewController.swift in Sources */, 9F5C2A7E257E64D500DE9B4B /* MessageOperationsProvider.swift in Sources */, + 9FC7EB76266EB67B00F3BF5D /* EncryptedStorageProtocols.swift in Sources */, 32DCACF9C6FC4B9330C9B362 /* Imap+send.swift in Sources */, 32DCAF95A6A329C3136B1C8E /* Imap+msg.swift in Sources */, 21EA3B592656611D00691848 /* OrganisationalRule.swift in Sources */, diff --git a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/FlowCrypt.xcscheme b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/FlowCrypt.xcscheme index 061c191e6..8b1a6f240 100644 --- a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/FlowCrypt.xcscheme +++ b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/FlowCrypt.xcscheme @@ -7,8 +7,8 @@ buildImplicitDependencies = "YES"> @@ -84,7 +84,8 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" language = "en" - region = "GB"> + region = "GB" + codeCoverageEnabled = "YES"> - - - - - - diff --git a/FlowCrypt/App/Logger.swift b/FlowCrypt/App/Logger.swift index bee940c46..0bb11aea2 100644 --- a/FlowCrypt/App/Logger.swift +++ b/FlowCrypt/App/Logger.swift @@ -162,6 +162,12 @@ extension Logger { /// log all db migration events case migration = "Migration" + + /// Core related logs + case core = "Core" + + /// Setup Flow logs + case setup = "Setup" } static func nested(in type: T.Type, with logLabel: LogLabels) -> Logger { diff --git a/FlowCrypt/Common UI/CommonNodesInputs.swift b/FlowCrypt/Common UI/CommonNodesInputs.swift new file mode 100644 index 000000000..10f06baca --- /dev/null +++ b/FlowCrypt/Common UI/CommonNodesInputs.swift @@ -0,0 +1,75 @@ +// +// CommonNodes.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 31.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import FlowCryptUI +import UIKit + +extension TextCellNode.Input { + static func loading(with size: CGSize) -> TextCellNode.Input { + .init( + backgroundColor: .backgroundColor, + title: "loading_title".localized + "...", + withSpinner: true, + size: size + ) + } +} + +extension ButtonCellNode.Input { + static let retry: ButtonCellNode.Input = .init( + title: "retry_title" + .localized + .attributed(.bold(16), color: .white, alignment: .center), + insets: UIEdgeInsets(top: 16, left: 24, bottom: 8, right: 24), + color: .main + ) + + static let chooseAnotherAccount: ButtonCellNode.Input = .init( + title: "setup_use_another" + .localized + .attributed( + .regular(15), + color: UIColor.colorFor( + darkStyle: .white, + lightStyle: .blueColor + ), + alignment: .center + ), + insets: .side(8), + color: .backgroundColor + ) +} + +extension CheckBoxTextNode.Input { + static func common(with text: String, isSelected: Bool) -> CheckBoxTextNode.Input { + let attributedTitle = text + .attributed(.bold(14), color: .mainTextColor, alignment: .center) + + let checkboxColor: UIColor = isSelected + ? .main + : .lightGray + + return CheckBoxTextNode.Input( + title: attributedTitle, + insets: .init(top: 8, left: 16, bottom: 8, right: 16), + preferredSize: CGSize(width: 30, height: 30), + checkBoxInput: CheckBoxNode.Input( + color: checkboxColor, + strokeWidth: 2 + ) + ) + } + + static func passPhraseLocally(isSelected: Bool) -> CheckBoxTextNode.Input { + Self.common(with: "setup_save_pass_locally".localized, isSelected: isSelected) + } + + static func passPhraseMemory(isSelected: Bool) -> CheckBoxTextNode.Input { + Self.common(with: "setup_save_pass_in_memory".localized, isSelected: isSelected) + } +} diff --git a/FlowCrypt/Common UI/View Controllers/SegmentedViewController.swift b/FlowCrypt/Common UI/View Controllers/SegmentedViewController.swift index 2f6378cbc..60e962ba3 100644 --- a/FlowCrypt/Common UI/View Controllers/SegmentedViewController.swift +++ b/FlowCrypt/Common UI/View Controllers/SegmentedViewController.swift @@ -9,7 +9,7 @@ import AsyncDisplayKit import FlowCryptUI -// TODO: - ANTON - Move to FlowCryptUI +// TODO: - Ticket - Move to FlowCryptUI struct Segment { let viewController: UIViewController let title: NSAttributedString diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 300b06ad9..7685dd38e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -46,7 +46,7 @@ final class ComposeViewController: TableNodeViewController { private let messageSender: MessageGateway private let notificationCenter: NotificationCenter - private let dataService: DataServiceType & KeyDataServiceType + private let dataService: KeyStorageType private let decorator: ComposeViewDecoratorType private let core: Core private let contactsService: ContactsServiceType @@ -59,11 +59,13 @@ final class ComposeViewController: TableNodeViewController { private var contextToSend = Context() private var state: State = .main + private let email: String init( + email: String, messageSender: MessageGateway = MailProvider.shared.messageSender, notificationCenter: NotificationCenter = .default, - dataService: DataServiceType & KeyDataServiceType = DataService.shared, + dataService: KeyStorageType = KeyDataStorage(), decorator: ComposeViewDecoratorType = ComposeViewDecorator(), input: ComposeViewController.Input = .empty, core: Core = Core.shared, @@ -71,6 +73,7 @@ final class ComposeViewController: TableNodeViewController { userDefaults: UserDefaults = .standard, contactsService: ContactsServiceType = ContactsService() ) { + self.email = email self.messageSender = messageSender self.notificationCenter = notificationCenter self.dataService = dataService @@ -243,7 +246,7 @@ extension ComposeViewController { ?? self.contextToSend.subject ?? "(no subject)" - guard let myPubKey = self.dataService.publicKey else { + guard let myPubKey = self.dataService.publicKey() else { self.showAlert(message: "compose_no_pub_sender".localized) return false } @@ -309,7 +312,7 @@ extension ComposeViewController { to: to, cc: cc, bcc: bcc, - from: dataService.email ?? "", + from: email, subject: subject, replyToMimeMsg: replyToMimeMsg, atts: atts diff --git a/FlowCrypt/Controllers/ImportKey/Import Key/EnterPassPhraseViewDecorator.swift b/FlowCrypt/Controllers/ImportKey/Import Key/EnterPassPhraseViewDecorator.swift deleted file mode 100644 index 40f028ae1..000000000 --- a/FlowCrypt/Controllers/ImportKey/Import Key/EnterPassPhraseViewDecorator.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ImportKeyDecorator.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 14.11.2019. -// Copyright © 2019 FlowCrypt Limited. All rights reserved. -// - -import FlowCryptUI -import UIKit - -protocol EnterPassPhraseViewDecoratorType { - var sceneTitle: String { get } - var title: NSAttributedString { get } - var fileImportTitle: NSAttributedString { get } - var pasteBoardTitle: NSAttributedString { get } - var titleInsets: UIEdgeInsets { get } - var subTitleInset: UIEdgeInsets { get } - var buttonInsets: UIEdgeInsets { get } - var subtitleStyle: (String) -> NSAttributedString { get } - - var passPhraseTitle: NSAttributedString { get } - var passPhraseContine: NSAttributedString { get } - var passPhraseChooseAnother: NSAttributedString { get } - var passPhraseInsets: UIEdgeInsets { get } - var passPhraseTextFieldStyle: TextFieldCellNode.Input { get } -} - -struct EnterPassPhraseViewDecorator: EnterPassPhraseViewDecoratorType { - let sceneTitle = "import_key_title".localized - - var title: NSAttributedString { - attributed(title: "import_key_description") - } - - var passPhraseTitle: NSAttributedString { - attributed(title: "import_key_description") - } - - var fileImportTitle: NSAttributedString { - attributed(subTitle: "import_key_file") - } - - var pasteBoardTitle: NSAttributedString { - attributed(subTitle: "import_key_paste") - } - - var passPhraseContine: NSAttributedString { - attributed(subTitle: "import_key_continue") - } - - var passPhraseChooseAnother: NSAttributedString { - attributed(subTitle: "import_key_choose", color: UIColor.white.withAlphaComponent(0.9)) - } - - let buttonInsets = UIEdgeInsets(top: 16, left: 16, bottom: 8, right: 16) - let passPhraseInsets = UIEdgeInsets(top: 32, left: 16, bottom: 0, right: 16) - let titleInsets = UIEdgeInsets(top: 100, left: 16, bottom: 30, right: 16) - let subTitleInset = UIEdgeInsets(top: 8, left: 16, bottom: 16, right: 16) - var subtitleStyle: (String) -> NSAttributedString { { $0.attributed(.regular(17), alignment: .center) } - } - - private func attributed(title: String) -> NSAttributedString { - title.localized.attributed(.bold(35), alignment: .center) - } - - private func attributed(subTitle: String, color: UIColor = .white) -> NSAttributedString { - subTitle.localized.attributed(.regular(17), color: color, alignment: .center) - } - - let passPhraseTextFieldStyle = SetupCommonStyle.passPhraseTextFieldStyle -} diff --git a/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainer.swift b/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainer.swift index 223d0b114..cf1ba02fa 100644 --- a/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainer.swift +++ b/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainer.swift @@ -147,7 +147,7 @@ extension InboxViewControllerContainer: ASTableDelegate, ASTableDataSource { switch self.state { case .loading: return TextCellNode( - input: self.decorator.loadingInput(with: size) + input: .loading(with: size) ) case .error(let error): return TextCellNode( diff --git a/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainerDecorator.swift b/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainerDecorator.swift index 479e4ff4e..29e10d695 100644 --- a/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainerDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/Container/InboxViewControllerContainerDecorator.swift @@ -31,13 +31,4 @@ struct InboxViewControllerContainerDecorator { func retryActionTitle() -> NSAttributedString { "retry_title".localized.attributed(color: .white) } - - func loadingInput(with size: CGSize) -> TextCellNode.Input { - TextCellNode.Input( - backgroundColor: .backgroundColor, - title: "loading_title".localized, - withSpinner: true, - size: size - ) - } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 01936fbf4..c1a0d7b2f 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -50,7 +50,7 @@ final class InboxViewController: ASDKViewController { private var state: State = .idle private let messageProvider: MessagesListProvider - private let decorator: InboxViewDecoratorType + private let decorator: InboxViewDecorator private let enterpriseServerApi: EnterpriseServerApiType private let refreshControl = UIRefreshControl() private let tableNode: ASTableNode @@ -64,7 +64,7 @@ final class InboxViewController: ASDKViewController { init( _ viewModel: InboxViewModel, messageProvider: MessagesListProvider = MailProvider.shared.messageListProvider, - decorator: InboxViewDecoratorType = InboxViewDecorator(), + decorator: InboxViewDecorator = InboxViewDecorator(), enterpriseServerApi: EnterpriseServerApiType = EnterpriseServerApi() ) { self.viewModel = viewModel @@ -273,8 +273,11 @@ extension InboxViewController { } private func btnComposeTap() { + guard let email = DataService.shared.email else { + return + } TapTicFeedback.generate(.light) - let composeVc = ComposeViewController() + let composeVc = ComposeViewController(email: email) navigationController?.pushViewController(composeVc, animated: true) } } @@ -330,17 +333,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - let height = tableNode.frame.size.height - - (navigationController?.navigationBar.frame.size.height ?? 0.0) - - safeAreaWindowInsets.top - - safeAreaWindowInsets.bottom - - let size = CGSize( - width: tableNode.frame.size.width, - height: max(height, 0) - ) - - return cellNode(for: indexPath, and: size) + cellNode(for: indexPath, and: visibleSize(for: tableNode)) } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { @@ -368,12 +361,7 @@ extension InboxViewController { case .fetching: guard let message = self.messages[safe: indexPath.row] else { return TextCellNode( - input: TextCellNode.Input( - backgroundColor: .backgroundColor, - title: "Loading ...", - withSpinner: true, - size: CGSize(width: 44, height: 44) - ) + input: .loading(with: CGSize(width: 44, height: 44)) ) } return InboxCellNode(message: InboxCellNode.Input(message)) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift index b1bcc792c..ca2e266dd 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewDecorator.swift @@ -36,13 +36,7 @@ extension InboxCellNode.Input { } } -protocol InboxViewDecoratorType { - func initialNodeInput(for size: CGSize) -> TextCellNode.Input - - func emptyStateNodeInput(for size: CGSize, title: String) -> TextCellNode.Input -} - -struct InboxViewDecorator: InboxViewDecoratorType { +struct InboxViewDecorator { func emptyStateNodeInput(for size: CGSize, title: String) -> TextCellNode.Input { TextCellNode.Input( backgroundColor: .backgroundColor, diff --git a/FlowCrypt/Controllers/Msg/AlertsFactory.swift b/FlowCrypt/Controllers/Msg/AlertsFactory.swift new file mode 100644 index 000000000..bf79666e1 --- /dev/null +++ b/FlowCrypt/Controllers/Msg/AlertsFactory.swift @@ -0,0 +1,93 @@ +// +// AlertsFactory.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 17.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import UIKit + +enum AlertsFactory { + typealias PassPhraseCompletion = ((String) -> Void) + typealias CancelCompletion = (() -> Void) + + static func makePassPhraseAlert( + onCancel: @escaping CancelCompletion, + onCompletion: @escaping PassPhraseCompletion + ) -> UIAlertController { + let alert = UIAlertController( + title: "setup_enter_pass_phrase".localized, + message: nil, + preferredStyle: .alert + ) + alert.addTextField { tf in + tf.isSecureTextEntry = true + } + + let saveAction = UIAlertAction(title: "Save", style: .default) { _ in + guard let textField = alert.textFields?.first, + let passPhrase = textField.text, + passPhrase.isNotEmpty + else { + alert.dismiss(animated: true, completion: nil) + return + } + + alert.dismiss(animated: true) { + onCompletion(passPhrase) + } + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { _ in + alert.dismiss(animated: true) { + onCancel() + } + } + + alert.addAction(cancelAction) + alert.addAction(saveAction) + + return alert + } + + static func makeWrongPassPhraseAlert( + onCancel: @escaping CancelCompletion, + onCompletion: @escaping PassPhraseCompletion + ) -> UIAlertController { + let alert = UIAlertController( + title: "setup_wrong_pass_phrase_retry".localized, + message: nil, + preferredStyle: .alert + ) + + alert.addTextField { tf in + tf.isSecureTextEntry = true + } + + let saveAction = UIAlertAction(title: "Save", style: .default) { _ in + guard let textField = alert.textFields?.first, + let passPhrase = textField.text, + passPhrase.isNotEmpty + else { + alert.dismiss(animated: true, completion: nil) + return + } + + alert.dismiss(animated: true) { + onCompletion(passPhrase) + } + } + + let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { _ in + alert.dismiss(animated: true) { + onCancel() + } + } + + alert.addAction(cancelAction) + alert.addAction(saveAction) + + return alert + } +} diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index e23f09675..f1a30549c 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -55,14 +55,18 @@ final class MessageViewController: TableNodeViewController { private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let trashFolderProvider: TrashFolderProviderType - private var fetchedMessage: FetchedMessage = .empty + private var processedMessage: ProcessedMessage = .empty + private let passPhraseStorage: PassPhraseStorageType init( messageService: MessageService = MessageService(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, decorator: MessageViewDecorator = MessageViewDecorator(dateFormatter: DateFormatter()), - storage: DataServiceType & KeyDataServiceType = DataService.shared, trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ), input: MessageViewController.Input, completion: MsgViewControllerCompletion? ) { @@ -72,6 +76,7 @@ final class MessageViewController: TableNodeViewController { self.decorator = decorator self.trashFolderProvider = trashFolderProvider self.onCompletion = completion + self.passPhraseStorage = passPhraseStorage super.init(node: TableNode()) } @@ -116,7 +121,7 @@ final class MessageViewController: TableNodeViewController { // we need to have only help and trash buttons items = [helpButton, trashButton] - // TODO: - ANTON - Check if this should be fixed + // TODO: - Ticket - Check if this should be fixed case "inbox": // for Gmail inbox we also need to have archive and unread buttons items = [helpButton, archiveButton, trashButton, unreadButton] @@ -140,29 +145,33 @@ extension MessageViewController { guard let self = self else { return } let promise = self.messageService.getMessage(with: input.objMessage, folder: input.path) let message = try awaitPromise(promise) - self.fetchedMessage = message + self.processedMessage = message } .then(on: .main) { [weak self] in - self?.hideSpinner() - self?.node.reloadData() - self?.asyncMarkAsReadIfNotAlreadyMarked() + self?.handleReceivedMessage() } .catch(on: .main) { [weak self] error in - self?.hideSpinner() - self?.handleError(error, path: input.path) + self?.handleError(error) } } - private func handleError(_ error: Error, path: String) { - if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { - // todo - the missing msg should be removed from the list in inbox view - // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox - showToast("Message not found in folder: \(path)") - } else { - // todo - this should be a retry / cancel alert - showAlert(error: error, message: "message_failed_open".localized + "\n\n\(error)") - } - navigationController?.popViewController(animated: true) + private func validateMessage(rawMimeData: Data, with passPhrase: String) { + showSpinner("loading_title".localized, isUserInteractionEnabled: true) + + messageService.validateMessage(rawMimeData: rawMimeData, with: passPhrase) + .then(on: .main) { [weak self] message in + self?.processedMessage = message + self?.handleReceivedMessage() + } + .catch(on: .main) { [weak self] error in + self?.handleError(error) + } + } + + private func handleReceivedMessage() { + hideSpinner() + node.reloadData() + asyncMarkAsReadIfNotAlreadyMarked() } private func asyncMarkAsReadIfNotAlreadyMarked() { @@ -188,6 +197,55 @@ extension MessageViewController { self?.onCompletion?(operation, input.objMessage) } } +} + +// MARK: - Error Handling + +extension MessageViewController { + private func handleError(_ error: Error) { + hideSpinner() + + switch error as? MessageServiceError { + case let .missedPassPhrase(rawMimeData): + handleMissedPassPhrase(for: rawMimeData) + case let .wrongPassPhrase(rawMimeData, passPhrase): + handleWrongPathPhrase(for: rawMimeData, with: passPhrase) + default: + // TODO: - Ticket - Improve error handling for MessageViewController + if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { + // todo - the missing msg should be removed from the list in inbox view + // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox + showToast("Message not found in folder: \(input?.path ?? "N/A")") + } else { + // todo - this should be a retry / cancel alert + showAlert(error: error, message: "message_failed_open".localized + "\n\n\(error)") + } + navigationController?.popViewController(animated: true) + } + } + + private func handleMissedPassPhrase(for rawMimeData: Data) { + let alert = AlertsFactory.makePassPhraseAlert( + onCancel: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onCompletion: { [weak self] passPhrase in + self?.validateMessage(rawMimeData: rawMimeData, with: passPhrase) + }) + + present(alert, animated: true, completion: nil) + } + + private func handleWrongPathPhrase(for rawMimeData: Data, with phrase: String) { + let alert = AlertsFactory.makeWrongPassPhraseAlert( + onCancel: { [weak self] in + self?.navigationController?.popViewController(animated: true) + }, + onCompletion: { [weak self] passPhrase in + self?.validateMessage(rawMimeData: rawMimeData, with: passPhrase) + }) + present(alert, animated: true, completion: nil) + } private func handleOpErr(operation: MessageAction) { hideSpinner() @@ -285,22 +343,19 @@ extension MessageViewController { } private func handleReplyTap() { - guard let input = input else { return } + guard let input = input, let email = DataService.shared.email else { return } let replyInfo = ComposeViewController.Input.ReplyInfo( recipient: input.objMessage.sender, subject: input.objMessage.subject, - mime: fetchedMessage.rawMimeData, + mime: processedMessage.rawMimeData, sentDate: input.objMessage.date, - message: fetchedMessage.text + message: processedMessage.text ) + let composeInput = ComposeViewController.Input(type: .reply(replyInfo)) navigationController?.pushViewController( - ComposeViewController( - input: ComposeViewController.Input( - type: .reply(replyInfo) - ) - ), + ComposeViewController(email: email, input: composeInput), animated: true ) } @@ -331,12 +386,12 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { case .main: return Parts.allCases.count case .attributes: - return fetchedMessage.attachments.count + return processedMessage.attachments.count } } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - { [weak self] in + return { [weak self] in guard let self = self, let section = Sections(rawValue: indexPath.section) else { return ASCellNode() } switch section { @@ -369,7 +424,7 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { case .subject: return MessageSubjectNode(subject, time: time) case .text: - let messageInput = self.decorator.attributedMessage(from: self.fetchedMessage) + let messageInput = self.decorator.attributedMessage(from: self.processedMessage) return MessageTextSubjectNode(messageInput) } } @@ -377,7 +432,7 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { private func attachmentNode(for index: Int) -> ASCellNode { AttachmentNode( input: .init( - msgAttachment: fetchedMessage.attachments[index] + msgAttachment: processedMessage.attachments[index] ) ) } diff --git a/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift b/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift index e30b49aef..82357ab9a 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewDecorator.swift @@ -31,8 +31,17 @@ struct MessageViewDecorator { (text ?? "").attributed(.regular(17), color: color) } - func attributedMessage(from fetchedMessage: FetchedMessage) -> NSAttributedString { - fetchedMessage.text.attributed() + func attributedMessage(from processedMessage: ProcessedMessage) -> NSAttributedString { + let textColor: UIColor + switch processedMessage.messageType { + case .encrypted: + textColor = .main + case .error: + textColor = .red + case .plain: + textColor = .mainTextColor + } + return processedMessage.text.attributed(color: textColor) } } diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index ed9b745ec..c1d2d0dad 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -12,7 +12,7 @@ import FlowCryptUI final class SearchViewController: TableNodeViewController { private enum Constants { - // TODO: - Add pagination for SearchViewController + // TODO: - Ticket - Add pagination for SearchViewController static let messageCount = 100 } enum State { @@ -279,11 +279,11 @@ extension SearchViewController: UISearchResultsUpdating { self?.handleError(with: error) } .then(on: .main) { [weak self] messages in - self?.handleFetchedMessages(with: messages) + self?.handleProcessedMessage(with: messages) } } - private func handleFetchedMessages(with messages: [Message]) { + private func handleProcessedMessage(with messages: [Message]) { if messages.isEmpty { state = .empty } else { diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift index 2a0d64d3f..7b78a92ea 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift @@ -36,7 +36,7 @@ final class BackupOptionsViewController: ASDKViewController { init( decorator: BackupOptionsViewDecoratorType = BackupOptionsViewDecorator(), - backupService: BackupServiceType = BackupService.shared, + backupService: BackupServiceType = BackupService(), backups: [KeyDetails], userId: UserId ) { diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewDecorator.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewDecorator.swift index 262277646..5291dc5f8 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewDecorator.swift @@ -54,22 +54,9 @@ struct BackupOptionsViewDecorator: BackupOptionsViewDecoratorType { default: title = "" } - let attributedTitle = title - .localized - .attributed(.bold(14), color: .textColor, alignment: .center) - - let checkboxColor: UIColor = isSelected - ? .main - : .lightGray - - return CheckBoxTextNode.Input( - title: attributedTitle, - insets: .side(16), - preferredSize: CGSize(width: 30, height: 30), - checkBoxInput: CheckBoxNode.Input( - color: checkboxColor, - strokeWidth: 2 - ) + return CheckBoxTextNode.Input.common( + with: title.localized, + isSelected: isSelected ) } } diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift index a32368891..11d7ae80b 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift @@ -41,7 +41,7 @@ final class BackupViewController: ASDKViewController { init( decorator: BackupViewDecoratorType = BackupViewDecorator(), - backupProvider: BackupServiceType = BackupService.shared, + backupProvider: BackupServiceType = BackupService(), userId: UserId ) { self.decorator = decorator @@ -75,7 +75,7 @@ extension BackupViewController { } private func fetchBackups() { - backupProvider.fetchBackups(for: userId) + backupProvider.fetchBackupsFromInbox(for: userId) .then { [weak self] keys in self?.state = keys.isEmpty ? .noBackups diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift index ad5eef482..f6a31314b 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift @@ -19,7 +19,7 @@ final class BackupSelectKeyViewController: ASDKViewController { init( decorator: BackupSelectKeyDecoratorType = BackupSelectKeyDecorator(), - backupService: BackupServiceType = BackupService.shared, + backupService: BackupServiceType = BackupService(), selectedOption: BackupOption, backups: [KeyDetails], userId: UserId diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift index 872d26de1..f8277d2b9 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift @@ -63,9 +63,10 @@ extension KeySettingsViewController { extension KeySettingsViewController { @objc private func handleAddButtonTap() { - navigationController?.pushViewController(ImportKeyViewController(), animated: true) + navigationController?.pushViewController(SetupManuallyImportKeyViewController(), animated: true) } } + extension KeySettingsViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { keys.count diff --git a/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift b/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift new file mode 100644 index 000000000..0b8f4e18b --- /dev/null +++ b/FlowCrypt/Controllers/Setup/PassPraseSaveable.swift @@ -0,0 +1,39 @@ +// +// PassPhraseSaveable.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 02.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import FlowCryptUI + +protocol PassPhraseSaveable { + var shouldSaveLocally: Bool { get set } + var passPhraseIndexes: [IndexPath] { get } + var saveLocallyNode: CellNode { get } + var saveInMemoryNode: CellNode { get } + + var passPhraseStorage: PassPhraseStorageType { get } + + func handleSelectedPassPhraseOption() + func showPassPhraseErrorAlert() +} + +extension PassPhraseSaveable where Self: TableNodeViewController { + func handleSelectedPassPhraseOption() { + node.reloadRows(at: passPhraseIndexes, with: .automatic) + } + + var saveLocallyNode: CellNode { + CheckBoxTextNode(input: .passPhraseLocally(isSelected: self.shouldSaveLocally)) + } + + var saveInMemoryNode: CellNode { + CheckBoxTextNode(input: .passPhraseMemory(isSelected: !self.shouldSaveLocally)) + } + + func showPassPhraseErrorAlert() { + showAlert(message: "setup_enter_pass_phrase".localized) + } +} diff --git a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift new file mode 100644 index 000000000..9c6772f94 --- /dev/null +++ b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift @@ -0,0 +1,256 @@ +// +// © 2017-2019 FlowCrypt Limited. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptUI +import Promises + +final class SetupBackupsViewController: TableNodeViewController, PassPhraseSaveable { + private enum Parts: Int, CaseIterable { + case title, description, passPhrase, divider, saveLocally, saveInMemory, action, optionalAction + } + + private let router: GlobalRouterType + private let decorator: SetupViewDecorator + private let core: Core + private let keyMethods: KeyMethodsType + private let user: UserId + private let fetchedEncryptedKeys: [KeyDetails] + private let keyStorage: KeyStorageType + let passPhraseStorage: PassPhraseStorageType + + private var passPhrase: String? + private lazy var logger = Logger.nested(in: Self.self, with: .setup) + + var shouldSaveLocally = true { + didSet { + handleSelectedPassPhraseOption() + } + } + + var passPhraseIndexes: [IndexPath] { + [Parts.saveLocally, Parts.saveInMemory] + .map { IndexPath(row: $0.rawValue, section: 0) } + } + + init( + fetchedEncryptedKeys: [KeyDetails], + router: GlobalRouterType = GlobalRouter(), + keyStorage: KeyStorageType = KeyDataStorage(), + decorator: SetupViewDecorator = SetupViewDecorator(), + core: Core = Core.shared, + keyMethods: KeyMethodsType = KeyMethods(), + user: UserId, + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ) + ) { + self.fetchedEncryptedKeys = fetchedEncryptedKeys + self.router = router + self.keyStorage = keyStorage + self.decorator = decorator + self.core = core + self.keyMethods = keyMethods + self.user = user + self.passPhraseStorage = passPhraseStorage + + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + handleBackups() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: animated) + } +} + +// MARK: - Setup +extension SetupBackupsViewController { + private func setupUI() { + node.delegate = self + node.dataSource = self + observeKeyboardNotifications() + } + + // 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: - Actions +extension SetupBackupsViewController { + private func handleBackups() { + guard fetchedEncryptedKeys.isNotEmpty else { + return assertionFailure("Should be handled in SetupInitialViewController") + } + + node.reloadData() + + node.visibleNodes + .compactMap { $0 as? TextFieldCellNode } + .first? + .becomeFirstResponder() + } + + private func recoverAccount(with backups: [KeyDetails], and passPhrase: String) { + let matchingKeyBackups = Set(keyMethods.filterByPassPhraseMatch(keys: backups, passPhrase: passPhrase)) + + guard matchingKeyBackups.isNotEmpty else { + showAlert(message: "setup_wrong_pass_phrase_retry".localized) + return + } + + // save pass phrase + matchingKeyBackups + .map { + PassPhrase(value: passPhrase, longid: $0.longid) + } + .forEach { + passPhraseStorage.savePassPhrase(with: $0, inStorage: shouldSaveLocally) + } + + // save keys + keyStorage.addKeys(keyDetails: Array(matchingKeyBackups), source: .backup) + + moveToMainFlow() + } + + private func handleButtonPressed() { + view.endEditing(true) + guard let passPhrase = passPhrase else { return } + + guard passPhrase.isNotEmpty else { + showPassPhraseErrorAlert() + return + } + + showSpinner() + + // TODO: - fix for spinner + // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self = self else { return } + self.recoverAccount(with: self.fetchedEncryptedKeys, and: passPhrase) + } + } + + private func handleOtherAccount() { + router.signOut() + } + + private func moveToMainFlow() { + router.proceed() + } +} + +// MARK: - ASTableDelegate, ASTableDataSource +extension SetupBackupsViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { + Parts.allCases.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: .common), + 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 + } + .then { + $0.becomeFirstResponder() + } + .onShouldReturn { [weak self] _ in + self?.view.endEditing(true) + self?.handleButtonPressed() + return true + } + case .action: + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .loadAccount), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in + self?.handleButtonPressed() + } + .then { + $0.button.accessibilityIdentifier = "load_account" + } + case .optionalAction: + return ButtonCellNode(input: .chooseAnotherAccount) { [weak self] in + self?.handleOtherAccount() + } + 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 .saveLocally: + shouldSaveLocally = true + case .saveInMemory: + shouldSaveLocally = false + default: + break + } + } +} diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift new file mode 100644 index 000000000..ce6fc10ec --- /dev/null +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -0,0 +1,344 @@ +// +// CreatePrivateKeyViewController.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 23.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptUI +import Promises + +enum CreateKeyError: Error { + case weakPassPhrase(_ strength: CoreRes.ZxcvbnStrengthBar) + // Missing user email + case missedUserEmail + // Missing user name + case missedUserName + // Pass phrases don't match + case doesntMatch + // silent abort + case conformingPassPhraseError +} + +final class SetupGenerateKeyViewController: TableNodeViewController, PassPhraseSaveable { + enum Parts: Int, CaseIterable { + case title, description, passPhrase, divider, saveLocally, saveInMemory, action, subtitle + } + + 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 passPhraseStorage: PassPhraseStorageType + + 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) + + init( + user: UserId, + backupService: BackupServiceType = BackupService(), + core: Core = .shared, + router: GlobalRouterType = GlobalRouter(), + decorator: SetupViewDecorator = SetupViewDecorator(), + storage: DataServiceType = DataService.shared, + keyStorage: KeyStorageType = KeyDataStorage(), + attester: AttesterApiType = AttesterApi(), + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ) + ) { + 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.passPhraseStorage = passPhraseStorage + + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + 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) + } +} + +// MARK: - Setup + +extension SetupGenerateKeyViewController { + private func setupAccountWithGeneratedKey(with passPhrase: String) { + Promise { [weak self] in + guard let self = self else { return } + self.showSpinner() + + let userId = try self.getUserId() + + try awaitPromise(self.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase)) + + let encryptedPrv = try self.core.generateKey(passphrase: passPhrase, variant: .curve25519, userIds: [userId]) + + try awaitPromise(self.backupService.backupToInbox(keys: [encryptedPrv.key], for: self.user)) + + let passPhrase = PassPhrase(value: passPhrase, longid: encryptedPrv.key.longid) + + self.keyStorage.addKeys(keyDetails: [encryptedPrv.key], source: .generated) + self.passPhraseStorage.savePassPhrase(with: passPhrase, inStorage: self.shouldSaveLocally) + + let updateKey = self.attester.updateKey( + email: userId.email, + pubkey: encryptedPrv.key.public, + token: self.storage.token + ) + + try awaitPromise(self.alertAndSkipOnRejection( + updateKey, + fail: "Failed to submit Public Key") + ) + let testWelcome = self.attester.testWelcome(email: userId.email, pubkey: encryptedPrv.key.public) + try awaitPromise(self.alertAndSkipOnRejection( + testWelcome, + fail: "Failed to send you welcome email") + ) + } + .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") + } + } + } + + private func getUserId() throws -> UserId { + guard let email = DataService.shared.email, !email.isEmpty else { + throw CreateKeyError.missedUserEmail + } + guard let name = DataService.shared.email, !name.isEmpty else { + throw CreateKeyError.missedUserName + } + 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() { + 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 new file mode 100644 index 000000000..ceea78488 --- /dev/null +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -0,0 +1,263 @@ +// +// SetupInitialViewController.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 26.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptUI + +final class SetupInitialViewController: TableNodeViewController { + private enum Parts: Int, CaseIterable { + case title, description, createKey, importKey, anotherAccount + } + + private enum State { + case idle, searching, noKeyBackups, error(Error) + + var numberOfRows: Int { + switch self { + // title + case .idle: + return 1 + // title, loading + case .searching: + return 2 + case .error: + return 3 + case .noKeyBackups: + return Parts.allCases.count + } + } + } + + private var state = State.idle { + didSet { handleNewState() } + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + .default + } + + private let backupService: BackupServiceType + private let user: UserId + private let router: GlobalRouterType + private let decorator: SetupViewDecorator + + private lazy var logger = Logger.nested(in: Self.self, with: .setup) + + init( + user: UserId, + backupService: BackupServiceType = BackupService(), + router: GlobalRouterType = GlobalRouter(), + decorator: SetupViewDecorator = SetupViewDecorator() + ) { + self.user = user + self.backupService = backupService + self.router = router + self.decorator = decorator + + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setNeedsStatusBarAppearanceUpdate() + state = .searching + } +} + +// MARK: - Action Handling +extension SetupInitialViewController { + + private func handleNewState() { + logger.logInfo("Changed to new state \(state)") + + switch state { + case .searching: + searchBackups() + default: + break + } + node.reloadData() + } + + private func searchBackups() { + logger.logInfo("Searching for backups in inbox") + + backupService.fetchBackupsFromInbox(for: user) + .then(on: .main) { [weak self] keys in + self?.proceedToSetupWith(keys: keys) + } + .catch(on: .main) { [weak self] error in + self?.handle(error: error) + } + } + + private func handleOtherAccount() { + router.signOut() + } + + private func handle(error: Error) { + handleCommon(error: error) + state = .error(error) + } +} + +// MARK: - ASTableDelegate, ASTableDataSource +extension SetupInitialViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { + state.numberOfRows + } + + func tableNode(_ node: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + return { [weak self] in + guard let self = self else { return ASCellNode() } + + switch self.state { + case .idle: + return ASCellNode() + case .searching: + return self.searchStateNode(for: indexPath) + case .error(let error): + return self.errorStateNode(for: indexPath, error: error) + case .noKeyBackups: + return self.noKeysStateNode(for: indexPath) + } + } + } +} + +// MARK: - UI +extension SetupInitialViewController { + private func setupUI() { + node.delegate = self + node.dataSource = self + + title = decorator.sceneTitle(for: .setup) + } + + private func searchStateNode(for indexPath: IndexPath) -> ASCellNode { + switch indexPath.row { + case 0: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.decorator.title(for: .setup), + insets: self.decorator.insets.titleInset, + backgroundColor: .backgroundColor + ) + ) + default: + return TextCellNode(input: .loading(with: CGSize(width: 40, height: 40))) + } + } + + private func noKeysStateNode(for indexPath: IndexPath) -> ASCellNode { + guard 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: .noBackups), + insets: self.decorator.insets.subTitleInset, + backgroundColor: .backgroundColor + ) + ) + case .createKey: + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .createKey), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in + self?.proceedToCreatingNewKey() + } + case .importKey: + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .importKey), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in + self?.proceedToKeyImport() + } + case .anotherAccount: + return ButtonCellNode(input: .chooseAnotherAccount) { [weak self] in + self?.handleOtherAccount() + } + } + } + + private func errorStateNode(for indexPath: IndexPath, error: Error) -> ASCellNode { + switch indexPath.row { + case 0: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.decorator.title(for: .setup), + insets: self.decorator.insets.titleInset, + backgroundColor: .backgroundColor + ) + ) + case 1: + return TextCellNode( + input: .init( + backgroundColor: .backgroundColor, + title: error.localizedDescription, + withSpinner: false, + size: CGSize(width: 200, height: 200) + ) + ) + case 2: + return ButtonCellNode(input: .retry) { [weak self] in + self?.state = .searching + } + default: + return ASCellNode() + } + } +} + +// MARK: - Navigation +extension SetupInitialViewController { + private func proceedToKeyImport() { + let viewController = SetupManuallyImportKeyViewController() + navigationController?.pushViewController(viewController, animated: true) + } + + private func proceedToCreatingNewKey() { + let viewController = SetupGenerateKeyViewController(user: user) + navigationController?.pushViewController(viewController, animated: true) + } + + private func proceedToSetupWith(keys: [KeyDetails]) { + logger.logInfo("Finish searching for backups in inbox") + + if keys.isEmpty { + logger.logInfo("No key backups found in inbox") + state = .noKeyBackups + } else { + logger.logInfo("\(keys.count) key backups found in inbox") + let viewController = SetupBackupsViewController(fetchedEncryptedKeys: keys, user: user) + navigationController?.pushViewController(viewController, animated: true) + } + } +} diff --git a/FlowCrypt/Controllers/ImportKey/Enter Pass Phrase/EnterPassPhraseViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift similarity index 68% rename from FlowCrypt/Controllers/ImportKey/Enter Pass Phrase/EnterPassPhraseViewController.swift rename to FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift index 55d4a5fff..9f77d5553 100644 --- a/FlowCrypt/Controllers/ImportKey/Enter Pass Phrase/EnterPassPhraseViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift @@ -9,31 +9,47 @@ import AsyncDisplayKit import FlowCryptUI -final class EnterPassPhraseViewController: TableNodeViewController { +final class SetupManuallyEnterPassPhraseViewController: TableNodeViewController, PassPhraseSaveable { private enum Parts: Int, CaseIterable { - case title, description, passPhrase, divider, enterPhrase, chooseAnother + case title, description, passPhrase, divider, saveLocally, saveInMemory, enterPhrase, chooseAnother var indexPath: IndexPath { IndexPath(row: rawValue, section: 0) } } - private let decorator: EnterPassPhraseViewDecoratorType + private let decorator: SetupViewDecorator private let email: String private let fetchedKeys: [KeyDetails] private let keyMethods: KeyMethodsType - private let keysDataService: KeyDataServiceType + private let keysStorage: KeyStorageType private let keyService: KeyServiceType private let router: GlobalRouterType + let passPhraseStorage: PassPhraseStorageType private var passPhrase: String? + var shouldSaveLocally = true { + didSet { + handleSelectedPassPhraseOption() + } + } + + var passPhraseIndexes: [IndexPath] { + [Parts.saveLocally, Parts.saveInMemory] + .map { IndexPath(row: $0.rawValue, section: 0) } + } + init( - decorator: EnterPassPhraseViewDecoratorType = EnterPassPhraseViewDecorator(), - keyMethods: KeyMethodsType = KeyMethods(core: .shared), - keysService: KeyDataServiceType = DataService.shared, + decorator: SetupViewDecorator = SetupViewDecorator(), + keyMethods: KeyMethodsType = KeyMethods(), + keysService: KeyStorageType = KeyDataStorage(), router: GlobalRouterType = GlobalRouter(), keyService: KeyServiceType = KeyService(), + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ), email: String, fetchedKeys: [KeyDetails] ) { @@ -41,9 +57,11 @@ final class EnterPassPhraseViewController: TableNodeViewController { self.email = email self.decorator = decorator self.keyMethods = keyMethods - self.keysDataService = keysService + self.keysStorage = keysService self.router = router self.keyService = keyService + self.passPhraseStorage = passPhraseStorage + super.init(node: TableNode()) } @@ -67,7 +85,7 @@ final class EnterPassPhraseViewController: TableNodeViewController { private func setupUI() { node.delegate = self node.dataSource = self - title = decorator.sceneTitle + title = decorator.sceneTitle(for: .enterPassPhrase) node.view.contentInsetAdjustmentBehavior = .never } @@ -83,7 +101,7 @@ final class EnterPassPhraseViewController: TableNodeViewController { // MARK: - Keyboard -extension EnterPassPhraseViewController { +extension SetupManuallyEnterPassPhraseViewController { // swiftlint:disable discarded_notification_center_observer /// Observation should be removed in a place where subscription is private func observeKeyboardNotifications() { @@ -113,20 +131,20 @@ extension EnterPassPhraseViewController { // MARK: - ASTableDelegate, ASTableDataSource -extension EnterPassPhraseViewController: ASTableDelegate, ASTableDataSource { +extension SetupManuallyEnterPassPhraseViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { Parts.allCases.count } func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - { [weak self] in + 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.passPhraseTitle, - insets: self.decorator.titleInsets, + title: self.decorator.title(for: .enterPassPhrase), + insets: self.decorator.insets.titleInset, backgroundColor: .backgroundColor ) ) @@ -134,12 +152,12 @@ extension EnterPassPhraseViewController: ASTableDelegate, ASTableDataSource { return SetupTitleNode( SetupTitleNode.Input( title: self.decorator.subtitleStyle(self.email), - insets: self.decorator.subTitleInset, + insets: self.decorator.insets.subTitleInset, backgroundColor: .backgroundColor ) ) case .passPhrase: - return TextFieldCellNode(input: self.decorator.passPhraseTextFieldStyle) { [weak self] action in + return TextFieldCellNode(input: .passPhraseTextFieldStyle) { [weak self] action in guard case let .didEndEditing(text) = action else { return } self?.passPhrase = text } @@ -151,30 +169,44 @@ extension EnterPassPhraseViewController: ASTableDelegate, ASTableDataSource { return true } case .enterPhrase: - return ButtonCellNode( - title: self.decorator.passPhraseContine, - insets: self.decorator.passPhraseInsets - ) { [weak self] in + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .passPhraseContinue), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in self?.handleContinueAction() } case .chooseAnother: - return ButtonCellNode( - title: self.decorator.passPhraseChooseAnother, - insets: self.decorator.buttonInsets, - color: .lightGray - ) { [weak self] in + return ButtonCellNode(input: .chooseAnotherAccount) { [weak self] in self?.navigationController?.popViewController(animated: true) } case .divider: return DividerCellNode(inset: UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24)) + 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 .saveLocally: + shouldSaveLocally = true + case .saveInMemory: + shouldSaveLocally = false + default: + break + } + } } // MARK: - Actions -extension EnterPassPhraseViewController { +extension SetupManuallyEnterPassPhraseViewController { private func handleContinueAction() { view.endEditing(true) guard let passPhrase = passPhrase else { return } @@ -207,17 +239,24 @@ extension EnterPassPhraseViewController { let keysToUpdate = Array(Set(existedKeys).intersection(fetchedKeys)) let newKeysToAdd = Array(Set(fetchedKeys).subtracting(existedKeys)) - keysDataService.addKeys( - keyDetails: newKeysToAdd, - passPhrase: passPhrase, - source: .imported - ) + keysStorage.addKeys(keyDetails: newKeysToAdd, source: .imported) + keysStorage.updateKeys(keyDetails: keysToUpdate, source: .imported) - keysDataService.updateKeys( - keyDetails: keysToUpdate, - passPhrase: passPhrase, - source: .imported - ) + keysToUpdate + .map { + PassPhrase(value: passPhrase, longid: $0.longid) + } + .forEach { + passPhraseStorage.updatePassPhrase(with: $0, inStorage: shouldSaveLocally) + } + + newKeysToAdd + .map { + PassPhrase(value: passPhrase, longid: $0.longid) + } + .forEach { + passPhraseStorage.savePassPhrase(with: $0, inStorage: shouldSaveLocally) + } hideSpinner() diff --git a/FlowCrypt/Controllers/ImportKey/Import Key/ImportKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift similarity index 79% rename from FlowCrypt/Controllers/ImportKey/Import Key/ImportKeyViewController.swift rename to FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift index f69f7fe67..7765d1100 100644 --- a/FlowCrypt/Controllers/ImportKey/Import Key/ImportKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift @@ -10,7 +10,7 @@ import AsyncDisplayKit import FlowCryptUI import MobileCoreServices -final class ImportKeyViewController: TableNodeViewController { +final class SetupManuallyImportKeyViewController: TableNodeViewController { private enum Parts: Int, CaseIterable { case title, description, fileImport, pasteBoardImport @@ -19,7 +19,7 @@ final class ImportKeyViewController: TableNodeViewController { } } - private let decorator: EnterPassPhraseViewDecoratorType + private let decorator: SetupViewDecorator private let pasteboard: UIPasteboard private let dataService: DataServiceType private let core: Core @@ -29,7 +29,7 @@ final class ImportKeyViewController: TableNodeViewController { } init( - decorator: EnterPassPhraseViewDecoratorType = EnterPassPhraseViewDecorator(), + decorator: SetupViewDecorator = SetupViewDecorator(), pasteboard: UIPasteboard = UIPasteboard.general, core: Core = Core.shared, dataService: DataServiceType = DataService.shared @@ -65,7 +65,7 @@ final class ImportKeyViewController: TableNodeViewController { private func setupUI() { node.delegate = self node.dataSource = self - title = decorator.sceneTitle + title = decorator.sceneTitle(for: .importKey) } private func updateSubtitle() { @@ -77,20 +77,20 @@ final class ImportKeyViewController: TableNodeViewController { // MARK: - ASTableDelegate, ASTableDataSource -extension ImportKeyViewController: ASTableDelegate, ASTableDataSource { +extension SetupManuallyImportKeyViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { Parts.allCases.count } func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - { [weak self] in + 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, - insets: self.decorator.titleInsets, + title: self.decorator.title(for: .importKey), + insets: self.decorator.insets.titleInset, backgroundColor: .backgroundColor ) ) @@ -98,22 +98,24 @@ extension ImportKeyViewController: ASTableDelegate, ASTableDataSource { return SetupTitleNode( SetupTitleNode.Input( title: self.decorator.subtitleStyle(self.userInfoMessage), - insets: self.decorator.subTitleInset, + insets: self.decorator.insets.subTitleInset, backgroundColor: .backgroundColor ) ) case .fileImport: - return ButtonCellNode( - title: self.decorator.fileImportTitle, - insets: self.decorator.buttonInsets - ) { [weak self] in + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .fileImport), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in self?.proceedToKeyImportFromFile() } case .pasteBoardImport: - return ButtonCellNode( - title: self.decorator.pasteBoardTitle, - insets: self.decorator.buttonInsets - ) { [weak self] in + let input = ButtonCellNode.Input( + title: self.decorator.buttonTitle(for: .pasteBoard), + insets: self.decorator.insets.buttonInsets + ) + return ButtonCellNode(input: input) { [weak self] in self?.proceedToKeyImportFromPasteboard() } .then { @@ -126,7 +128,7 @@ extension ImportKeyViewController: ASTableDelegate, ASTableDataSource { // MARK: - Actions -extension ImportKeyViewController { +extension SetupManuallyImportKeyViewController { private func proceedToKeyImportFromFile() { let acceptableDocumentTypes = [ String(kUTTypeText), @@ -150,10 +152,10 @@ extension ImportKeyViewController { private func proceedToKeyImportFromPasteboard() { guard let armoredKey = pasteboard.string else { return } - parseFetched(data: Data(armoredKey.utf8)) + parseUserProvided(data: Data(armoredKey.utf8)) } - private func parseFetched(data keyData: Data) { + private func parseUserProvided(data keyData: Data) { do { let keys = try core.parseKeys(armoredOrBinary: keyData) let privateKey = keys.keyDetails.filter { $0.private != nil } @@ -171,7 +173,7 @@ extension ImportKeyViewController { } private func proceedToPassPhrase(with email: String, keys: [KeyDetails]) { - let viewController = EnterPassPhraseViewController( + let viewController = SetupManuallyEnterPassPhraseViewController( decorator: decorator, email: email, fetchedKeys: keys @@ -186,7 +188,7 @@ extension ImportKeyViewController { // MARK: - UIDocumentPickerDelegate -extension ImportKeyViewController: UIDocumentPickerDelegate { +extension SetupManuallyImportKeyViewController: UIDocumentPickerDelegate { func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let pickedURL = urls.first else { return } handlePicked(document: pickedURL) @@ -204,7 +206,7 @@ extension ImportKeyViewController: UIDocumentPickerDelegate { document.open { [weak self] success in guard success else { assertionFailure("Failed to open doc"); return } guard let metadata = document.data else { assertionFailure("Failed to fetch data"); return } - self?.parseFetched(data: metadata) + self?.parseUserProvided(data: metadata) } } } diff --git a/FlowCrypt/Controllers/Setup/SetupViewController.swift b/FlowCrypt/Controllers/Setup/SetupViewController.swift deleted file mode 100644 index d17896b94..000000000 --- a/FlowCrypt/Controllers/Setup/SetupViewController.swift +++ /dev/null @@ -1,480 +0,0 @@ -// -// © 2017-2019 FlowCrypt Limited. All rights reserved. -// - -import AsyncDisplayKit -import FlowCryptUI -import Promises - -// swiftlint:disable line_length -final class SetupViewController: TableNodeViewController { - private enum Parts: Int, CaseIterable { - case title, description, passPhrase, divider, action, optionalAction - } - - private let router: GlobalRouterType - private let storage: DataServiceType & KeyDataServiceType - private let decorator: SetupViewDecoratorType - private let core: Core - private let keyMethods: KeyMethodsType - private let attester: AttesterApiType - private let backupService: BackupServiceType - private let user: UserId - - private var passPhrase: String? - - enum SetupError: Error { - /// fetched keys error - case emptyFetchedKeys - /// error while key parsing (associated error for verbose message) - case parseKey(Error) - /// no backups found while searching - case noBackups - } - - enum State { - /// initial state - case idle - /// start searching backups - case searchingBackups - /// encrypted keys found - case fetchedEncrypted([KeyDetails]) - /// creating new key - case createKey - /// error state - case error(SetupError) - - var isSearchingBackups: Bool { - guard case .searchingBackups = self else { - return false - } - return true - } - } - - private var state: State = .idle { - didSet { - handle(newState: state) - } - } - - init( - router: GlobalRouterType = GlobalRouter(), - storage: DataServiceType & KeyDataServiceType = DataService.shared, - decorator: SetupViewDecoratorType = SetupViewDecorator(), - core: Core = Core.shared, - keyMethods: KeyMethodsType = KeyMethods(core: .shared), - attester: AttesterApiType = AttesterApi(), - backupService: BackupServiceType = BackupService.shared, - user: UserId - ) { - self.router = router - self.storage = storage - self.decorator = decorator - self.core = core - self.keyMethods = keyMethods - self.attester = attester - self.backupService = backupService - self.user = user - - super.init(node: TableNode()) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - setupUI() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(true, animated: animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - state = .searchingBackups - } - - deinit { - NotificationCenter.default.removeObserver(self) - } -} - -// MARK: - Setup - -extension SetupViewController { - private func setupUI() { - node.delegate = self - node.dataSource = self - observeKeyboardNotifications() - - state = .idle - } - - // 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: - State Handling - -extension SetupViewController { - private func handle(newState: State) { - switch newState { - case .idle: - node.reloadData() - case .searchingBackups: - showSpinner() - searchBackups() - case let .fetchedEncrypted(details): - handleBackupsFetchResult(with: details) - hideSpinner() - case let .error(error): - hideSpinner() - handleError(with: error) - case .createKey: - hideSpinner() - handleCreateKey() - } - } - - private func searchBackups() { - Logger.logInfo("[Setup] searching for backups in inbox") - backupService.fetchBackups(for: user) - .then(on: .main) { [weak self] keys in - Logger.logInfo("[Setup] done searching for backups in inbox") - guard keys.isNotEmpty else { - Logger.logInfo("[Setup] no key backups found in inbox") - self?.state = .error(.noBackups) - return - } - Logger.logInfo("[Setup] \(keys.count) key backups found in inbox") - self?.state = .fetchedEncrypted(keys) - } - .catch(on: .main) { [weak self] error in - self?.handleCommon(error: error) - } - } - - private func handleBackupsFetchResult(with keys: [KeyDetails]) { - guard keys.isNotEmpty else { - state = .error(.emptyFetchedKeys) - return - } - - reloadNodes() - - node.visibleNodes - .compactMap { $0 as? TextFieldCellNode } - .first? - .becomeFirstResponder() - } - - private func handleCreateKey() { - reloadNodes() - } - - private func reloadNodes() { - let indexes = [ - IndexPath(row: Parts.action.rawValue, section: 0), - IndexPath(row: Parts.description.rawValue, section: 0) - ] - node.reloadRows(at: indexes, with: .fade) - } -} - -// MARK: - Error Handling - -extension SetupViewController { - private func handleError(with error: SetupError) { - Logger.logWarning("[Setup] handling error during setup: \(error)") - switch error { - case .emptyFetchedKeys: - let user = DataService.shared.email ?? "unknown_title".localized - let msg = "setup_no_backups".localized + user - showSearchBackupError(with: msg) - case .noBackups: - showSearchBackupError(with: "setup_no_backups".localized) - case let .parseKey(error): - showErrorAlert(with: "setup_action_failed".localized, error: error) - } - } - - private func errorAlert(with message: String) -> UIAlertController { - let alert = UIAlertController(title: "Notice", message: message, preferredStyle: .alert) - - let useOtherAccountAction = UIAlertAction( - title: "setup_use_otherAccount".localized, - style: .default - ) { [weak self] _ in - self?.handleOtherAccount() - } - - let retryAction = UIAlertAction( - title: "Retry", - style: .default - ) { [weak self] _ in - self?.state = .idle - self?.state = .searchingBackups - } - - alert.addAction(useOtherAccountAction) - alert.addAction(retryAction) - - return alert - } - - private func showErrorAlert(with msg: String, error: Error? = nil) { - hideSpinner() - - let errStr: String = { - guard let err = error else { return "" } - return "\n\n\(err)" - }() - - let alert = errorAlert(with: msg + errStr) - - present(alert, animated: true, completion: nil) - } - - private func showSearchBackupError(with message: String) { - let alert = errorAlert(with: message) - - let importAction = UIAlertAction( - title: "setup_action_import".localized, - style: .default - ) { [weak self] _ in - self?.handleImportKey() - } - - let createNewPrivateKeyAction = UIAlertAction( - title: "setup_action_create_new".localized, - style: .default - ) { [weak self] _ in - self?.state = .createKey - } - - alert.addAction(importAction) - alert.addAction(createNewPrivateKeyAction) - - present(alert, animated: true, completion: nil) - } -} - -// MARK: - Recover account - -extension SetupViewController { - private func recoverAccount(with backups: [KeyDetails], and passPhrase: String) { - - let matchingKeyBackups = keyMethods.filterByPassPhraseMatch(keys: backups, passPhrase: passPhrase) - - guard matchingKeyBackups.isNotEmpty else { - showAlert(message: "setup_wrong_pass_phrase_retry".localized) - return - } - - do { - try storePrvs(prvs: matchingKeyBackups, passPhrase: passPhrase, source: .backup) - } catch { - fatalError() - } - moveToMainFlow() - } - - private func setupAccountWithGeneratedKey(with passPhrase: String) { - Promise { [weak self] in - guard let self = self else { return } - let userId = try self.getUserId() - try awaitPromise(self.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase)) - let encryptedPrv = try self.core.generateKey(passphrase: passPhrase, variant: .curve25519, userIds: [userId]) - try awaitPromise(self.backupService.backupToInbox(keys: [encryptedPrv.key], for: self.user)) - try self.storePrvs(prvs: [encryptedPrv.key], passPhrase: passPhrase, source: .generated) - - let updateKey = self.attester.updateKey( - email: userId.email, - pubkey: encryptedPrv.key.public, - token: self.storage.token - ) - try awaitPromise(self.alertAndSkipOnRejection( - updateKey, - fail: "Failed to submit Public Key") - ) - let testWelcome = self.attester.testWelcome(email: userId.email, pubkey: encryptedPrv.key.public) - try awaitPromise(self.alertAndSkipOnRejection( - testWelcome, - fail: "Failed to send you welcome email") - ) - } - .then(on: .main) { [weak self] in - self?.moveToMainFlow() - } - .catch(on: .main) { [weak self] error in - guard let self = self else { return } - let isErrorHandled = self.handleCommon(error: error) - - if !isErrorHandled { - self.showAlert(error: error, message: "Could not finish setup, please try again") - } - } - } - - private func validateAndConfirmNewPassPhraseOrReject(passPhrase: String) -> Promise { - Promise { - let strength = try self.core.zxcvbnStrengthBar(passPhrase: passPhrase) - guard strength.word.pass else { throw AppErr.user("Pass phrase strength: \(strength.word.word)\ncrack time: \(strength.time)\n\nWe recommend to use 5-6 unrelated words as your Pass Phrase.") } - let confirmPassPhrase = try awaitPromise(self.awaitUserPassPhraseEntry(title: "Confirm Pass Phrase")) - guard confirmPassPhrase != nil else { throw AppErr.silentAbort } - guard confirmPassPhrase == passPhrase else { throw AppErr.user("Pass phrases don't match") } - } - } - - private func getUserId() throws -> UserId { - guard let email = DataService.shared.email, !email.isEmpty else { throw AppErr.unexpected("Missing user email") } - guard let name = DataService.shared.email, !name.isEmpty else { throw AppErr.unexpected("Missing user name") } - return UserId(email: email, name: name) - } - - private func storePrvs(prvs: [KeyDetails], passPhrase: String, source: KeySource) throws { - storage.addKeys(keyDetails: prvs, passPhrase: passPhrase, source: source) - } -} - -// MARK: - Events - -extension SetupViewController { - private func handleImportKey() { - let viewController = ImportKeyViewController() - navigationController?.pushViewController(viewController, animated: true) - } - - private func handleButtonPressed() { - // ignore if we are still fetching keys - guard !state.isSearchingBackups else { - return - } - - view.endEditing(true) - guard let passPhrase = passPhrase else { return } - - guard passPhrase.isNotEmpty else { - showAlert(message: "setup_enter_pass_phrase".localized) - return - } - - showSpinner() - - // TODO: - fix for spinner - // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - switch self.state { - case .createKey: - self.setupAccountWithGeneratedKey(with: passPhrase) - case let .fetchedEncrypted(backups): - self.recoverAccount(with: backups, and: passPhrase) - default: - assertionFailure("Not proper state for the screen") - } - } - } - - private func handleOtherAccount() { - router.signOut() - } - - private func moveToMainFlow() { - router.proceed() - } -} - -// MARK: - ASTableDelegate, ASTableDataSource - -extension SetupViewController: ASTableDelegate, ASTableDataSource { - func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - Parts.allCases.count - } - - func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - { [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, - insets: self.decorator.titleInset, - backgroundColor: .backgroundColor - ) - ) - case .description: - return SetupTitleNode( - SetupTitleNode.Input( - title: self.decorator.subtitle(for: self.state), - insets: self.decorator.subTitleInset, - backgroundColor: .backgroundColor - ) - ) - case .passPhrase: - return TextFieldCellNode(input: self.decorator.textFieldStyle) { [weak self] action in - guard case let .didEndEditing(value) = action else { return } - self?.passPhrase = value - } - .then { - $0.becomeFirstResponder() - } - .onShouldReturn { [weak self] _ in - self?.view.endEditing(true) - self?.handleButtonPressed() - return true - } - case .action: - return ButtonCellNode( - title: self.decorator.buttonTitle(for: self.state), - insets: self.decorator.buttonInsets - ) { [weak self] in - self?.handleButtonPressed() - } - .then { - $0.button.accessibilityIdentifier = "load_account" - } - case .optionalAction: - return ButtonCellNode( - title: self.decorator.useAnotherAccountTitle, - insets: self.decorator.optionalButtonInsets, - color: .white - ) { [weak self] in - self?.handleOtherAccount() - } - case .divider: - return DividerCellNode(inset: UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24)) - } - } - } -} diff --git a/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift b/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift index 8f09f59da..a68d224cd 100644 --- a/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift +++ b/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift @@ -10,71 +10,140 @@ import FlowCryptCommon import FlowCryptUI import UIKit -protocol SetupViewDecoratorType { - var title: NSAttributedString { get } - var useAnotherAccountTitle: NSAttributedString { get } - var titleInset: UIEdgeInsets { get } - var subTitleInset: UIEdgeInsets { get } - var buttonInsets: UIEdgeInsets { get } - var optionalButtonInsets: UIEdgeInsets { get } - var textFieldStyle: TextFieldCellNode.Input { get } - - func buttonTitle(for state: SetupViewController.State) -> NSAttributedString - func subtitle(for state: SetupViewController.State) -> NSAttributedString +struct SetupViewInsets { + let titleInset = UIEdgeInsets(top: 64, left: 16, bottom: 64, right: 16) + let subTitleInset = UIEdgeInsets(top: 0, left: 16, bottom: 24, right: 16) + let buttonInsets = UIEdgeInsets(top: 8, left: 24, bottom: 8, right: 24) + let optionalButtonInsets = UIEdgeInsets(top: 0, left: 24, bottom: 8, right: 24) + let dividerInsets = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24) } -struct SetupViewDecorator: SetupViewDecoratorType { - let textFieldStyle = SetupCommonStyle.passPhraseTextFieldStyle - - var title: NSAttributedString { - "setup_title".localized.attributed(.bold(35), color: .mainTextColor, alignment: .center) - } +struct SetupViewDecorator { + let insets = SetupViewInsets() - var useAnotherAccountTitle: NSAttributedString { - "setup_use_another".localized.attributed( - .regular(15), - color: UIColor.colorFor(darkStyle: .black, lightStyle: .blueColor), + let passPhraseLostDescription = "create_pass_phrase_lost" + .localized + .attributed( + .regular(16), + color: .lightGray, alignment: .center ) + + // MARK: Title + enum TitleType { + case setup, enterPassPhrase, importKey, createKey } - let titleInset = UIEdgeInsets(top: 92, left: 16, bottom: 20, right: 16) - let subTitleInset = UIEdgeInsets(top: 0, left: 16, bottom: 60, right: 16) - let buttonInsets = UIEdgeInsets(top: 80, left: 24, bottom: 8, right: 24) - let optionalButtonInsets = UIEdgeInsets(top: 0, left: 24, bottom: 8, right: 24) + func title(for titleType: TitleType) -> NSAttributedString { + let text: String + + switch titleType { + case .setup, .createKey: + text = "setup_title" + case .enterPassPhrase, .importKey: + text = "import_key_description" + } + + return text + .localized + .attributed( + .bold(35), + color: .mainTextColor, + alignment: .center + ) + } + + func sceneTitle(for titleType: TitleType) -> String { + switch titleType { + case .setup: + return "FlowCrypt" + case .enterPassPhrase, .importKey: + return "import_key_title".localized + case .createKey: + return "setup_create_key_title".localized + } + } + + // MARK: Subtitle + enum SubtitleType { + case common, fetchedKeys(Int), choosingPassPhrase, noBackups + } - func buttonTitle(for state: SetupViewController.State) -> NSAttributedString { - let title: String - switch state { - case .createKey: title = "setup_create_key" - default: title = "setup_load" + func subtitle(for subtitleType: SubtitleType) -> NSAttributedString { + let subtitle: String + + switch subtitleType { + case let .fetchedKeys(count): + subtitle = "Found \(count) key backup\(count > 1 ? "s" : "")" + case .common: + subtitle = "setup_description".localized + case .choosingPassPhrase: + subtitle = "create_pass_phrase_description".localized + case .noBackups: + let user = DataService.shared.email ?? "unknown_title".localized + let msg = "setup_no_backups".localized + user + subtitle = msg } - return title.localized.attributed(.regular(17), color: .white, alignment: .center) + return subtitle + .attributed( + .regular(17), + alignment: .center + ) + } + + var subtitleStyle: (String) -> NSAttributedString { { $0.attributed(.regular(17), alignment: .center) } } - func subtitle(for state: SetupViewController.State) -> NSAttributedString { - let subtitle: String = { - switch state { - case let .fetchedEncrypted(keys): - return "Found \(keys.count) key backup\(keys.count > 1 ? "s" : "")" - case .createKey: - return "setup_action_create_new_subtitle".localized - default: - return "setup_description".localized - } - }() - - return subtitle.attributed(.regular(17)) + // MARK: Button + enum ButtonAction { + case createKey, importKey, loadAccount, setPassPhrase, pasteBoard, passPhraseContinue, passPhraseChooseAnother, fileImport + } + + func buttonTitle(for action: ButtonAction) -> NSAttributedString { + let buttonTitle: String + + switch action { + case .createKey: + buttonTitle = "setup_initial_create_key" + case .importKey: + buttonTitle = "setup_initial_import_key" + case .loadAccount: + buttonTitle = "setup_load" + case .setPassPhrase: + buttonTitle = "create_pass_phrase_set_title" + case .pasteBoard: + buttonTitle = "import_key_paste" + case .passPhraseContinue: + buttonTitle = "import_key_continue" + case .passPhraseChooseAnother: + buttonTitle = "import_key_choose" + case .fileImport: + buttonTitle = "import_key_file" + } + + return buttonTitle + .localized + .attributed( + .regular(17), + color: .white, + alignment: .center + ) } } -enum SetupCommonStyle { +extension TextFieldCellNode.Input { static let passPhraseTextFieldStyle: TextFieldCellNode.Input = TextFieldCellNode.Input( - placeholder: "setup_enter".localized.attributed(.bold(16), color: .lightGray, alignment: .center), - isSecureTextEntry: true, - textInsets: 0, - textAlignment: .center, - insets: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) + placeholder: "setup_enter" + .localized + .attributed( + .bold(16), + color: .lightGray, + alignment: .center + ), + isSecureTextEntry: true, + textInsets: 0, + textAlignment: .center, + insets: UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) ) } diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift index ed6cfe440..f5c2391e5 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift @@ -308,6 +308,7 @@ extension MyMenuViewController { // MARK: - SideMenuViewController extension MyMenuViewController: SideMenuViewController { func didOpen() { + tableNode.reloadData() fetchFolders() } } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index ce8d7c1ad..b2115515e 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -12,7 +12,11 @@ enum CoreError: Error { case value(String) } -final class Core { +protocol KeyDecrypter { + func decryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.DecryptKey +} + +final class Core: KeyDecrypter { static let shared = Core() private var jsEndpointListener: JSValue? @@ -101,7 +105,7 @@ final class Core { return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) } - public func startInBackgroundIfNotAlreadyRunning() { + public func startInBackgroundIfNotAlreadyRunning(_ completion: (() -> Void)? = nil) { if !started { started = true DispatchQueue.global(qos: .default).async { [weak self] in @@ -123,6 +127,7 @@ final class Core { self.context!.setObject(unsafeBitCast(cb_last_value_filler, to: AnyObject.self), forKeyedSubscript: "engine_host_cb_catcher" as (NSCopying & NSObjectProtocol)?) self.ready = true self.logger.logInfo("JsContext took \(trace.finish()) to start") + completion?() } } } diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 49a0ccd22..20d12e14e 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -186,3 +186,10 @@ struct MsgBlock: Decodable { // case cryptupVerification; // not sure if Swift code will ever encounter this } } + +// TODO: - ANTON - tests +extension MsgBlock { + var isAttachmentBlock: Bool { + type == .plainAtt || type == .encryptedAtt || type == .decryptedAtt + } +} diff --git a/FlowCrypt/Core/Models/KeyDetails.swift b/FlowCrypt/Core/Models/KeyDetails.swift index 5fa653af7..aa9b8bd63 100644 --- a/FlowCrypt/Core/Models/KeyDetails.swift +++ b/FlowCrypt/Core/Models/KeyDetails.swift @@ -18,6 +18,10 @@ struct KeyDetails: Decodable { let created: Int let users: [String] let algo: KeyAlgo? + + var longid: String { + ids[0].longid + } } extension KeyDetails: Hashable { diff --git a/FlowCrypt/Core/Models/PassPhrase.swift b/FlowCrypt/Core/Models/PassPhrase.swift new file mode 100644 index 000000000..963929e9d --- /dev/null +++ b/FlowCrypt/Core/Models/PassPhrase.swift @@ -0,0 +1,49 @@ +// +// PassPhraseInfo.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 04.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import RealmSwift + +// Should be operated in app +struct PassPhrase: Codable, Hashable, Equatable { + let value: String + let longid: String + + init(value: String, longid: String) { + self.value = value + self.longid = longid + } +} + +extension PassPhrase { + init(object: PassPhraseObject) { + self.value = object.value + self.longid = object.longid + } +} + +/// PassPhrase object to store in Realm +final class PassPhraseObject: Object { + @objc dynamic var longid: String = "" + @objc dynamic var value: String = "" + + convenience init( + longid: String = "", + value: String = "" + ) { + self.init() + self.value = value + self.longid = longid + } +} + +extension PassPhraseObject { + convenience init(_ passPhrase: PassPhrase) { + self.init(longid: passPhrase.longid, value: passPhrase.value) + } +} diff --git a/FlowCrypt/Core/Models/PrvKeyInfo.swift b/FlowCrypt/Core/Models/PrvKeyInfo.swift index 3b9978564..419aded42 100644 --- a/FlowCrypt/Core/Models/PrvKeyInfo.swift +++ b/FlowCrypt/Core/Models/PrvKeyInfo.swift @@ -11,13 +11,5 @@ import Foundation struct PrvKeyInfo: Encodable { let `private`: String let longid: String - let passphrase: String? -} - -extension PrvKeyInfo { - init(from keyInfo: KeyInfo) { - self.private = keyInfo.private - self.longid = keyInfo.longid - self.passphrase = keyInfo.passphrase - } + let passphrase: String } diff --git a/FlowCrypt/Extensions/UIViewControllerExtensions.swift b/FlowCrypt/Extensions/UIViewControllerExtensions.swift index bc9c953a4..e925ac996 100644 --- a/FlowCrypt/Extensions/UIViewControllerExtensions.swift +++ b/FlowCrypt/Extensions/UIViewControllerExtensions.swift @@ -65,10 +65,6 @@ extension UIViewController { } extension UIViewController { - var safeAreaWindowInsets: UIEdgeInsets { - UIApplication.shared.keyWindow?.safeAreaInsets ?? .zero - } - var statusBarHeight: CGFloat { UIApplication.shared.statusBarFrame.height } @@ -141,26 +137,6 @@ extension UIViewController { } } - func awaitUserPassPhraseEntry(title: String) -> Promise { - Promise(on: .main) { [weak self] resolve, _ in - guard let self = self else { throw AppErr.nilSelf } - let alert = UIAlertController(title: "Pass Phrase", message: title, preferredStyle: .alert) - alert.addTextField { textField in - textField.isSecureTextEntry = true - textField.accessibilityLabel = "textField" - } - - alert.addAction(UIAlertAction(title: "Cancel", style: .default) { _ in - resolve(nil) - }) - - alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak alert] _ in - resolve(alert?.textFields?[0].text) - }) - self.present(alert, animated: true, completion: nil) - } - } - func keyboardHeight(from notification: Notification) -> CGFloat { (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0 } diff --git a/FlowCrypt/Functionality/DataManager/DataService.swift b/FlowCrypt/Functionality/DataManager/DataService.swift index e162227df..40da8f7f5 100644 --- a/FlowCrypt/Functionality/DataManager/DataService.swift +++ b/FlowCrypt/Functionality/DataManager/DataService.swift @@ -10,7 +10,7 @@ import Foundation import Promises import RealmSwift -protocol DataServiceType { +protocol DataServiceType: EmailProviderType { // data var email: String? { get } var currentUser: User? { get } @@ -27,9 +27,18 @@ protocol ImapSessionProvider { func smtpSession() -> SMTPSession? } -enum SessionType { +enum SessionType: CustomStringConvertible { case google(_ email: String, name: String, token: String) case session(_ userObject: UserObject) + + var description: String { + switch self { + case let .google(email, name, _): + return "Google \(email) \(name)" + case let .session(user): + return "Session \(user.email)" + } + } } // MARK: - DataService @@ -64,10 +73,8 @@ extension DataService: DataServiceType { guard let currentUser = currentUser else { return false } - guard let keys = encryptedStorage.keys() else { - return false - } - let isAnyKeysForCurrentUser = keys + + let isAnyKeysForCurrentUser = encryptedStorage.keysInfo() .map(\.account) .map { $0.contains(currentUser.email) } .contains(true) @@ -111,26 +118,6 @@ extension DataService: DataServiceType { } } -// MARK: - DataKeyServiceType -extension DataService: KeyDataServiceType { - var keys: [PrvKeyInfo]? { - guard let keys = encryptedStorage.keys() else { return nil } - return Array(keys).map(PrvKeyInfo.init) - } - - var publicKey: String? { - encryptedStorage.publicKey() - } - - func addKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) { - encryptedStorage.addKeys(keyDetails: keyDetails, passPhrase: passPhrase, source: source) - } - - func updateKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) { - encryptedStorage.updateKeys(keyDetails: keyDetails, passPhrase: passPhrase, source: source) - } -} - // MARK: - Migration extension DataService: DBMigration { /// Perform all kind of migrations diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 2941ef728..1079ced62 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -11,14 +11,9 @@ import Foundation import Promises import RealmSwift -protocol EncryptedStorageType { +protocol EncryptedStorageType: KeyStorageType { var storage: Realm { get } - func addKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) - func updateKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) - func publicKey() -> String? - func keys() -> Results? - func getAllUsers() -> [UserObject] func saveActiveUser(with user: UserObject) var activeUser: UserObject? { get } @@ -109,14 +104,20 @@ extension EncryptedStorage: LogOutHandler { destroyEncryptedStorage() } else { // remove user and keys for this user - let userToDelete = users.filter { $0.email == email } - let keys = storage.objects(KeyInfo.self).filter { $0.account.contains(email) } - let sessions = storage.objects(SessionObject.self).filter { $0.email == email } + let userToDelete = users + .filter { $0.email == email } + let keys = storage.objects(KeyInfo.self) + .filter { $0.account.contains(email) } + let passPhrases = storage.objects(PassPhraseObject.self) + .filter { keys.map(\.longid).contains($0.longid) } + let sessions = storage.objects(SessionObject.self) + .filter { $0.email == email } try storage.write { storage.delete(userToDelete) storage.delete(keys) storage.delete(sessions) + storage.delete(passPhrases) } } } @@ -153,35 +154,25 @@ extension EncryptedStorage { // MARK: - Keys extension EncryptedStorage { - func addKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) { + func addKeys(keyDetails: [KeyDetails], source: KeySource) { try! storage.write { for key in keyDetails { - storage.add(try! KeyInfo(key, passphrase: passPhrase, source: source)) + storage.add(try! KeyInfo(key, source: source)) } } } - func updateKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) { - // KeyInfo doesn't have primaty key, to avoid migration we need to delete keys and then save them - - // delete keys - keyDetails.forEach { keyDetail in - try? storage.write { - storage.delete(storage.objects(KeyInfo.self) - .filter("longid=%@", keyDetail.ids[0].longid)) - } - } - - // add new keys + func updateKeys(keyDetails: [KeyDetails], source: KeySource) { try! storage.write { for key in keyDetails { - storage.add(try! KeyInfo(key, passphrase: passPhrase, source: source)) + storage.add(try! KeyInfo(key, source: source), update: .all) } } } - func keys() -> Results? { - storage.objects(KeyInfo.self) + func keysInfo() -> [KeyInfo] { + let result = storage.objects(KeyInfo.self) + return Array(result) } func publicKey() -> String? { @@ -191,6 +182,31 @@ extension EncryptedStorage { } } +// MARK: - PassPhrase +extension EncryptedStorage: EncryptedPassPhraseStorage { + func addPassPhrase(object: PassPhraseObject) { + try! storage.write { + storage.add(object) + } + } + + func updatePassPhrase(object: PassPhraseObject) { + try! storage.write { + storage.add(object, update: .all) + } + } + + func removePassPhrase(object: PassPhraseObject) { + try! storage.write { + storage.delete(object) + } + } + + func getPassPhrases() -> [PassPhraseObject] { + Array(storage.objects(PassPhraseObject.self)) + } +} + // MARK: - User extension EncryptedStorage { var activeUser: UserObject? { diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorageProtocols.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorageProtocols.swift new file mode 100644 index 000000000..44ec94c44 --- /dev/null +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorageProtocols.swift @@ -0,0 +1,24 @@ +// +// EncryptedStorageProtocols.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +protocol KeyStorageType { + func addKeys(keyDetails: [KeyDetails], source: KeySource) + func updateKeys(keyDetails: [KeyDetails], source: KeySource) + func publicKey() -> String? + func keysInfo() -> [KeyInfo] +} + +protocol EncryptedPassPhraseStorage { + func addPassPhrase(object: PassPhraseObject) + func updatePassPhrase(object: PassPhraseObject) + func getPassPhrases() -> [PassPhraseObject] + func removePassPhrase(object: PassPhraseObject) + func keysInfo() -> [KeyInfo] +} diff --git a/FlowCrypt/Functionality/Error Handling/ErrorHandler.swift b/FlowCrypt/Functionality/Error Handling/ErrorHandler.swift index 49d7ffab6..7f8693011 100644 --- a/FlowCrypt/Functionality/Error Handling/ErrorHandler.swift +++ b/FlowCrypt/Functionality/Error Handling/ErrorHandler.swift @@ -32,7 +32,8 @@ private struct ComposedErrorHandler: ErrorHandler { static let shared: ComposedErrorHandler = ComposedErrorHandler( handlers: [ KeyServiceErrorHandler(), - BackupServiceErrorHandler() + BackupServiceErrorHandler(), + CreateKeyErrorHandler() ] ) diff --git a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift index 19b2970e2..d9081cb7f 100644 --- a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift +++ b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift @@ -8,16 +8,19 @@ import UIKit +// KeyServiceError struct KeyServiceErrorHandler: ErrorHandler { func handle(error: Error, for viewController: UIViewController) -> Bool { let errorMessage: String? - switch error { - case KeyServiceError.retrieve: + switch error as? KeyServiceError { + case .retrieve: errorMessage = "keyServiceError_retrieve_error" - case KeyServiceError.parse: + case .parsingError: errorMessage = "keyServiceError_retrieve_parse" - case KeyServiceError.unexpected: + case .unexpected: errorMessage = "keyServiceError_retrieve_unexpected" + case .emptyKeys: + errorMessage = nil default: errorMessage = nil } @@ -29,3 +32,31 @@ struct KeyServiceErrorHandler: ErrorHandler { return true } } + +// CreateKeyError +struct CreateKeyErrorHandler: ErrorHandler { + func handle(error: Error, for viewController: UIViewController) -> Bool { + let errorMessage: String? + + switch error as? CreateKeyError { + case .weakPassPhrase(let strength): + errorMessage = "Pass phrase strength: \(strength.word.word)\ncrack time: \(strength.time)\n\nWe recommend to use 5-6 unrelated words as your Pass Phrase." + case .missedUserEmail: + errorMessage = "backupServiceError_email".localized + case .missedUserName: + errorMessage = "backupServiceError_name".localized + case .doesntMatch: + errorMessage = "pass_phrase_match_error".localized + case .conformingPassPhraseError: + errorMessage = nil + case .none: + errorMessage = nil + } + + guard let message = errorMessage else { return false } + + viewController.showAlert(message: message) + + return true + } +} diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index f340c3d84..e2227599a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -29,14 +29,14 @@ extension Imap { logger.logInfo("Creating a new IMAP session") let newImapSession = MCOIMAPSession(session: imap) imapSess = newImapSession - //logIMAPConnection(for: imapSess!) + logIMAPConnection(for: imapSess!) } if let smtp = smtpSession { logger.logInfo("Creating a new SMTP session") let newSmtpSession = MCOSMTPSession(session: smtp) smtpSess = newSmtpSession - //logSMTPConnection(for: smtpSess!) + logSMTPConnection(for: smtpSess!) } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 4c86e8983..9e97fd622 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -15,8 +15,8 @@ struct MessageAttachment { let size: Int } -// MARK: - FetchedMessage -struct FetchedMessage { +// MARK: - ProcessedMessage +struct ProcessedMessage { enum MessageType { case error, encrypted, plain } @@ -27,9 +27,9 @@ struct FetchedMessage { let messageType: MessageType } -extension FetchedMessage { - // TODO: - ANTON - fix with empty state for MessageViewController - static let empty = FetchedMessage( +extension ProcessedMessage { + // TODO: - Ticket - fix with empty state for MessageViewController + static let empty = ProcessedMessage( rawMimeData: Data(), text: "loading_title".localized + "...", attachments: [], @@ -38,34 +38,43 @@ extension FetchedMessage { } // MARK: - MessageService +enum MessageServiceError: Error { + case missedPassPhrase(_ rawMimeData: Data) + case wrongPassPhrase(_ rawMimeData: Data, _ passPhrase: String) + // Could not fetch keys + case emptyKeys +} + final class MessageService { private let messageProvider: MessageProvider - private let dataService: DataServiceType & KeyDataServiceType + private let keyService: KeyServiceType + private let passPhraseStorage: PassPhraseStorageType private let core: Core init( messageProvider: MessageProvider = MailProvider.shared.messageProvider, - dataService: DataServiceType & KeyDataServiceType = DataService.shared, - core: Core = Core.shared + keyService: KeyServiceType = KeyService(), + core: Core = Core.shared, + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ) ) { self.messageProvider = messageProvider - self.dataService = dataService + self.keyService = keyService self.core = core + self.passPhraseStorage = passPhraseStorage } - func getMessage(with input: Message, folder: String) -> Promise { - Promise { [weak self] resolve, reject in + func validateMessage(rawMimeData: Data, with passPhrase: String) -> Promise { + Promise { [weak self] resolve, reject in guard let self = self else { return } - let rawMimeData = try awaitPromise( - self.messageProvider.fetchMsg(message: input, folder: folder) - ) - - guard let keys = self.dataService.keys else { - reject(CoreError.notReady("Could not fetch keys")) - return + guard let keys = try? self.keyService.getPrivateKeys(with: passPhrase).get(), keys.isNotEmpty else { + return reject(MessageServiceError.emptyKeys) } + // TODO: - Tom - is it possible to get longid of the key which was used for decryption? let decrypted = try self.core.parseDecryptMsg( encrypted: rawMimeData, keys: keys, @@ -73,36 +82,83 @@ final class MessageService { isEmail: true ) - let decryptErrBlocks = decrypted.blocks - .filter { $0.decryptErr != nil } + let isWrongPassPhraseError = decrypted.blocks.first(where: { (block) -> Bool in + guard let errorBlock = block.decryptErr, case .needPassphrase = errorBlock.error.type else { + return false + } + return true + }) - let attachments = decrypted.blocks - .filter(\.isAttachmentBlock) - .map(MessageAttachment.init) + if isWrongPassPhraseError != nil { + reject(MessageServiceError.wrongPassPhrase(rawMimeData, passPhrase)) + } else { + keys + .map { PassPhrase(value: passPhrase, longid: $0.longid) } + .forEach { self.passPhraseStorage.savePassPhrase(with: $0, inStorage: false) } - let messageType: FetchedMessage.MessageType - let text: String + let processedMessage = self.processMessage(rawMimeData: rawMimeData, with: decrypted) - if let decryptErrBlock = decryptErrBlocks.first { - let rawMsg = decryptErrBlock.content - let err = decryptErrBlock.decryptErr?.error - text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" - messageType = .error - } else { - text = decrypted.text - messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain + resolve(processedMessage) } + } + } - let fetchedMessage = FetchedMessage( - rawMimeData: rawMimeData, - text: text, - attachments: attachments, - messageType: messageType + func getMessage(with input: Message, folder: String) -> Promise { + Promise { [weak self] resolve, reject in + guard let self = self else { return } + + let rawMimeData = try awaitPromise( + self.messageProvider.fetchMsg(message: input, folder: folder) ) - resolve(fetchedMessage) + guard let keys = try? self.keyService.getPrivateKeys(with: nil).get() else { + return reject(MessageServiceError.missedPassPhrase(rawMimeData)) + } + + guard keys.isNotEmpty else { + reject(CoreError.notReady("Could not fetch keys")) + return + } + let decrypted = try self.core.parseDecryptMsg( + encrypted: rawMimeData, + keys: keys, + msgPwd: nil, + isEmail: true + ) + + let processedMessage = self.processMessage(rawMimeData: rawMimeData, with: decrypted) + resolve(processedMessage) } } + + private func processMessage(rawMimeData: Data, with decrypted: CoreRes.ParseDecryptMsg) -> ProcessedMessage { + let decryptErrBlocks = decrypted.blocks + .filter { $0.decryptErr != nil } + + let attachments = decrypted.blocks + .filter(\.isAttachmentBlock) + .map(MessageAttachment.init) + + let messageType: ProcessedMessage.MessageType + let text: String + + if let decryptErrBlock = decryptErrBlocks.first { + let rawMsg = decryptErrBlock.content + let err = decryptErrBlock.decryptErr?.error + text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" + messageType = .error + } else { + text = decrypted.text + messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain + } + + return ProcessedMessage( + rawMimeData: rawMimeData, + text: text, + attachments: attachments, + messageType: messageType + ) + } } private extension MessageAttachment { @@ -111,9 +167,3 @@ private extension MessageAttachment { self.size = block.attMeta?.length ?? 0 } } - -private extension MsgBlock { - var isAttachmentBlock: Bool { - type == .plainAtt || type == .encryptedAtt || type == .decryptedAtt - } -} diff --git a/FlowCrypt/Functionality/Pgp/KeyMethods.swift b/FlowCrypt/Functionality/Pgp/KeyMethods.swift index 1d997991f..6892d6fa8 100644 --- a/FlowCrypt/Functionality/Pgp/KeyMethods.swift +++ b/FlowCrypt/Functionality/Pgp/KeyMethods.swift @@ -12,15 +12,38 @@ protocol KeyMethodsType { func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) -> [KeyDetails] } -struct KeyMethods: KeyMethodsType { - let core: Core +final class KeyMethods: KeyMethodsType { + + let decrypter: KeyDecrypter + + init(decrypter: KeyDecrypter = Core.shared) { + self.decrypter = decrypter + } func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) -> [KeyDetails] { - keys.compactMap { key -> KeyDetails? in - guard let privateKey = key.private, - let decrypted = try? self.core.decryptKey(armoredPrv: privateKey, passphrase: passPhrase), - decrypted.decryptedKey != nil - else { return nil } + let logger = Logger.nested(in: Self.self, with: .core) + + guard keys.isNotEmpty else { + logger.logInfo("Keys are empty") + return [] + } + + return keys.compactMap { key -> KeyDetails? in + guard let privateKey = key.private else { + logger.logInfo("Filtered not private key") + return nil + } + + guard let decrypted = try? self.decrypter.decryptKey(armoredPrv: privateKey, passphrase: passPhrase) else { + logger.logInfo("Filtered not decrypted key") + return nil + } + + guard decrypted.decryptedKey != nil else { + logger.logInfo("Filtered. decryptedKey = nil") + return nil + } + return key } } diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index cee8e4ce2..8e318ac65 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -54,15 +54,19 @@ struct AppStartup { return } + let viewController: UIViewController + switch entryPoint { case .mainFlow: - window.rootViewController = SideMenuNavigationController() + viewController = SideMenuNavigationController() case .signIn: - window.rootViewController = MainNavigationController(rootViewController: SignInViewController()) + viewController = MainNavigationController(rootViewController: SignInViewController()) case .setupFlow(let userId): - let setupViewController = SetupViewController(user: userId) - window.rootViewController = MainNavigationController(rootViewController: setupViewController) + let setupViewController = SetupInitialViewController(user: userId) + viewController = MainNavigationController(rootViewController: setupViewController) } + + window.rootViewController = viewController } private func entryPointForUser(session: SessionType?) -> EntryPoint? { diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 2d789521a..55d78a1f5 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -9,32 +9,28 @@ import Promises import UIKit -protocol BackupServiceType { - /// get all existed backups - func fetchBackups(for userId: UserId) -> Promise<[KeyDetails]> - /// backup keys to user inbox - func backupToInbox(keys: [KeyDetails], for userId: UserId) -> Promise - /// show activity sheet to save keys as file - func backupAsFile(keys: [KeyDetails], for viewController: UIViewController) -} - -// MARK: - BackupService -struct BackupService { - static let shared: BackupService = BackupService( - backupProvider: MailProvider.shared.backupProvider, - core: Core.shared, - messageSender: MailProvider.shared.messageSender - ) - +final class BackupService { let backupProvider: BackupProvider let core: Core let messageSender: MessageGateway + + init( + backupProvider: BackupProvider = MailProvider.shared.backupProvider, + core: Core = .shared, + messageSender: MessageGateway = MailProvider.shared.messageSender + ) { + self.backupProvider = backupProvider + self.core = core + self.messageSender = messageSender + } } // MARK: - BackupServiceType extension BackupService: BackupServiceType { - func fetchBackups(for userId: UserId) -> Promise<[KeyDetails]> { - Promise<[KeyDetails]> { resolve, reject in + func fetchBackupsFromInbox(for userId: UserId) -> Promise<[KeyDetails]> { + Promise<[KeyDetails]> { [weak self] resolve, reject in + guard let self = self else { throw AppErr.nilSelf } + let backupData = try awaitPromise(self.backupProvider.searchBackups(for: userId.email)) do { @@ -48,7 +44,9 @@ extension BackupService: BackupServiceType { } func backupToInbox(keys: [KeyDetails], for userId: UserId) -> Promise { - Promise { () -> Void in + Promise { [weak self] () -> Void in + guard let self = self else { throw AppErr.nilSelf } + let isFullyEncryptedKeys = keys.map(\.isFullyDecrypted).contains(false) guard isFullyEncryptedKeys else { @@ -61,7 +59,7 @@ extension BackupService: BackupServiceType { let privateKeyData = privateKeyContext.data().base64EncodedString() - let filename = "flowcrypt-backup-\(userId.email.userReadableEmail).key" + let filename = "flowcrypt-backup-\(userId.email.withoutSpecialCharacters).key" let attachments = [SendableMsg.Attachment(name: filename, type: "text/plain", base64: privateKeyData)] let message = SendableMsg( text: "setup_backup_email".localized, @@ -74,7 +72,7 @@ extension BackupService: BackupServiceType { atts: attachments ) let backupEmail = try self.core.composeEmail(msg: message, fmt: .plain, pubKeys: nil) - try awaitPromise(messageSender.sendMail(mime: backupEmail.mimeEncoded)) + try awaitPromise(self.messageSender.sendMail(mime: backupEmail.mimeEncoded)) } } @@ -90,7 +88,7 @@ extension BackupService: BackupServiceType { // MARK: - Helpers private extension String { - var userReadableEmail: String { + var withoutSpecialCharacters: String { self.replacingOccurrences( of: "[^a-z0-9]", with: "", diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupServiceType.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupServiceType.swift new file mode 100644 index 000000000..d46a7519f --- /dev/null +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupServiceType.swift @@ -0,0 +1,19 @@ +// +// BackupServiceType.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 31.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises + +protocol BackupServiceType { + /// get all existed backups + func fetchBackupsFromInbox(for userId: UserId) -> Promise<[KeyDetails]> + /// backup keys to user inbox + func backupToInbox(keys: [KeyDetails], for userId: UserId) -> Promise + /// show activity sheet to save keys as file + func backupAsFile(keys: [KeyDetails], for viewController: UIViewController) +} diff --git a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift index d58c32b5a..e5f93adeb 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift @@ -18,7 +18,7 @@ protocol FoldersServiceType { } final class FoldersService: FoldersServiceType { - // TODO: - ANTON - consider rework with CacheService for trash path instead + // TODO: - Ticket? - consider rework with CacheService for trash path instead private let localStorage: LocalStorageType let localFoldersProvider: LocalFoldersProviderType @@ -53,7 +53,7 @@ final class FoldersService: FoldersServiceType { let remoteFolders = try awaitPromise(self.remoteFoldersProvider.fetchFolders()) DispatchQueue.main.async { - // TODO: - ANTON - instead of removing all folders remove only + // TODO: - Ticket? - instead of removing all folders remove only // those folders which are in DB and not in remoteFolders self.localFoldersProvider.removeFolders() diff --git a/FlowCrypt/Functionality/Services/Key Services/InMemoryPassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Key Services/InMemoryPassPhraseStorage.swift new file mode 100644 index 000000000..a18ef8291 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Key Services/InMemoryPassPhraseStorage.swift @@ -0,0 +1,45 @@ +// +// LocalPassPhraseStorage.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import UIKit + +protocol InMemoryPassPhraseStorageType { + var passPhrases: Set { get } + func save(passPhrase: InMemoryPassPhrase) + func removePassPhrases(with objects: [InMemoryPassPhrase]) +} + +struct InMemoryPassPhrase: Codable, Hashable, Equatable { + let passPhrase: PassPhrase + let date: Date + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.passPhrase.longid == rhs.passPhrase.longid + } +} + +final class InMemoryPassPhraseStorage: InMemoryPassPhraseStorageType { + static let shared: InMemoryPassPhraseStorage = InMemoryPassPhraseStorage() + + private(set) var passPhrases: Set = [] + + private init() { + } + + func save(passPhrase: InMemoryPassPhrase) { + passPhrases.insert(passPhrase) + } + + func removePassPhrases(with objects: [InMemoryPassPhrase]) { + objects.forEach { + if passPhrases.contains($0) { + passPhrases.remove($0) + } + } + } +} diff --git a/FlowCrypt/Functionality/Services/Key Services/KeyDataStorage.swift b/FlowCrypt/Functionality/Services/Key Services/KeyDataStorage.swift new file mode 100644 index 000000000..39d0b7c6a --- /dev/null +++ b/FlowCrypt/Functionality/Services/Key Services/KeyDataStorage.swift @@ -0,0 +1,43 @@ +// +// KeyDataStorageService.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 03.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +final class KeyDataStorage { + private let encryptedStorage: EncryptedStorageType + private let passPhraseStorage: PassPhraseStorageType + + init( + encryptedStorage: EncryptedStorageType = EncryptedStorage(), + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ) + ) { + self.encryptedStorage = encryptedStorage + self.passPhraseStorage = passPhraseStorage + } +} + +extension KeyDataStorage: KeyStorageType { + func updateKeys(keyDetails: [KeyDetails], source: KeySource) { + encryptedStorage.updateKeys(keyDetails: keyDetails, source: source) + } + + func publicKey() -> String? { + encryptedStorage.publicKey() + } + + func keysInfo() -> [KeyInfo] { + encryptedStorage.keysInfo() + } + + func addKeys(keyDetails: [KeyDetails], source: KeySource) { + encryptedStorage.addKeys(keyDetails: keyDetails, source: source) + } +} diff --git a/FlowCrypt/Functionality/Services/Key Services/KeyService.swift b/FlowCrypt/Functionality/Services/Key Services/KeyService.swift index 48145db15..55cacba32 100644 --- a/FlowCrypt/Functionality/Services/Key Services/KeyService.swift +++ b/FlowCrypt/Functionality/Services/Key Services/KeyService.swift @@ -8,39 +8,104 @@ import Foundation -// Data Service -protocol KeyDataServiceType { - var keys: [PrvKeyInfo]? { get } - var publicKey: String? { get } - func addKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) - func updateKeys(keyDetails: [KeyDetails], passPhrase: String, source: KeySource) -} - protocol KeyServiceType { func retrieveKeyDetails() -> Result<[KeyDetails], KeyServiceError> + func getPrivateKeys(with passPhrase: String?) -> Result<[PrvKeyInfo], KeyServiceError> +} + +enum KeyServiceError: Error { + case emptyKeys, unexpected, parsingError, retrieve } -struct KeyService: KeyServiceType { +final class KeyService: KeyServiceType { let coreService: Core = .shared - let dataService: KeyDataServiceType = DataService.shared + let storage: KeyStorageType + let passPhraseStorage: PassPhraseStorageType + let currentUserEmail: () -> (String?) + + init( + storage: KeyStorageType = KeyDataStorage(), + passPhraseStorage: PassPhraseStorageType = PassPhraseStorage( + storage: EncryptedStorage(), + emailProvider: DataService.shared + ), + currentUserEmail: @autoclosure @escaping () -> (String?) = DataService.shared.email + ) { + self.storage = storage + self.passPhraseStorage = passPhraseStorage + self.currentUserEmail = currentUserEmail + } func retrieveKeyDetails() -> Result<[KeyDetails], KeyServiceError> { - guard let keys = dataService.keys else { - return .failure(.retrieve) + guard let privateKeys = try? getPrivateKeys().get(), privateKeys.isNotEmpty else { + return .failure(.emptyKeys) } - let keyDetails = keys + let keyDetails = privateKeys .compactMap { - try? coreService - .parseKeys(armoredOrBinary: $0.private.data()) + try? coreService.parseKeys(armoredOrBinary: $0.private.data()) .keyDetails } .flatMap { $0 } - guard keyDetails.count == keys.count else { - return .failure(.parse) + guard keyDetails.count == privateKeys.count else { + return .failure(.parsingError) } return .success(keyDetails) } + + func getPrivateKeys(with passPhrase: String? = nil) -> Result<[PrvKeyInfo], KeyServiceError> { + guard let email = currentUserEmail() else { + return .failure(.retrieve) + } + + let keysInfo = storage.keysInfo() + .filter { $0.account.contains(email) } + + let storedPassPhrases = passPhraseStorage.getPassPhrases() + + guard keysInfo.isNotEmpty else { + return .failure(.emptyKeys) + } + + // get all private keys with already saved pass phrases + var privateKeys = keysInfo + .compactMap { (keyInfo) -> PrvKeyInfo? in + guard let passPhrase = storedPassPhrases.first(where: { $0.longid == keyInfo.longid }) else { + return nil + } + + let passPhraseValue = passPhrase.value + + guard passPhraseValue.isNotEmpty else { + return nil + } + + return PrvKeyInfo( + private: keyInfo.private, + longid: keyInfo.longid, + passphrase: passPhraseValue + ) + } + + // append keys to ensure with a pass phrase + if let passPhrase = passPhrase { + let keysToEnsure = keysInfo.map { + PrvKeyInfo( + private: $0.private, + longid: $0.longid, + passphrase: passPhrase + ) + } + + privateKeys.append(contentsOf: keysToEnsure) + } + + guard privateKeys.isNotEmpty else { + return .failure(.emptyKeys) + } + + return .success(privateKeys) + } } diff --git a/FlowCrypt/Functionality/Services/Key Services/PassPhraseStorage.swift b/FlowCrypt/Functionality/Services/Key Services/PassPhraseStorage.swift new file mode 100644 index 000000000..498e75a6a --- /dev/null +++ b/FlowCrypt/Functionality/Services/Key Services/PassPhraseStorage.swift @@ -0,0 +1,107 @@ +// +// PassPhraseStorageService.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 02.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import UIKit + +protocol PassPhraseStorageType { + func getPassPhrases() -> [PassPhrase] + func savePassPhrase(with passPhrase: PassPhrase, inStorage: Bool) + func updatePassPhrase(with passPhrase: PassPhrase, inStorage: Bool) +} + +protocol EmailProviderType { + var email: String? { get } +} + +final class PassPhraseStorage: PassPhraseStorageType { + private lazy var logger = Logger.nested(Self.self) + + let currentUserEmail: String? + let encryptedStorage: EncryptedPassPhraseStorage + let inMemoryStorage: InMemoryPassPhraseStorageType + let timeoutInSeconds: Int + + init( + storage: EncryptedPassPhraseStorage, + localStorage: InMemoryPassPhraseStorageType = InMemoryPassPhraseStorage.shared, + timeoutInSeconds: Int = 4*60*60, // 4 hours + emailProvider: EmailProviderType, + isHours: Bool = true + ) { + self.encryptedStorage = storage + self.inMemoryStorage = localStorage + self.timeoutInSeconds = timeoutInSeconds + self.currentUserEmail = emailProvider.email + } + + func savePassPhrase(with passPhrase: PassPhrase, inStorage: Bool) { + if inStorage { + logger.logInfo("Save to storage \(passPhrase.longid)") + encryptedStorage.addPassPhrase(object: PassPhraseObject(passPhrase)) + } else { + logger.logInfo("Save locally \(passPhrase.longid)") + + let inMemoryPassPhrase = InMemoryPassPhrase(passPhrase: passPhrase, date: Date()) + inMemoryStorage.save(passPhrase: inMemoryPassPhrase) + + let alreadySaved = encryptedStorage.getPassPhrases() + + if alreadySaved.contains(where: { $0.longid == passPhrase.longid }) { + encryptedStorage.removePassPhrase(object: PassPhraseObject(passPhrase)) + } + } + } + + func updatePassPhrase(with passPhrase: PassPhrase, inStorage: Bool) { + if inStorage { + encryptedStorage.updatePassPhrase(object: PassPhraseObject(passPhrase)) + } else { + let updated = InMemoryPassPhrase(passPhrase: passPhrase, date: Date()) + inMemoryStorage.save(passPhrase: updated) + } + } + + func getPassPhrases() -> [PassPhrase] { + let dbPassPhrases = encryptedStorage.getPassPhrases() + .map(PassPhrase.init) + + logger.logInfo("dbPassPhrases \(dbPassPhrases.count)") + + let calendar = Calendar.current + + var validPassPhrases: [PassPhrase] = [] + var invalidPassPhrases: [InMemoryPassPhrase] = [] + + inMemoryStorage.passPhrases + .forEach { localPassPhrases in + let components = calendar.dateComponents( + [.second], + from: localPassPhrases.date, + to: Date() + ) + + let timePassed = components.second ?? 0 + + let isPassPhraseValid = timePassed < timeoutInSeconds + + if isPassPhraseValid { + validPassPhrases.append(localPassPhrases.passPhrase) + } else { + invalidPassPhrases.append(localPassPhrases) + } + + let message = "pass phrase is \(isPassPhraseValid ? "valid" : "invalid") \(localPassPhrases.passPhrase.longid)" + self.logger.logInfo(message) + } + + inMemoryStorage.removePassPhrases(with: invalidPassPhrases) + + logger.logInfo("validPassPhrases \(validPassPhrases.count)") + return dbPassPhrases + validPassPhrases + } +} diff --git a/FlowCrypt/Models/OrganisationalRule.swift b/FlowCrypt/Models/OrganisationalRule.swift index 7321bd8ea..7c009be16 100644 --- a/FlowCrypt/Models/OrganisationalRule.swift +++ b/FlowCrypt/Models/OrganisationalRule.swift @@ -43,12 +43,12 @@ class OrganisationalRules { self.domainRules = domainRules self.domain = domain } - + init?(domainRules: DomainRules, email: String) { guard let recipientDomain = email.recipientDomain else { return nil } - + self.domain = recipientDomain self.domainRules = domainRules } diff --git a/FlowCrypt/Models/Realm Models/KeyInfo.swift b/FlowCrypt/Models/Realm Models/KeyInfo.swift index 77d7e721e..08beb9449 100644 --- a/FlowCrypt/Models/Realm Models/KeyInfo.swift +++ b/FlowCrypt/Models/Realm Models/KeyInfo.swift @@ -19,11 +19,10 @@ final class KeyInfo: Object { @objc dynamic var `private`: String = "" @objc dynamic var `public`: String = "" @objc dynamic var longid: String = "" - @objc dynamic var passphrase: String = "" @objc dynamic var source: String = "" @objc dynamic var account: String = "" - convenience init(_ keyDetails: KeyDetails, passphrase: String, source: KeySource) throws { + convenience init(_ keyDetails: KeyDetails, source: KeySource) throws { self.init() guard let privateKey = keyDetails.private else { assertionFailure("storing pubkey as private") // crash tests @@ -33,11 +32,18 @@ final class KeyInfo: Object { assertionFailure("Will not store Private Key that is not fully encrypted") // crash tests throw KeyInfoError.notEncrypted("Will not store Private Key that is not fully encrypted") } - `private` = privateKey - `public` = keyDetails.public - longid = keyDetails.ids[0].longid - self.passphrase = passphrase + self.`private` = privateKey + self.`public` = keyDetails.public + self.longid = keyDetails.longid self.source = source.rawValue self.account = keyDetails.users.first ?? "" } + + override class func primaryKey() -> String? { + "private" + } + + override var description: String { + "account = \(account) ####### longid = \(longid)" + } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 2b8723717..230db00fe 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -3,6 +3,8 @@ "sending_title" = "Sending"; "unknown_title" = "(unknown)"; "retry_title" = "retry"; +"ok" = "Ok"; +"cancel" = "Cancel"; // EMAIL "email_removed" = "Email moved to Trash"; @@ -39,7 +41,9 @@ "backupServiceError_parse" = "Key can't be parsed"; "backupServiceError_email" = "Missing user email"; +"backupServiceError_name" = "Missing user name"; "backupServiceError_notEncrypted" = "Private Key must be fully encrypted before backing up"; +"pass_phrase_match_error" = "Pass phrases don't match"; // Compose "compose_enter_recipient" = "Enter recipient"; @@ -86,6 +90,12 @@ "setup_wrong_pass_phrase_retry" = "Wrong pass phrase, please try again"; "setup_backup_email" = "This email contains a key backup. It will help you access your encrypted messages from other computers (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nThe key below is protected with pass phrase that only you know. You should make sure to note your pass phrase down.\n\nDO NOT DELETE THIS EMAIL. Write us at human@flowcrypt.com so that we can help."; "setup_create_key" = "Create"; +"setup_initial_create_key" = "Create a new key"; +"setup_initial_import_key" = "Import my key"; +"setup_create_key_title" = "Create key"; +"setup_save_pass_locally" = "Store pass phrase locally"; +"setup_save_pass_in_memory" = "Keep pass phrase in memory"; +"setup_enter_pass_phrase" = "Please enter pass phrase"; // Key Import "import_key_title" = "Import Key"; @@ -99,6 +109,11 @@ "import_key_add_new" = "Keys imported: %@"; "import_key_add_update" = "Keys updated: %@"; +// Key Create +"create_pass_phrase_description" = "See choosing secure pass phrase ?"; +"create_pass_phrase_set_title" = "Set pass phrase"; +"create_pass_phrase_lost" = "Lost pass phrase cannot be recovered"; + // Search "search_title" = "Search"; "search_placeholder" = "Search"; diff --git a/FlowCryptTests/FlowCryptCoreTests.swift b/FlowCryptTests/FlowCryptCoreTests.swift index 8478d4c3f..1eaba1116 100644 --- a/FlowCryptTests/FlowCryptCoreTests.swift +++ b/FlowCryptTests/FlowCryptCoreTests.swift @@ -15,11 +15,12 @@ class FlowCryptCoreTests: XCTestCase { super.setUp() // DispatchQueue.promises = .global() // this helps prevent Promise deadlocks - but currently Promises are not in use by tests core = Core.shared - core.startInBackgroundIfNotAlreadyRunning() - do { - try core.blockUntilReadyOrThrow() - } catch { - XCTFail("Core did not get ready in time") + core.startInBackgroundIfNotAlreadyRunning() { [weak self] in + do { + try self?.core.blockUntilReadyOrThrow() + } catch { + XCTFail("Core did not get ready in time") + } } } @@ -67,13 +68,13 @@ class FlowCryptCoreTests: XCTestCase { XCTAssertNil(k0.private) XCTAssertNil(k0.isFullyDecrypted) XCTAssertNil(k0.isFullyEncrypted) - XCTAssertEqual(k0.ids[0].longid, TestData.k0.longid) + XCTAssertEqual(k0.longid, TestData.k0.longid) // k1 is private let k1 = r.keyDetails[1] XCTAssertNotNil(k1.private) XCTAssertEqual(k1.isFullyDecrypted, false) XCTAssertEqual(k1.isFullyEncrypted, true) - XCTAssertEqual(k1.ids[0].longid, TestData.k1.longid) + XCTAssertEqual(k1.longid, TestData.k1.longid) // todo - could test user ids } diff --git a/FlowCryptTests/Functionallity/PGP/KeyMethodsTest.swift b/FlowCryptTests/Functionallity/PGP/KeyMethodsTest.swift new file mode 100644 index 000000000..b362e5af1 --- /dev/null +++ b/FlowCryptTests/Functionallity/PGP/KeyMethodsTest.swift @@ -0,0 +1,113 @@ +// +// KeyMethodsTest.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 20.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import XCTest + +class KeyMethodsTest: XCTestCase { + + var sut: KeyMethods! + var decrypter: MockKeyDecrypter! + var passPhrase = "Some long frase" + + override func setUp() { + decrypter = MockKeyDecrypter() + sut = KeyMethods(decrypter: decrypter) + } + + func testEmptyParsingKey() { + let emptyKeys: [KeyDetails] = [] + let result = sut.filterByPassPhraseMatch(keys: emptyKeys, passPhrase: passPhrase) + + XCTAssertTrue(result.isEmpty) + } + + func testNoPrivateKey() { + // private part = nil + let keys = [ + KeyDetails( + public: "Public part", + private: nil, + isFullyDecrypted: false, + isFullyEncrypted: false, + ids: [], + created: 1, + users: [], + algo: nil + ), + KeyDetails( + public: "Public part2", + private: nil, + isFullyDecrypted: false, + isFullyEncrypted: false, + ids: [], + created: 1, + users: [], + algo: nil + ) + ] + let result = sut.filterByPassPhraseMatch(keys: keys, passPhrase: passPhrase) + + XCTAssertTrue(result.isEmpty) + } + + func testCantDecryptKey() { + decrypter.result = .failure(.some) + let result = sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) + XCTAssertTrue(result.isEmpty) + } + + func testNoDecryptedKey() { + decrypter.result = .success(CoreRes.DecryptKey(decryptedKey: nil)) + let result = sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) + XCTAssertTrue(result.isEmpty) + } + + func testSuccessDecryption() { + decrypter.result = .success(CoreRes.DecryptKey(decryptedKey: "some key")) + let result = sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) + XCTAssertTrue(result.isNotEmpty) + } +} + +extension KeyMethodsTest { + var validKeys: [KeyDetails] {[ + KeyDetails( + public: "Public part", + private: "private 1", + isFullyDecrypted: false, + isFullyEncrypted: false, + ids: [], + created: 1, + users: [], + algo: nil + ), + KeyDetails( + public: "Public part2", + private: "private 2", + isFullyDecrypted: false, + isFullyEncrypted: false, + ids: [], + created: 1, + users: [], + algo: nil + ) + ]} +} + +class MockKeyDecrypter: KeyDecrypter { + var result: Result = .success(CoreRes.DecryptKey(decryptedKey: "decrypted")) + + func decryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.DecryptKey { + switch result { + case .success(let key): + return key + case .failure(let error): + throw error + } + } +} diff --git a/FlowCryptTests/Functionallity/Services/Backup Services/BackupServiceMock.swift b/FlowCryptTests/Functionallity/Services/Backup Services/BackupServiceMock.swift new file mode 100644 index 000000000..b681506ec --- /dev/null +++ b/FlowCryptTests/Functionallity/Services/Backup Services/BackupServiceMock.swift @@ -0,0 +1,27 @@ +// +// BackupServiceMock.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 31.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises + +final class BackupServiceMock: BackupServiceType { + var fetchBackupsResult: Result<[KeyDetails], Error> = .success([]) + func fetchBackupsFromInbox(for userId: UserId) -> Promise<[KeyDetails]> { + Promise<[KeyDetails]>.resolveAfter(with: fetchBackupsResult) + } + + var backupToInboxResult: Result = .success(()) + func backupToInbox(keys: [KeyDetails], for userId: UserId) -> Promise { + Promise.resolveAfter(with: backupToInboxResult) + } + + var isBackupAsFile = false + func backupAsFile(keys: [KeyDetails], for viewController: UIViewController) { + isBackupAsFile = true + } +} diff --git a/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EmailProviderMock.swift b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EmailProviderMock.swift new file mode 100644 index 000000000..28f7a1849 --- /dev/null +++ b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EmailProviderMock.swift @@ -0,0 +1,13 @@ +// +// EmailProviderMock.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +class EmailProviderMock: EmailProviderType { + var email: String? = "test@gmail.com" +} diff --git a/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EncryptedPassPhraseStorageMock.swift b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EncryptedPassPhraseStorageMock.swift new file mode 100644 index 000000000..ea0404af0 --- /dev/null +++ b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/EncryptedPassPhraseStorageMock.swift @@ -0,0 +1,36 @@ +// +// EncryptedPassPhraseStorageMock.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +class EncryptedPassPhraseStorageMock: EncryptedPassPhraseStorage { + func addPassPhrase(object: PassPhraseObject) { + + } + + func updatePassPhrase(object: PassPhraseObject) { + + } + + var getPassPhrasesResult: () -> ([PassPhraseObject]) = { + [PassPhraseObject(longid: "longid", value: "value")] + } + func getPassPhrases() -> [PassPhraseObject] { + getPassPhrasesResult() + } + + var isRemovePassPhraseResult: ((PassPhraseObject) -> ())? + func removePassPhrase(object: PassPhraseObject) { + isRemovePassPhraseResult?(object) + } + + var keysInfoResult: () -> ([KeyInfo]) = { [] } + func keysInfo() -> [KeyInfo] { + keysInfoResult() + } +} diff --git a/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/LocalPassPhraseStorageMock.swift b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/LocalPassPhraseStorageMock.swift new file mode 100644 index 000000000..9910c6417 --- /dev/null +++ b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/LocalPassPhraseStorageMock.swift @@ -0,0 +1,27 @@ +// +// LocalPassPhraseStorageMock.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation + +class LocalPassPhraseStorageMock: InMemoryPassPhraseStorageType { + var passPhrases: Set = [] + + var isSaveCalled = false + func save(passPhrase: InMemoryPassPhrase) { + isSaveCalled = true + passPhrases.insert(passPhrase) + + print("^^ \(passPhrases)") + } + + func removePassPhrases(with objects: [InMemoryPassPhrase]) { + objects.forEach { + passPhrases.remove($0) + } + } +} diff --git a/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift new file mode 100644 index 000000000..f707bbd8d --- /dev/null +++ b/FlowCryptTests/Functionallity/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift @@ -0,0 +1,219 @@ +// +// PassPhraseStorageTests.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 07.06.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import XCTest + +class PassPhraseStorageTests: XCTestCase { + + var sut: PassPhraseStorage! + var storage: EncryptedPassPhraseStorageMock! + var emailProvider: EmailProviderMock! + var localStorage: LocalPassPhraseStorageMock! + + override func setUp() { + storage = EncryptedPassPhraseStorageMock() + emailProvider = EmailProviderMock() + localStorage = LocalPassPhraseStorageMock() + + sut = PassPhraseStorage( + storage: storage, + localStorage: localStorage, + timeoutInSeconds: 2, + emailProvider: emailProvider, + isHours: false + ) + } + + func testGetPassPhrasesWhenEmpty() { + // no pass phrases in storage + storage.getPassPhrasesResult = { [] } + // no pass phrases in localStorage + localStorage.passPhrases = [] + + let result = sut.getPassPhrases() + + XCTAssertTrue(result.isEmpty) + } + + func testGetValidPassPhraseFromStorage() { + let passPhrase1 = PassPhraseObject( + longid: "A123", + value: "some" + ) + let passPhrase2 = PassPhraseObject( + longid: "A123", + value: "some" + ) + + storage.getPassPhrasesResult = { [passPhrase1] } + // no pass phrases in localStorage + localStorage.passPhrases = [] + + var result = sut.getPassPhrases() + + XCTAssertTrue(result.count == 1) + + storage.getPassPhrasesResult = { + [passPhrase1, passPhrase2] + } + + result = sut.getPassPhrases() + + XCTAssertTrue(result.count == 2) + } + + func testGetValidPassPhraseInLocalStorage() { + storage.getPassPhrasesResult = { [] } + + let savedDate = Date() + let localPassPhrase = InMemoryPassPhrase( + passPhrase: PassPhrase( + value: "value", + longid: "longid"), + date: savedDate + ) + localStorage.passPhrases = [localPassPhrase] + + // current timeout = 2 + sleep(1) + + let result = sut.getPassPhrases() + XCTAssertTrue(result.isNotEmpty) + } + + func testGetExpiredPassPhraseInLocalStorage() { + storage.getPassPhrasesResult = { [] } + + let savedDate = Date() + let localPassPhrase = InMemoryPassPhrase( + passPhrase: PassPhrase( + value: "value", + longid: "longid"), + date: savedDate + ) + localStorage.passPhrases = [localPassPhrase] + + // current timeout = 2 + sleep(3) + + let result = sut.getPassPhrases() + XCTAssertTrue(result.isEmpty) + } + + func testBothStorageContainsValidPassPhrase() { + let passPhrase1 = PassPhraseObject( + longid: "A123", + value: "some" + ) + let passPhrase2 = PassPhraseObject( + longid: "A123", + value: "some" + ) + + storage.getPassPhrasesResult = { + [passPhrase1, passPhrase2] + } + + let savedDate = Date() + let localPassPhrase = InMemoryPassPhrase( + passPhrase: PassPhrase( + value: "value", + longid: "longid"), + date: savedDate + ) + + localStorage.passPhrases = [localPassPhrase] + + let result = sut.getPassPhrases() + XCTAssertTrue(result.count == 3) + } + + func testSavePassPhraseInStorage() { + let passPhraseToSave = PassPhrase(value: "pass", longid: "12345") + + let expectation = XCTestExpectation() + expectation.expectedFulfillmentCount = 1 + expectation.isInverted = true + + // encrypted storage contains pass phrase which should be saved locally + storage.getPassPhrasesResult = { + [ + PassPhraseObject(longid: "12345", value: "pass") + ] + } + + + // encrypted storage should not contains pass phrase which user decide to save locally + storage.isRemovePassPhraseResult = { passPhraseToRemove in + if passPhraseToRemove.longid == "12345" { + expectation.fulfill() + } + } + + sut.savePassPhrase(with: passPhraseToSave, inStorage: true) + + XCTAssertFalse(localStorage.isSaveCalled) + + wait(for: [expectation], timeout: 0.1, enforceOrder: false) + } + + func testSavePassPhraseInStorageWithoutAnyPassPhrases() { + let passPhraseToSave = PassPhrase(value: "pass", longid: "12345") + + let expectation = XCTestExpectation() + expectation.isInverted = true + + // encrypted storage is empty + storage.getPassPhrasesResult = { [ ] } + + storage.isRemovePassPhraseResult = { _ in + expectation.fulfill() + } + + sut.savePassPhrase(with: passPhraseToSave, inStorage: true) + + XCTAssertFalse(localStorage.isSaveCalled) + + wait(for: [expectation], timeout: 0.1, enforceOrder: false) + } + + func testSavePassPhraseInMemory() { + let passPhraseToSave = PassPhrase(value: "pass", longid: "12345") + sut.savePassPhrase(with: passPhraseToSave, inStorage: false) + + XCTAssertTrue(localStorage.isSaveCalled) + } +} + +extension KeyInfo { + // extend with more parameters if needed + static func mock( + with publicValue: String, + account: String = "", + longid: String + ) -> KeyInfo { + let key = try! KeyInfo( + KeyDetails( + public: publicValue, + private: "private", + isFullyDecrypted: true, + isFullyEncrypted: true, + ids: [ + KeyId(shortid: "shortId", longid: "longid", fingerprint: "fingerprint", keywords: "keywords") + ], + created: 1234, + users: [], + algo: nil + ), + source: .backup + ) + key.account = account + key.longid = longid + return key + } +} diff --git a/FlowCryptTests/PromiseExtensions.swift b/FlowCryptTests/PromiseExtensions.swift new file mode 100644 index 000000000..f4a7dce81 --- /dev/null +++ b/FlowCryptTests/PromiseExtensions.swift @@ -0,0 +1,29 @@ +// +// PromiseExtensions.swift +// FlowCryptTests +// +// Created by Anton Kharchevskyi on 31.05.2021. +// Copyright © 2021 FlowCrypt Limited. All rights reserved. +// + +import Foundation +import Promises + +extension Promise { + static func resolveAfter(timeout: TimeInterval = 5, with result: Result) -> Promise { + Promise { resolve, reject in + DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { + switch result { + case .success(let value): + resolve(value) + case .failure(let error): + reject(error) + } + } + } + } +} + +enum MockError: Error { + case some +} diff --git a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift index 489341551..346bceba1 100644 --- a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift @@ -9,6 +9,22 @@ import AsyncDisplayKit public final class ButtonCellNode: CellNode { + public struct Input { + let title: NSAttributedString + let insets: UIEdgeInsets + let color: UIColor? + + public init( + title: NSAttributedString, + insets: UIEdgeInsets, + color: UIColor? = nil + ) { + self.title = title + self.insets = insets + self.color = color + } + } + private var onTap: (() -> Void)? public lazy var button = ButtonNode { [weak self] in self?.onTap?() @@ -25,7 +41,19 @@ public final class ButtonCellNode: CellNode { .withAlphaComponent(alpha) } } + + public init(input: Input, action: (() -> Void)?) { + onTap = action + self.insets = input.insets + buttonColor = input.color + super.init() + button.cornerRadius = 5 + button.backgroundColor = input.color ?? .main + button.style.preferredSize.height = 50 + button.setAttributedTitle(input.title, for: .normal) + } + @available(*, deprecated, message: "Deprecated. Use init(input: Input)") public init(title: NSAttributedString, insets: UIEdgeInsets, color: UIColor? = nil, action: (() -> Void)?) { onTap = action self.insets = insets diff --git a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift index d165155cd..34e0668a9 100644 --- a/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift +++ b/FlowCryptUI/Cell Nodes/CheckBoxTextNode.swift @@ -55,22 +55,11 @@ public final class CheckBoxTextNode: CellNode { checkBox.style.preferredSize = input.preferredSize if input.subtitle != nil { -// textNode.style.flexGrow = 1 -// textNode.style.flexShrink = 1 -// subtitleTextNode.style.flexGrow = 1 -// subtitleTextNode.style.flexShrink = 1 - let textStack = ASStackLayoutSpec() textStack.direction = .vertical textStack.style.flexGrow = 1 textStack.style.flexShrink = 1 textStack.children = [textNode, subtitleTextNode] -// direction: .vertical, -// spacing: 8, -// justifyContent: .start, -// alignItems: .baselineFirst, -// children: -// ) let stack = ASStackLayoutSpec( direction: .horizontal, diff --git a/FlowCryptUI/Cell Nodes/TextCellNode.swift b/FlowCryptUI/Cell Nodes/TextCellNode.swift index 4291c9b73..40c752611 100644 --- a/FlowCryptUI/Cell Nodes/TextCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextCellNode.swift @@ -30,7 +30,7 @@ public final class TextCellNode: CellNode { } private let spinner = SpinnerNode() - private let text = ASTextNode2() + private let textNode = ASTextNode2() private let size: CGSize private let withSpinner: Bool @@ -38,8 +38,8 @@ public final class TextCellNode: CellNode { withSpinner = input.withSpinner size = input.size super.init() - addSubnode(text) - text.attributedText = NSAttributedString.text(from: input.title, style: .medium(16), color: .lightGray) + addSubnode(textNode) + textNode.attributedText = NSAttributedString.text(from: input.title, style: .medium(16), color: .lightGray) if input.withSpinner { addSubnode(spinner) } @@ -48,13 +48,15 @@ public final class TextCellNode: CellNode { public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { let spec = ASStackLayoutSpec( - direction: .horizontal, + direction: .vertical, spacing: 16, justifyContent: .center, alignItems: .center, - children: withSpinner ? [text, spinner] : [text] + children: withSpinner + ? [textNode, spinner] + : [textNode] ) - spec.style.preferredSize = size + return spec } } @@ -67,10 +69,7 @@ final class SpinnerNode: ASDisplayNode { override init() { super.init() setViewBlock { - switch UITraitCollection.current.userInterfaceStyle { - case .dark: return UIActivityIndicatorView(style: .white) - default: return UIActivityIndicatorView(style: .gray) - } + UIActivityIndicatorView(style: .medium) } style.preferredSize = CGSize(width: 20.0, height: 20.0) } diff --git a/FlowCryptUI/Nodes/TableNode.swift b/FlowCryptUI/Nodes/TableNode.swift index 416df34fa..9c46f56c9 100644 --- a/FlowCryptUI/Nodes/TableNode.swift +++ b/FlowCryptUI/Nodes/TableNode.swift @@ -38,3 +38,26 @@ public final class TableNode: ASTableNode { } } } + +public extension UIViewController { + var safeAreaWindowInsets: UIEdgeInsets { + UIApplication.shared.keyWindow?.safeAreaInsets ?? .zero + } +} + +public extension UIViewController { + /// should be called on a main thread + func visibleSize(for tableNode: ASTableNode) -> CGSize { + let height = tableNode.frame.size.height + - (navigationController?.navigationBar.frame.size.height ?? 0.0) + - safeAreaWindowInsets.top + - safeAreaWindowInsets.bottom + + let size = CGSize( + width: tableNode.frame.size.width, + height: max(height, 0) + ) + + return size + } +} diff --git a/Scripts/format.sh b/Scripts/format.sh index 44475d2f5..6c64824c0 100755 --- a/Scripts/format.sh +++ b/Scripts/format.sh @@ -1,7 +1,11 @@ #!/bin/bash -# Do not run format on CI -if "$CI"; then +set -euxo pipefail # debug + fail when any command fails + +if [ -z "${CI:-}" ]; then + echo "Not on CI - running SwiftFormat" +else + echo "On CI - skipping SwiftFormat" exit 0 fi @@ -10,12 +14,34 @@ if which swiftformat >/dev/null; then echo "Start formatting" # swiftlint autocorrect --path . swiftformat "FlowCrypt" \ - --rules trailingSpace, blankLinesAtEndOfScope, consecutiveBlankLines, consecutiveSpaces, \ - duplicateImports, initCoderUnavailable, isEmpty, leadingDelimiters, preferKeyPath, redundantBreak, \ - redundantExtensionACL, redundantFileprivate, redundantGet, redundantLet, redundantLetError, \ - redundantNilInit, redundantParens, redundantPattern, redundantReturn, redundantVoidReturnType, semicolons, \ - sortedImports, spaceAroundBraces, spaceAroundBrackets, spaceAroundGenerics, spaceInsideBraces, spaceInsideGenerics, \ - strongifiedSelf, trailingClosures, void + --rules trailingSpace \ + --rules blankLinesAtEndOfScope \ + --rules consecutiveBlankLines \ + --rules consecutiveSpaces \ + --rules duplicateImports \ + --rules isEmpty \ + --rules leadingDelimiters \ + --rules redundantBreak \ + --rules redundantExtensionACL \ + --rules redundantFileprivate \ + --rules redundantGet \ + --rules redundantLet \ + --rules redundantLetError \ + --rules redundantNilInit\ + --rules redundantParens \ + --rules redundantPattern \ + --rules redundantVoidReturnType \ + --rules semicolons \ + --rules sortedImports \ + --rules spaceAroundBraces \ + --rules spaceAroundBrackets \ + --rules spaceAroundGenerics \ + --rules spaceInsideBraces \ + --rules spaceInsideGenerics \ + --rules strongifiedSelf \ + --rules trailingClosures \ + --rules void + else echo "warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat" brew install swiftformat @@ -23,3 +49,8 @@ else fi ################### RULES https://github.com/nicklockwood/SwiftFormat/blob/master/Rules.md +# ASCellNodeBlock - removed due to Opening Brace Spacing Violation when dealing with ASCellNodeBlock + +# following rules were not available on swiftformat version 0.40.12 +# --rules preferKeyPath \ +# --rules initCoderUnavailable \