From af41297e7314669a652bf3a45593231bc8c98617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Guly=C3=A1s?= Date: Tue, 9 Jul 2024 11:26:41 +0200 Subject: [PATCH] NEVISACCESSAPP-5981: Added Password authenticator capabilities --- NevisExampleApp.xcodeproj/project.pbxproj | 178 +++--- .../xcschemes/NevisExampleApp.xcscheme | 2 +- .../Dependency Provider/AppAssembly.swift | 62 ++- .../Model/AppConfiguration.swift | 6 + .../Configuration/Model/Environment.swift | 4 +- NevisExampleApp/Common/Error/AppError.swift | 16 +- .../Extensions/Authenticator+Extension.swift | 6 +- .../Interaction/AccountSelectorImpl.swift | 12 +- ...henticationAuthenticatorSelectorImpl.swift | 63 --- .../AuthenticatorSelectorImpl.swift | 86 +++ .../Password/PasswordChangerImpl.swift | 57 ++ .../Password/PasswordEnrollerImpl.swift | 56 ++ .../Password/PasswordUserVerifierImpl.swift | 52 ++ .../{ => Pin}/PinChangerImpl.swift | 0 .../{ => Pin}/PinEnrollerImpl.swift | 0 .../{ => Pin}/PinUserVerifierImpl.swift | 0 ...egistrationAuthenticatorSelectorImpl.swift | 59 -- .../Domain/Model/AuthenticatorItem.swift | 2 +- .../Domain/Model/ChangePasswordResponse.swift | 39 ++ .../Domain/Model/EnrollPasswordResponse.swift | 33 ++ NevisExampleApp/Domain/Model/Operation.swift | 3 + .../Domain/Model/VerifyPasswordResponse.swift | 39 ++ .../AuthCloudApiRegistrationUseCaseImpl.swift | 7 + .../Use Case/ChangePasswordUseCase.swift | 17 + .../Use Case/ChangePasswordUseCaseImpl.swift | 69 +++ .../DeleteAuthenticatorsUseCaseImpl.swift | 4 +- .../InBandAuthenticationUseCaseImpl.swift | 9 + .../OutOfBandOperationUseCaseImpl.swift | 14 + .../Use Case/RegistrationUseCaseImpl.swift | 7 + .../Validators/AuthenticatorValidator.swift | 26 + .../AuthenticatorValidatorImpl.swift | 82 +++ .../Coordinator/App/AppCoordinator.swift | 4 +- .../Coordinator/App/AppCoordinatorImpl.swift | 22 +- .../CredentialScreen.swift} | 83 +-- .../Credential/CredentialViewModel.swift | 527 ++++++++++++++++++ .../CredentialProtectionInformation.swift} | 8 +- .../Screens/Home/HomeScreen.swift | 13 + .../Screens/Home/HomeViewModel.swift | 79 ++- .../Screens/Pin/PinViewModel.swift | 362 ------------ .../Authenticator+LocalizedExtension.swift | 12 +- .../AuthenticatorAaid+Extension.swift | 29 + .../Extensions/BusinessError+Extension.swift | 8 +- .../Extensions/Operation+Extension.swift | 22 +- ...henticatorProtectionStatus+Extension.swift | 37 ++ ...henticatorProtectionStatus+Extension.swift | 14 +- .../Utility/Localization/Strings.swift | 248 ++++++--- .../ResponseObserverImpl.swift | 23 +- .../Presentation/Utility/UI/Label/Style.swift | 20 +- .../Utility/Validation/ValidationResult.swift | 8 +- .../Resources/ConfigAuthenticationCloud.plist | 8 + .../Resources/ConfigIdentitySuite.plist | 8 + .../Resources/en.lproj/Localizable.strings | 27 +- 52 files changed, 1785 insertions(+), 787 deletions(-) delete mode 100644 NevisExampleApp/Domain/Interaction/AuthenticationAuthenticatorSelectorImpl.swift create mode 100644 NevisExampleApp/Domain/Interaction/AuthenticatorSelectorImpl.swift create mode 100644 NevisExampleApp/Domain/Interaction/Password/PasswordChangerImpl.swift create mode 100644 NevisExampleApp/Domain/Interaction/Password/PasswordEnrollerImpl.swift create mode 100644 NevisExampleApp/Domain/Interaction/Password/PasswordUserVerifierImpl.swift rename NevisExampleApp/Domain/Interaction/{ => Pin}/PinChangerImpl.swift (100%) rename NevisExampleApp/Domain/Interaction/{ => Pin}/PinEnrollerImpl.swift (100%) rename NevisExampleApp/Domain/Interaction/{ => Pin}/PinUserVerifierImpl.swift (100%) delete mode 100644 NevisExampleApp/Domain/Interaction/RegistrationAuthenticatorSelectorImpl.swift create mode 100644 NevisExampleApp/Domain/Model/ChangePasswordResponse.swift create mode 100644 NevisExampleApp/Domain/Model/EnrollPasswordResponse.swift create mode 100644 NevisExampleApp/Domain/Model/VerifyPasswordResponse.swift create mode 100644 NevisExampleApp/Domain/Use Case/ChangePasswordUseCase.swift create mode 100644 NevisExampleApp/Domain/Use Case/ChangePasswordUseCaseImpl.swift create mode 100644 NevisExampleApp/Domain/Validators/AuthenticatorValidator.swift create mode 100644 NevisExampleApp/Domain/Validators/AuthenticatorValidatorImpl.swift rename NevisExampleApp/Presentation/Screens/{Pin/PinScreen.swift => Credential/CredentialScreen.swift} (63%) create mode 100644 NevisExampleApp/Presentation/Screens/Credential/CredentialViewModel.swift rename NevisExampleApp/Presentation/Screens/{Pin/Model/PinProtectionInformation.swift => Credential/Model/CredentialProtectionInformation.swift} (68%) delete mode 100644 NevisExampleApp/Presentation/Screens/Pin/PinViewModel.swift create mode 100644 NevisExampleApp/Presentation/Utility/Extensions/AuthenticatorAaid+Extension.swift create mode 100644 NevisExampleApp/Presentation/Utility/Extensions/PasswordAuthenticatorProtectionStatus+Extension.swift diff --git a/NevisExampleApp.xcodeproj/project.pbxproj b/NevisExampleApp.xcodeproj/project.pbxproj index 84ede6a..ff262e5 100644 --- a/NevisExampleApp.xcodeproj/project.pbxproj +++ b/NevisExampleApp.xcodeproj/project.pbxproj @@ -7,6 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + 0698AA542C3BEB20001B4000 /* PasswordAuthenticatorProtectionStatus+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA532C3BEB20001B4000 /* PasswordAuthenticatorProtectionStatus+Extension.swift */; }; + 0698AA562C3BF8E2001B4000 /* AuthenticatorAaid+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA552C3BF8E2001B4000 /* AuthenticatorAaid+Extension.swift */; }; + 0698AA582C3BFD64001B4000 /* AuthenticatorValidatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA572C3BFD64001B4000 /* AuthenticatorValidatorImpl.swift */; }; + 0698AA5A2C3BFED9001B4000 /* AuthenticatorValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA592C3BFED9001B4000 /* AuthenticatorValidator.swift */; }; + 0698AA5E2C3D10A5001B4000 /* PasswordChangerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA5D2C3D10A5001B4000 /* PasswordChangerImpl.swift */; }; + 0698AA602C3D116C001B4000 /* ChangePasswordResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA5F2C3D116C001B4000 /* ChangePasswordResponse.swift */; }; + 0698AA622C3D11DD001B4000 /* PasswordEnrollerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA612C3D11DD001B4000 /* PasswordEnrollerImpl.swift */; }; + 0698AA642C3D12A7001B4000 /* EnrollPasswordResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA632C3D12A7001B4000 /* EnrollPasswordResponse.swift */; }; + 0698AA662C3D12EA001B4000 /* VerifyPasswordResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA652C3D12EA001B4000 /* VerifyPasswordResponse.swift */; }; + 0698AA682C3D1345001B4000 /* PasswordUserVerifierImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA672C3D1345001B4000 /* PasswordUserVerifierImpl.swift */; }; + 0698AA6A2C3D1DF8001B4000 /* ChangePasswordUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA692C3D1DF8001B4000 /* ChangePasswordUseCase.swift */; }; + 0698AA6C2C3D1E21001B4000 /* ChangePasswordUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0698AA6B2C3D1E21001B4000 /* ChangePasswordUseCaseImpl.swift */; }; 3810439F268B0BBA0095D449 /* NavigationParameterizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3810439E268B0BBA0095D449 /* NavigationParameterizable.swift */; }; 381043A2268B2C940095D449 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381043A1268B2C940095D449 /* ActivityIndicator.swift */; }; 381043A6268B33620095D449 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381043A5268B33620095D449 /* UIViewController+Extension.swift */; }; @@ -65,8 +77,7 @@ 3872BFD12A29E8D40041C594 /* AuthenticatorItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3872BFD02A29E8D40041C594 /* AuthenticatorItem.swift */; }; 3872BFD32A29E98E0041C594 /* Authenticator+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3872BFD22A29E98E0041C594 /* Authenticator+Extension.swift */; }; 3875ADCC291BF6D100939D16 /* PinUserVerifierImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC2291BF6D100939D16 /* PinUserVerifierImpl.swift */; }; - 3875ADCD291BF6D100939D16 /* RegistrationAuthenticatorSelectorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC3291BF6D100939D16 /* RegistrationAuthenticatorSelectorImpl.swift */; }; - 3875ADD0291BF6D100939D16 /* AuthenticationAuthenticatorSelectorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC7291BF6D100939D16 /* AuthenticationAuthenticatorSelectorImpl.swift */; }; + 3875ADD0291BF6D100939D16 /* AuthenticatorSelectorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC7291BF6D100939D16 /* AuthenticatorSelectorImpl.swift */; }; 3875ADD1291BF6D100939D16 /* PinChangerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC8291BF6D100939D16 /* PinChangerImpl.swift */; }; 3875ADD2291BF6D100939D16 /* AccountSelectorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADC9291BF6D100939D16 /* AccountSelectorImpl.swift */; }; 3875ADD3291BF6D100939D16 /* PinEnrollerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADCA291BF6D100939D16 /* PinEnrollerImpl.swift */; }; @@ -75,10 +86,10 @@ 3875ADD9291BF73000939D16 /* AccountValidatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3875ADD7291BF73000939D16 /* AccountValidatorImpl.swift */; }; 38821E6E292500ED0009522A /* ConfigIdentitySuite.plist in Resources */ = {isa = PBXBuildFile; fileRef = 38821E6D292500ED0009522A /* ConfigIdentitySuite.plist */; }; 38821E70292502670009522A /* ConfigAuthenticationCloud.plist in Resources */ = {isa = PBXBuildFile; fileRef = 38821E6F292502670009522A /* ConfigAuthenticationCloud.plist */; }; - 388844DC2695C1E500CCFB00 /* PinScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388844DB2695C1E500CCFB00 /* PinScreen.swift */; }; - 388844DE2695C1F200CCFB00 /* PinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388844DD2695C1F200CCFB00 /* PinViewModel.swift */; }; + 388844DC2695C1E500CCFB00 /* CredentialScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388844DB2695C1E500CCFB00 /* CredentialScreen.swift */; }; + 388844DE2695C1F200CCFB00 /* CredentialViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388844DD2695C1F200CCFB00 /* CredentialViewModel.swift */; }; 388D0A622968236400EBFC1D /* Dictionary+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388D0A612968236400EBFC1D /* Dictionary+Extension.swift */; }; - 38A0C0ED26E0C7F700898B6A /* PinProtectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0C0EC26E0C7F700898B6A /* PinProtectionInformation.swift */; }; + 38A0C0ED26E0C7F700898B6A /* CredentialProtectionInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A0C0EC26E0C7F700898B6A /* CredentialProtectionInformation.swift */; }; 38ABBB4D2906A151006B19B9 /* ChangeDeviceInformationUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38ABBB4C2906A151006B19B9 /* ChangeDeviceInformationUseCase.swift */; }; 38ABBB4F2906A158006B19B9 /* ChangeDeviceInformationUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38ABBB4E2906A158006B19B9 /* ChangeDeviceInformationUseCaseImpl.swift */; }; 38ABBB522906AD41006B19B9 /* ChangeDeviceInformationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38ABBB512906AD41006B19B9 /* ChangeDeviceInformationScreen.swift */; }; @@ -164,11 +175,21 @@ 38FD7AD128FEEAD500F64099 /* DecodePayloadUseCaseImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FD7AD028FEEAD500F64099 /* DecodePayloadUseCaseImpl.swift */; }; 38FE58C4269344340014DF93 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE58C3269344340014DF93 /* HomeScreen.swift */; }; 38FE58C62693443C0014DF93 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE58C52693443C0014DF93 /* HomeViewModel.swift */; }; - 6C37659B6DE0DDE851A056F3 /* Pods_NevisExampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E792052CC61AC2811699E15F /* Pods_NevisExampleApp.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 2E256C9B57CBCD476D68E544 /* Pods-NevisExampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NevisExampleApp.release.xcconfig"; path = "Target Support Files/Pods-NevisExampleApp/Pods-NevisExampleApp.release.xcconfig"; sourceTree = ""; }; + 0698AA532C3BEB20001B4000 /* PasswordAuthenticatorProtectionStatus+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PasswordAuthenticatorProtectionStatus+Extension.swift"; sourceTree = ""; }; + 0698AA552C3BF8E2001B4000 /* AuthenticatorAaid+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthenticatorAaid+Extension.swift"; sourceTree = ""; }; + 0698AA572C3BFD64001B4000 /* AuthenticatorValidatorImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorValidatorImpl.swift; sourceTree = ""; }; + 0698AA592C3BFED9001B4000 /* AuthenticatorValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorValidator.swift; sourceTree = ""; }; + 0698AA5D2C3D10A5001B4000 /* PasswordChangerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordChangerImpl.swift; sourceTree = ""; }; + 0698AA5F2C3D116C001B4000 /* ChangePasswordResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordResponse.swift; sourceTree = ""; }; + 0698AA612C3D11DD001B4000 /* PasswordEnrollerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordEnrollerImpl.swift; sourceTree = ""; }; + 0698AA632C3D12A7001B4000 /* EnrollPasswordResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrollPasswordResponse.swift; sourceTree = ""; }; + 0698AA652C3D12EA001B4000 /* VerifyPasswordResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyPasswordResponse.swift; sourceTree = ""; }; + 0698AA672C3D1345001B4000 /* PasswordUserVerifierImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordUserVerifierImpl.swift; sourceTree = ""; }; + 0698AA692C3D1DF8001B4000 /* ChangePasswordUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordUseCase.swift; sourceTree = ""; }; + 0698AA6B2C3D1E21001B4000 /* ChangePasswordUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordUseCaseImpl.swift; sourceTree = ""; }; 3810439E268B0BBA0095D449 /* NavigationParameterizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationParameterizable.swift; sourceTree = ""; }; 381043A1268B2C940095D449 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 381043A5268B33620095D449 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; @@ -230,8 +251,7 @@ 3872BFD02A29E8D40041C594 /* AuthenticatorItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatorItem.swift; sourceTree = ""; }; 3872BFD22A29E98E0041C594 /* Authenticator+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Authenticator+Extension.swift"; sourceTree = ""; }; 3875ADC2291BF6D100939D16 /* PinUserVerifierImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinUserVerifierImpl.swift; sourceTree = ""; }; - 3875ADC3291BF6D100939D16 /* RegistrationAuthenticatorSelectorImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationAuthenticatorSelectorImpl.swift; sourceTree = ""; }; - 3875ADC7291BF6D100939D16 /* AuthenticationAuthenticatorSelectorImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationAuthenticatorSelectorImpl.swift; sourceTree = ""; }; + 3875ADC7291BF6D100939D16 /* AuthenticatorSelectorImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticatorSelectorImpl.swift; sourceTree = ""; }; 3875ADC8291BF6D100939D16 /* PinChangerImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinChangerImpl.swift; sourceTree = ""; }; 3875ADC9291BF6D100939D16 /* AccountSelectorImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSelectorImpl.swift; sourceTree = ""; }; 3875ADCA291BF6D100939D16 /* PinEnrollerImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinEnrollerImpl.swift; sourceTree = ""; }; @@ -240,10 +260,10 @@ 3875ADD7291BF73000939D16 /* AccountValidatorImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountValidatorImpl.swift; sourceTree = ""; }; 38821E6D292500ED0009522A /* ConfigIdentitySuite.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ConfigIdentitySuite.plist; sourceTree = ""; }; 38821E6F292502670009522A /* ConfigAuthenticationCloud.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = ConfigAuthenticationCloud.plist; sourceTree = ""; }; - 388844DB2695C1E500CCFB00 /* PinScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinScreen.swift; sourceTree = ""; }; - 388844DD2695C1F200CCFB00 /* PinViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinViewModel.swift; sourceTree = ""; }; + 388844DB2695C1E500CCFB00 /* CredentialScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialScreen.swift; sourceTree = ""; }; + 388844DD2695C1F200CCFB00 /* CredentialViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialViewModel.swift; sourceTree = ""; }; 388D0A612968236400EBFC1D /* Dictionary+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Extension.swift"; sourceTree = ""; }; - 38A0C0EC26E0C7F700898B6A /* PinProtectionInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinProtectionInformation.swift; sourceTree = ""; }; + 38A0C0EC26E0C7F700898B6A /* CredentialProtectionInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialProtectionInformation.swift; sourceTree = ""; }; 38ABBB4C2906A151006B19B9 /* ChangeDeviceInformationUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeDeviceInformationUseCase.swift; sourceTree = ""; }; 38ABBB4E2906A158006B19B9 /* ChangeDeviceInformationUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeDeviceInformationUseCaseImpl.swift; sourceTree = ""; }; 38ABBB512906AD41006B19B9 /* ChangeDeviceInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeDeviceInformationScreen.swift; sourceTree = ""; }; @@ -331,8 +351,6 @@ 38FD7AD028FEEAD500F64099 /* DecodePayloadUseCaseImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodePayloadUseCaseImpl.swift; sourceTree = ""; }; 38FE58C3269344340014DF93 /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; 38FE58C52693443C0014DF93 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; - 62C2371A7532EBAD0A0021FF /* Pods-NevisExampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NevisExampleApp.debug.xcconfig"; path = "Target Support Files/Pods-NevisExampleApp/Pods-NevisExampleApp.debug.xcconfig"; sourceTree = ""; }; - E792052CC61AC2811699E15F /* Pods_NevisExampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NevisExampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -340,13 +358,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6C37659B6DE0DDE851A056F3 /* Pods_NevisExampleApp.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0698AA5B2C3D1064001B4000 /* Pin */ = { + isa = PBXGroup; + children = ( + 3875ADC8291BF6D100939D16 /* PinChangerImpl.swift */, + 3875ADCA291BF6D100939D16 /* PinEnrollerImpl.swift */, + 3875ADC2291BF6D100939D16 /* PinUserVerifierImpl.swift */, + ); + path = Pin; + sourceTree = ""; + }; + 0698AA5C2C3D106B001B4000 /* Password */ = { + isa = PBXGroup; + children = ( + 0698AA5D2C3D10A5001B4000 /* PasswordChangerImpl.swift */, + 0698AA612C3D11DD001B4000 /* PasswordEnrollerImpl.swift */, + 0698AA672C3D1345001B4000 /* PasswordUserVerifierImpl.swift */, + ); + path = Password; + sourceTree = ""; + }; 380E12A626E7500300AF5DB4 /* Deep Link */ = { isa = PBXGroup; children = ( @@ -382,10 +419,12 @@ children = ( 3872BFD02A29E8D40041C594 /* AuthenticatorItem.swift */, 38EB17782902B49600E82236 /* BusinessError.swift */, + 0698AA5F2C3D116C001B4000 /* ChangePasswordResponse.swift */, 3863F3AB2907CAFF0028D26E /* ChangePinResponse.swift */, 38D67ECA26C6AC630068334F /* CompletedResponse.swift */, 38EB177A2902CB8500E82236 /* ConfirmTransactionResponse.swift */, 386FC3272909228F00CB30A7 /* Credentials.swift */, + 0698AA632C3D12A7001B4000 /* EnrollPasswordResponse.swift */, 38F0769A2900129700EFDAF8 /* EnrollPinResponse.swift */, 386DA8FD26CA9B9A00060ADD /* Operation.swift */, 381EC646290AAE790005546B /* OperationError.swift */, @@ -394,6 +433,7 @@ 381D598226C56003006B02DD /* SelectAuthenticatorResponse.swift */, 38C6F35A2BB17778002909BE /* VerifyBiometricResponse.swift */, 38C6F35C2BB178F3002909BE /* VerifyDevicePasscodeResponse.swift */, + 0698AA652C3D12EA001B4000 /* VerifyPasswordResponse.swift */, 38D67EC826C69BCC0068334F /* VerifyPinResponse.swift */, ); path = Model; @@ -413,8 +453,6 @@ children = ( 3833EB1B2685EA03002C5E0C /* NevisExampleApp */, 3833EB1A2685EA03002C5E0C /* Products */, - 5982DE8405D7B002EEB48C3D /* Pods */, - 93491D3F2F2A1133C938BE92 /* Frameworks */, ); sourceTree = ""; }; @@ -488,6 +526,8 @@ 38B085A72907F44F006295D8 /* AuthCloudApiRegistrationUseCaseImpl.swift */, 38ABBB4C2906A151006B19B9 /* ChangeDeviceInformationUseCase.swift */, 38ABBB4E2906A158006B19B9 /* ChangeDeviceInformationUseCaseImpl.swift */, + 0698AA692C3D1DF8001B4000 /* ChangePasswordUseCase.swift */, + 0698AA6B2C3D1E21001B4000 /* ChangePasswordUseCaseImpl.swift */, 3863F3AD2907CE370028D26E /* ChangePinUseCase.swift */, 3863F3AF2907CE3D0028D26E /* ChangePinUseCaseImpl.swift */, 38F0768E28FFD89000EFDAF8 /* CreateDeviceInformationUseCase.swift */, @@ -528,7 +568,7 @@ 38FE58C22693442B0014DF93 /* Home */, 3852F3A4268624470051FF73 /* Launch */, 382D51AC26B194DC00B774CF /* Logging */, - 388844DA2695C1D600CCFB00 /* Pin */, + 388844DA2695C1D600CCFB00 /* Credential */, 38FD7AC728FEDCAA00F64099 /* Qr Scanner */, 38C2D11A290BC20800E41764 /* Result */, 38DF7273290144F300429337 /* Select Account */, @@ -559,8 +599,10 @@ isa = PBXGroup; children = ( 38F0769829000BA400EFDAF8 /* Authenticator+LocalizedExtension.swift */, + 0698AA552C3BF8E2001B4000 /* AuthenticatorAaid+Extension.swift */, 381EC648290ABEDB0005546B /* BusinessError+Extension.swift */, 385B176E26CBC33B005A0B58 /* Operation+Extension.swift */, + 0698AA532C3BEB20001B4000 /* PasswordAuthenticatorProtectionStatus+Extension.swift */, 38EB176929027B9C00E82236 /* PinAuthenticatorProtectionStatus+Extension.swift */, 38C1B384269DB56C00ED5782 /* UINavigationController+Extension.swift */, 3852F39926860C590051FF73 /* UIStackView+Extension.swift */, @@ -735,14 +777,12 @@ 3875ADC1291BF6D100939D16 /* Interaction */ = { isa = PBXGroup; children = ( + 0698AA5C2C3D106B001B4000 /* Password */, + 0698AA5B2C3D1064001B4000 /* Pin */, 3875ADC9291BF6D100939D16 /* AccountSelectorImpl.swift */, - 3875ADC7291BF6D100939D16 /* AuthenticationAuthenticatorSelectorImpl.swift */, + 3875ADC7291BF6D100939D16 /* AuthenticatorSelectorImpl.swift */, 3875ADCB291BF6D100939D16 /* BiometricUserVerifierImpl.swift */, 38E1CBF42A8E3A0E00D4F7EB /* DevicePasscodeUserVerifierImpl.swift */, - 3875ADC8291BF6D100939D16 /* PinChangerImpl.swift */, - 3875ADCA291BF6D100939D16 /* PinEnrollerImpl.swift */, - 3875ADC2291BF6D100939D16 /* PinUserVerifierImpl.swift */, - 3875ADC3291BF6D100939D16 /* RegistrationAuthenticatorSelectorImpl.swift */, ); path = Interaction; sourceTree = ""; @@ -752,24 +792,26 @@ children = ( 3875ADD6291BF73000939D16 /* AccountValidator.swift */, 3875ADD7291BF73000939D16 /* AccountValidatorImpl.swift */, + 0698AA592C3BFED9001B4000 /* AuthenticatorValidator.swift */, + 0698AA572C3BFD64001B4000 /* AuthenticatorValidatorImpl.swift */, ); path = Validators; sourceTree = ""; }; - 388844DA2695C1D600CCFB00 /* Pin */ = { + 388844DA2695C1D600CCFB00 /* Credential */ = { isa = PBXGroup; children = ( 38A0C0E926E0C28A00898B6A /* Model */, - 388844DB2695C1E500CCFB00 /* PinScreen.swift */, - 388844DD2695C1F200CCFB00 /* PinViewModel.swift */, + 388844DB2695C1E500CCFB00 /* CredentialScreen.swift */, + 388844DD2695C1F200CCFB00 /* CredentialViewModel.swift */, ); - path = Pin; + path = Credential; sourceTree = ""; }; 38A0C0E926E0C28A00898B6A /* Model */ = { isa = PBXGroup; children = ( - 38A0C0EC26E0C7F700898B6A /* PinProtectionInformation.swift */, + 38A0C0EC26E0C7F700898B6A /* CredentialProtectionInformation.swift */, ); path = Model; sourceTree = ""; @@ -896,8 +938,8 @@ children = ( 38DD19A726A070F000FE08A4 /* ValidationError.swift */, 38DD19A926A070F000FE08A4 /* ValidationResult.swift */, - 382D51AA26B1679B00B774CF /* JWTValidator.swift */, 38B085AE29081EAF006295D8 /* AuthCloudApiRegistrationValidator.swift */, + 382D51AA26B1679B00B774CF /* JWTValidator.swift */, ); path = Validation; sourceTree = ""; @@ -998,21 +1040,10 @@ 5982DE8405D7B002EEB48C3D /* Pods */ = { isa = PBXGroup; children = ( - 62C2371A7532EBAD0A0021FF /* Pods-NevisExampleApp.debug.xcconfig */, - 2E256C9B57CBCD476D68E544 /* Pods-NevisExampleApp.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; - 93491D3F2F2A1133C938BE92 /* Frameworks */ = { - isa = PBXGroup; - children = ( - E792052CC61AC2811699E15F /* Pods_NevisExampleApp.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1020,12 +1051,10 @@ isa = PBXNativeTarget; buildConfigurationList = 3833EB2D2685EA03002C5E0C /* Build configuration list for PBXNativeTarget "NevisExampleApp" */; buildPhases = ( - 597F9B730AA7680BCE6E1F38 /* [CP] Check Pods Manifest.lock */, 3867C1972685EE1F00A8B98B /* Swiftformat */, 3833EB152685EA03002C5E0C /* Sources */, 3833EB162685EA03002C5E0C /* Frameworks */, 3833EB172685EA03002C5E0C /* Resources */, - 47890EDE050EC58C6AAB876C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1042,8 +1071,9 @@ 3833EB112685EA03002C5E0C /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1250; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1540; TargetAttributes = { 3833EB182685EA03002C5E0C = { CreatedOnToolsVersion = 12.5; @@ -1104,45 +1134,6 @@ shellPath = /bin/sh; shellScript = "if which swiftformat >/dev/null; then\n echo \"SwiftFormat version: $(swiftformat --version)\"\n echo \"Swift version: ${SWIFT_VERSION}\"\n swiftformat \"${SRCROOT}/\" --config \"${SRCROOT}/.swiftformat\" --swiftversion ${SWIFT_VERSION}\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n"; }; - 47890EDE050EC58C6AAB876C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NevisExampleApp/Pods-NevisExampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-NevisExampleApp/Pods-NevisExampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NevisExampleApp/Pods-NevisExampleApp-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 597F9B730AA7680BCE6E1F38 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NevisExampleApp-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1153,6 +1144,7 @@ 38ED024426E0B6BF00FAFC38 /* InteractionCountDownTimer.swift in Sources */, 38EFD3D326DCB9B30051A19E /* SharedSequenceConvertibleType+Extension.swift in Sources */, 38F0767E28FFD42900EFDAF8 /* OutOfBandOperationUseCaseImpl.swift in Sources */, + 0698AA602C3D116C001B4000 /* ChangePasswordResponse.swift in Sources */, 38C6F3572BB17398002909BE /* ConfirmationViewModel.swift in Sources */, 38B0A2BD269C477900FABCC1 /* ErrorHandlerChainImpl.swift in Sources */, 38EB17792902B49600E82236 /* BusinessError.swift in Sources */, @@ -1175,8 +1167,8 @@ 3852F39E2686193C0051FF73 /* ScreenViewModel.swift in Sources */, 38ABBB4D2906A151006B19B9 /* ChangeDeviceInformationUseCase.swift in Sources */, 38487849268C777F00FD8419 /* DeepLinkHandler.swift in Sources */, - 3875ADCD291BF6D100939D16 /* RegistrationAuthenticatorSelectorImpl.swift in Sources */, 38FD7AD128FEEAD500F64099 /* DecodePayloadUseCaseImpl.swift in Sources */, + 0698AA542C3BEB20001B4000 /* PasswordAuthenticatorProtectionStatus+Extension.swift in Sources */, 38C2D11D290BC20800E41764 /* ResultViewModel.swift in Sources */, 381EC649290ABEDB0005546B /* BusinessError+Extension.swift in Sources */, 38CAEE9D2B021FA600143059 /* DeleteAuthenticatorsUseCaseImpl.swift in Sources */, @@ -1188,7 +1180,7 @@ 381EC647290AAE7A0005546B /* OperationError.swift in Sources */, 386FC3262909215000CB30A7 /* LoginRequest.swift in Sources */, 38DD19AD26A070F000FE08A4 /* ValidationResult.swift in Sources */, - 3875ADD0291BF6D100939D16 /* AuthenticationAuthenticatorSelectorImpl.swift in Sources */, + 3875ADD0291BF6D100939D16 /* AuthenticatorSelectorImpl.swift in Sources */, 3852F39A26860C590051FF73 /* UIStackView+Extension.swift in Sources */, 381D598326C56003006B02DD /* SelectAuthenticatorResponse.swift in Sources */, 38F0769B2900129700EFDAF8 /* EnrollPinResponse.swift in Sources */, @@ -1207,6 +1199,7 @@ 3867C1A32685F73A00A8B98B /* AppLoader.swift in Sources */, 3867C1A62685F7AF00A8B98B /* DependencyProvider.swift in Sources */, 38ABBB4F2906A158006B19B9 /* ChangeDeviceInformationUseCaseImpl.swift in Sources */, + 0698AA562C3BF8E2001B4000 /* AuthenticatorAaid+Extension.swift in Sources */, 38DF72772901451100429337 /* SelectAccountViewModel.swift in Sources */, 38AC7AB42689DA8C00A5A5AD /* Coordinator.swift in Sources */, 38B0A2C3269C483D00FABCC1 /* GeneralErrorHandler.swift in Sources */, @@ -1225,33 +1218,40 @@ 38FD7AB128FE954700F64099 /* InitClientUseCaseImpl.swift in Sources */, 3810439F268B0BBA0095D449 /* NavigationParameterizable.swift in Sources */, 386FC3282909228F00CB30A7 /* Credentials.swift in Sources */, - 388844DE2695C1F200CCFB00 /* PinViewModel.swift in Sources */, + 388844DE2695C1F200CCFB00 /* CredentialViewModel.swift in Sources */, + 0698AA662C3D12EA001B4000 /* VerifyPasswordResponse.swift in Sources */, + 0698AA642C3D12A7001B4000 /* EnrollPasswordResponse.swift in Sources */, 38B0A2B9269C2BCC00FABCC1 /* OSLog+Extension.swift in Sources */, 38FD7AB928FEA10000F64099 /* ConfigurationLoader.swift in Sources */, 3867C1A82685F7DD00A8B98B /* AppAssembly.swift in Sources */, 386423FC26984B89005F5961 /* UIDevice+Extension.swift in Sources */, + 0698AA6C2C3D1E21001B4000 /* ChangePasswordUseCaseImpl.swift in Sources */, 381043A2268B2C940095D449 /* ActivityIndicator.swift in Sources */, 386F25302902E38A0036615B /* DeregistrationUseCase.swift in Sources */, 38FD7ACD28FEE8F800F64099 /* OutOfBandOperationUseCase.swift in Sources */, 38ABBB522906AD41006B19B9 /* ChangeDeviceInformationScreen.swift in Sources */, 38487847268C6B5100FD8419 /* SelectAuthenticatorViewModel.swift in Sources */, 386FC336290926AD00CB30A7 /* LoginResponse.swift in Sources */, + 0698AA6A2C3D1DF8001B4000 /* ChangePasswordUseCase.swift in Sources */, 38CAEE9B2B021F9D00143059 /* DeleteAuthenticatorsUseCase.swift in Sources */, 38C2D11E290BC20800E41764 /* ResultScreen.swift in Sources */, 3863F3AC2907CAFF0028D26E /* ChangePinResponse.swift in Sources */, 38EB1770290293B000E82236 /* TransactionConfirmationScreen.swift in Sources */, + 0698AA5A2C3BFED9001B4000 /* AuthenticatorValidator.swift in Sources */, 3840BEF3268DCCB200DBEA62 /* SelectAuthenticatorItemViewModel.swift in Sources */, 38DD19AB26A070F000FE08A4 /* ValidationError.swift in Sources */, + 0698AA622C3D11DD001B4000 /* PasswordEnrollerImpl.swift in Sources */, 38FD7AC128FEA30B00F64099 /* AppConfiguration.swift in Sources */, 386FC3342909262200CB30A7 /* LoginDataSourceImpl.swift in Sources */, 38FD7AB628FE981A00F64099 /* ClientProviderImpl.swift in Sources */, 38C6F35D2BB178F3002909BE /* VerifyDevicePasscodeResponse.swift in Sources */, 38EB1772290293B900E82236 /* TransactionConfirmationViewModel.swift in Sources */, + 0698AA582C3BFD64001B4000 /* AuthenticatorValidatorImpl.swift in Sources */, 38B0A2BB269C474E00FABCC1 /* ErrorHandlerChain.swift in Sources */, 38BAF1AB26B291A700725619 /* SDKLogger.swift in Sources */, 383D0A912912B98B00E94068 /* OutlinedButton.swift in Sources */, 38FD7AC528FEA8B200F64099 /* ConfigurationLoaderImpl.swift in Sources */, - 388844DC2695C1E500CCFB00 /* PinScreen.swift in Sources */, + 388844DC2695C1E500CCFB00 /* CredentialScreen.swift in Sources */, 386FC33B29094F2C00CB30A7 /* RegistrationUseCaseImpl.swift in Sources */, 3875ADD3291BF6D100939D16 /* PinEnrollerImpl.swift in Sources */, 3875ADCC291BF6D100939D16 /* PinUserVerifierImpl.swift in Sources */, @@ -1261,6 +1261,7 @@ 386FC33929094F2600CB30A7 /* RegistrationUseCase.swift in Sources */, 386FC32B290925CB00CB30A7 /* LoginRepository.swift in Sources */, 38FD7AB428FE97C700F64099 /* ClientProvider.swift in Sources */, + 0698AA682C3D1345001B4000 /* PasswordUserVerifierImpl.swift in Sources */, 38ABBB582906C234006B19B9 /* GetDeviceInformationUseCaseImpl.swift in Sources */, 3863F3A52907BEE80028D26E /* GetAuthenticatorsUseCase.swift in Sources */, 38F0769128FFD8C100EFDAF8 /* CreateDeviceInformationUseCaseImpl.swift in Sources */, @@ -1279,7 +1280,7 @@ 3863F3B02907CE3D0028D26E /* ChangePinUseCaseImpl.swift in Sources */, 38DF72752901450700429337 /* SelectAccountScreen.swift in Sources */, 38F0769929000BA400EFDAF8 /* Authenticator+LocalizedExtension.swift in Sources */, - 38A0C0ED26E0C7F700898B6A /* PinProtectionInformation.swift in Sources */, + 38A0C0ED26E0C7F700898B6A /* CredentialProtectionInformation.swift in Sources */, 38AC7AB62689DC8600A5A5AD /* AppCoordinator.swift in Sources */, 38DABD7329D58B11008B8116 /* UsernamePasswordLoginScreen.swift in Sources */, 38F0769D290041FB00EFDAF8 /* GetAccountsUseCase.swift in Sources */, @@ -1292,6 +1293,7 @@ 3852F3A6268624550051FF73 /* LaunchScreen.swift in Sources */, 38EB176A29027B9C00E82236 /* PinAuthenticatorProtectionStatus+Extension.swift in Sources */, 38AC7ABB268A077200A5A5AD /* DeepLink.swift in Sources */, + 0698AA5E2C3D10A5001B4000 /* PasswordChangerImpl.swift in Sources */, 386A74942913AF7600951EF8 /* NSLabel.swift in Sources */, 381D598B26C5605F006B02DD /* OperationResponse.swift in Sources */, 38FE58C4269344340014DF93 /* HomeScreen.swift in Sources */, @@ -1449,7 +1451,6 @@ }; 3833EB2E2685EA03002C5E0C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 62C2371A7532EBAD0A0021FF /* Pods-NevisExampleApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -1481,7 +1482,6 @@ }; 3833EB2F2685EA03002C5E0C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2E256C9B57CBCD476D68E544 /* Pods-NevisExampleApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; diff --git a/NevisExampleApp.xcodeproj/xcshareddata/xcschemes/NevisExampleApp.xcscheme b/NevisExampleApp.xcodeproj/xcshareddata/xcschemes/NevisExampleApp.xcscheme index 6936157..4c2185e 100644 --- a/NevisExampleApp.xcodeproj/xcshareddata/xcschemes/NevisExampleApp.xcscheme +++ b/NevisExampleApp.xcodeproj/xcshareddata/xcschemes/NevisExampleApp.xcscheme @@ -1,6 +1,6 @@ (PinViewModel.self, argument: arg)) + container.register(CredentialScreen.self) { (res: Resolver, arg: NavigationParameterizable) in + CredentialScreen(viewModel: res ~> (CredentialViewModel.self, argument: arg)) }.inObjectScope(.weak) container.register(TransactionConfirmationScreen.self) { (res: Resolver, arg: NavigationParameterizable) in @@ -93,6 +95,8 @@ private extension AppAssembly { .inObjectScope(.container) } + // MARK: Coordinators + /// Registers the coordinators. /// /// - Parameter container: The container provided by the `Assembler`. @@ -102,6 +106,8 @@ private extension AppAssembly { .inObjectScope(.container) } + // MARK: View models + /// Registers the view models. /// /// - Parameter container: The container provided by the `Assembler`. @@ -141,9 +147,9 @@ private extension AppAssembly { initializer: SelectAuthenticatorViewModel.init) .inObjectScope(.transient) - container.autoregister(PinViewModel.self, + container.autoregister(CredentialViewModel.self, argument: NavigationParameterizable.self, - initializer: PinViewModel.init) + initializer: CredentialViewModel.init) .inObjectScope(.transient) container.autoregister(TransactionConfirmationViewModel.self, @@ -166,6 +172,8 @@ private extension AppAssembly { .inObjectScope(.transient) } + // MARK: Use-cases + /// Registers the use cases. /// /// - Parameter container: The container provided by the `Assembler`. @@ -188,6 +196,8 @@ private extension AppAssembly { authenticationAuthenticatorSelector: authSelectorForAuth, pinEnroller: res~>, pinUserVerifier: res~>, + passwordEnroller: res~>, + passwordUserVerifier: res~>, biometricUserVerifier: res~>, devicePasscodeUserVerifier: res~>, logger: res~>) @@ -200,6 +210,7 @@ private extension AppAssembly { createDeviceInformationUseCase: res~>, authenticatorSelector: authenticatorSelector, pinEnroller: res~>, + passwordEnroller: res~>, biometricUserVerifier: res~>, devicePasscodeUserVerifier: res~>, logger: res~>) @@ -211,6 +222,7 @@ private extension AppAssembly { return InBandAuthenticationUseCaseImpl(clientProvider: res~>, authenticatorSelector: authenticatorSelector, pinUserVerifier: res~>, + passwordUserVerifier: res~>, biometricUserVerifier: res~>, devicePasscodeUserVerifier: res~>, logger: res~>) @@ -222,6 +234,9 @@ private extension AppAssembly { container.autoregister(ChangePinUseCase.self, initializer: ChangePinUseCaseImpl.init) + container.autoregister(ChangePasswordUseCase.self, + initializer: ChangePasswordUseCaseImpl.init) + container.register(AuthCloudApiRegistrationUseCase.self) { res in let authenticatorSelector = res ~> (AuthenticatorSelector.self, name: RegistrationAuthenticatorSelectorName) @@ -229,6 +244,7 @@ private extension AppAssembly { createDeviceInformationUseCase: res~>, authenticatorSelector: authenticatorSelector, pinEnroller: res~>, + passwordEnroller: res~>, biometricUserVerifier: res~>, devicePasscodeUserVerifier: res~>, logger: res~>) @@ -256,6 +272,8 @@ private extension AppAssembly { initializer: LoginUseCaseImpl.init) } + // MARK: Repositories + /// Registers the repositories. /// /// - Parameter container: The container provided by the `Assembler`. @@ -264,6 +282,8 @@ private extension AppAssembly { initializer: LoginRepositoryImpl.init) } + // MARK: Data sources + /// Registers the data sources. /// /// - Parameter container: The container provided by the `Assembler`. @@ -272,6 +292,8 @@ private extension AppAssembly { initializer: LoginDataSourceImpl.init) } + // MARK: Components + /// Registers the components. /// /// - Parameter container: The container provided by the `Assembler`. @@ -287,13 +309,21 @@ private extension AppAssembly { container.autoregister(AccountSelector.self, initializer: AccountSelectorImpl.init) - container.autoregister(AuthenticatorSelector.self, - name: AuthenticationAuthenticatorSelectorName, - initializer: AuthenticationAuthenticatorSelectorImpl.init) + container.register(AuthenticatorSelector.self, + name: AuthenticationAuthenticatorSelectorName) { res in + AuthenticatorSelectorImpl(authenticatorValidator: res~>, + operation: .authentication, + responseEmitter: res~>, + logger: res~>) + } - container.autoregister(AuthenticatorSelector.self, - name: RegistrationAuthenticatorSelectorName, - initializer: RegistrationAuthenticatorSelectorImpl.init) + container.register(AuthenticatorSelector.self, + name: RegistrationAuthenticatorSelectorName) { res in + AuthenticatorSelectorImpl(authenticatorValidator: res~>, + operation: .registration, + responseEmitter: res~>, + logger: res~>) + } container.autoregister(PinEnroller.self, initializer: PinEnrollerImpl.init) @@ -304,6 +334,15 @@ private extension AppAssembly { container.autoregister(PinUserVerifier.self, initializer: PinUserVerifierImpl.init) + container.autoregister(PasswordEnroller.self, + initializer: PasswordEnrollerImpl.init) + + container.autoregister(PasswordChanger.self, + initializer: PasswordChangerImpl.init) + + container.autoregister(PasswordUserVerifier.self, + initializer: PasswordUserVerifierImpl.init) + container.autoregister(BiometricUserVerifier.self, initializer: BiometricUserVerifierImpl.init) @@ -321,6 +360,9 @@ private extension AppAssembly { container.autoregister(AccountValidator.self, initializer: AccountValidatorImpl.init) + container.autoregister(AuthenticatorValidator.self, + initializer: AuthenticatorValidatorImpl.init) + container.autoregister(SDKLogger.self, initializer: SDKLoggerImpl.init) .inObjectScope(.container) diff --git a/NevisExampleApp/Common/Configuration/Model/AppConfiguration.swift b/NevisExampleApp/Common/Configuration/Model/AppConfiguration.swift index e43b450..059e6a4 100644 --- a/NevisExampleApp/Common/Configuration/Model/AppConfiguration.swift +++ b/NevisExampleApp/Common/Configuration/Model/AppConfiguration.swift @@ -17,6 +17,9 @@ struct AppConfiguration: Codable { /// The Nevis Mobile Authentication SDK configuration. let sdkConfiguration: Configuration + /// The allowed authenticators. /// The allowed authenticators. + let authenticatorWhitelist: [AuthenticatorAaid] + // MARK: - CodingKey /// Enumeration for keys used during coding. @@ -25,6 +28,8 @@ struct AppConfiguration: Codable { case loginConfiguration = "login" /// Key for the SDK configuration. case sdkConfiguration = "sdk" + /// Key for the authenticator whitelist. + case authenticatorWhitelist /// Enumeration for the nested SDK configuration keys used during coding. enum NestedCodingKeys: String, CodingKey { @@ -73,5 +78,6 @@ struct AppConfiguration: Codable { else { self.sdkConfiguration = try container.decode(Configuration.self, forKey: .sdkConfiguration) } + self.authenticatorWhitelist = try container.decode([AuthenticatorAaid].self, forKey: .authenticatorWhitelist) } } diff --git a/NevisExampleApp/Common/Configuration/Model/Environment.swift b/NevisExampleApp/Common/Configuration/Model/Environment.swift index 344609f..f2cc03f 100644 --- a/NevisExampleApp/Common/Configuration/Model/Environment.swift +++ b/NevisExampleApp/Common/Configuration/Model/Environment.swift @@ -16,8 +16,8 @@ enum Environment { /// The configuration file name for an environment. var configFileName: String { switch self { - case .authenticationCloud: return "ConfigAuthenticationCloud" - case .identitySuite: return "ConfigIdentitySuite" + case .authenticationCloud: "ConfigAuthenticationCloud" + case .identitySuite: "ConfigIdentitySuite" } } } diff --git a/NevisExampleApp/Common/Error/AppError.swift b/NevisExampleApp/Common/Error/AppError.swift index 11affed..8e8876f 100644 --- a/NevisExampleApp/Common/Error/AppError.swift +++ b/NevisExampleApp/Common/Error/AppError.swift @@ -16,6 +16,10 @@ enum AppError: Error { case cookieNotFound /// No PIN authenticator found in the list of registered authenticators. case pinAuthenticatorNotFound + /// No Password authenticator found in the list of registered authenticators. + case passwordAuthenticatorNotFound + /// Unknown error. + case unknown } // MARK: - LocalizedError @@ -24,13 +28,17 @@ extension AppError: LocalizedError { public var errorDescription: String? { switch self { case .loadAppConfigurationError: - return L10n.Error.App.loadAppConfigurationError + L10n.Error.App.loadAppConfigurationError case .readLoginConfigurationError: - return L10n.Error.App.readLoginConfigurationError + L10n.Error.App.readLoginConfigurationError case .cookieNotFound: - return L10n.Error.App.cookieNotFound + L10n.Error.App.cookieNotFound case .pinAuthenticatorNotFound: - return L10n.Error.App.pinAuthenticatorNotFound + L10n.Error.App.pinAuthenticatorNotFound + case .passwordAuthenticatorNotFound: + L10n.Error.App.passwordAuthenticatorNotFound + case .unknown: + L10n.Error.App.Generic.message } } } diff --git a/NevisExampleApp/Common/Extensions/Authenticator+Extension.swift b/NevisExampleApp/Common/Extensions/Authenticator+Extension.swift index 8860fd7..2c20248 100644 --- a/NevisExampleApp/Common/Extensions/Authenticator+Extension.swift +++ b/NevisExampleApp/Common/Extensions/Authenticator+Extension.swift @@ -16,11 +16,11 @@ extension Authenticator { func isEnrolled(username: Username) -> Bool { switch userEnrollment { case let enrollment as SdkUserEnrollment: - return enrollment.isEnrolled(username) + enrollment.isEnrolled(username) case let enrollment as OsUserEnrollment: - return enrollment.isEnrolled() + enrollment.isEnrolled() default: - return false + false } } } diff --git a/NevisExampleApp/Domain/Interaction/AccountSelectorImpl.swift b/NevisExampleApp/Domain/Interaction/AccountSelectorImpl.swift index 2501b85..7748559 100644 --- a/NevisExampleApp/Domain/Interaction/AccountSelectorImpl.swift +++ b/NevisExampleApp/Domain/Interaction/AccountSelectorImpl.swift @@ -23,9 +23,6 @@ class AccountSelectorImpl { /// The response emitter. private let responseEmitter: ResponseEmitter - /// The error handler chain. - private let errorHandlerChain: ErrorHandlerChain - /// The logger. private let logger: SDKLogger @@ -36,15 +33,13 @@ class AccountSelectorImpl { /// - Parameters: /// - accountValidator: The account validator. /// - responseEmitter: The response emitter. - /// - errorHandlerChain: The error handler chain. - /// - responseEmitter: The response emitter. + /// - logger: The logger. init(accountValidator: AccountValidator, responseEmitter: ResponseEmitter, - errorHandlerChain: ErrorHandlerChain, + errorHandlerChain _: ErrorHandlerChain, logger: SDKLogger) { self.accountValidator = accountValidator self.responseEmitter = responseEmitter - self.errorHandlerChain = errorHandlerChain self.logger = logger } } @@ -90,7 +85,8 @@ extension AccountSelectorImpl: AccountSelector { } } catch { - errorHandlerChain.handle(error: error) + logger.log("Authenticator selection failed due to \(error.localizedDescription)", color: .red) + handler.cancel() } } } diff --git a/NevisExampleApp/Domain/Interaction/AuthenticationAuthenticatorSelectorImpl.swift b/NevisExampleApp/Domain/Interaction/AuthenticationAuthenticatorSelectorImpl.swift deleted file mode 100644 index 719b256..0000000 --- a/NevisExampleApp/Domain/Interaction/AuthenticationAuthenticatorSelectorImpl.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// Nevis Mobile Authentication SDK Example App -// -// Copyright © 2022. Nevis Security AG. All rights reserved. -// - -import NevisMobileAuthentication - -/// The unique name of authenticator selector implementation for Authentication operation. -let AuthenticationAuthenticatorSelectorName = "auth_selector_auth" - -/// Default implementation of ``AuthenticatorSelector`` protocol used for authentication. -/// -/// With the help of the ``ResponseEmitter`` it will emit a ``SelectAuthenticatorResponse``. -class AuthenticationAuthenticatorSelectorImpl { - - // MARK: - Properties - - /// The response emitter. - private let responseEmitter: ResponseEmitter - - /// The logger. - private let logger: SDKLogger - - // MARK: - Initialization - - /// Creates a new instance. - /// - /// - Parameters: - /// - responseEmitter: The response emitter. - /// - logger: The logger. - init(responseEmitter: ResponseEmitter, - logger: SDKLogger) { - self.responseEmitter = responseEmitter - self.logger = logger - } -} - -// MARK: - AuthenticatorSelector - -extension AuthenticationAuthenticatorSelectorImpl: AuthenticatorSelector { - func selectAuthenticator(context: AuthenticatorSelectionContext, handler: AuthenticatorSelectionHandler) { - logger.log("Please select one of the received available authenticators!") - let authenticatorItems = context.authenticators.filter { - guard let registration = $0.registration else { - return false - } - - // Do not display: - // - policy non-registered authenticators - // - not hardware supported authenticators. - return $0.isSupportedByHardware && registration.isRegistered(context.account.username) - }.map { - AuthenticatorItem(authenticator: $0, - isPolicyCompliant: context.isPolicyCompliant(authenticatorAaid: $0.aaid), - isUserEnrolled: $0.isEnrolled(username: context.account.username)) - } - - let response = SelectAuthenticatorResponse(authenticatorItems: authenticatorItems, - handler: handler) - responseEmitter.subject.onNext(response) - } -} diff --git a/NevisExampleApp/Domain/Interaction/AuthenticatorSelectorImpl.swift b/NevisExampleApp/Domain/Interaction/AuthenticatorSelectorImpl.swift new file mode 100644 index 0000000..239136b --- /dev/null +++ b/NevisExampleApp/Domain/Interaction/AuthenticatorSelectorImpl.swift @@ -0,0 +1,86 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2022. Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication +import RxSwift + +/// The unique name of authenticator selector implementation for Registration operation. +let RegistrationAuthenticatorSelectorName = "auth_selector_reg" + +/// The unique name of authenticator selector implementation for Authentication operation. +let AuthenticationAuthenticatorSelectorName = "auth_selector_auth" + +/// Default implementation of ``AuthenticatorSelector`` protocol. +/// +/// With the help of the ``ResponseEmitter`` it will emit a ``SelectAuthenticatorResponse``. +class AuthenticatorSelectorImpl { + + enum Operation { + case registration + case authentication + } + + // MARK: - Properties + + /// The authenticator validator. + private let authenticatorValidator: AuthenticatorValidator + + /// The current operation. + private let operation: Operation + + /// The response emitter. + private let responseEmitter: ResponseEmitter + + /// The logger. + private let logger: SDKLogger + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - authenticatorValidator: The authenticator validator. + /// - operation: The current operation. + /// - responseEmitter: The response emitter. + /// - logger: The logger. + init(authenticatorValidator: AuthenticatorValidator, operation: Operation, responseEmitter: ResponseEmitter, logger: SDKLogger) { + self.authenticatorValidator = authenticatorValidator + self.operation = operation + self.responseEmitter = responseEmitter + self.logger = logger + } +} + +// MARK: - AuthenticatorSelector + +extension AuthenticatorSelectorImpl: AuthenticatorSelector { + func selectAuthenticator(context: AuthenticatorSelectionContext, handler: AuthenticatorSelectionHandler) { + logger.log("Please select one of the received available authenticators!") + + let validationResult = switch operation { + case .registration: authenticatorValidator.validateForRegistration(context: context) + case .authentication: authenticatorValidator.validateForAuthentication(context: context) + } + + validationResult.subscribe( + onNext: { authenticators in + let autheticators = authenticators.map { authenticator in + AuthenticatorItem(authenticator: authenticator, + isPolicyCompliant: context.isPolicyCompliant(authenticatorAaid: authenticator.aaid), + isUserEnrolled: authenticator.isEnrolled(username: context.account.username)) + } + + let response = SelectAuthenticatorResponse(authenticatorItems: autheticators, + handler: handler) + self.responseEmitter.subject.onNext(response) + }, + onError: { error in + self.logger.log("Authenticator selection failed due to \(error.localizedDescription)", color: .red) + handler.cancel() + } + ).dispose() + } +} diff --git a/NevisExampleApp/Domain/Interaction/Password/PasswordChangerImpl.swift b/NevisExampleApp/Domain/Interaction/Password/PasswordChangerImpl.swift new file mode 100644 index 0000000..c6dcff6 --- /dev/null +++ b/NevisExampleApp/Domain/Interaction/Password/PasswordChangerImpl.swift @@ -0,0 +1,57 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024. Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Default implementation of ``PasswordChanger`` protocol. +/// +/// With the help of the ``ResponseEmitter`` it will emit a ``ChangePasswordResponse``. +class PasswordChangerImpl { + + // MARK: - Properties + + /// The response emitter. + private let responseEmitter: ResponseEmitter + + /// The logger. + private let logger: SDKLogger + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - responseEmitter: The response emitter. + /// - logger: The logger. + init(responseEmitter: ResponseEmitter, + logger: SDKLogger) { + self.responseEmitter = responseEmitter + self.logger = logger + } +} + +// MARK: - PasswordChanger + +extension PasswordChangerImpl: PasswordChanger { + func changePassword(context: PasswordChangeContext, handler: PasswordChangeHandler) { + if context.lastRecoverableError != nil { + logger.log("Password change failed. Please try again.") + } + else { + logger.log("Please start Password change.") + } + + let response = ChangePasswordResponse(protectionStatus: context.authenticatorProtectionStatus, + lastRecoverableError: context.lastRecoverableError, + handler: handler) + responseEmitter.subject.onNext(response) + } + + /// You can add custom Password policy by overriding the `passwordPolicy` getter. +// func passwordPolicy() -> PasswordPolicy { +// // custom PasswordPolicy implementation +// } +} diff --git a/NevisExampleApp/Domain/Interaction/Password/PasswordEnrollerImpl.swift b/NevisExampleApp/Domain/Interaction/Password/PasswordEnrollerImpl.swift new file mode 100644 index 0000000..b2f0e51 --- /dev/null +++ b/NevisExampleApp/Domain/Interaction/Password/PasswordEnrollerImpl.swift @@ -0,0 +1,56 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024. Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Default implementation of ``PasswordEnroller`` protocol. +/// +/// With the help of the ``ResponseEmitter`` it will emit an ``EnrollPasswordResponse``. +class PasswordEnrollerImpl { + + // MARK: - Properties + + /// The response emitter. + private let responseEmitter: ResponseEmitter + + /// The logger. + private let logger: SDKLogger + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - responseEmitter: The response emitter. + /// - logger: The logger. + init(responseEmitter: ResponseEmitter, + logger: SDKLogger) { + self.responseEmitter = responseEmitter + self.logger = logger + } +} + +// MARK: - PasswordEnroller + +extension PasswordEnrollerImpl: PasswordEnroller { + func enrollPassword(context: PasswordEnrollmentContext, handler: PasswordEnrollmentHandler) { + if context.lastRecoverableError != nil { + logger.log("Password enrollment failed. Please try again.") + } + else { + logger.log("Please start Password enrollment.") + } + + let response = EnrollPasswordResponse(lastRecoverableError: context.lastRecoverableError, + handler: handler) + responseEmitter.subject.onNext(response) + } + + /// You can add custom Password policy by overriding the `passwordPolicy` getter. +// func passwordPolicy() -> PasswordPolicy { +// // custom PasswordPolicy implementation +// } +} diff --git a/NevisExampleApp/Domain/Interaction/Password/PasswordUserVerifierImpl.swift b/NevisExampleApp/Domain/Interaction/Password/PasswordUserVerifierImpl.swift new file mode 100644 index 0000000..a6d86a2 --- /dev/null +++ b/NevisExampleApp/Domain/Interaction/Password/PasswordUserVerifierImpl.swift @@ -0,0 +1,52 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024. Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Default implementation of ``PasswordUserVerifier`` protocol. +/// +/// With the help of the ``ResponseEmitter`` it will emit a ``VerifyPasswordResponse``. +class PasswordUserVerifierImpl { + + // MARK: - Properties + + /// The response emitter. + private let responseEmitter: ResponseEmitter + + /// The logger. + private let logger: SDKLogger + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - responseEmitter: The response emitter. + /// - logger: The logger. + init(responseEmitter: ResponseEmitter, + logger: SDKLogger) { + self.responseEmitter = responseEmitter + self.logger = logger + } +} + +// MARK: - PasswordUserVerifier + +extension PasswordUserVerifierImpl: PasswordUserVerifier { + func verifyPassword(context: PasswordUserVerificationContext, handler: PasswordUserVerificationHandler) { + if context.lastRecoverableError != nil { + logger.log("Password user verification failed. Please try again.") + } + else { + logger.log("Please start Password user verification.") + } + + let response = VerifyPasswordResponse(protectionStatus: context.authenticatorProtectionStatus, + lastRecoverableError: context.lastRecoverableError, + handler: handler) + responseEmitter.subject.onNext(response) + } +} diff --git a/NevisExampleApp/Domain/Interaction/PinChangerImpl.swift b/NevisExampleApp/Domain/Interaction/Pin/PinChangerImpl.swift similarity index 100% rename from NevisExampleApp/Domain/Interaction/PinChangerImpl.swift rename to NevisExampleApp/Domain/Interaction/Pin/PinChangerImpl.swift diff --git a/NevisExampleApp/Domain/Interaction/PinEnrollerImpl.swift b/NevisExampleApp/Domain/Interaction/Pin/PinEnrollerImpl.swift similarity index 100% rename from NevisExampleApp/Domain/Interaction/PinEnrollerImpl.swift rename to NevisExampleApp/Domain/Interaction/Pin/PinEnrollerImpl.swift diff --git a/NevisExampleApp/Domain/Interaction/PinUserVerifierImpl.swift b/NevisExampleApp/Domain/Interaction/Pin/PinUserVerifierImpl.swift similarity index 100% rename from NevisExampleApp/Domain/Interaction/PinUserVerifierImpl.swift rename to NevisExampleApp/Domain/Interaction/Pin/PinUserVerifierImpl.swift diff --git a/NevisExampleApp/Domain/Interaction/RegistrationAuthenticatorSelectorImpl.swift b/NevisExampleApp/Domain/Interaction/RegistrationAuthenticatorSelectorImpl.swift deleted file mode 100644 index b0a32fe..0000000 --- a/NevisExampleApp/Domain/Interaction/RegistrationAuthenticatorSelectorImpl.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Nevis Mobile Authentication SDK Example App -// -// Copyright © 2022. Nevis Security AG. All rights reserved. -// - -import NevisMobileAuthentication - -/// The unique name of authenticator selector implementation for Registration operation. -let RegistrationAuthenticatorSelectorName = "auth_selector_reg" - -/// Default implementation of ``AuthenticatorSelector`` protocol used for registration. -/// -/// With the help of the ``ResponseEmitter`` it will emit a ``SelectAuthenticatorResponse``. -class RegistrationAuthenticatorSelectorImpl { - - // MARK: - Properties - - /// The response emitter. - private let responseEmitter: ResponseEmitter - - /// The logger. - private let logger: SDKLogger - - // MARK: - Initialization - - /// Creates a new instance. - /// - /// - Parameters: - /// - responseEmitter: The response emitter. - /// - logger: The logger. - init(responseEmitter: ResponseEmitter, - logger: SDKLogger) { - self.responseEmitter = responseEmitter - self.logger = logger - } -} - -// MARK: - AuthenticatorSelector - -extension RegistrationAuthenticatorSelectorImpl: AuthenticatorSelector { - func selectAuthenticator(context: AuthenticatorSelectionContext, handler: AuthenticatorSelectionHandler) { - logger.log("Please select one of the received available authenticators!") - let authenticatorItems = context.authenticators.filter { - // Do not display: - // - policy non-compliant authenticators (this includes already registered authenticators) - // - not hardware supported authenticators. - $0.isSupportedByHardware && context.isPolicyCompliant(authenticatorAaid: $0.aaid) - }.map { - AuthenticatorItem(authenticator: $0, - isPolicyCompliant: true, - isUserEnrolled: $0.isEnrolled(username: context.account.username)) - } - - let response = SelectAuthenticatorResponse(authenticatorItems: authenticatorItems, - handler: handler) - responseEmitter.subject.onNext(response) - } -} diff --git a/NevisExampleApp/Domain/Model/AuthenticatorItem.swift b/NevisExampleApp/Domain/Model/AuthenticatorItem.swift index 3d1a835..9e4d01b 100644 --- a/NevisExampleApp/Domain/Model/AuthenticatorItem.swift +++ b/NevisExampleApp/Domain/Model/AuthenticatorItem.swift @@ -23,6 +23,6 @@ struct AuthenticatorItem { /// Tells that whether this authenticator item is selectable on select authenticator view or not. /// The value is calculated based on the ``AuthenticatorItem/isPolicyCompliant`` and ``AuthenticatorItem/isUserEnrolled`` flags. var isEnabled: Bool { - isPolicyCompliant && (authenticator.aaid == AuthenticatorAaid.Pin.rawValue || isUserEnrolled) + isPolicyCompliant && (authenticator.aaid == AuthenticatorAaid.Pin.rawValue || authenticator.aaid == AuthenticatorAaid.Password.rawValue || isUserEnrolled) } } diff --git a/NevisExampleApp/Domain/Model/ChangePasswordResponse.swift b/NevisExampleApp/Domain/Model/ChangePasswordResponse.swift new file mode 100644 index 0000000..4c662a2 --- /dev/null +++ b/NevisExampleApp/Domain/Model/ChangePasswordResponse.swift @@ -0,0 +1,39 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Response class indicating the SDK operation asks for user verification using Password authenticator. +final class ChangePasswordResponse: OperationResponse { + + // MARK: - Properties + + /// Status object of the Password authenticator. + let protectionStatus: PasswordAuthenticatorProtectionStatus + + /// The object describing the last credential verification error. + let lastRecoverableError: PasswordChangeRecoverableError? + + /// The object that is notified of the change result. + let handler: PasswordChangeHandler + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - protectionStatus: Status object of the Password authenticator. + /// - lastRecoverableError: The object describing the last credential verification error. + /// - handler: The object that is notified of the change result. + init(protectionStatus: PasswordAuthenticatorProtectionStatus, + lastRecoverableError: PasswordChangeRecoverableError?, + handler: PasswordChangeHandler) { + self.protectionStatus = protectionStatus + self.lastRecoverableError = lastRecoverableError + self.handler = handler + super.init() + } +} diff --git a/NevisExampleApp/Domain/Model/EnrollPasswordResponse.swift b/NevisExampleApp/Domain/Model/EnrollPasswordResponse.swift new file mode 100644 index 0000000..54aa869 --- /dev/null +++ b/NevisExampleApp/Domain/Model/EnrollPasswordResponse.swift @@ -0,0 +1,33 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Response class indicating the SDK operation asks for enrolling the Password authenticator. +final class EnrollPasswordResponse: OperationResponse { + + // MARK: - Properties + + /// The object describing the latest recoverable error (if any). + var lastRecoverableError: PasswordEnrollmentError? + + /// The object that is notified of the enrollment result. + let handler: PasswordEnrollmentHandler + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - lastRecoverableError: The object describing the latest recoverable error (if any). + /// - handler: The object that is notified of the enrollment result. + init(lastRecoverableError: PasswordEnrollmentError?, + handler: PasswordEnrollmentHandler) { + self.lastRecoverableError = lastRecoverableError + self.handler = handler + super.init() + } +} diff --git a/NevisExampleApp/Domain/Model/Operation.swift b/NevisExampleApp/Domain/Model/Operation.swift index e5fc1eb..3b84548 100644 --- a/NevisExampleApp/Domain/Model/Operation.swift +++ b/NevisExampleApp/Domain/Model/Operation.swift @@ -29,6 +29,9 @@ public enum Operation { /// PIN change operation. case pinChange + /// Password change operation. + case passwordChange + /// Device information change operation. case deviceInformationChange diff --git a/NevisExampleApp/Domain/Model/VerifyPasswordResponse.swift b/NevisExampleApp/Domain/Model/VerifyPasswordResponse.swift new file mode 100644 index 0000000..962b80f --- /dev/null +++ b/NevisExampleApp/Domain/Model/VerifyPasswordResponse.swift @@ -0,0 +1,39 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Response class indicating the SDK operation asks for user verification using Password authenticator. +final class VerifyPasswordResponse: OperationResponse { + + // MARK: - Properties + + /// Status object of the Password authenticator. + let protectionStatus: PasswordAuthenticatorProtectionStatus + + /// The object describing the last credential verification error. + let lastRecoverableError: PasswordUserVerificationError? + + /// The object that is notified of the verification result. + let handler: PasswordUserVerificationHandler + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - protectionStatus: Status object of the Password authenticator. + /// - lastRecoverableError: The object describing the last credential verification error. + /// - handler: The object that is notified of the verification result. + init(protectionStatus: PasswordAuthenticatorProtectionStatus, + lastRecoverableError: PasswordUserVerificationError?, + handler: PasswordUserVerificationHandler) { + self.protectionStatus = protectionStatus + self.lastRecoverableError = lastRecoverableError + self.handler = handler + super.init() + } +} diff --git a/NevisExampleApp/Domain/Use Case/AuthCloudApiRegistrationUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/AuthCloudApiRegistrationUseCaseImpl.swift index d87c195..8a00b6e 100644 --- a/NevisExampleApp/Domain/Use Case/AuthCloudApiRegistrationUseCaseImpl.swift +++ b/NevisExampleApp/Domain/Use Case/AuthCloudApiRegistrationUseCaseImpl.swift @@ -24,6 +24,9 @@ class AuthCloudApiRegistrationUseCaseImpl { /// The PIN enroller. private let pinEnroller: PinEnroller + /// The Password enroller. + private let passwordEnroller: PasswordEnroller + /// The biometric user verifier. private let biometricUserVerifier: BiometricUserVerifier @@ -42,6 +45,7 @@ class AuthCloudApiRegistrationUseCaseImpl { /// - createDeviceInformationUseCase: Use case for creating device information. /// - authenticatorSelector: The authenticator selector. /// - pinEnroller: The PIN enroller. + /// - passwordEnroller: The Password enroller. /// - biometricUserVerifier: The biometric user verifier. /// - devicePasscodeUserVerifier: The device passcode user verifier. /// - logger: The logger. @@ -49,6 +53,7 @@ class AuthCloudApiRegistrationUseCaseImpl { createDeviceInformationUseCase: CreateDeviceInformationUseCase, authenticatorSelector: AuthenticatorSelector, pinEnroller: PinEnroller, + passwordEnroller: PasswordEnroller, biometricUserVerifier: BiometricUserVerifier, devicePasscodeUserVerifier: DevicePasscodeUserVerifier, logger: SDKLogger) { @@ -56,6 +61,7 @@ class AuthCloudApiRegistrationUseCaseImpl { self.createDeviceInformationUseCase = createDeviceInformationUseCase self.authenticatorSelector = authenticatorSelector self.pinEnroller = pinEnroller + self.passwordEnroller = passwordEnroller self.biometricUserVerifier = biometricUserVerifier self.devicePasscodeUserVerifier = devicePasscodeUserVerifier self.logger = logger @@ -76,6 +82,7 @@ extension AuthCloudApiRegistrationUseCaseImpl: AuthCloudApiRegistrationUseCase { .deviceInformation(createDeviceInformationUseCase.execute()) .authenticatorSelector(authenticatorSelector) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .onSuccess { diff --git a/NevisExampleApp/Domain/Use Case/ChangePasswordUseCase.swift b/NevisExampleApp/Domain/Use Case/ChangePasswordUseCase.swift new file mode 100644 index 0000000..a43463c --- /dev/null +++ b/NevisExampleApp/Domain/Use Case/ChangePasswordUseCase.swift @@ -0,0 +1,17 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import RxSwift + +/// Use case for changing Password. +protocol ChangePasswordUseCase { + + /// Changes the Password. + /// + /// - Parameter username: The username whose Password needs to be changed. + /// - Returns: The observable sequence that will emit an ``OperationResponse`` object. + func execute(username: String) -> Observable +} diff --git a/NevisExampleApp/Domain/Use Case/ChangePasswordUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/ChangePasswordUseCaseImpl.swift new file mode 100644 index 0000000..52a8b16 --- /dev/null +++ b/NevisExampleApp/Domain/Use Case/ChangePasswordUseCaseImpl.swift @@ -0,0 +1,69 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication +import RxSwift + +/// Default implementation of ``ChangePasswordUseCase`` protocol. +class ChangePasswordUseCaseImpl { + + // MARK: - Properties + + /// The client provider. + private let clientProvider: ClientProvider + + /// The Password changer. + private let passwordChanger: PasswordChanger + + /// The logger. + private let logger: SDKLogger + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - clientProvider: The client provider. + /// - passwordChanger: The Password changer. + /// - logger: The logger. + init(clientProvider: ClientProvider, + passwordChanger: PasswordChanger, + logger: SDKLogger) { + self.clientProvider = clientProvider + self.passwordChanger = passwordChanger + self.logger = logger + } +} + +// MARK: - ChangePasswordUseCase + +extension ChangePasswordUseCaseImpl: ChangePasswordUseCase { + func execute(username: String) -> Observable { + Observable.create { [weak self] observer in + guard let self else { + return Disposables.create() + } + + let client = clientProvider.get() + client?.operations.passwordChange + .username(username) + .passwordChanger(passwordChanger) + .onSuccess { + self.logger.log("Password change succeeded.", color: .green) + observer.onNext(CompletedResponse(operation: .passwordChange)) + observer.onCompleted() + } + .onError { + self.logger.log("Password change failed.", color: .red) + observer.onError(OperationError(operation: .passwordChange, + underlyingError: $0)) + } + .execute() + + return Disposables.create() + } + } +} diff --git a/NevisExampleApp/Domain/Use Case/DeleteAuthenticatorsUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/DeleteAuthenticatorsUseCaseImpl.swift index 278fc92..7937246 100644 --- a/NevisExampleApp/Domain/Use Case/DeleteAuthenticatorsUseCaseImpl.swift +++ b/NevisExampleApp/Domain/Use Case/DeleteAuthenticatorsUseCaseImpl.swift @@ -71,8 +71,8 @@ private extension DeleteAuthenticatorsUseCaseImpl { func deleteAuthenticators(of username: Username) -> Observable { Observable.create { [unowned self] observer in do { - try self.provider.get()?.localData.deleteAuthenticator(username: username, aaid: nil) - self.logger.log("Delete authenticators succeeded for user \(username).", color: .green) + try provider.get()?.localData.deleteAuthenticator(username: username, aaid: nil) + logger.log("Delete authenticators succeeded for user \(username).", color: .green) observer.onNext(CompletedResponse(operation: .localData)) observer.onCompleted() } diff --git a/NevisExampleApp/Domain/Use Case/InBandAuthenticationUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/InBandAuthenticationUseCaseImpl.swift index 983344a..cbbbe02 100644 --- a/NevisExampleApp/Domain/Use Case/InBandAuthenticationUseCaseImpl.swift +++ b/NevisExampleApp/Domain/Use Case/InBandAuthenticationUseCaseImpl.swift @@ -21,6 +21,9 @@ class InBandAuthenticationUseCaseImpl { /// The PIN user verifier. private let pinUserVerifier: PinUserVerifier + /// The Password user verifier. + private let passwordUserVerifier: PasswordUserVerifier + /// The biometric user verifier. private let biometricUserVerifier: BiometricUserVerifier @@ -38,18 +41,21 @@ class InBandAuthenticationUseCaseImpl { /// - clientProvider: The client provider. /// - authenticatorSelector: The authenticator selector. /// - pinUserVerifier: The PIN user verifier. + /// - passwordUserVerifier: The Password user verifier. /// - biometricUserVerifier: The biometric user verifier. /// - devicePasscodeUserVerifier: The device passcode user verifier. /// - logger: The logger. init(clientProvider: ClientProvider, authenticatorSelector: AuthenticatorSelector, pinUserVerifier: PinUserVerifier, + passwordUserVerifier: PasswordUserVerifier, biometricUserVerifier: BiometricUserVerifier, devicePasscodeUserVerifier: DevicePasscodeUserVerifier, logger: SDKLogger) { self.clientProvider = clientProvider self.authenticatorSelector = authenticatorSelector self.pinUserVerifier = pinUserVerifier + self.passwordUserVerifier = passwordUserVerifier self.biometricUserVerifier = biometricUserVerifier self.devicePasscodeUserVerifier = devicePasscodeUserVerifier self.logger = logger @@ -68,6 +74,7 @@ extension InBandAuthenticationUseCaseImpl: InBandAuthenticationUseCase { .username(username) .authenticatorSelector(authenticatorSelector) .pinUserVerifier(pinUserVerifier) + .passwordUserVerifier(passwordUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .onSuccess { @@ -84,6 +91,8 @@ extension InBandAuthenticationUseCaseImpl: InBandAuthenticationUseCase { case let .FidoError(_, _, sessionProvider), let .NetworkError(_, sessionProvider): self.printSessionInfo(sessionProvider) + case .NoDeviceLockError: + fallthrough case .Unknown: fallthrough @unknown default: diff --git a/NevisExampleApp/Domain/Use Case/OutOfBandOperationUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/OutOfBandOperationUseCaseImpl.swift index b6d6bb6..7f7a0b1 100644 --- a/NevisExampleApp/Domain/Use Case/OutOfBandOperationUseCaseImpl.swift +++ b/NevisExampleApp/Domain/Use Case/OutOfBandOperationUseCaseImpl.swift @@ -33,6 +33,12 @@ class OutOfBandOperationUseCaseImpl { /// The PIN user verifier. private let pinUserVerifier: PinUserVerifier + /// The Password enroller. + private let passwordEnroller: PasswordEnroller + + /// The Password user verifier. + private let passwordUserVerifier: PasswordUserVerifier + /// The biometric user verifier. private let biometricUserVerifier: BiometricUserVerifier @@ -54,6 +60,8 @@ class OutOfBandOperationUseCaseImpl { /// - authenticationAuthenticatorSelector: The authenticator selector used during authentication. /// - pinEnroller: The PIN enroller. /// - pinUserVerifier: The PIN user verifier. + /// - passwordEnroller: The Password enroller. + /// - passwordUserVerifier: The Password user verifier. /// - biometricUserVerifier: The biometric user verifier. /// - devicePasscodeUserVerifier: The device passcode user verifier. /// - logger: The logger. @@ -64,6 +72,8 @@ class OutOfBandOperationUseCaseImpl { authenticationAuthenticatorSelector: AuthenticatorSelector, pinEnroller: PinEnroller, pinUserVerifier: PinUserVerifier, + passwordEnroller: PasswordEnroller, + passwordUserVerifier: PasswordUserVerifier, biometricUserVerifier: BiometricUserVerifier, devicePasscodeUserVerifier: DevicePasscodeUserVerifier, logger: SDKLogger) { @@ -74,6 +84,8 @@ class OutOfBandOperationUseCaseImpl { self.authenticationAuthenticatorSelector = authenticationAuthenticatorSelector self.pinEnroller = pinEnroller self.pinUserVerifier = pinUserVerifier + self.passwordEnroller = passwordEnroller + self.passwordUserVerifier = passwordUserVerifier self.biometricUserVerifier = biometricUserVerifier self.devicePasscodeUserVerifier = devicePasscodeUserVerifier self.logger = logger @@ -124,6 +136,7 @@ private extension OutOfBandOperationUseCaseImpl { .deviceInformation(createDeviceInformationUseCase.execute()) .authenticatorSelector(registrationAuthenticatorSelector) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .onSuccess { @@ -149,6 +162,7 @@ private extension OutOfBandOperationUseCaseImpl { .accountSelector(accountSelector) .authenticatorSelector(authenticationAuthenticatorSelector) .pinUserVerifier(pinUserVerifier) + .passwordUserVerifier(passwordUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .onSuccess { diff --git a/NevisExampleApp/Domain/Use Case/RegistrationUseCaseImpl.swift b/NevisExampleApp/Domain/Use Case/RegistrationUseCaseImpl.swift index 1f1c285..f20a0cc 100644 --- a/NevisExampleApp/Domain/Use Case/RegistrationUseCaseImpl.swift +++ b/NevisExampleApp/Domain/Use Case/RegistrationUseCaseImpl.swift @@ -24,6 +24,9 @@ class RegistrationUseCaseImpl { /// The PIN enroller. private let pinEnroller: PinEnroller + /// The Password enroller. + private let passwordEnroller: PasswordEnroller + /// The biometric user verifier. private let biometricUserVerifier: BiometricUserVerifier @@ -42,6 +45,7 @@ class RegistrationUseCaseImpl { /// - createDeviceInformationUseCase: Use case for creating device information. /// - authenticatorSelector: The authenticator selector. /// - pinEnroller: The PIN enroller. + /// - passwordEnroller: The Password enroller. /// - biometricUserVerifier: The biometric user verifier. /// - devicePasscodeUserVerifier: The device passcode user verifier. /// - logger: The logger. @@ -49,6 +53,7 @@ class RegistrationUseCaseImpl { createDeviceInformationUseCase: CreateDeviceInformationUseCase, authenticatorSelector: AuthenticatorSelector, pinEnroller: PinEnroller, + passwordEnroller: PasswordEnroller, biometricUserVerifier: BiometricUserVerifier, devicePasscodeUserVerifier: DevicePasscodeUserVerifier, logger: SDKLogger) { @@ -56,6 +61,7 @@ class RegistrationUseCaseImpl { self.createDeviceInformationUseCase = createDeviceInformationUseCase self.authenticatorSelector = authenticatorSelector self.pinEnroller = pinEnroller + self.passwordEnroller = passwordEnroller self.biometricUserVerifier = biometricUserVerifier self.devicePasscodeUserVerifier = devicePasscodeUserVerifier self.logger = logger @@ -80,6 +86,7 @@ extension RegistrationUseCaseImpl: RegistrationUseCase { .deviceInformation(createDeviceInformationUseCase.execute()) .authenticatorSelector(authenticatorSelector) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .onSuccess { diff --git a/NevisExampleApp/Domain/Validators/AuthenticatorValidator.swift b/NevisExampleApp/Domain/Validators/AuthenticatorValidator.swift new file mode 100644 index 0000000..a280e7c --- /dev/null +++ b/NevisExampleApp/Domain/Validators/AuthenticatorValidator.swift @@ -0,0 +1,26 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication +import RxSwift + +/// Protocol declaration for authenticator validation. +protocol AuthenticatorValidator { + + /// Validates authenticators for registration. + /// + /// - Parameter context: The context holding the accounts to validate. + /// - Throws: ``BusinessError`` if supported authenticators are not found. + /// - Returns: The observable sequence that will emit an ``Authenticator`` array. + func validateForRegistration(context: AuthenticatorSelectionContext) -> Observable<[any Authenticator]> + + /// Validates authenticators for authentication. + /// + /// - Parameter context: The context holding the accounts to validate. + /// - Throws: ``BusinessError`` if supported authenticators are not found. + /// - Returns: The observable sequence that will emit an ``Authenticator`` array. + func validateForAuthentication(context: AuthenticatorSelectionContext) -> Observable<[any Authenticator]> +} diff --git a/NevisExampleApp/Domain/Validators/AuthenticatorValidatorImpl.swift b/NevisExampleApp/Domain/Validators/AuthenticatorValidatorImpl.swift new file mode 100644 index 0000000..546e621 --- /dev/null +++ b/NevisExampleApp/Domain/Validators/AuthenticatorValidatorImpl.swift @@ -0,0 +1,82 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication +import RxSwift + +/// Default implementation of ``AuthenticatorValidator`` protocol. +final class AuthenticatorValidatorImpl { + // MARK: - Properties + + /// The configuration loader. + private let configurationLoader: ConfigurationLoader + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - configurationLoader: The configuration loader. + init(configurationLoader: ConfigurationLoader) { + self.configurationLoader = configurationLoader + } +} + +// MARK: - AuthenticatorValidator + +extension AuthenticatorValidatorImpl: AuthenticatorValidator { + func validateForRegistration(context: AuthenticatorSelectionContext) -> Observable<[any Authenticator]> { + configurationLoader.load().flatMap { configuration -> Observable<[any Authenticator]> in + let allowedAuthenticators = self.allowedAuthenticators(context: context, whitelistedAuthenticators: configuration.authenticatorWhitelist).filter { authenticator in + // Do not display: + // - policy non-compliant authenticators (this includes already registered authenticators) + // - not hardware supported authenticators. + authenticator.isSupportedByHardware && context.isPolicyCompliant(authenticatorAaid: authenticator.aaid) + } + + if allowedAuthenticators.isEmpty { + return .error(BusinessError.authenticatorNotFound) + } + + return .just(allowedAuthenticators) + } + } + + func validateForAuthentication(context: AuthenticatorSelectionContext) -> Observable<[any Authenticator]> { + configurationLoader.load().flatMap { configuration -> Observable<[any Authenticator]> in + let allowedAuthenticators = self.allowedAuthenticators(context: context, whitelistedAuthenticators: configuration.authenticatorWhitelist).filter { authenticator in + guard let registration = authenticator.registration else { return false } + + // Do not display: + // - policy non-registered authenticators, + // - not hardware supported authenticators. + return authenticator.isSupportedByHardware && registration.isRegistered(context.account.username) + } + + if allowedAuthenticators.isEmpty { + return .error(BusinessError.authenticatorNotFound) + } + + return .just(allowedAuthenticators) + } + } +} + +// MARK: Filtering based on the authenticator whitelist + +private extension AuthenticatorValidatorImpl { + /// Filters out non-whitelisted authenticators. + /// + /// - Parameter context: The context holding the accounts to validate. + /// - Parameter whitelistedAuthenticators: The array holding the whitelisted authenticators. + /// - Returns: The list of allowed authenticators. + func allowedAuthenticators(context: AuthenticatorSelectionContext, whitelistedAuthenticators: [AuthenticatorAaid]) -> [any Authenticator] { + context.authenticators.filter { authenticator in + guard let authenticatorAaid = AuthenticatorAaid(rawValue: authenticator.aaid) else { return false } + return whitelistedAuthenticators.contains(authenticatorAaid) + } + } +} diff --git a/NevisExampleApp/Presentation/Coordinator/App/AppCoordinator.swift b/NevisExampleApp/Presentation/Coordinator/App/AppCoordinator.swift index 8ed54da..0d83bda 100644 --- a/NevisExampleApp/Presentation/Coordinator/App/AppCoordinator.swift +++ b/NevisExampleApp/Presentation/Coordinator/App/AppCoordinator.swift @@ -39,10 +39,10 @@ protocol AppCoordinator: Coordinator { /// - Parameter parameter: The navigation parameter. func navigateToAuthenticatorSelection(with parameter: SelectAuthenticatorParameter) - /// Navigates to the Pin screen. + /// Navigates to the Credential screen. /// /// - Parameter parameter: The navigation parameter. - func navigateToPin(with parameter: PinParameter) + func navigateToCredential(with parameter: CredentialParameter) /// Navigates to the Transaction Confirmation screen. /// diff --git a/NevisExampleApp/Presentation/Coordinator/App/AppCoordinatorImpl.swift b/NevisExampleApp/Presentation/Coordinator/App/AppCoordinatorImpl.swift index 676f7bd..36b9bd4 100644 --- a/NevisExampleApp/Presentation/Coordinator/App/AppCoordinatorImpl.swift +++ b/NevisExampleApp/Presentation/Coordinator/App/AppCoordinatorImpl.swift @@ -138,21 +138,21 @@ extension AppCoordinatorImpl: AppCoordinator { rootNavigationController?.pushViewController(screen, animated: true) } - func navigateToPin(with parameter: PinParameter) { - guard let screen = DependencyProvider.shared.container.resolve(PinScreen.self, + func navigateToCredential(with parameter: CredentialParameter) { + guard let screen = DependencyProvider.shared.container.resolve(CredentialScreen.self, argument: parameter as NavigationParameterizable) else { return } - if let pinScreen = topScreen as? PinScreen { - // the `PinEnrollerImpl` or `PinUserVerifierImpl` emitted a response again - // `ResponseObserver` navigates to the PIN screen although that is the visible screen - // It means that the user entered an invalid PIN and a recoverable error recevied - // Just refresh the screen with the new view model - pinScreen.viewModel = DependencyProvider.shared.container.resolve(PinViewModel.self, - argument: parameter as NavigationParameterizable) - logger.log("Refreshing Pin screen.", color: .purple) - pinScreen.refresh() + if let credentialScreen = topScreen as? CredentialScreen { + // the `PinEnrollerImpl`, `PinUserVerifierImpl`, `PasswordEnrollerImpl` or `PasswordUserVerifierImpl` emitted a response again + // `ResponseObserver` navigates to the Credential screen although that is the visible screen. + // It means that the user entered an invalid credential and a recoverable error recevied. + // Just refresh the screen with the new view model. + credentialScreen.viewModel = DependencyProvider.shared.container.resolve(CredentialViewModel.self, + argument: parameter as NavigationParameterizable) + logger.log("Refreshing Credential screen.", color: .purple) + credentialScreen.refresh() return } diff --git a/NevisExampleApp/Presentation/Screens/Pin/PinScreen.swift b/NevisExampleApp/Presentation/Screens/Credential/CredentialScreen.swift similarity index 63% rename from NevisExampleApp/Presentation/Screens/Pin/PinScreen.swift rename to NevisExampleApp/Presentation/Screens/Credential/CredentialScreen.swift index 5301873..3bc9fe2 100644 --- a/NevisExampleApp/Presentation/Screens/Pin/PinScreen.swift +++ b/NevisExampleApp/Presentation/Screens/Credential/CredentialScreen.swift @@ -4,11 +4,12 @@ // Copyright © 2022. Nevis Security AG. All rights reserved. // +import NevisMobileAuthentication import RxSwift import UIKit -/// The Pin view. Used for Pin code creation verification and change. -class PinScreen: BaseScreen, Screen { +/// The Credential view. Used for PIN / Password creation verification and change. +class CredentialScreen: BaseScreen, Screen { // MARK: - UI @@ -18,11 +19,11 @@ class PinScreen: BaseScreen, Screen { /// The description label. private let descriptionLabel = NSLabel(style: .normal) - /// The text field for the old PIN. - private let oldPinField = NSTextField(placeholder: L10n.Pin.oldPinPlaceholder, returnKeyType: .next) + /// The text field for the old credential. + private let oldCredentialField = NSTextField(returnKeyType: .next) - /// The text field for the PIN. - private let pinField = NSTextField(placeholder: L10n.Pin.pinPlaceholder) + /// The text field for the credential. + private let credentialField = NSTextField() /// The error label. private let errorLabel = NSLabel(style: .error) @@ -31,10 +32,10 @@ class PinScreen: BaseScreen, Screen { private let infoMessageLabel = NSLabel(style: .info) /// The confirm button. - private let confirmButton = OutlinedButton(title: L10n.Pin.confirm) + private let confirmButton = OutlinedButton(title: L10n.Credential.confirm) /// The cancel button. - private let cancelButton = OutlinedButton(title: L10n.Pin.cancel) + private let cancelButton = OutlinedButton(title: L10n.Credential.cancel) /// The toolbar for the keyboard with type *numberPad*. private let keyboardToolbar = UIToolbar() @@ -42,7 +43,7 @@ class PinScreen: BaseScreen, Screen { // MARK: - Properties /// The view model. - var viewModel: PinViewModel! + var viewModel: CredentialViewModel! /// Thread safe bag that disposes added disposables. private let disposeBag = DisposeBag() @@ -52,20 +53,20 @@ class PinScreen: BaseScreen, Screen { /// Creates a new instance. /// /// - Parameter viewModel: The view model. - init(viewModel: PinViewModel) { + init(viewModel: CredentialViewModel) { self.viewModel = viewModel super.init() } /// :nodoc: deinit { - os_log("PinScreen", log: OSLog.deinit, type: .debug) + os_log("CredentialScreen", log: OSLog.deinit, type: .debug) } } // MARK: - Lifecycle -extension PinScreen { +extension CredentialScreen { /// Override of the `viewDidLoad()` lifecycle method. Sets up the user interface and binds the view model. override func viewDidLoad() { @@ -90,14 +91,14 @@ extension PinScreen { // MARK: - Setups /// :nodoc: -private extension PinScreen { +private extension CredentialScreen { func setupUI() { setupTitleLabel() setupDescriptionLabel() setupToolbar() - setupOldPinField() - setupPinField() + setupOldCredentialField() + setupCredentialField() setupErrorLabel() setupInfoMessageLabel() setupConfirmButton() @@ -120,7 +121,7 @@ private extension PinScreen { let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - let doneButton = UIBarButtonItem(title: L10n.Pin.done, + let doneButton = UIBarButtonItem(title: L10n.Credential.done, style: .done, target: self, action: #selector(done)) @@ -131,18 +132,17 @@ private extension PinScreen { } } - func setupOldPinField() { - oldPinField.do { + func setupOldCredentialField() { + oldCredentialField.do { addItem($0, topSpacing: 16) $0.setHeight(with: 40) - $0.keyboardType = .numberPad $0.isSecureTextEntry = true $0.inputAccessoryView = keyboardToolbar } } - func setupPinField() { - pinField.do { + func setupCredentialField() { + credentialField.do { addItem($0, topSpacing: 16) $0.setHeight(with: 40) $0.keyboardType = .numberPad @@ -178,11 +178,11 @@ private extension PinScreen { } func setupTextFields() { - if let superview = oldPinField.superview, !superview.isHidden { - oldPinField.becomeFirstResponder() + if let superview = oldCredentialField.superview, !superview.isHidden { + oldCredentialField.becomeFirstResponder() } else { - pinField.becomeFirstResponder() + credentialField.becomeFirstResponder() } } } @@ -190,7 +190,7 @@ private extension PinScreen { // MARK: - Actions /// :nodoc: -private extension PinScreen { +private extension CredentialScreen { @objc func done() { view.endEditing(true) @@ -200,7 +200,7 @@ private extension PinScreen { // MARK: - Binding /// :nodoc: -private extension PinScreen { +private extension CredentialScreen { func bindViewModel() { assert(viewModel != nil) @@ -209,31 +209,44 @@ private extension PinScreen { .mapToVoid() .asDriverOnErrorJustComplete() - let input = PinViewModel.Input(oldPin: oldPinField.rx.text.orEmpty.asDriver(), - pin: pinField.rx.text.orEmpty.asDriver(), - clearTrigger: clearTrigger, - confirmTrigger: confirmButton.rx.tap.asDriver(), - cancelTrigger: cancelButton.rx.tap.asDriver()) + let input = CredentialViewModel.Input(oldCredential: oldCredentialField.rx.text.orEmpty.asDriver(), + credential: credentialField.rx.text.orEmpty.asDriver(), + clearTrigger: clearTrigger, + confirmTrigger: confirmButton.rx.tap.asDriver(), + cancelTrigger: cancelButton.rx.tap.asDriver()) let output = viewModel.transform(input: input) [output.title.drive(titleLabel.rx.text), output.description.drive(descriptionLabel.rx.text), output.lastRecoverableError.drive(errorLabel.rx.text), - output.hideOldPin.drive(hideOldPinBinder), + output.credentialType.drive(credentialTypeBinder), + output.hideOldCredential.drive(hideOldPinBinder), output.clear.drive(), output.confirm.drive(), output.cancel.drive(), output.error.drive(rx.error), - output.pinProtectionInformation.drive(pinProtectionInfoBinder)] + output.credentialProtectionInformation.drive(pinProtectionInfoBinder)] .forEach { $0.disposed(by: disposeBag) } } + var credentialTypeBinder: Binder { + Binder(self) { base, credentialType in + base.oldCredentialField.do { + $0.placeholder = credentialType == .Pin ? L10n.Credential.Pin.oldPinPlaceholder : L10n.Credential.Password.oldPasswordPlaceholder + $0.keyboardType = credentialType == .Pin ? .numberPad : .default + } + base.credentialField.do { + $0.keyboardType = credentialType == .Pin ? .numberPad : .default + } + } + } + var hideOldPinBinder: Binder { Binder(self) { base, isHidden in - base.oldPinField.superview?.isHidden = isHidden + base.oldCredentialField.superview?.isHidden = isHidden } } - var pinProtectionInfoBinder: Binder { + var pinProtectionInfoBinder: Binder { Binder(self) { base, info in base.infoMessageLabel.text = info.message base.confirmButton.isEnabled = !info.isInCoolDown diff --git a/NevisExampleApp/Presentation/Screens/Credential/CredentialViewModel.swift b/NevisExampleApp/Presentation/Screens/Credential/CredentialViewModel.swift new file mode 100644 index 0000000..6854990 --- /dev/null +++ b/NevisExampleApp/Presentation/Screens/Credential/CredentialViewModel.swift @@ -0,0 +1,527 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2022. Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication +import RxCocoa +import RxSwift + +// MARK: - Navigation + +/// Navigation parameter of the Credential view. +protocol CredentialParameter: NavigationParameterizable {} + +/// Navigation parameter of the Credential view in case of PIN authenticator. +enum PinParameter: CredentialParameter { + /// Represents Pin enrollment + /// . + /// - Parameters: + /// - lastRecoverableError: The object that informs that an error occurred during PIN enrollment. + /// - handler: The PIN enrollment handler. + case enrollment(lastRecoverableError: PinEnrollmentError?, + handler: PinEnrollmentHandler) + + /// Represents Pin verification. + /// + /// - Parameters: + /// - protectionStatus: The object describing the PIN authenticator protection status. + /// - lastRecoverableError: The object that informs that an error occurred during PIN verification. + /// - handler: The PIN verification handler. + case verification(protectionStatus: PinAuthenticatorProtectionStatus, + lastRecoverableError: PinUserVerificationError?, + handler: PinUserVerificationHandler) + + /// Represents Pin change. + /// + /// - Parameters: + /// - protectionStatus: The object describing the PIN authenticator protection status. + /// - lastRecoverableError: The object that informs that an error occurred during PIN change. + /// - handler: The PIN change handler. + case credentialChange(protectionStatus: PinAuthenticatorProtectionStatus, + lastRecoverableError: PinChangeRecoverableError?, + handler: PinChangeHandler) +} + +/// Navigation parameter of the Credential view in case of Password authenticator. +enum PasswordParameter: CredentialParameter { + /// Represents Password enrollment. + /// + /// - Parameters: + /// - lastRecoverableError: The object that informs that an error occurred during Password enrollment. + /// - handler: The Password enrollment handler. + case enrollment(lastRecoverableError: PasswordEnrollmentError?, + handler: PasswordEnrollmentHandler) + + /// Represents Password verification. + /// + /// - Parameters: + /// - protectionStatus: The object describing the Password authenticator protection status. + /// - lastRecoverableError: The object that informs that an error occurred during Password verification. + /// - handler: The Password verification handler. + case verification(protectionStatus: PasswordAuthenticatorProtectionStatus, + lastRecoverableError: PasswordUserVerificationError?, + handler: PasswordUserVerificationHandler) + + /// Represents Password change. + /// + /// - Parameters: + /// - protectionStatus: The object describing the Password authenticator protection status. + /// - lastRecoverableError: The object that informs that an error occurred during Password change. + /// - handler: The Password change handler. + case credentialChange(protectionStatus: PasswordAuthenticatorProtectionStatus, + lastRecoverableError: PasswordChangeRecoverableError?, + handler: PasswordChangeHandler) +} + +// MARK: - View model + +/// View model of Credential view. +final class CredentialViewModel { + + /// Available Credential operations. + private enum CredentialOperation { + /// Enrollment operation. + case enrollment + /// Change operation. + case credentialChange + /// Verification operation. + case verification + } + + // MARK: - Properties + + /// The logger. + private let logger: SDKLogger + + /// The current Credential type. + private var credentialType: AuthenticatorAaid = .Pin + + /// The current operation. + private var operation: CredentialOperation = .enrollment + + /// An observable sequence that can be used for tracking the Credential protection status. + private let credentialProtectionInfoSubject = BehaviorRelay(value: CredentialProtectionInformation()) + + /// The cooldown timer. + private var coolDownTimer: InteractionCountDownTimer? + + // MARK: Pin + + /// The PIN authenticator protection status. + private var pinProtectionStatus: PinAuthenticatorProtectionStatus? + + /// Error that can occur during PIN enrollment. + private var pinEnrollmentError: PinEnrollmentError? + + /// Error that can occur during PIN verification. + private var pinVerificationError: PinUserVerificationError? + + /// Error that can occur during PIN change. + private var pinCredentialChangeError: PinChangeRecoverableError? + + /// The PIN enrollment handler. + private var pinEnrollmentHandler: PinEnrollmentHandler? + + /// The PIN verification handler. + private var pinVerificationHandler: PinUserVerificationHandler? + + /// The PIN change handler. + private var pinCredentialChangeHandler: PinChangeHandler? + + // MARK: Password + + /// The Password authenticator protection status. + private var passwordProtectionStatus: PasswordAuthenticatorProtectionStatus? + + /// Error that can occur during Password enrollment. + private var passwordEnrollmentError: PasswordEnrollmentError? + + /// Error that can occur during Password verification. + private var passwordVerificationError: PasswordUserVerificationError? + + /// Error that can occur during Password change. + private var passwordCredentialChangeError: PasswordChangeRecoverableError? + + /// The Password enrollment handler. + private var passwordEnrollmentHandler: PasswordEnrollmentHandler? + + /// The Password verification handler. + private var passwordVerificationHandler: PasswordUserVerificationHandler? + + /// The Password change handler. + private var passwordCredentialChangeHandler: PasswordChangeHandler? + + // MARK: - Initialization + + /// Creates a new instance. + /// + /// - Parameters: + /// - logger: The logger. + /// - parameter: The navigation parameter. + init(logger: SDKLogger, + parameter: NavigationParameterizable? = nil) { + self.logger = logger + setParameter(parameter as? CredentialParameter) + } + + /// :nodoc: + deinit { + os_log("CredentialViewModel", log: OSLog.deinit, type: .debug) + // If it is not nil at this moment, it means that a concurrent operation will be started + pinEnrollmentHandler?.cancel() + pinVerificationHandler?.cancel() + pinCredentialChangeHandler?.cancel() + + passwordEnrollmentHandler?.cancel() + passwordVerificationHandler?.cancel() + passwordCredentialChangeHandler?.cancel() + } +} + +// MARK: - ScreenViewModel + +extension CredentialViewModel: ScreenViewModel { + + /// The input of the view model. + struct Input { + /// Observable sequence used for listening to Credential. + let oldCredential: Driver + /// Observable sequence used for listening to Credential confirmation. + let credential: Driver + /// Observable sequence used for clearing the state. + let clearTrigger: Driver<()> + /// Observable sequence used for starting confirmation. + let confirmTrigger: Driver<()> + /// Observable sequence used for starting cancellation. + let cancelTrigger: Driver<()> + } + + /// The output of the view model. + struct Output { + /// Observable sequence used for listening to actual title. + let title: Driver + /// Observable sequence used for listening to actual description. + let description: Driver + /// Observable sequence used for listening to last recoverable error. + let lastRecoverableError: Driver + /// Observable sequence used for listening to actual credentail type. + let credentialType: Driver + /// Observable sequence used for listening to whether Credential confirmation is needed. + let hideOldCredential: Driver + /// Observable sequence used for clearing the state. + let clear: Driver<()> + /// Observable sequence used for listening to confirm event. + let confirm: Driver<()> + /// Observable sequence used for listening to cancel event. + let cancel: Driver<()> + /// Observable sequence used for listening to error events. + let error: Driver + /// Observable sequence used for listening to Credential protection events. + let credentialProtectionInformation: Driver + } + + /// Performs pure transformation of a user `Input` to the `Output`. + /// + /// - Parameter input: The input need to be transformed. + /// - Returns: The transformed output. + func transform(input: Input) -> Output { + let errorTracker = ErrorTracker() + let title = Driver.just(title()) + let description = Driver.just(description()) + let lastRecoverableError = Driver.just(lastRecoverableError()) + let hideOldCredential = Driver.just(operation != .credentialChange) + let credentialType = Driver.just(credentialType) + + handleProtectionStatus() + + let clear = input.clearTrigger + .asObservable() + .flatMap(clear) + .asDriverOnErrorJustComplete() + + let confirm = input.confirmTrigger + .withLatestFrom(Driver.combineLatest(input.oldCredential, input.credential)) + .asObservable() + .flatMap(confirm(oldCredential:credential:)) + .trackError(errorTracker) + .asDriverOnErrorJustComplete() + + let cancel = input.cancelTrigger + .do(onNext: cancel) + + let error = errorTracker.asDriver() + let credentialProtectionInformation = credentialProtectionInfoSubject.asDriver() + return Output(title: title, + description: description, + lastRecoverableError: lastRecoverableError, + credentialType: credentialType, + hideOldCredential: hideOldCredential, + clear: clear, + confirm: confirm, + cancel: cancel, + error: error, + credentialProtectionInformation: credentialProtectionInformation) + } +} + +// MARK: - Actions + +private extension CredentialViewModel { + + /// Handles the recevied parameter. + /// + /// - Parameter paramter: The parameter to handle. + func setParameter(_ parameter: CredentialParameter?) { + guard let parameter else { + preconditionFailure("Parameter type mismatch!") + } + + if let parameter = parameter as? PinParameter { + credentialType = .Pin + switch parameter { + case let .enrollment(error, handler): + operation = .enrollment + pinEnrollmentError = error + pinEnrollmentHandler = handler + case let .verification(status, error, handler): + operation = .verification + pinProtectionStatus = status + pinVerificationError = error + pinVerificationHandler = handler + case let .credentialChange(status, error, handler): + operation = .credentialChange + pinProtectionStatus = status + pinCredentialChangeError = error + pinCredentialChangeHandler = handler + } + } + else if let parameter = parameter as? PasswordParameter { + credentialType = .Password + switch parameter { + case let .enrollment(error, handler): + operation = .enrollment + passwordEnrollmentError = error + passwordEnrollmentHandler = handler + case let .verification(status, error, handler): + operation = .verification + passwordProtectionStatus = status + passwordVerificationError = error + passwordVerificationHandler = handler + case let .credentialChange(status, error, handler): + operation = .credentialChange + passwordProtectionStatus = status + passwordCredentialChangeError = error + passwordCredentialChangeHandler = handler + } + } + else { + preconditionFailure("Parameter type mismatch!") + } + } + + /// Returns the actual screen title based on the operation and credential type. + /// + /// - Returns: The actual screen title based on the operation and credential type. + func title() -> String { + switch operation { + case .enrollment where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Enrollment.title + case .verification where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Verify.title + case .credentialChange where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Change.title + case .enrollment where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Enrollment.title + case .verification where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Verify.title + case .credentialChange where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Change.title + default: + String() + } + } + + /// Returns the actual screen description based on the operation and credential type. + /// + /// - Returns: The actual screen description based on the operation and credential type. + func description() -> String { + switch operation { + case .enrollment where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Enrollment.description + case .verification where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Verify.description + case .credentialChange where credentialType == AuthenticatorAaid.Pin: + L10n.Credential.Pin.Change.description + case .enrollment where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Enrollment.description + case .verification where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Verify.description + case .credentialChange where credentialType == AuthenticatorAaid.Password: + L10n.Credential.Password.Change.description + default: + String() + } + } + + /// Returns the actual last recoverable error based on the operation and credential type. + /// + /// - Returns: The actual last recoverable error based on the operation and credential type. + func lastRecoverableError() -> String { + switch operation { + case .enrollment: + pinEnrollmentError?.localizedDescription ?? passwordEnrollmentError?.localizedDescription ?? String() + case .verification: + pinVerificationError?.localizedDescription ?? passwordVerificationError?.localizedDescription ?? String() + case .credentialChange: + pinCredentialChangeError?.localizedDescription ?? passwordCredentialChangeError?.localizedDescription ?? String() + } + } + + /// Clears the state of the view model. + /// + /// - Returns: An observable sequence. + func clear() -> Observable<()> { + Observable.create { + self.pinEnrollmentHandler = nil + self.pinVerificationHandler = nil + self.pinCredentialChangeHandler = nil + + self.passwordEnrollmentHandler = nil + self.passwordVerificationHandler = nil + self.passwordCredentialChangeHandler = nil + + $0.onCompleted() + return Disposables.create() + } + } + + /// Confirms the given Credentials. + /// + /// - Parameters: + /// - oldCredential: The old Credential. + /// - credential: The Credential. + /// - Returns: An observable sequence. + func confirm(oldCredential: String, credential: String) -> Observable<()> { + logger.log("Confirming entered credentials.") + return Observable.create { + switch self.operation { + case .enrollment: + self.pinEnrollmentHandler?.pin(credential) + self.passwordEnrollmentHandler?.password(credential) + case .verification: + self.pinVerificationHandler?.verify(credential) + self.passwordVerificationHandler?.verify(credential) + case .credentialChange: + self.pinCredentialChangeHandler?.pins(oldCredential, credential) + self.passwordCredentialChangeHandler?.passwords(oldCredential, credential) + } + + $0.onCompleted() + return Disposables.create() + } + } + + /// Cancels the actual operation. + func cancel() { + switch operation { + case .enrollment: + logger.log("Cancelling credential enrollment.") + pinEnrollmentHandler?.cancel() + pinEnrollmentHandler = nil + passwordEnrollmentHandler?.cancel() + passwordEnrollmentHandler = nil + case .verification: + logger.log("Cancelling credential verification.") + pinVerificationHandler?.cancel() + pinVerificationHandler = nil + passwordVerificationHandler?.cancel() + passwordVerificationHandler = nil + case .credentialChange: + logger.log("Cancelling credential change.") + pinCredentialChangeHandler?.cancel() + pinCredentialChangeHandler = nil + passwordCredentialChangeHandler?.cancel() + passwordCredentialChangeHandler = nil + } + } + + /// Handles the authenticator protection status. + func handleProtectionStatus() { + if case .Pin = credentialType { + switch pinProtectionStatus { + case .Unlocked, .none: + logger.log("PIN authenticator is unlocked.") + credentialProtectionInfoSubject.accept(CredentialProtectionInformation()) + case let .LastAttemptFailed(remainingTries, coolDown): + logger.log("Last attempt failed using the PIN authenticator.") + let info = CredentialProtectionInformation(message: pinProtectionStatus?.localizedDescription ?? String(), + isInCoolDown: coolDown > 0) + credentialProtectionInfoSubject.accept(info) + + if coolDown > 0 { + startCoolDownTimer(with: coolDown, remainingTries: remainingTries) + } + case .LockedOut: + logger.log("PIN authenticator is locked.") + let info = CredentialProtectionInformation(message: pinProtectionStatus?.localizedDescription ?? String()) + credentialProtectionInfoSubject.accept(info) + case .some: + logger.log("Unknown PIN authenticator protection status.") + credentialProtectionInfoSubject.accept(CredentialProtectionInformation()) + } + } + else if case .Password = credentialType { + switch passwordProtectionStatus { + case .Unlocked, .none: + logger.log("Password authenticator is unlocked.") + credentialProtectionInfoSubject.accept(CredentialProtectionInformation()) + case let .LastAttemptFailed(remainingTries, coolDown): + let info = CredentialProtectionInformation(message: passwordProtectionStatus?.localizedDescription ?? String(), + isInCoolDown: coolDown > 0) + credentialProtectionInfoSubject.accept(info) + + if coolDown > 0 { + startCoolDownTimer(with: coolDown, remainingTries: remainingTries) + } + case .LockedOut: + logger.log("Password authenticator is locked.") + let info = CredentialProtectionInformation(message: passwordProtectionStatus?.localizedDescription ?? String()) + credentialProtectionInfoSubject.accept(info) + case .some: + logger.log("Unknown Password authenticator protection status.") + credentialProtectionInfoSubject.accept(CredentialProtectionInformation()) + } + } + else { + credentialProtectionInfoSubject.accept(CredentialProtectionInformation()) + } + } + + /// Starts the cooldown timer. + /// + /// - Parameters: + /// - cooldown: The cooldown of the timer. + /// - remainingTries: The number of remaining tries. + func startCoolDownTimer(with coolDown: Int, remainingTries: Int) { + coolDownTimer = InteractionCountDownTimer(timerLifeTime: TimeInterval(coolDown)) { remainingCoolDown in + let localizedDescription = switch self.credentialType { + case .Pin: + PinAuthenticatorProtectionStatus.LastAttemptFailed(remainingTries: remainingTries, + coolDownTimeInSeconds: remainingCoolDown).localizedDescription + case .Password: + PasswordAuthenticatorProtectionStatus.LastAttemptFailed(remainingTries: remainingTries, + coolDownTimeInSeconds: remainingCoolDown).localizedDescription + default: + String() + } + + let info = CredentialProtectionInformation(message: localizedDescription, + isInCoolDown: remainingCoolDown > 0) + self.credentialProtectionInfoSubject.accept(info) + } + + coolDownTimer?.start() + } +} diff --git a/NevisExampleApp/Presentation/Screens/Pin/Model/PinProtectionInformation.swift b/NevisExampleApp/Presentation/Screens/Credential/Model/CredentialProtectionInformation.swift similarity index 68% rename from NevisExampleApp/Presentation/Screens/Pin/Model/PinProtectionInformation.swift rename to NevisExampleApp/Presentation/Screens/Credential/Model/CredentialProtectionInformation.swift index 22d24c6..877bfd8 100644 --- a/NevisExampleApp/Presentation/Screens/Pin/Model/PinProtectionInformation.swift +++ b/NevisExampleApp/Presentation/Screens/Credential/Model/CredentialProtectionInformation.swift @@ -6,12 +6,12 @@ import Foundation -/// Represents Pin protection related information that is used by the PinScreen view. -struct PinProtectionInformation { +/// Represents the protection related information of a credential that is used by the CredentialScreen view. +struct CredentialProtectionInformation { // MARK: - Properties - /// Pin protection related message. + /// Protection related message. let message: String /// Flag that tells whether the authenticator is in cooldown. @@ -22,7 +22,7 @@ struct PinProtectionInformation { /// Creates a new instance. /// /// - Parameters: - /// - message: Pin protection related message. Default value is an empty string. + /// - message: Protection related message. Default value is an empty string. /// - isInCoolDown: Flag that tells whether the authenticator is in cooldown. Default value is false. init(message: String = "", isInCoolDown: Bool = false) { self.message = message diff --git a/NevisExampleApp/Presentation/Screens/Home/HomeScreen.swift b/NevisExampleApp/Presentation/Screens/Home/HomeScreen.swift index f250ab0..0220674 100644 --- a/NevisExampleApp/Presentation/Screens/Home/HomeScreen.swift +++ b/NevisExampleApp/Presentation/Screens/Home/HomeScreen.swift @@ -31,6 +31,9 @@ final class HomeScreen: BaseScreen, Screen { /// The PIN Change button. private let pinChangeButton = OutlinedButton(title: L10n.Home.changePin) + /// The Password Change button. + private let passwordChangeButton = OutlinedButton(title: L10n.Home.changePassword) + /// The Change Device Information button. private let changeDeviceInformationButton = OutlinedButton(title: L10n.Home.changeDeviceInformation) @@ -106,6 +109,7 @@ private extension HomeScreen { setupAuthenticateButton() setupDeregisterButton() setupPinChangeButton() + setupPasswordChangeButton() setupChangeDeviceInformationButton() setupAuthCloudApiRegisterButton() setupDeleteAuthenticatorsButton() @@ -153,6 +157,13 @@ private extension HomeScreen { } } + func setupPasswordChangeButton() { + passwordChangeButton.do { + addItemToBottom($0, spacing: 16) + $0.setHeight(with: 40) + } + } + func setupChangeDeviceInformationButton() { changeDeviceInformationButton.do { addItemToBottom($0, spacing: 16) @@ -204,6 +215,7 @@ private extension HomeScreen { authenticateTrigger: authenticateButton.rx.tap.asDriver(), deregisterTrigger: deregisterButton.rx.tap.asDriver(), pinChangeTrigger: pinChangeButton.rx.tap.asDriver(), + passwordChangeTrigger: passwordChangeButton.rx.tap.asDriver(), changeDeviceInformationTrigger: changeDeviceInformationButton.rx.tap.asDriver(), authCloudApiRegistrationTrigger: authCloudApiRegisterButton.rx.tap.asDriver(), deleteAuthenticatorsTrigger: deleteAuthenticatorsButton.rx.tap.asDriver(), @@ -215,6 +227,7 @@ private extension HomeScreen { output.authenticate.drive(), output.deregister.drive(), output.pinChange.drive(), + output.passwordChange.drive(), output.changeDeviceInformation.drive(), output.authCloudApiRegistration.drive(), output.deleteAuthenticators.drive(), diff --git a/NevisExampleApp/Presentation/Screens/Home/HomeViewModel.swift b/NevisExampleApp/Presentation/Screens/Home/HomeViewModel.swift index b82d2a3..27a6b97 100644 --- a/NevisExampleApp/Presentation/Screens/Home/HomeViewModel.swift +++ b/NevisExampleApp/Presentation/Screens/Home/HomeViewModel.swift @@ -25,6 +25,9 @@ final class HomeViewModel { /// Use case for PIN change. private let changePinUseCase: ChangePinUseCase + /// Use case for Password change. + private let changePasswordUseCase: ChangePasswordUseCase + /// Use case for retrieving the accounts. private let getAccountsUseCase: GetAccountsUseCase @@ -58,6 +61,7 @@ final class HomeViewModel { /// - initClientUseCase: Use case for initializing the client. /// - deregistrationUseCase: Use case for deregistration. /// - changePinUseCase: Use case for PIN change. + /// - changePasswordUseCase: Use case for Password change. /// - getAccountsUseCase: Use case for retrieving the accounts. /// - getAuthenticatorsUseCase: Use case for retrieving the authenticators. /// - deleteAuthenticatorsUseCase: Use case for deleting local authenticators. @@ -68,6 +72,7 @@ final class HomeViewModel { initClientUseCase: InitClientUseCase, deregistrationUseCase: DeregistrationUseCase, changePinUseCase: ChangePinUseCase, + changePasswordUseCase: ChangePasswordUseCase, getAccountsUseCase: GetAccountsUseCase, getAuthenticatorsUseCase: GetAuthenticatorsUseCase, deleteAuthenticatorsUseCase: DeleteAuthenticatorsUseCase, @@ -78,6 +83,7 @@ final class HomeViewModel { self.initClientUseCase = initClientUseCase self.deregistrationUseCase = deregistrationUseCase self.changePinUseCase = changePinUseCase + self.changePasswordUseCase = changePasswordUseCase self.getAccountsUseCase = getAccountsUseCase self.getAuthenticatorsUseCase = getAuthenticatorsUseCase self.deleteAuthenticatorsUseCase = deleteAuthenticatorsUseCase @@ -108,6 +114,8 @@ extension HomeViewModel: ScreenViewModel { let deregisterTrigger: Driver<()> /// Observable sequence used for starting PIN change. let pinChangeTrigger: Driver<()> + /// Observable sequence used for starting Password change. + let passwordChangeTrigger: Driver<()> /// Observable sequence used for starting device information change. let changeDeviceInformationTrigger: Driver<()> /// Observable sequence used for starting auth cloud api registration. @@ -132,6 +140,8 @@ extension HomeViewModel: ScreenViewModel { let deregister: Driver<()> /// Observable sequence used for listening to PIN change event. let pinChange: Driver<()> + /// Observable sequence used for listening to Password change event. + let passwordChange: Driver<()> /// Observable sequence used for listening to deregister event. let changeDeviceInformation: Driver<()> /// Observable sequence used for listening to auth cloud api registration event. @@ -185,6 +195,12 @@ extension HomeViewModel: ScreenViewModel { .trackError(errorTracker) .asDriverOnErrorJustComplete() + let passwordChange = input.passwordChangeTrigger + .asObservable() + .flatMapLatest(changePassword) + .trackError(errorTracker) + .asDriverOnErrorJustComplete() + let changeDeviceInformation = input.changeDeviceInformationTrigger .asObservable() .flatMapLatest(changeDeviceInformation) @@ -212,6 +228,7 @@ extension HomeViewModel: ScreenViewModel { authenticate: authenticate, deregister: deregister, pinChange: pinChange, + passwordChange: passwordChange, changeDeviceInformation: changeDeviceInformation, authCloudApiRegistration: authCloudApiRegistration, deleteAuthenticators: deleteAuthenticators, @@ -245,8 +262,8 @@ private extension HomeViewModel { func deregister() -> Observable<()> { switch configurationLoader.environment { case .authenticationCloud: - return deregistrationUseCase.execute(username: nil, - authorizationProvider: nil) + deregistrationUseCase.execute(username: nil, + authorizationProvider: nil) .flatMap(responseObserver.observe(response:)) .trackActivity(activityIndicator) .trackError(errorTracker) @@ -255,7 +272,7 @@ private extension HomeViewModel { // and as such we need to provide a cookie to the deregister call. // Also in Identity Siute a deregistration has to be authenticated for every user, // so batch deregistration is not really possible. - return getAccountsUseCase.execute() + getAccountsUseCase.execute() .flatMap { let parameter: SelectAccountParameter = .select(accounts: $0, operation: .deregistration, @@ -270,7 +287,7 @@ private extension HomeViewModel { /// /// - Returns: An observable sequence. func changePin() -> Observable<()> { - Observable.combineLatest(getPinEnrollment(), + Observable.combineLatest(getSdkEnrollment(for: .Pin), getAccountsUseCase.execute()) .flatMap { enrollment, accounts -> Observable<()> in let eligibleAccounts = accounts.filter { account in @@ -297,19 +314,57 @@ private extension HomeViewModel { } } - /// Starts In-Band authentication. + /// Starts Password changing. + /// + /// - Returns: An observable sequence. + func changePassword() -> Observable<()> { + Observable.combineLatest(getSdkEnrollment(for: .Password), + getAccountsUseCase.execute()) + .flatMap { enrollment, accounts -> Observable<()> in + let eligibleAccounts = accounts.filter { account in + enrollment.enrolledAccounts.contains { enrolledAccount in + enrolledAccount.username == account.username + } + } + + switch eligibleAccounts.count { + case 0: + return .error(BusinessError.accountsNotFound) + case 1: + // in case of one account we can select it automatically + return self.changePasswordUseCase.execute(username: eligibleAccounts.first!.username) + .flatMap(self.responseObserver.observe(response:)) + default: + // in case of multiple eligible accounts we have to show the account selection screen + let parameter: SelectAccountParameter = .select(accounts: eligibleAccounts, + operation: .passwordChange, + handler: nil, + message: nil) + return .just(self.appCoordinator.navigateToAccountSelection(with: parameter)) + } + } + } + + /// Fetching the SDK managed authenticator enrollments. /// + /// - Parameter authenticator: The SDK managed authenticator. /// - Returns: An observable sequence that will emit an ``SdkUserEnrollment`` object. - func getPinEnrollment() -> Observable { - getAuthenticatorsUseCase.execute() + func getSdkEnrollment(for authenticator: AuthenticatorAaid) -> Observable { + let error = switch authenticator { + case .Password: AppError.passwordAuthenticatorNotFound + case .Pin: AppError.pinAuthenticatorNotFound + default: AppError.unknown + } + + return getAuthenticatorsUseCase.execute() .flatMap { authenticators -> Observable in - // searching for the PIN authenticator - guard let pinAuthenticator = authenticators.filter({ $0.aaid == AuthenticatorAaid.Pin.rawValue }).first else { - return .error(AppError.pinAuthenticatorNotFound) + // searching for the given authenticator + guard let credentialAuthenticator = authenticators.filter({ $0.aaid == authenticator.rawValue }).first else { + return .error(error) } - guard let enrollment = pinAuthenticator.userEnrollment as? SdkUserEnrollment else { - return .error(AppError.pinAuthenticatorNotFound) + guard let enrollment = credentialAuthenticator.userEnrollment as? SdkUserEnrollment else { + return .error(error) } return .just(enrollment) diff --git a/NevisExampleApp/Presentation/Screens/Pin/PinViewModel.swift b/NevisExampleApp/Presentation/Screens/Pin/PinViewModel.swift deleted file mode 100644 index b4dc466..0000000 --- a/NevisExampleApp/Presentation/Screens/Pin/PinViewModel.swift +++ /dev/null @@ -1,362 +0,0 @@ -// -// Nevis Mobile Authentication SDK Example App -// -// Copyright © 2022. Nevis Security AG. All rights reserved. -// - -import NevisMobileAuthentication -import RxCocoa -import RxSwift - -/// Navigation parameter of the Pin view. -enum PinParameter: NavigationParameterizable { - /// Represents Pin enrollment - /// . - /// - Parameters: - /// - lastRecoverableError: The object that informs that an error occurred during PIN enrollment. - /// - handler: The PIN enrollment handler. - case enrollment(lastRecoverableError: PinEnrollmentError?, - handler: PinEnrollmentHandler) - - /// Represents Pin verification. - /// - /// - Parameters: - /// - protectionStatus: The object describing the PIN authenticator protection status. - /// - lastRecoverableError: The object that informs that an error occurred during PIN verification. - /// - handler: The PIN verification handler. - case verification(protectionStatus: PinAuthenticatorProtectionStatus, - lastRecoverableError: PinUserVerificationError?, - handler: PinUserVerificationHandler) - - /// Represents Pin change. - /// - /// - Parameters: - /// - protectionStatus: The object describing the PIN authenticator protection status. - /// - lastRecoverableError: The object that informs that an error occurred during PIN change. - /// - handler: The PIN change handler. - case credentialChange(protectionStatus: PinAuthenticatorProtectionStatus, - lastRecoverableError: PinChangeRecoverableError?, - handler: PinChangeHandler) -} - -/// View model of Pin view. -final class PinViewModel { - - /// Available PIN operations. - private enum PinOperation { - /// PIN enrollment operation. - case enrollment - /// PIN change operation. - case credentialChange - /// PIN verification operation. - case verification - } - - // MARK: - Properties - - /// The logger. - private let logger: SDKLogger - - /// The PIN authenticator protection status. - private var protectionStatus: PinAuthenticatorProtectionStatus? - - /// Error that can occur during PIN enrollment. - private var enrollmentError: PinEnrollmentError? - - /// Error that can occur during PIN verification. - private var verificationError: PinUserVerificationError? - - /// Error that can occur during PIN change. - private var credentialChangeError: PinChangeRecoverableError? - - /// The PIN enrollment handler. - private var enrollmentHandler: PinEnrollmentHandler? - - /// The PIN verification handler. - private var verificationHandler: PinUserVerificationHandler? - - /// The PIN change handler. - private var credentialChangeHandler: PinChangeHandler? - - /// The current PIN operation. - private var operation: PinOperation = .enrollment - - /// An observable sequence that can be used for tracking the PIN protection status. - private let pinProtectionInfoSubject = BehaviorRelay(value: PinProtectionInformation()) - - /// The cooldown timer. - private var coolDownTimer: InteractionCountDownTimer? - - // MARK: - Initialization - - /// Creates a new instance. - /// - /// - Parameters: - /// - logger: The logger. - /// - parameter: The navigation parameter. - init(logger: SDKLogger, - parameter: NavigationParameterizable? = nil) { - self.logger = logger - setParameter(parameter as? PinParameter) - } - - /// :nodoc: - deinit { - os_log("PinViewModel", log: OSLog.deinit, type: .debug) - // If it is not nil at this moment, it means that a concurrent operation will be started - enrollmentHandler?.cancel() - verificationHandler?.cancel() - credentialChangeHandler?.cancel() - } -} - -// MARK: - ScreenViewModel - -extension PinViewModel: ScreenViewModel { - - /// The input of the view model. - struct Input { - /// Observable sequence used for listening to Pin. - let oldPin: Driver - /// Observable sequence used for listening to Pin confirmation. - let pin: Driver - /// Observable sequence used for clearing the state. - let clearTrigger: Driver<()> - /// Observable sequence used for starting confirmation. - let confirmTrigger: Driver<()> - /// Observable sequence used for starting cancellation. - let cancelTrigger: Driver<()> - } - - /// The output of the view model. - struct Output { - /// Observable sequence used for listening to actual title. - let title: Driver - /// Observable sequence used for listening to actual description. - let description: Driver - /// Observable sequence used for listening to last recoverable error. - let lastRecoverableError: Driver - /// Observable sequence used for listening to whether Pin confirmation is needed. - let hideOldPin: Driver - /// Observable sequence used for clearing the state. - let clear: Driver<()> - /// Observable sequence used for listening to confirm event. - let confirm: Driver<()> - /// Observable sequence used for listening to cancel event. - let cancel: Driver<()> - /// Observable sequence used for listening to error events. - let error: Driver - /// Observable sequence used for listening to Pin protection events. - let pinProtectionInformation: Driver - } - - /// Performs pure transformation of a user `Input` to the `Output`. - /// - /// - Parameter input: The input need to be transformed. - /// - Returns: The transformed output. - func transform(input: Input) -> Output { - let errorTracker = ErrorTracker() - let title = Driver.just(title()) - let description = Driver.just(description()) - let lastRecoverableError = Driver.just(lastRecoverableError()) - let hideOldPin = Driver.just(operation != .credentialChange) - - handleProtectionStatus() - - let clear = input.clearTrigger - .asObservable() - .flatMap(clear) - .asDriverOnErrorJustComplete() - - let confirm = input.confirmTrigger - .withLatestFrom(Driver.combineLatest(input.oldPin, input.pin)) - .asObservable() - .flatMap(confirm(oldPin:pin:)) - .trackError(errorTracker) - .asDriverOnErrorJustComplete() - - let cancel = input.cancelTrigger - .do(onNext: cancel) - - let error = errorTracker.asDriver() - let pinProtectionInformation = pinProtectionInfoSubject.asDriver() - return Output(title: title, - description: description, - lastRecoverableError: lastRecoverableError, - hideOldPin: hideOldPin, - clear: clear, - confirm: confirm, - cancel: cancel, - error: error, - pinProtectionInformation: pinProtectionInformation) - } -} - -// MARK: - Actions - -private extension PinViewModel { - - /// Handles the recevied parameter. - /// - /// - Parameter paramter: The parameter to handle. - func setParameter(_ parameter: PinParameter?) { - guard let parameter else { - preconditionFailure("Parameter type mismatch!") - } - - switch parameter { - case let .enrollment(error, handler): - operation = .enrollment - enrollmentError = error - enrollmentHandler = handler - case let .verification(status, error, handler): - operation = .verification - protectionStatus = status - verificationError = error - verificationHandler = handler - case let .credentialChange(status, error, handler): - operation = .credentialChange - protectionStatus = status - credentialChangeError = error - credentialChangeHandler = handler - } - } - - /// Returns the actual screen title based on the operation. - /// - /// - Returns: The actual screen title based on the operation. - func title() -> String { - switch operation { - case .enrollment: - return L10n.Pin.Enrollment.title - case .verification: - return L10n.Pin.Verify.title - case .credentialChange: - return L10n.Pin.Change.title - } - } - - /// Returns the actual screen description based on the operation. - /// - /// - Returns: The actual screen description based on the operation. - func description() -> String { - switch operation { - case .enrollment: - return L10n.Pin.Enrollment.description - case .verification: - return L10n.Pin.Verify.description - case .credentialChange: - return L10n.Pin.Change.description - } - } - - /// Returns the actual last recoverable error based on the operation. - /// - /// - Returns: The actual last recoverable error based on the operation. - func lastRecoverableError() -> String { - switch operation { - case .enrollment: - return enrollmentError?.localizedDescription ?? String() - case .verification: - return verificationError?.localizedDescription ?? String() - case .credentialChange: - return credentialChangeError?.localizedDescription ?? String() - } - } - - /// Clears the state of the view model. - /// - /// - Returns: An observable sequence. - func clear() -> Observable<()> { - Observable.create { - self.enrollmentHandler = nil - self.verificationHandler = nil - self.credentialChangeHandler = nil - $0.onCompleted() - return Disposables.create() - } - } - - /// Confirms the given credentials. - /// - /// - Parameters: - /// - oldPin: The old PIN. - /// - pin: The PIN. - /// - Returns: An observable sequence. - func confirm(oldPin: String, pin: String) -> Observable<()> { - logger.log("Confirming entered credentials.") - return Observable.create { - switch self.operation { - case .enrollment: - self.enrollmentHandler!.pin(pin) - case .verification: - self.verificationHandler!.verify(pin) - case .credentialChange: - self.credentialChangeHandler!.pins(oldPin, pin) - } - - $0.onCompleted() - return Disposables.create() - } - } - - /// Cancels the actual operation. - func cancel() { - switch operation { - case .enrollment: - logger.log("Cancelling PIN enrollment.") - enrollmentHandler?.cancel() - enrollmentHandler = nil - case .verification: - logger.log("Cancelling PIN verification.") - verificationHandler?.cancel() - verificationHandler = nil - case .credentialChange: - logger.log("Cancelling PIN change.") - credentialChangeHandler?.cancel() - credentialChangeHandler = nil - } - } - - /// Handles the PIN authenticator protection status. - func handleProtectionStatus() { - switch protectionStatus { - case .Unlocked, .none: - logger.log("PIN authenticator is unlocked.") - pinProtectionInfoSubject.accept(PinProtectionInformation()) - case let .LastAttemptFailed(remainingTries, coolDown): - logger.log("Last attempt failed using the PIN authenticator.") - let info = PinProtectionInformation(message: protectionStatus?.localizedDescription ?? String(), - isInCoolDown: coolDown > 0) - pinProtectionInfoSubject.accept(info) - - if coolDown > 0 { - startCoolDownTimer(with: coolDown, remainingTries: remainingTries) - } - case .LockedOut: - logger.log("PIN authenticator is locked.") - let info = PinProtectionInformation(message: protectionStatus?.localizedDescription ?? String()) - pinProtectionInfoSubject.accept(info) - case .some: - logger.log("Unknown PIN authenticator protection status.") - pinProtectionInfoSubject.accept(PinProtectionInformation()) - } - } - - /// Starts the cooldown timer. - /// - /// - Parameters: - /// - cooldown: The cooldown of the timer. - /// - remainingTries: The number of remaining tries. - func startCoolDownTimer(with coolDown: Int, remainingTries: Int) { - coolDownTimer = InteractionCountDownTimer(timerLifeTime: TimeInterval(coolDown)) { remainingCoolDown in - let status: PinAuthenticatorProtectionStatus = .LastAttemptFailed(remainingTries: remainingTries, - coolDownTimeInSeconds: remainingCoolDown) - let info = PinProtectionInformation(message: status.localizedDescription, - isInCoolDown: remainingCoolDown > 0) - self.pinProtectionInfoSubject.accept(info) - } - - coolDownTimer?.start() - } -} diff --git a/NevisExampleApp/Presentation/Utility/Extensions/Authenticator+LocalizedExtension.swift b/NevisExampleApp/Presentation/Utility/Extensions/Authenticator+LocalizedExtension.swift index 1b78c39..e343b10 100644 --- a/NevisExampleApp/Presentation/Utility/Extensions/Authenticator+LocalizedExtension.swift +++ b/NevisExampleApp/Presentation/Utility/Extensions/Authenticator+LocalizedExtension.swift @@ -12,15 +12,17 @@ extension Authenticator { var localizedTitle: String { switch aaid { case AuthenticatorAaid.Pin.rawValue: - return L10n.Authenticator.Pin.title + L10n.Authenticator.Pin.title case AuthenticatorAaid.FaceRecognition.rawValue: - return L10n.Authenticator.FaceRecognition.title + L10n.Authenticator.FaceRecognition.title case AuthenticatorAaid.Fingerprint.rawValue: - return L10n.Authenticator.Fingerprint.title + L10n.Authenticator.Fingerprint.title case AuthenticatorAaid.DevicePasscode.rawValue: - return L10n.Authenticator.DevicePasscode.title + L10n.Authenticator.DevicePasscode.title + case AuthenticatorAaid.Password.rawValue: + L10n.Authenticator.Password.title default: - return String() + String() } } } diff --git a/NevisExampleApp/Presentation/Utility/Extensions/AuthenticatorAaid+Extension.swift b/NevisExampleApp/Presentation/Utility/Extensions/AuthenticatorAaid+Extension.swift new file mode 100644 index 0000000..ca225a3 --- /dev/null +++ b/NevisExampleApp/Presentation/Utility/Extensions/AuthenticatorAaid+Extension.swift @@ -0,0 +1,29 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +/// Codable extension for AuthenticatorAaids. +extension AuthenticatorAaid: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let aaid = try container.decode(String.self) + guard let authenticatorAaid = AuthenticatorAaid(rawValue: aaid) else { + throw NSError(domain: "AuthenticatorAaid cannot be decoced", code: 1) + } + self = authenticatorAaid + } +} + +extension AuthenticatorAaid { + static func == (lhs: String, rhs: AuthenticatorAaid) -> Bool { + rhs.rawValue == lhs + } + + static func == (lhs: AuthenticatorAaid, rhs: String) -> Bool { + rhs == lhs.rawValue + } +} diff --git a/NevisExampleApp/Presentation/Utility/Extensions/BusinessError+Extension.swift b/NevisExampleApp/Presentation/Utility/Extensions/BusinessError+Extension.swift index 7e7859e..1aa92fb 100644 --- a/NevisExampleApp/Presentation/Utility/Extensions/BusinessError+Extension.swift +++ b/NevisExampleApp/Presentation/Utility/Extensions/BusinessError+Extension.swift @@ -10,13 +10,13 @@ extension BusinessError: LocalizedError { public var errorDescription: String? { switch self { case .authenticatorNotFound: - return L10n.Error.Business.authenticatorNotFound + L10n.Error.Business.authenticatorNotFound case .deviceInformationNotFound: - return L10n.Error.Business.deviceInformationNotFound + L10n.Error.Business.deviceInformationNotFound case .accountsNotFound: - return L10n.Error.Business.accountsNotFound + L10n.Error.Business.accountsNotFound case .loginFailed: - return L10n.Error.Business.loginFailed + L10n.Error.Business.loginFailed } } } diff --git a/NevisExampleApp/Presentation/Utility/Extensions/Operation+Extension.swift b/NevisExampleApp/Presentation/Utility/Extensions/Operation+Extension.swift index 4de3fa4..3eeb742 100644 --- a/NevisExampleApp/Presentation/Utility/Extensions/Operation+Extension.swift +++ b/NevisExampleApp/Presentation/Utility/Extensions/Operation+Extension.swift @@ -12,25 +12,27 @@ extension Operation { var localizedTitle: String { switch self { case .initClient: - return L10n.Operation.InitClient.title + L10n.Operation.InitClient.title case .payloadDecode: - return L10n.Operation.PayloadDecode.title + L10n.Operation.PayloadDecode.title case .outOfBand: - return L10n.Operation.OutOfBand.title + L10n.Operation.OutOfBand.title case .registration: - return L10n.Operation.Registration.title + L10n.Operation.Registration.title case .authentication: - return L10n.Operation.Authentication.title + L10n.Operation.Authentication.title case .deregistration: - return L10n.Operation.Deregistration.title + L10n.Operation.Deregistration.title case .pinChange: - return L10n.Operation.Pinchange.title + L10n.Operation.PinChange.title + case .passwordChange: + L10n.Operation.PasswordChange.title case .deviceInformationChange: - return L10n.Operation.DeviceInformationChange.title + L10n.Operation.DeviceInformationChange.title case .localData: - return L10n.Operation.LocalData.title + L10n.Operation.LocalData.title case .unknown: - return String() + String() } } } diff --git a/NevisExampleApp/Presentation/Utility/Extensions/PasswordAuthenticatorProtectionStatus+Extension.swift b/NevisExampleApp/Presentation/Utility/Extensions/PasswordAuthenticatorProtectionStatus+Extension.swift new file mode 100644 index 0000000..5c42ac8 --- /dev/null +++ b/NevisExampleApp/Presentation/Utility/Extensions/PasswordAuthenticatorProtectionStatus+Extension.swift @@ -0,0 +1,37 @@ +// +// Nevis Mobile Authentication SDK Example App +// +// Copyright © 2024 Nevis Security AG. All rights reserved. +// + +import NevisMobileAuthentication + +extension PasswordAuthenticatorProtectionStatus { + /// Returns the localized description. + var localizedDescription: String { + switch self { + case .Unlocked: + return String() + case .LockedOut: + return L10n.Credential.Password.ProtectionStatus.lockedOut + case let .LastAttemptFailed(remainingTries, coolDown): + var localizedDescription = "" + switch (remainingTries, coolDown) { + case (1, 0): + localizedDescription = L10n.Credential.Password.ProtectionStatus.lastRetryWithoutCoolDown(remainingTries) + case (1, 1...): + localizedDescription = L10n.Credential.Password.ProtectionStatus.lastRetryWithCoolDown(remainingTries, + String(format: "%.0f", Double(coolDown))) + case (2..., 1...): + localizedDescription = L10n.Credential.Password.ProtectionStatus.retriesWithCoolDown(remainingTries, + String(format: "%.0f", Double(coolDown))) + default: + localizedDescription = L10n.Credential.Password.ProtectionStatus.retriesWithoutCoolDown(remainingTries) + } + + return localizedDescription + @unknown default: + return String() + } + } +} diff --git a/NevisExampleApp/Presentation/Utility/Extensions/PinAuthenticatorProtectionStatus+Extension.swift b/NevisExampleApp/Presentation/Utility/Extensions/PinAuthenticatorProtectionStatus+Extension.swift index 9647702..54b9a35 100644 --- a/NevisExampleApp/Presentation/Utility/Extensions/PinAuthenticatorProtectionStatus+Extension.swift +++ b/NevisExampleApp/Presentation/Utility/Extensions/PinAuthenticatorProtectionStatus+Extension.swift @@ -13,20 +13,20 @@ extension PinAuthenticatorProtectionStatus { case .Unlocked: return String() case .LockedOut: - return L10n.Pin.ProtectionStatus.lockedOut + return L10n.Credential.Pin.ProtectionStatus.lockedOut case let .LastAttemptFailed(remainingTries, coolDown): var localizedDescription = "" switch (remainingTries, coolDown) { case (1, 0): - localizedDescription = L10n.Pin.ProtectionStatus.lastRetryWithoutCoolDown(remainingTries) + localizedDescription = L10n.Credential.Pin.ProtectionStatus.lastRetryWithoutCoolDown(remainingTries) case (1, 1...): - localizedDescription = L10n.Pin.ProtectionStatus.lastRetryWithCoolDown(remainingTries, - String(format: "%.0f", Double(coolDown))) + localizedDescription = L10n.Credential.Pin.ProtectionStatus.lastRetryWithCoolDown(remainingTries, + String(format: "%.0f", Double(coolDown))) case (2..., 1...): - localizedDescription = L10n.Pin.ProtectionStatus.retriesWithCoolDown(remainingTries, - String(format: "%.0f", Double(coolDown))) + localizedDescription = L10n.Credential.Pin.ProtectionStatus.retriesWithCoolDown(remainingTries, + String(format: "%.0f", Double(coolDown))) default: - localizedDescription = L10n.Pin.ProtectionStatus.retriesWithoutCoolDown(remainingTries) + localizedDescription = L10n.Credential.Pin.ProtectionStatus.retriesWithoutCoolDown(remainingTries) } return localizedDescription diff --git a/NevisExampleApp/Presentation/Utility/Localization/Strings.swift b/NevisExampleApp/Presentation/Utility/Localization/Strings.swift index 42be3dc..44d4fb3 100644 --- a/NevisExampleApp/Presentation/Utility/Localization/Strings.swift +++ b/NevisExampleApp/Presentation/Utility/Localization/Strings.swift @@ -37,6 +37,8 @@ enum L10n { static let deregister = L10n.tr("home_deregister_button") /// Change PIN button: "PIN Change" static let changePin = L10n.tr("home_pin_change_button") + /// Change Password button: "Password Change" + static let changePassword = L10n.tr("home_password_change_button") /// Change device information button: "Change Device Information" static let changeDeviceInformation = L10n.tr("home_change_device_information_button") /// Auth Cloud Api registration button: "Auth Cloud Api Registration" @@ -65,92 +67,178 @@ enum L10n { static let authenticatorNotPolicyCompliant = L10n.tr("authenticator_selection_authenticator_is_not_policy_compliant") } - /// Pin screen related localized strings. - enum Pin { - /// Pin enrollment related localized strings. - enum Enrollment { - /// Screen title: "Create PIN" - static let title = L10n.tr("pin_enrollment_title") - /// Screen description: "Please define a six digit PIN." - static let description = L10n.tr("pin_enrollment_description") - } + /// Credential screen related localized strings. + enum Credential { + /// PIN screen related localized strings. + enum Pin { + /// PIN enrollment related localized strings. + enum Enrollment { + /// Screen title: "Create PIN" + static let title = L10n.tr("pin_enrollment_title") + /// Screen description: "Please define a six digit PIN." + static let description = L10n.tr("pin_enrollment_description") + } - /// Pin verification related localized strings. - enum Verify { - /// Screen title: "Verify PIN" - static let title = L10n.tr("pin_verify_title") - /// Screen description: "Please enter your PIN to complete the process." - static let description = L10n.tr("pin_verify_description") - } + /// PIN verification related localized strings. + enum Verify { + /// Screen title: "Verify PIN" + static let title = L10n.tr("pin_verify_title") + /// Screen description: "Please enter your PIN to complete the process." + static let description = L10n.tr("pin_verify_description") + } - /// Pin change related localized strings. - enum Change { - /// Screen title: "Change PIN" - static let title = L10n.tr("pin_change_title") - /// Screen description: "Please define a six digit PIN." - static let description = L10n.tr("pin_change_description") - } + /// PIN change related localized strings. + enum Change { + /// Screen title: "Change PIN" + static let title = L10n.tr("pin_change_title") + /// Screen description: "Please define a six digit PIN." + static let description = L10n.tr("pin_change_description") + } - /// Pin protection status related localized strings. - enum ProtectionStatus { - /// Screen description: "Please define a six digit PIN." - static let lockedOut = L10n.tr("pin_protection_status_locked_out") + /// PIN protection status related localized strings. + enum ProtectionStatus { + /// Screen description: "Please define a six digit PIN." + static let lockedOut = L10n.tr("pin_protection_status_locked_out") + + /// PIN protection status message for last retry with cool down: "You have %@ try left.\nPlease retry in %@ seconds.\nAfter that your PIN will be blocked." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - parameter coolDown: The cool down time. + /// - returns: The localized string. + static func lastRetryWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { + L10n.tr("pin_protection_status_last_retry_with_cool_down", + "Localizable", + String(describing: remainingTries), + String(describing: coolDown)) + } - /// Pin protection status message for last retry with cool down: "You have %@ try left.\nPlease retry in %@ seconds.\nAfter that your PIN will be blocked." - /// - /// - parameter remainingTries: The number of remaining retries. - /// - parameter coolDown: The cool down time. - /// - returns: The localized string. - static func lastRetryWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { - L10n.tr("pin_protection_status_last_retry_with_cool_down", - "Localizable", - String(describing: remainingTries), - String(describing: coolDown)) + /// PIN protection status message for last retry without cool down: "You have %@ try left.\nAfter that your PIN will be blocked." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - returns: The localized string. + static func lastRetryWithoutCoolDown(_ remainingTries: Any) -> String { + L10n.tr("pin_protection_status_last_retry_without_cool_down", + "Localizable", + String(describing: remainingTries)) + } + + /// PIN protection status message for remaining retries with cool down: "You have %@ tries left.\nPlease retry in %@ seconds." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - parameter coolDown: The cool down time. + /// - returns: The localized string. + static func retriesWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { + L10n.tr("pin_protection_status_retries_with_cool_down", + "Localizable", + String(describing: remainingTries), + String(describing: coolDown)) + } + + /// PIN protection status message for remaining retries without cool down: "You have %@ tries left." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - returns: The localized string. + static func retriesWithoutCoolDown(_ remainingTries: Any) -> String { + L10n.tr("pin_protection_status_retries_without_cool_down", + "Localizable", + String(describing: remainingTries)) + } } - /// Pin protection status message for last retry without cool down: "You have %@ try left.\nAfter that your PIN will be blocked." - /// - /// - parameter remainingTries: The number of remaining retries. - /// - returns: The localized string. - static func lastRetryWithoutCoolDown(_ remainingTries: Any) -> String { - L10n.tr("pin_protection_status_last_retry_without_cool_down", - "Localizable", - String(describing: remainingTries)) + /// PIN field placeholder: "Enter PIN" + static let pinPlaceholder = L10n.tr("pin_pin_placeholder") + /// PIN confirm field placeholder: "Enter old PIN" + static let oldPinPlaceholder = L10n.tr("pin_old_pin_placeholder") + } + + /// Password related localized strings. + enum Password { + /// Password enrollment related localized strings. + enum Enrollment { + /// Screen title: "Create Password" + static let title = L10n.tr("password_enrollment_title") + /// Screen description: "Please define a Password." + static let description = L10n.tr("password_enrollment_description") } - /// Pin protection status message for remaining retries with cool down: "You have %@ tries left.\nPlease retry in %@ seconds." - /// - /// - parameter remainingTries: The number of remaining retries. - /// - parameter coolDown: The cool down time. - /// - returns: The localized string. - static func retriesWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { - L10n.tr("pin_protection_status_retries_with_cool_down", - "Localizable", - String(describing: remainingTries), - String(describing: coolDown)) + /// Password verification related localized strings. + enum Verify { + /// Screen title: "Verify Password" + static let title = L10n.tr("password_verify_title") + /// Screen description: "Please enter your Password to complete the process." + static let description = L10n.tr("password_verify_description") } - /// Pin protection status message for remaining retries without cool down: "You have %@ tries left." - /// - /// - parameter remainingTries: The number of remaining retries. - /// - returns: The localized string. - static func retriesWithoutCoolDown(_ remainingTries: Any) -> String { - L10n.tr("pin_protection_status_retries_without_cool_down", - "Localizable", - String(describing: remainingTries)) + /// Password change related localized strings. + enum Change { + /// Screen title: "Change Password" + static let title = L10n.tr("password_change_title") + /// Screen description: "Please define a six digit Password." + static let description = L10n.tr("password_change_description") } + + /// Password protection status related localized strings. + enum ProtectionStatus { + /// Screen description: "Please define a six digit Password." + static let lockedOut = L10n.tr("password_protection_status_locked_out") + + /// Password protection status message for last retry with cool down: "You have %@ try left.\nPlease retry in %@ seconds.\nAfter that your Password will be blocked." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - parameter coolDown: The cool down time. + /// - returns: The localized string. + static func lastRetryWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { + L10n.tr("password_protection_status_last_retry_with_cool_down", + "Localizable", + String(describing: remainingTries), + String(describing: coolDown)) + } + + /// Password protection status message for last retry without cool down: "You have %@ try left.\nAfter that your Password will be blocked." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - returns: The localized string. + static func lastRetryWithoutCoolDown(_ remainingTries: Any) -> String { + L10n.tr("password_protection_status_last_retry_without_cool_down", + "Localizable", + String(describing: remainingTries)) + } + + /// Password protection status message for remaining retries with cool down: "You have %@ tries left.\nPlease retry in %@ seconds." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - parameter coolDown: The cool down time. + /// - returns: The localized string. + static func retriesWithCoolDown(_ remainingTries: Any, _ coolDown: Any) -> String { + L10n.tr("password_protection_status_retries_with_cool_down", + "Localizable", + String(describing: remainingTries), + String(describing: coolDown)) + } + + /// Password protection status message for remaining retries without cool down: "You have %@ tries left." + /// + /// - parameter remainingTries: The number of remaining retries. + /// - returns: The localized string. + static func retriesWithoutCoolDown(_ remainingTries: Any) -> String { + L10n.tr("password_protection_status_retries_without_cool_down", + "Localizable", + String(describing: remainingTries)) + } + } + + /// Password field placeholder: "Enter Password" + static let passwordPlaceholder = L10n.tr("password_password_placeholder") + /// Password confirm field placeholder: "Enter old Password" + static let oldPasswordPlaceholder = L10n.tr("password_old_password_placeholder") } - /// Pin field placeholder: "Enter PIN" - static let pinPlaceholder = L10n.tr("pin_pin_placeholder") - /// Pin confirm field placeholder: "Enter old PIN" - static let oldPinPlaceholder = L10n.tr("pin_old_pin_placeholder") /// Done button: "Done" - static let done = L10n.tr("pin_done_button") + static let done = L10n.tr("credential_done_button") /// Confirm button: "Confirm" - static let confirm = L10n.tr("pin_confirm_button") + static let confirm = L10n.tr("credential_confirm_button") /// Cancel button: "Cancel" - static let cancel = L10n.tr("pin_cancel_button") + static let cancel = L10n.tr("credential_cancel_button") } /// Transaction Confirmation screen related localized strings. @@ -181,9 +269,9 @@ enum L10n { /// Name field placeholder: "New name" static let namePlaceholder = L10n.tr("change_device_information_name_placeholder") /// Confirm button: "Confirm" - static let confirm = L10n.tr("transaction_confirmation_confirm_button") + static let confirm = L10n.tr("change_device_information_confirm_button") /// Confirm button: "Cancel" - static let cancel = L10n.tr("transaction_confirmation_cancel_button") + static let cancel = L10n.tr("change_device_information_cancel_button") } /// Auth Cloud Api Registration screen related localized strings. @@ -292,11 +380,17 @@ enum L10n { } /// PIN change operation related localized strings. - enum Pinchange { + enum PinChange { /// Operation title: "PIN change" static let title = L10n.tr("operation_pin_change_title") } + /// Password change operation related localized strings. + enum PasswordChange { + /// Operation title: "Password change" + static let title = L10n.tr("operation_password_change_title") + } + /// Device information change operation related localized strings. enum DeviceInformationChange { /// Operation title: "Device information change" @@ -375,6 +469,12 @@ enum L10n { /// Title: "Device Passcode" static let title = L10n.tr("authenticator_device_passcode_title") } + + /// Password authenticator related localized strings. + enum Password { + /// Title: "Password" + static let title = L10n.tr("authenticator_device_password_title") + } } /// Error related localized strings. @@ -418,6 +518,8 @@ enum L10n { static let cookieNotFound = L10n.tr("error_app_cookie_not_found_error_message") /// PIN authenticator not found error message: "Pin authenticator not found." static let pinAuthenticatorNotFound = L10n.tr("error_app_pin_authenticator_not_found_message") + /// Password authenticator not found error message: "Password authenticator not found." + static let passwordAuthenticatorNotFound = L10n.tr("error_app_password_authenticator_not_found_message") } /// Business error related localized strings. diff --git a/NevisExampleApp/Presentation/Utility/Response Observer/ResponseObserverImpl.swift b/NevisExampleApp/Presentation/Utility/Response Observer/ResponseObserverImpl.swift index 438cf82..e734bc1 100644 --- a/NevisExampleApp/Presentation/Utility/Response Observer/ResponseObserverImpl.swift +++ b/NevisExampleApp/Presentation/Utility/Response Observer/ResponseObserverImpl.swift @@ -75,13 +75,24 @@ private extension ResponseObserverImpl { else if let response = response as? EnrollPinResponse { let parameter: PinParameter = .enrollment(lastRecoverableError: response.lastRecoverableError, handler: response.handler) - appCoordinator.navigateToPin(with: parameter) + appCoordinator.navigateToCredential(with: parameter) + } + else if let response = response as? EnrollPasswordResponse { + let parameter: PasswordParameter = .enrollment(lastRecoverableError: response.lastRecoverableError, + handler: response.handler) + appCoordinator.navigateToCredential(with: parameter) } else if let response = response as? VerifyPinResponse { let parameter: PinParameter = .verification(protectionStatus: response.protectionStatus, lastRecoverableError: response.lastRecoverableError, handler: response.handler) - appCoordinator.navigateToPin(with: parameter) + appCoordinator.navigateToCredential(with: parameter) + } + else if let response = response as? VerifyPasswordResponse { + let parameter: PasswordParameter = .verification(protectionStatus: response.protectionStatus, + lastRecoverableError: response.lastRecoverableError, + handler: response.handler) + appCoordinator.navigateToCredential(with: parameter) } else if let response = response as? VerifyBiometricResponse { let parameter: ConfirmationParameter = .confirmBiometric(authenticator: response.authenticator, @@ -103,7 +114,13 @@ private extension ResponseObserverImpl { let parameter: PinParameter = .credentialChange(protectionStatus: response.protectionStatus, lastRecoverableError: response.lastRecoverableError, handler: response.handler) - appCoordinator.navigateToPin(with: parameter) + appCoordinator.navigateToCredential(with: parameter) + } + else if let response = response as? ChangePasswordResponse { + let parameter: PasswordParameter = .credentialChange(protectionStatus: response.protectionStatus, + lastRecoverableError: response.lastRecoverableError, + handler: response.handler) + appCoordinator.navigateToCredential(with: parameter) } else if let response = response as? CompletedResponse { appCoordinator.navigateToResult(with: .success(operation: response.operation)) diff --git a/NevisExampleApp/Presentation/Utility/UI/Label/Style.swift b/NevisExampleApp/Presentation/Utility/UI/Label/Style.swift index fafc6a6..81cd8cd 100644 --- a/NevisExampleApp/Presentation/Utility/UI/Label/Style.swift +++ b/NevisExampleApp/Presentation/Utility/UI/Label/Style.swift @@ -28,28 +28,28 @@ enum Style { /// The font based on the current style. var font: UIFont? { switch self { - case .title: return UIFont(name: "HelveticaNeue-Bold", size: 22) - case .normal: return UIFont(name: "HelveticaNeue", size: 17) - case .detail: return UIFont(name: "HelveticaNeue-Light", size: 14) - case .info, .error: return UIFont(name: "HelveticaNeue", size: 12) + case .title: UIFont(name: "HelveticaNeue-Bold", size: 22) + case .normal: UIFont(name: "HelveticaNeue", size: 17) + case .detail: UIFont(name: "HelveticaNeue-Light", size: 14) + case .info, .error: UIFont(name: "HelveticaNeue", size: 12) } } /// The text color based on the current style. var textColor: UIColor { switch self { - case .detail: return .lightGray - case .info: return .blue - case .error: return .red - default: return .black + case .detail: .lightGray + case .info: .blue + case .error: .red + default: .black } } /// The text alignment based on the current style. var textAlignment: NSTextAlignment { switch self { - case .title: return .center - default: return .left + case .title: .center + default: .left } } } diff --git a/NevisExampleApp/Presentation/Utility/Validation/ValidationResult.swift b/NevisExampleApp/Presentation/Utility/Validation/ValidationResult.swift index 214835a..407d7b7 100644 --- a/NevisExampleApp/Presentation/Utility/Validation/ValidationResult.swift +++ b/NevisExampleApp/Presentation/Utility/Validation/ValidationResult.swift @@ -15,9 +15,9 @@ extension Result where Failure == ValidationError { var message: String { switch self { case .success: - return "" + "" case let .failure(error): - return error.description + error.description } } @@ -25,9 +25,9 @@ extension Result where Failure == ValidationError { var isValid: Bool { switch self { case .success: - return true + true case .failure: - return false + false } } } diff --git a/NevisExampleApp/Resources/ConfigAuthenticationCloud.plist b/NevisExampleApp/Resources/ConfigAuthenticationCloud.plist index 307c7f5..f6bfc90 100644 --- a/NevisExampleApp/Resources/ConfigAuthenticationCloud.plist +++ b/NevisExampleApp/Resources/ConfigAuthenticationCloud.plist @@ -12,5 +12,13 @@ hostName myinstance.mauth.nevis.cloud + authenticatorWhitelist + + F1D0#1001 + F1D0#1002 + F1D0#1003 + F1D0#1004 + F1D0#1005 + diff --git a/NevisExampleApp/Resources/ConfigIdentitySuite.plist b/NevisExampleApp/Resources/ConfigIdentitySuite.plist index 1b93c8e..98fde73 100644 --- a/NevisExampleApp/Resources/ConfigIdentitySuite.plist +++ b/NevisExampleApp/Resources/ConfigIdentitySuite.plist @@ -35,5 +35,13 @@ userInteractionTimeoutInSeconds 240 + authenticatorWhitelist + + F1D0#1001 + F1D0#1002 + F1D0#1003 + F1D0#1004 + F1D0#1005 + diff --git a/NevisExampleApp/Resources/en.lproj/Localizable.strings b/NevisExampleApp/Resources/en.lproj/Localizable.strings index 80879c5..dac9cec 100644 --- a/NevisExampleApp/Resources/en.lproj/Localizable.strings +++ b/NevisExampleApp/Resources/en.lproj/Localizable.strings @@ -9,6 +9,7 @@ "home_authenticate_button" = "In-Band Authenticate"; "home_deregister_button" = "Deregister"; "home_pin_change_button" = "PIN change"; +"home_password_change_button" = "Password change"; "home_change_device_information_button" = "Change Device Information"; "home_auth_cloud_api_registration_button" = "Auth Cloud Api Registration"; "home_delete_authenticators_button" = "Delete Authenticators"; @@ -23,6 +24,11 @@ "authenticator_selection_authenticator_is_not_enrolled" = "Before using the authenticator, enroll it in the phone System Settings"; "authenticator_selection_authenticator_is_not_policy_compliant" = "This authentication method cannot be used with your application"; +// Credential screen +"credential_done_button" = "Done"; +"credential_confirm_button" = "Confirm"; +"credential_cancel_button" = "Cancel"; + // Pin screen "pin_enrollment_title" = "Create PIN"; "pin_enrollment_description" = "Please define a six digit PIN."; @@ -32,15 +38,27 @@ "pin_change_description" = "Please define a six digit PIN."; "pin_old_pin_placeholder" = "Enter old PIN"; "pin_pin_placeholder" = "Enter PIN"; -"pin_done_button" = "Done"; -"pin_confirm_button" = "Confirm"; -"pin_cancel_button" = "Cancel"; "pin_protection_status_locked_out" = "Your PIN is blocked."; "pin_protection_status_last_retry_with_cool_down" = "You have %@ try left.\nPlease retry in %@ seconds.\nAfter that your PIN will be blocked."; "pin_protection_status_last_retry_without_cool_down" = "You have %@ try left.\nAfter that your PIN will be blocked."; "pin_protection_status_retries_with_cool_down" = "You have %@ tries left.\nPlease retry in %@ seconds."; "pin_protection_status_retries_without_cool_down" = "You have %@ tries left."; +// Password screen +"password_enrollment_title" = "Create Password"; +"password_enrollment_description" = "Please define a Password."; +"password_verify_title" = "Verify Password"; +"password_verify_description" = "Please enter your Password to complete the process."; +"password_change_title" = "Change Password"; +"password_change_description" = "Please define a Password."; +"password_old_password_placeholder" = "Enter old Password"; +"password_password_placeholder" = "Enter Password"; +"password_protection_status_locked_out" = "Your Password is blocked."; +"password_protection_status_last_retry_with_cool_down" = "You have %@ try left.\nPlease retry in %@ seconds.\nAfter that your Password will be blocked."; +"password_protection_status_last_retry_without_cool_down" = "You have %@ try left.\nAfter that your Password will be blocked."; +"password_protection_status_retries_with_cool_down" = "You have %@ tries left.\nPlease retry in %@ seconds."; +"password_protection_status_retries_without_cool_down" = "You have %@ tries left."; + // Transaction Confirmation screen "transaction_confirmation_title" = "Transaction Confirmation"; "transaction_confirmation_confirm_button" = "Confirm"; @@ -90,6 +108,7 @@ "operation_authentication_title" = "Authentication"; "operation_deregistration_title" = "Deregistration"; "operation_pin_change_title" = "PIN change"; +"operation_password_change_title" = "Password change"; "operation_device_information_change_title" = "Device information change"; "operation_local_data_title" = "Local data operation"; @@ -106,6 +125,7 @@ "error_app_read_login_configuration_error_message" = "Failed to read the login configuration."; "error_app_cookie_not_found_error_message" = "No cookie was provided in the login response."; "error_app_pin_authenticator_not_found_message" = "Pin authenticator not found."; +"error_app_password_authenticator_not_found_message" = "Password authenticator not found."; "error_app_qr_scan_title" = "Error"; "error_app_qr_scan_unavailable_message" = "Qr Code scanning is not supported using a simulator. You can start an Out-of-Band operation by opening a deep link or try out the Auth Cloud API registration."; "error_app_qr_scan_unauthorized_message" = "Camera access is required to scan Qr code."; @@ -120,3 +140,4 @@ "authenticator_face_recognition_title" = "Face ID"; "authenticator_fingerprint_title" = "Touch ID"; "authenticator_device_passcode_title" = "Device Passcode"; +"authenticator_device_password_title" = "Password";