From 93f4e2e344068b73a3f9679e530bb13a7b08ed00 Mon Sep 17 00:00:00 2001 From: Tamas Toth Date: Mon, 29 Jul 2024 09:20:34 +0200 Subject: [PATCH] NEVISACCESSAPP-5666: Password authenticator and policy --- README.md | 6 +- .../application/ExampleApplication.kt | 2 +- .../exampleapp/dagger/ApplicationModule.kt | 31 ++ .../password/PasswordChangerImpl.kt | 63 ++++ .../password/PasswordEnrollerImpl.kt | 66 ++++ .../password/PasswordUserVerifierImpl.kt | 59 ++++ .../interaction/{ => pin}/PinChangerImpl.kt | 14 +- .../interaction/{ => pin}/PinEnrollerImpl.kt | 12 +- .../{ => pin}/PinUserVerifierImpl.kt | 14 +- .../domain/model/error/BusinessException.kt | 32 +- .../domain/model/operation/Operation.kt | 7 +- ...orProtectionStatusLastAttemptFailedImpl.kt | 21 ++ ...orProtectionStatusLastAttemptFailedImpl.kt | 21 ++ .../exampleapp/domain/util/Authenticator.kt | 1 + .../PasswordAuthenticatorProtectionStatus.kt | 43 +++ .../util/PinAuthenticatorProtectionStatus.kt | 43 +++ .../domain/validation/PasswordPolicyImpl.kt | 71 ++++ .../AuthCloudRegistrationViewModel.kt | 7 + .../CancelOperationOnBackPressedCallback.kt | 4 +- .../ChangeDeviceInformationFragment.kt | 6 +- .../ui/credential/CredentialFragment.kt | 260 ++++++++++++++ .../ui/credential/CredentialViewModel.kt | 316 ++++++++++++++++++ .../model/CredentialProtectionInformation.kt | 32 ++ .../ui/credential/model/CredentialViewData.kt | 131 ++++++++ .../ui/credential/model/CredentialViewMode.kt | 28 ++ .../CredentialNavigationParameter.kt | 24 ++ .../parameter/PasswordNavigationParameter.kt | 66 ++++ .../parameter/PinNavigationParameter.kt | 39 ++- .../nevis/exampleapp/ui/home/HomeFragment.kt | 9 +- .../nevis/exampleapp/ui/home/HomeViewModel.kt | 134 +++++++- .../ui/outOfBand/OutOfBandViewModel.kt | 14 + .../ch/nevis/exampleapp/ui/pin/PinFragment.kt | 205 ------------ .../nevis/exampleapp/ui/pin/PinViewModel.kt | 154 --------- .../exampleapp/ui/pin/model/PinViewData.kt | 90 ----- .../exampleapp/ui/pin/model/PinViewMode.kt | 28 -- .../ui/qrReader/QrReaderViewModel.kt | 14 + .../selectAccount/SelectAccountViewModel.kt | 40 ++- .../model/AuthenticatorItem.kt | 6 +- .../SelectAuthenticatorNavigationParameter.kt | 1 - .../UserNamePasswordLoginViewModel.kt | 7 + ...agment_pin.xml => fragment_credential.xml} | 45 ++- app/src/main/res/layout/fragment_home.xml | 240 +++++++------ .../main/res/navigation/navigation_graph.xml | 16 +- app/src/main/res/values/strings.xml | 48 ++- 44 files changed, 1766 insertions(+), 704 deletions(-) create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordChangerImpl.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordEnrollerImpl.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordUserVerifierImpl.kt rename app/src/main/java/ch/nevis/exampleapp/domain/interaction/{ => pin}/PinChangerImpl.kt (74%) rename app/src/main/java/ch/nevis/exampleapp/domain/interaction/{ => pin}/PinEnrollerImpl.kt (79%) rename app/src/main/java/ch/nevis/exampleapp/domain/interaction/{ => pin}/PinUserVerifierImpl.kt (81%) create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PinAuthenticatorProtectionStatusLastAttemptFailedImpl.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/util/PasswordAuthenticatorProtectionStatus.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/util/PinAuthenticatorProtectionStatus.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/domain/validation/PasswordPolicyImpl.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialFragment.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialProtectionInformation.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewData.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewMode.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/CredentialNavigationParameter.kt create mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PasswordNavigationParameter.kt rename app/src/main/java/ch/nevis/exampleapp/ui/{pin => credential}/parameter/PinNavigationParameter.kt (56%) delete mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/pin/PinFragment.kt delete mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/pin/PinViewModel.kt delete mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewData.kt delete mode 100644 app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewMode.kt rename app/src/main/res/layout/{fragment_pin.xml => fragment_credential.xml} (72%) diff --git a/README.md b/README.md index f456d0e..baa8723 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,11 @@ The [HomeViewModel](app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel. #### Change PIN -The change PIN operation is implemented in the [HomeViewModel](app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt), [SelectAccountViewModel](app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt) and [PinViewModel](app/src/main/java/ch/nevis/exampleapp/ui/pin/PinViewModel.kt) classes with which you can modify the PIN of a registered PIN authenticator for a given user. +The change PIN operation is implemented in the [HomeViewModel](app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt), [SelectAccountViewModel](app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt) and [CredentialViewModel](app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt) classes with which you can modify the PIN of a registered PIN authenticator for a given user. + +#### Change Password + +The change Password operation is implemented in the [HomeViewModel](app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt), [SelectAccountViewModel](app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt) and [CredentialViewModel](app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt) classes with which you can modify the password of a registered Password authenticator for a given user. #### Change device information diff --git a/app/src/main/java/ch/nevis/exampleapp/application/ExampleApplication.kt b/app/src/main/java/ch/nevis/exampleapp/application/ExampleApplication.kt index f25911f..db9eb34 100644 --- a/app/src/main/java/ch/nevis/exampleapp/application/ExampleApplication.kt +++ b/app/src/main/java/ch/nevis/exampleapp/application/ExampleApplication.kt @@ -32,4 +32,4 @@ class ExampleApplication : Application() { Timber.plant(ExampleAppTimberDebugTree(sdkLogger)) } //endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/dagger/ApplicationModule.kt b/app/src/main/java/ch/nevis/exampleapp/dagger/ApplicationModule.kt index e11912b..14d2529 100644 --- a/app/src/main/java/ch/nevis/exampleapp/dagger/ApplicationModule.kt +++ b/app/src/main/java/ch/nevis/exampleapp/dagger/ApplicationModule.kt @@ -21,17 +21,24 @@ import ch.nevis.exampleapp.domain.client.ClientProviderImpl import ch.nevis.exampleapp.domain.deviceInformation.DeviceInformationFactory import ch.nevis.exampleapp.domain.deviceInformation.DeviceInformationFactoryImpl import ch.nevis.exampleapp.domain.interaction.* +import ch.nevis.exampleapp.domain.interaction.password.* +import ch.nevis.exampleapp.domain.interaction.pin.* import ch.nevis.exampleapp.domain.log.SdkLogger import ch.nevis.exampleapp.domain.log.SdkLoggerImpl import ch.nevis.exampleapp.domain.validation.AuthenticatorValidator import ch.nevis.exampleapp.domain.validation.AuthenticatorValidatorImpl +import ch.nevis.exampleapp.domain.validation.PasswordPolicyImpl import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.navigation.NavigationDispatcherImpl import ch.nevis.mobile.sdk.api.Configuration import ch.nevis.mobile.sdk.api.localdata.Authenticator.BIOMETRIC_AUTHENTICATOR_AAID import ch.nevis.mobile.sdk.api.localdata.Authenticator.DEVICE_PASSCODE_AUTHENTICATOR_AAID import ch.nevis.mobile.sdk.api.localdata.Authenticator.FINGERPRINT_AUTHENTICATOR_AAID +import ch.nevis.mobile.sdk.api.localdata.Authenticator.PASSWORD_AUTHENTICATOR_AAID import ch.nevis.mobile.sdk.api.localdata.Authenticator.PIN_AUTHENTICATOR_AAID +import ch.nevis.mobile.sdk.api.operation.password.PasswordChanger +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller +import ch.nevis.mobile.sdk.api.operation.password.PasswordPolicy import ch.nevis.mobile.sdk.api.operation.pin.PinChanger import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AccountSelector @@ -39,6 +46,7 @@ import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.DevicePasscodeUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier import dagger.Module import dagger.Provides @@ -115,6 +123,7 @@ class ApplicationModule { @Provides fun provideAuthenticatorAllowlist(): List = listOf( PIN_AUTHENTICATOR_AAID, + PASSWORD_AUTHENTICATOR_AAID, FINGERPRINT_AUTHENTICATOR_AAID, BIOMETRIC_AUTHENTICATOR_AAID, DEVICE_PASSCODE_AUTHENTICATOR_AAID @@ -231,6 +240,28 @@ class ApplicationModule { @Provides fun providePinUserVerifier(navigationDispatcher: NavigationDispatcher): PinUserVerifier = PinUserVerifierImpl(navigationDispatcher) + + @Provides + fun providePasswordChanger( + passwordPolicy: PasswordPolicy, + navigationDispatcher: NavigationDispatcher + ): PasswordChanger = + PasswordChangerImpl(passwordPolicy, navigationDispatcher) + + @Provides + fun providePasswordEnroller( + passwordPolicy: PasswordPolicy, + navigationDispatcher: NavigationDispatcher + ): PasswordEnroller = + PasswordEnrollerImpl(passwordPolicy, navigationDispatcher) + + @Provides + fun providePasswordUserVerifier(navigationDispatcher: NavigationDispatcher): PasswordUserVerifier = + PasswordUserVerifierImpl(navigationDispatcher) + + @Provides + fun providePasswordPolicy(@ApplicationContext context: Context): PasswordPolicy = + PasswordPolicyImpl(context) //endregion //region Logger diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordChangerImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordChangerImpl.kt new file mode 100644 index 0000000..082df1e --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordChangerImpl.kt @@ -0,0 +1,63 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.interaction.password + +import ch.nevis.exampleapp.NavigationGraphDirections +import ch.nevis.exampleapp.logging.sdk +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PasswordNavigationParameter +import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher +import ch.nevis.mobile.sdk.api.operation.password.PasswordChangeContext +import ch.nevis.mobile.sdk.api.operation.password.PasswordChangeHandler +import ch.nevis.mobile.sdk.api.operation.password.PasswordChanger +import ch.nevis.mobile.sdk.api.operation.password.PasswordPolicy +import timber.log.Timber + +/** + * Default implementation of [PasswordChanger] interface. Navigates to Credential view with the received + * [PasswordChangeHandler], [ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus] + * and [ch.nevis.mobile.sdk.api.operation.password.PasswordChangeRecoverableError] objects. + */ +class PasswordChangerImpl( + /** + * An instance of a [PasswordPolicy] interface implementation. + */ + private val policy: PasswordPolicy, + + /** + * An instance of a [NavigationDispatcher] interface implementation. + */ + private val navigationDispatcher: NavigationDispatcher +) : PasswordChanger { + + //region PasswordChanger + override fun changePassword( + context: PasswordChangeContext, + handler: PasswordChangeHandler + ) { + if (context.lastRecoverableError().isPresent) { + Timber.asTree().sdk("Password change failed. Please try again.") + } else { + Timber.asTree().sdk("Please start Password change.") + } + + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalCredentialFragment( + PasswordNavigationParameter( + CredentialViewMode.CHANGE, + lastRecoverableError = context.lastRecoverableError().orElse(null), + passwordAuthenticatorProtectionStatus = context.authenticatorProtectionStatus(), + passwordChangeHandler = handler + ) + ) + ) + } + + // You can add custom password policy by overriding the `passwordPolicy` getter + override fun passwordPolicy(): PasswordPolicy = policy + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordEnrollerImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordEnrollerImpl.kt new file mode 100644 index 0000000..a56ba2a --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordEnrollerImpl.kt @@ -0,0 +1,66 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.interaction.password + +import ch.nevis.exampleapp.NavigationGraphDirections +import ch.nevis.exampleapp.logging.sdk +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PasswordNavigationParameter +import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentContext +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentHandler +import ch.nevis.mobile.sdk.api.operation.password.PasswordPolicy +import timber.log.Timber + +/** + * Default implementation of [PasswordEnroller] interface. Navigates to Credential view with the + * received [PasswordEnrollmentHandler] and [ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentError] + * objects. + */ +class PasswordEnrollerImpl( + /** + * An instance of a [PasswordPolicy] interface implementation. + */ + private val policy: PasswordPolicy, + + /** + * An instance of a [NavigationDispatcher] interface implementation. + */ + private val navigationDispatcher: NavigationDispatcher +) : PasswordEnroller { + + //region PasswordEnroller + override fun enrollPassword( + context: PasswordEnrollmentContext, + handler: PasswordEnrollmentHandler + ) { + if (context.lastRecoverableError().isPresent) { + Timber.asTree().sdk("Password enrollment failed. Please try again.") + } else { + Timber.asTree().sdk("Please start Password enrollment.") + } + + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalCredentialFragment( + PasswordNavigationParameter( + CredentialViewMode.ENROLLMENT, + lastRecoverableError = context.lastRecoverableError().orElse(null), + passwordEnrollmentHandler = handler + ) + ) + ) + } + + override fun onValidCredentialsProvided() { + Timber.asTree().sdk("Valid credentials provided during Password enrollment.") + } + + // You can add custom password policy by overriding the `passwordPolicy` getter + override fun passwordPolicy(): PasswordPolicy = policy + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordUserVerifierImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordUserVerifierImpl.kt new file mode 100644 index 0000000..b27267a --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/password/PasswordUserVerifierImpl.kt @@ -0,0 +1,59 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.interaction.password + +import ch.nevis.exampleapp.NavigationGraphDirections +import ch.nevis.exampleapp.logging.sdk +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PasswordNavigationParameter +import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerificationContext +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerificationHandler +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier +import timber.log.Timber + +/** + * Default implementation of [PasswordUserVerifier] interface. Navigates to Credential view with the + * received [PasswordUserVerificationHandler], [ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus] + * and [ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerificationError] objects. + */ +class PasswordUserVerifierImpl( + + /** + * An instance of a [NavigationDispatcher] interface implementation. + */ + private val navigationDispatcher: NavigationDispatcher +) : PasswordUserVerifier { + + //region PasswordUserVerifier + override fun verifyPassword( + context: PasswordUserVerificationContext, + handler: PasswordUserVerificationHandler + ) { + if (context.lastRecoverableError().isPresent) { + Timber.asTree().sdk("Password user verification failed. Please try again.") + } else { + Timber.asTree().sdk("Please start Password user verification.") + } + + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalCredentialFragment( + PasswordNavigationParameter( + CredentialViewMode.VERIFICATION, + lastRecoverableError = context.lastRecoverableError().orElse(null), + passwordAuthenticatorProtectionStatus = context.authenticatorProtectionStatus(), + passwordUserVerificationHandler = handler + ) + ) + ) + } + + override fun onValidCredentialsProvided() { + Timber.asTree().sdk("Valid credentials provided during Password verification.") + } + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinChangerImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinChangerImpl.kt similarity index 74% rename from app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinChangerImpl.kt rename to app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinChangerImpl.kt index 895f0b8..f597f3e 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinChangerImpl.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinChangerImpl.kt @@ -4,20 +4,20 @@ * Copyright © 2023. Nevis Security AG. All rights reserved. */ -package ch.nevis.exampleapp.domain.interaction +package ch.nevis.exampleapp.domain.interaction.pin import ch.nevis.exampleapp.NavigationGraphDirections import ch.nevis.exampleapp.logging.sdk import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher -import ch.nevis.exampleapp.ui.pin.model.PinViewMode -import ch.nevis.exampleapp.ui.pin.parameter.PinNavigationParameter +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PinNavigationParameter import ch.nevis.mobile.sdk.api.operation.pin.PinChangeContext import ch.nevis.mobile.sdk.api.operation.pin.PinChangeHandler import ch.nevis.mobile.sdk.api.operation.pin.PinChanger import timber.log.Timber /** - * Default implementation of [PinChanger] interface. It navigates to PIN view with the received + * Default implementation of [PinChanger] interface. Navigates to Credential view with the received * [PinChangeHandler], [ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus] and * [ch.nevis.mobile.sdk.api.operation.pin.PinChangeRecoverableError] objects. */ @@ -39,11 +39,11 @@ class PinChangerImpl( } navigationDispatcher.requestNavigation( - NavigationGraphDirections.actionGlobalPinFragment( + NavigationGraphDirections.actionGlobalCredentialFragment( PinNavigationParameter( - PinViewMode.CHANGE_PIN, - context.authenticatorProtectionStatus(), + CredentialViewMode.CHANGE, lastRecoverableError = context.lastRecoverableError().orElse(null), + pinAuthenticatorProtectionStatus = context.authenticatorProtectionStatus(), pinChangeHandler = handler ) ) diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinEnrollerImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinEnrollerImpl.kt similarity index 79% rename from app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinEnrollerImpl.kt rename to app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinEnrollerImpl.kt index 8148358..b06d879 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinEnrollerImpl.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinEnrollerImpl.kt @@ -4,20 +4,20 @@ * Copyright © 2023. Nevis Security AG. All rights reserved. */ -package ch.nevis.exampleapp.domain.interaction +package ch.nevis.exampleapp.domain.interaction.pin import ch.nevis.exampleapp.NavigationGraphDirections import ch.nevis.exampleapp.logging.sdk import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher -import ch.nevis.exampleapp.ui.pin.model.PinViewMode -import ch.nevis.exampleapp.ui.pin.parameter.PinNavigationParameter +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PinNavigationParameter import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.pin.PinEnrollmentContext import ch.nevis.mobile.sdk.api.operation.pin.PinEnrollmentHandler import timber.log.Timber /** - * Default implementation of [PinEnroller] interface. It navigates to PIN view with the received + * Default implementation of [PinEnroller] interface. Navigates to Credential view with the received * [PinEnrollmentHandler] and [ch.nevis.mobile.sdk.api.operation.pin.PinEnrollmentError] objects. */ class PinEnrollerImpl( @@ -40,9 +40,9 @@ class PinEnrollerImpl( } navigationDispatcher.requestNavigation( - NavigationGraphDirections.actionGlobalPinFragment( + NavigationGraphDirections.actionGlobalCredentialFragment( PinNavigationParameter( - PinViewMode.ENROLL_PIN, + CredentialViewMode.ENROLLMENT, lastRecoverableError = context.lastRecoverableError().orElse(null), pinEnrollmentHandler = handler ) diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinUserVerifierImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinUserVerifierImpl.kt similarity index 81% rename from app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinUserVerifierImpl.kt rename to app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinUserVerifierImpl.kt index 472b10b..2b3d67c 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/interaction/PinUserVerifierImpl.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/interaction/pin/PinUserVerifierImpl.kt @@ -4,20 +4,20 @@ * Copyright © 2023. Nevis Security AG. All rights reserved. */ -package ch.nevis.exampleapp.domain.interaction +package ch.nevis.exampleapp.domain.interaction.pin import ch.nevis.exampleapp.NavigationGraphDirections import ch.nevis.exampleapp.logging.sdk import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher -import ch.nevis.exampleapp.ui.pin.model.PinViewMode -import ch.nevis.exampleapp.ui.pin.parameter.PinNavigationParameter +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.PinNavigationParameter import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerificationContext import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerificationHandler import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier import timber.log.Timber /** - * Default implementation of [PinUserVerifier] interface. It navigates to PIN view with the received + * Default implementation of [PinUserVerifier] interface. Navigates to Credential view with the received * [PinUserVerificationHandler], [ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus] and * [ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerificationError] objects. */ @@ -41,11 +41,11 @@ class PinUserVerifierImpl( } navigationDispatcher.requestNavigation( - NavigationGraphDirections.actionGlobalPinFragment( + NavigationGraphDirections.actionGlobalCredentialFragment( PinNavigationParameter( - PinViewMode.VERIFY_PIN, - pinAuthenticatorProtectionStatus = context.authenticatorProtectionStatus(), + CredentialViewMode.VERIFICATION, lastRecoverableError = context.lastRecoverableError().orElse(null), + pinAuthenticatorProtectionStatus = context.authenticatorProtectionStatus(), pinUserVerificationHandler = handler ) ) diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/model/error/BusinessException.kt b/app/src/main/java/ch/nevis/exampleapp/domain/model/error/BusinessException.kt index cf8f991..8f64594 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/model/error/BusinessException.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/model/error/BusinessException.kt @@ -16,27 +16,37 @@ enum class BusinessExceptionType( ) { /** - * Registered accounts not found + * Registered accounts not found. */ ACCOUNTS_NOT_FOUND(R.string.business_error_type_accounts_not_found), /** - * Client not initialized + * The PIN authenticator not found. + */ + PIN_AUTHENTICATOR_NOT_FOUND(R.string.business_error_type_pin_authenticator_not_found), + + /** + * The Password authenticator not found. + */ + PASSWORD_AUTHENTICATOR_NOT_FOUND(R.string.business_error_type_password_authenticator_not_found), + + /** + * Client not initialized. */ CLIENT_NOT_INITIALIZED(R.string.business_error_type_client_not_initialized), /** - * Device information not found + * Device information not found. */ DEVICE_INFORMATION_NOT_FOUND(R.string.business_error_type_device_information_not_found), /** - * Invalid state + * Invalid state. */ INVALID_STATE(R.string.business_error_type_invalid_state), /** - * Password login failed + * Password login failed. */ LOGIN_FAILED(R.string.business_error_type_login_failed) } @@ -57,6 +67,16 @@ class BusinessException private constructor( */ fun accountsNotFound() = BusinessException(BusinessExceptionType.ACCOUNTS_NOT_FOUND) + /** + * Helper static method to initialize a [BusinessException] with type [BusinessExceptionType.PIN_AUTHENTICATOR_NOT_FOUND]. + */ + fun pinAuthenticatorNotFound() = BusinessException(BusinessExceptionType.PIN_AUTHENTICATOR_NOT_FOUND) + + /** + * Helper static method to initialize a [BusinessException] with type [BusinessExceptionType.PASSWORD_AUTHENTICATOR_NOT_FOUND]. + */ + fun passwordAuthenticatorNotFound() = BusinessException(BusinessExceptionType.PASSWORD_AUTHENTICATOR_NOT_FOUND) + /** * Helper static method to initialize a [BusinessException] with type [BusinessExceptionType.CLIENT_NOT_INITIALIZED]. */ @@ -78,4 +98,4 @@ class BusinessException private constructor( */ fun loginFailed() = BusinessException(BusinessExceptionType.LOGIN_FAILED) } -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/model/operation/Operation.kt b/app/src/main/java/ch/nevis/exampleapp/domain/model/operation/Operation.kt index ebf28e3..4613582 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/model/operation/Operation.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/model/operation/Operation.kt @@ -40,6 +40,11 @@ enum class Operation( */ CHANGE_PIN(R.string.operation_change_pin), + /** + * Change Password + */ + CHANGE_PASSWORD(R.string.operation_change_password), + /** * Out-of-band authentication */ @@ -69,4 +74,4 @@ enum class Operation( * Local Data */ LOCAL_DATA(R.string.operation_local_data) -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl.kt new file mode 100644 index 0000000..ba62994 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl.kt @@ -0,0 +1,21 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.model.sdk + +import ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus + +/** + * Implementation of [PasswordAuthenticatorProtectionStatus.LastAttemptFailed] interface. + */ +data class PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl( + val remainingRetries: Int, + val coolDownTimeInSeconds: Long +) : PasswordAuthenticatorProtectionStatus.LastAttemptFailed { + override fun remainingRetries(): Int = remainingRetries + + override fun coolDownTimeInSeconds(): Long = coolDownTimeInSeconds +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PinAuthenticatorProtectionStatusLastAttemptFailedImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PinAuthenticatorProtectionStatusLastAttemptFailedImpl.kt new file mode 100644 index 0000000..505e8c2 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/model/sdk/PinAuthenticatorProtectionStatusLastAttemptFailedImpl.kt @@ -0,0 +1,21 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.model.sdk + +import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus + +/** + * Implementation of [PinAuthenticatorProtectionStatus.LastAttemptFailed] interface. + */ +data class PinAuthenticatorProtectionStatusLastAttemptFailedImpl( + val remainingRetries: Int, + val coolDownTimeInSeconds: Long +) : PinAuthenticatorProtectionStatus.LastAttemptFailed { + override fun remainingRetries(): Int = remainingRetries + + override fun coolDownTimeInSeconds(): Long = coolDownTimeInSeconds +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/util/Authenticator.kt b/app/src/main/java/ch/nevis/exampleapp/domain/util/Authenticator.kt index 32abe2a..4d03aeb 100644 --- a/app/src/main/java/ch/nevis/exampleapp/domain/util/Authenticator.kt +++ b/app/src/main/java/ch/nevis/exampleapp/domain/util/Authenticator.kt @@ -43,6 +43,7 @@ fun Authenticator.isUserEnrolled(username: String, allowClass2Sensors: Boolean): fun Authenticator.titleResId(): Int { return when (val aaid = aaid()) { PIN_AUTHENTICATOR_AAID -> R.string.authenticator_pin_title + PASSWORD_AUTHENTICATOR_AAID -> R.string.authenticator_password_title BIOMETRIC_AUTHENTICATOR_AAID -> R.string.authenticator_biometric_title FINGERPRINT_AUTHENTICATOR_AAID -> R.string.authenticator_fingerprint_title DEVICE_PASSCODE_AUTHENTICATOR_AAID -> R.string.authenticator_device_passcode_title diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/util/PasswordAuthenticatorProtectionStatus.kt b/app/src/main/java/ch/nevis/exampleapp/domain/util/PasswordAuthenticatorProtectionStatus.kt new file mode 100644 index 0000000..7ce9354 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/util/PasswordAuthenticatorProtectionStatus.kt @@ -0,0 +1,43 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.util + +import android.content.Context +import ch.nevis.exampleapp.R +import ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus + +/** + * Extension function of [PasswordAuthenticatorProtectionStatus] to get the localized description. + * + * @param context An Android [Context] object for [String] resource resolving. + * @return The localized description of the instance. + */ +fun PasswordAuthenticatorProtectionStatus.message(context: Context): String { + return when (this) { + is PasswordAuthenticatorProtectionStatus.Unlocked -> String() + is PasswordAuthenticatorProtectionStatus.LockedOut -> context.getString(R.string.pin_protection_status_locked_out) + is PasswordAuthenticatorProtectionStatus.LastAttemptFailed -> { + when (remainingRetries()) { + 1 -> { + if (coolDownTimeInSeconds() == 0L) { + return context.getString(R.string.password_protection_status_last_retry_without_cool_down) + } else { + return context.getString(R.string.password_protection_status_last_retry_with_cool_down, coolDownTimeInSeconds()) + } + } + else -> { + if (coolDownTimeInSeconds() == 0L) { + return context.getString(R.string.password_protection_status_retries_without_cool_down, remainingRetries()) + } else { + return context.getString(R.string.password_protection_status_retries_with_cool_down, remainingRetries(), coolDownTimeInSeconds()) + } + } + } + } + else -> throw IllegalStateException("Unsupported Password authenticator protection status.") + } +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/util/PinAuthenticatorProtectionStatus.kt b/app/src/main/java/ch/nevis/exampleapp/domain/util/PinAuthenticatorProtectionStatus.kt new file mode 100644 index 0000000..448b014 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/util/PinAuthenticatorProtectionStatus.kt @@ -0,0 +1,43 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.util + +import android.content.Context +import ch.nevis.exampleapp.R +import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus + +/** + * Extension function of [PinAuthenticatorProtectionStatus] to get the localized description. + * + * @param context An Android [Context] object for [String] resource resolving. + * @return The localized description of the instance. + */ +fun PinAuthenticatorProtectionStatus.message(context: Context): String { + return when (this) { + is PinAuthenticatorProtectionStatus.Unlocked -> String() + is PinAuthenticatorProtectionStatus.LockedOut -> context.getString(R.string.pin_protection_status_locked_out) + is PinAuthenticatorProtectionStatus.LastAttemptFailed -> { + when (remainingRetries()) { + 1 -> { + if (coolDownTimeInSeconds() == 0L) { + return context.getString(R.string.pin_protection_status_last_retry_without_cool_down) + } else { + return context.getString(R.string.pin_protection_status_last_retry_with_cool_down, coolDownTimeInSeconds()) + } + } + else -> { + if (coolDownTimeInSeconds() == 0L) { + return context.getString(R.string.pin_protection_status_retries_without_cool_down, remainingRetries()) + } else { + return context.getString(R.string.pin_protection_status_retries_with_cool_down, remainingRetries(), coolDownTimeInSeconds()) + } + } + } + } + else -> throw IllegalStateException("Unsupported PIN authenticator protection status.") + } +} diff --git a/app/src/main/java/ch/nevis/exampleapp/domain/validation/PasswordPolicyImpl.kt b/app/src/main/java/ch/nevis/exampleapp/domain/validation/PasswordPolicyImpl.kt new file mode 100644 index 0000000..d2b881a --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/domain/validation/PasswordPolicyImpl.kt @@ -0,0 +1,71 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.domain.validation + +import android.content.Context +import ch.nevis.exampleapp.R +import ch.nevis.mobile.sdk.api.operation.password.PasswordChangeRecoverableError +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentError +import ch.nevis.mobile.sdk.api.operation.password.PasswordPolicy +import ch.nevis.mobile.sdk.api.util.Consumer + +/** + * Implementation of [PasswordPolicy] interface. + * This policy validates the password entered by the user during registration or password changing, + * and allows only password that are different from the string `password`. + */ +class PasswordPolicyImpl( + /** + * An Android [Context] object for [String] resource resolving. + */ + private val context: Context, +) : PasswordPolicy { + + //region PasswordPolicy + override fun validatePasswordForEnrollment( + password: CharArray, + onSuccess: Runnable, + errorConsumer: Consumer + ) { + if (isValid(password)) { + onSuccess.run() + } else { + errorConsumer.accept(PasswordEnrollmentError.builder() + .description(context.getString(R.string.password_policy_error_message)) + .cause(Exception(context.getString(R.string.password_policy_error_cause))) + .build()) + } + } + + override fun validatePasswordForPasswordChange( + password: CharArray, + onSuccess: Runnable, + errorConsumer: Consumer + ) { + if (isValid(password)) { + onSuccess.run() + } else { + errorConsumer.accept(PasswordChangeRecoverableError.CustomValidationError.builder() + .description(context.getString(R.string.password_policy_error_message)) + .cause(Exception(context.getString(R.string.password_policy_error_cause))) + .build()) + } + } + //endregion + + //region Private Interface + + /** + * Validates the password. + * + * @param password The password to validate. + */ + private fun isValid(password: CharArray): Boolean { + return String(password).trim() != "password" + } + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/authCloudRegistration/AuthCloudRegistrationViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/authCloudRegistration/AuthCloudRegistrationViewModel.kt index dab9287..de5f35b 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/authCloudRegistration/AuthCloudRegistrationViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/authCloudRegistration/AuthCloudRegistrationViewModel.kt @@ -18,6 +18,7 @@ import ch.nevis.exampleapp.domain.model.operation.Operation import ch.nevis.exampleapp.ui.base.BaseViewModel import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.result.parameter.ResultNavigationParameter +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier @@ -63,6 +64,11 @@ class AuthCloudRegistrationViewModel @Inject constructor( */ private val pinEnroller: PinEnroller, + /** + * An instance of a [PasswordEnroller] interface implementation. + */ + private val passwordEnroller: PasswordEnroller, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -110,6 +116,7 @@ class AuthCloudRegistrationViewModel @Inject constructor( .allowClass2Sensors(settings.allowClass2Sensors) .deviceInformation(deviceInformation) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .fingerprintUserVerifier(fingerprintUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/base/CancelOperationOnBackPressedCallback.kt b/app/src/main/java/ch/nevis/exampleapp/ui/base/CancelOperationOnBackPressedCallback.kt index 10a5ce0..7618498 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/base/CancelOperationOnBackPressedCallback.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/base/CancelOperationOnBackPressedCallback.kt @@ -12,7 +12,7 @@ import androidx.activity.OnBackPressedCallback * A common implementation of [OnBackPressedCallback] class that cancels * the running operation if there is any. */ -class CancelOperationOnBackPressedCallback( +open class CancelOperationOnBackPressedCallback( /** * The view model that runs/handles the cancellable operation. @@ -25,4 +25,4 @@ class CancelOperationOnBackPressedCallback( viewModel.cancelOperation() } //endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/changeDeviceInformation/ChangeDeviceInformationFragment.kt b/app/src/main/java/ch/nevis/exampleapp/ui/changeDeviceInformation/ChangeDeviceInformationFragment.kt index 5b3a6c7..a1f42d6 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/changeDeviceInformation/ChangeDeviceInformationFragment.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/changeDeviceInformation/ChangeDeviceInformationFragment.kt @@ -67,8 +67,8 @@ class ChangeDeviceInformationFragment : BaseFragment() { override fun updateView(viewData: ViewData) { super.updateView(viewData) - val changePinViewData = viewData as? ChangeDeviceInformationViewData ?: return - binding.currentNameTextView.text = changePinViewData.deviceInformation.name() + val changeDeviceInformationViewData = viewData as? ChangeDeviceInformationViewData ?: return + binding.currentNameTextView.text = changeDeviceInformationViewData.deviceInformation.name() } //endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialFragment.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialFragment.kt new file mode 100644 index 0000000..4e31aef --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialFragment.kt @@ -0,0 +1,260 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2023. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential + +import android.os.Bundle +import android.os.CountDownTimer +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.navArgs +import ch.nevis.exampleapp.databinding.FragmentCredentialBinding +import ch.nevis.exampleapp.domain.model.sdk.PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl +import ch.nevis.exampleapp.domain.model.sdk.PinAuthenticatorProtectionStatusLastAttemptFailedImpl +import ch.nevis.exampleapp.domain.util.message +import ch.nevis.exampleapp.ui.base.BaseFragment +import ch.nevis.exampleapp.ui.base.CancelOperationOnBackPressedCallback +import ch.nevis.exampleapp.ui.base.model.NavigationParameter +import ch.nevis.exampleapp.ui.base.model.ViewData +import ch.nevis.exampleapp.ui.credential.model.CredentialViewData +import ch.nevis.exampleapp.ui.credential.parameter.CredentialNavigationParameter +import ch.nevis.mobile.sdk.api.localdata.Authenticator +import dagger.hilt.android.AndroidEntryPoint + +/** + * [androidx.fragment.app.Fragment] implementation of Credential view where the user + * can enroll, change and verify credential (PIN or password). + */ +@AndroidEntryPoint +class CredentialFragment : BaseFragment() { + + //region Properties + /** + * UI component bindings. + */ + private var _binding: FragmentCredentialBinding? = null + private val binding get() = _binding!! + + /** + * View model implementation of this view. + */ + override val viewModel: CredentialViewModel by viewModels() + + /** + * Safe Args navigation arguments. + */ + private val navigationArguments: CredentialFragmentArgs by navArgs() + + /** + * A [CountDownTimer] instance that is used to disable the screen for a cool down time period if necessary. + */ + private var timer: CountDownTimer? = null + //endregion + + //region Fragment + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentCredentialBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.updateViewModel(navigationArguments.parameter) + + binding.confirmButton.setOnClickListener { + viewModel.confirm( + binding.oldCredentialTextInputEditText.text?.toList()?.toCharArray() ?: charArrayOf(), + binding.credentialTextInputEditText.text?.toList()?.toCharArray() ?: charArrayOf() + ) + } + + binding.oldCredentialTextInputEditText.addTextChangedListener(onTextChanged = { _, _, _, _ -> + clearErrors() + }) + + binding.credentialTextInputEditText.addTextChangedListener(onTextChanged = { _, _, _, _ -> + clearErrors() + }) + + // Override back button handling. + val callback = object : CancelOperationOnBackPressedCallback(viewModel) { + override fun handleOnBackPressed() { + timer?.cancel() + timer = null + super.handleOnBackPressed() + } + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + //endregion + + //region BaseFragment + override fun updateViewParameter(parameter: NavigationParameter): Boolean { + if (parameter is CredentialNavigationParameter) { + viewModel.updateViewModel(parameter) + return true + } + return false + } + + override fun updateView(viewData: ViewData) { + super.updateView(viewData) + + val credentialViewData = viewData as? CredentialViewData ?: return + + binding.titleTextView.setText(credentialViewData.title) + binding.descriptionTextView.setText(credentialViewData.description) + + updateTextViews(credentialViewData) + + binding.messageTextView.visibility = View.GONE + credentialViewData.protectionInformation?.let { protectionInformation -> + protectionInformation.message.let { + binding.messageTextView.visibility = View.VISIBLE + binding.messageTextView.text = it + } + setViewState(!protectionInformation.isLocked) + protectionInformation.remainingRetries?.let { remainingRetries -> + updateMessage( + credentialViewData.credentialType, + remainingRetries, + protectionInformation.coolDownTime + ) + if (protectionInformation.coolDownTime > 0) { + startCoolDownTimer( + credentialViewData.credentialType, + remainingRetries, + protectionInformation.coolDownTime + ) + } + } + } + } + //endregion + + //region Private Interface + + /** + * Updates the text views based on the view data. + * + * @param credentialViewData The view data. + */ + private fun updateTextViews(credentialViewData: CredentialViewData) { + binding.oldCredentialTextInputLayout.setHint(credentialViewData.oldCredentialTextFieldHint) + binding.oldCredentialTextInputLayout.visibility = credentialViewData.oldCredentialVisibility + binding.credentialTextInputLayout.setHint(credentialViewData.credentialTextFieldHint) + when (credentialViewData.credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> { + binding.oldCredentialTextInputEditText.inputType = (InputType.TYPE_CLASS_NUMBER or + InputType.TYPE_NUMBER_VARIATION_PASSWORD) + binding.credentialTextInputEditText.inputType = (InputType.TYPE_CLASS_NUMBER or + InputType.TYPE_NUMBER_VARIATION_PASSWORD) + } + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> { + binding.oldCredentialTextInputEditText.inputType = + (InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD) + binding.credentialTextInputEditText.inputType = (InputType.TYPE_CLASS_TEXT or + InputType.TYPE_TEXT_VARIATION_PASSWORD) + } + } + + credentialViewData.lastRecoverableError?.let { lastRecoverableError -> + var errorMessage = lastRecoverableError.description() + if (lastRecoverableError.cause().isPresent) { + errorMessage += " " + lastRecoverableError.cause().get().message + + } + + binding.credentialTextInputLayout.error = errorMessage + } + } + + /** + * Clears all error messages on screen. + */ + private fun clearErrors() { + binding.oldCredentialTextInputLayout.error = "" + binding.credentialTextInputLayout.error = "" + } + + /** + * Enables or disables the UI components of this view. + * + * @param isEnabled A flag that tells whether the UI components should be enabled. + */ + private fun setViewState(isEnabled: Boolean) { + binding.oldCredentialTextInputLayout.isEnabled = isEnabled + binding.credentialTextInputLayout.isEnabled = isEnabled + binding.confirmButton.isEnabled = isEnabled + } + + /** + * Starts the cool down timer. + * + * @param credentialType The type of the actual credential. + * @param remainingRetries The number of remaining retries available. + * @param coolDownTime The time that must be passed before the user can try to provide credentials again. + */ + private fun startCoolDownTimer(credentialType: String, remainingRetries: Int, coolDownTime: Long) { + if (timer != null) { + return + } + + timer = object : CountDownTimer( + coolDownTime * 1000, + 1000 + ) { + override fun onTick(millisUntilFinished: Long) { + updateMessage(credentialType, remainingRetries, millisUntilFinished / 1000) + } + + override fun onFinish() { + setViewState(true) + updateMessage(credentialType, remainingRetries, 0) + } + }.start() + } + + /** + * Updates the text of message text view. + * + * @param credentialType The type of the actual credential. + * @param remainingRetries The number of remaining retries available. + * @param coolDownTime The time that must be passed before the user can try to provide credentials again. + */ + private fun updateMessage(credentialType: String, remainingRetries: Int, coolDownTime: Long) { + binding.messageTextView.visibility = View.VISIBLE + binding.messageTextView.text = context?.let { + when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> + PinAuthenticatorProtectionStatusLastAttemptFailedImpl( + remainingRetries = remainingRetries, + coolDownTimeInSeconds = coolDownTime + ).message(it) + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> + PasswordAuthenticatorProtectionStatusLastAttemptFailedImpl( + remainingRetries = remainingRetries, + coolDownTimeInSeconds = coolDownTime + ).message(it) + else -> String() + } + } + } + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt new file mode 100644 index 0000000..183fe42 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/CredentialViewModel.kt @@ -0,0 +1,316 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2023. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential + +import android.annotation.SuppressLint +import android.content.Context +import ch.nevis.exampleapp.common.error.ErrorHandler +import ch.nevis.exampleapp.domain.model.error.BusinessException +import ch.nevis.exampleapp.domain.util.message +import ch.nevis.exampleapp.logging.sdk +import ch.nevis.exampleapp.ui.base.CancellableOperationViewModel +import ch.nevis.exampleapp.ui.credential.model.CredentialProtectionInformation +import ch.nevis.exampleapp.ui.credential.model.CredentialViewData +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.exampleapp.ui.credential.parameter.CredentialNavigationParameter +import ch.nevis.exampleapp.ui.credential.parameter.PasswordNavigationParameter +import ch.nevis.exampleapp.ui.credential.parameter.PinNavigationParameter +import ch.nevis.mobile.sdk.api.localdata.Authenticator +import ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus +import ch.nevis.mobile.sdk.api.operation.password.PasswordChangeHandler +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentHandler +import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus +import ch.nevis.mobile.sdk.api.operation.pin.PinChangeHandler +import ch.nevis.mobile.sdk.api.operation.pin.PinEnrollmentHandler +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerificationHandler +import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerificationHandler +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +/** + * View model implementation of Credential view. + */ +@HiltViewModel +class CredentialViewModel @Inject constructor( + /** + * An Android [Context] object for [String] resource resolving. + */ + @ApplicationContext + private val context: Context, + + /** + * An instance of an [ErrorHandler] interface implementation. Received errors will be passed to + * this error handler instance. + */ + private val errorHandler: ErrorHandler +) : CancellableOperationViewModel() { + + /** + * The mode, the Credential view intend to be used/initialized. + */ + private var credentialViewMode: CredentialViewMode? = null + + /** + * An instance of a [PinChangeHandler] in case a PIN change operation is started and we navigate + * to Credential view to ask the user to enter the old and new PINs to be able to continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.CHANGE]. + */ + private var pinChangeHandler: PinChangeHandler? = null + + /** + * An instance of a [PinEnrollmentHandler] in case a PIN enrollment is started as part of a + * registration operation and we navigate to Credential view to ask the user to enter, define the + * PIN to be able to continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.ENROLLMENT]. + */ + private var pinEnrollmentHandler: PinEnrollmentHandler? = null + + /** + * An instance of a [PinUserVerificationHandler] in case a PIN verification is started as part of + * an authentication operation and we navigate to Credential view to ask the user to enter the PIN + * to be able to continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.VERIFICATION]. + */ + private var pinUserVerificationHandler: PinUserVerificationHandler? = null + + /** + * An instance of a [PasswordChangeHandler] in case a Password change operation is started and we + * navigate to Credential view to ask the user to enter the old and new passwords to be able to + * continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.CHANGE]. + */ + private var passwordChangeHandler: PasswordChangeHandler? = null + + /** + * An instance of a [PasswordEnrollmentHandler] in case a Password enrollment is started as part + * of a registration operation and we navigate to Credential view to ask the user to enter, define + * the password to be able to continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.ENROLLMENT]. + */ + private var passwordEnrollmentHandler: PasswordEnrollmentHandler? = null + + /** + * An instance of a [PasswordUserVerificationHandler] in case a Password verification is started + * as part of an authentication operation and we navigate to Credential view to ask the user to + * enter the password to be able to continue the operation. + * [CredentialViewModel.credentialViewMode] must be [CredentialViewMode.VERIFICATION]. + */ + private var passwordUserVerificationHandler: PasswordUserVerificationHandler? = null + + /** + * The type of the credential. + */ + private lateinit var credentialType: String + + //region Public Interface + /** + * Updates this view model instance based on the [CredentialNavigationParameter] that was received + * by the owner [CredentialFragment]. This method must be called by the owner fragment. + * + * @param parameter The [CredentialNavigationParameter] that was received by the owner [CredentialFragment]. + */ + fun updateViewModel(parameter: CredentialNavigationParameter) { + credentialViewMode = parameter.credentialViewMode + + try { + updateHandlers(parameter) + val credentialViewData = CredentialViewData( + parameter.credentialViewMode, + credentialType, + getProtectionInfo(parameter), + parameter.lastRecoverableError + ) + requestViewUpdate(credentialViewData) + } catch (exception: Exception) { + errorHandler.handle(exception) + } + } + + /** + * Confirms the given credentials by invoking the corresponding handlers with the received credential. + * + * @param oldCredential The text entered into old credential text field. It is only used when the current [CredentialViewMode] + * is [CredentialViewMode.CHANGE] otherwise it will be ignored. + * @param credential The text entered into credential text field. + */ + fun confirm(oldCredential: CharArray, credential: CharArray) { + try { + when (credentialViewMode) { + CredentialViewMode.CHANGE -> { + pinChangeHandler?.pins(oldCredential, credential) + this.pinChangeHandler = null + passwordChangeHandler?.passwords(oldCredential, credential) + this.passwordChangeHandler = null + } + CredentialViewMode.ENROLLMENT -> { + pinEnrollmentHandler?.pin(credential) + this.pinEnrollmentHandler = null + passwordEnrollmentHandler?.password(credential) + this.passwordEnrollmentHandler = null + } + CredentialViewMode.VERIFICATION -> { + pinUserVerificationHandler?.verifyPin(credential) + this.pinUserVerificationHandler = null + passwordUserVerificationHandler?.verifyPassword(credential) + this.passwordUserVerificationHandler = null + } + else -> throw BusinessException.invalidState() + } + } catch (exception: Exception) { + errorHandler.handle(exception) + } + } + //endregion + + //region Private Interface + + /** + * Updates the different handlers based on the navigation parameter. + * + * @param parameter The [CredentialNavigationParameter] that was received by the owner [CredentialFragment]. + */ + private fun updateHandlers(parameter: CredentialNavigationParameter) { + when (parameter) { + is PinNavigationParameter -> { + credentialType = Authenticator.PIN_AUTHENTICATOR_AAID + pinChangeHandler = parameter.pinChangeHandler + pinEnrollmentHandler = parameter.pinEnrollmentHandler + pinUserVerificationHandler = parameter.pinUserVerificationHandler + } + is PasswordNavigationParameter -> { + credentialType = Authenticator.PASSWORD_AUTHENTICATOR_AAID + passwordChangeHandler = parameter.passwordChangeHandler + passwordEnrollmentHandler = parameter.passwordEnrollmentHandler + passwordUserVerificationHandler = parameter.passwordUserVerificationHandler + } + else -> throw BusinessException.invalidState() + } + } + + /** + * Creates the credential protection information only if authenticator protection status is present. + * + * @param parameter The [CredentialNavigationParameter] that was received by the owner [CredentialFragment]. + * @return The created [CredentialProtectionInformation] instance. + */ + private fun getProtectionInfo(parameter: CredentialNavigationParameter): CredentialProtectionInformation? { + return when (parameter) { + is PinNavigationParameter -> { + parameter.pinAuthenticatorProtectionStatus?.let { + getPinProtectionInfo(it) + } + } + is PasswordNavigationParameter -> { + parameter.passwordAuthenticatorProtectionStatus?.let { + getPasswordProtectionInfo(it) + } + } + else -> throw BusinessException.invalidState() + } + } + + /** + * Creates PIN authenticator specific credential protection information. + * + * @param protectionStatus The [PinAuthenticatorProtectionStatus] that presented in the received [CredentialNavigationParameter]. + * @return The PIN authenticator specific [CredentialProtectionInformation] instance. + */ + @SuppressLint("DefaultLocale") + private fun getPinProtectionInfo(protectionStatus: PinAuthenticatorProtectionStatus): CredentialProtectionInformation { + return when (protectionStatus) { + is PinAuthenticatorProtectionStatus.Unlocked -> { + Timber.asTree().sdk("PIN authenticator is unlocked.") + CredentialProtectionInformation() + } + is PinAuthenticatorProtectionStatus.LastAttemptFailed -> { + Timber.asTree().sdk("Last attempt failed using the PIN authenticator.") + Timber.asTree() + .sdk(String.format( + "Remaining tries: %d, cool down period: %d", + protectionStatus.remainingRetries(), + protectionStatus.coolDownTimeInSeconds()) + ) + CredentialProtectionInformation( + isLocked = protectionStatus.coolDownTimeInSeconds() > 0, + remainingRetries = protectionStatus.remainingRetries(), + coolDownTime = protectionStatus.coolDownTimeInSeconds(), + ) + } + is PinAuthenticatorProtectionStatus.LockedOut -> { + Timber.asTree().sdk("PIN authenticator is locked.") + CredentialProtectionInformation( + isLocked = true, + message = protectionStatus.message(context) + ) + } + else -> throw IllegalStateException("Unsupported PIN authenticator protection status.") + } + } + + /** + * Creates Password authenticator specific credential protection information. + * + * @param protectionStatus The [PasswordAuthenticatorProtectionStatus] that presented in the received [CredentialNavigationParameter]. + * @return The Password authenticator specific [CredentialProtectionInformation] instance. + */ + @SuppressLint("DefaultLocale") + private fun getPasswordProtectionInfo(protectionStatus: PasswordAuthenticatorProtectionStatus): CredentialProtectionInformation { + return when (protectionStatus) { + is PasswordAuthenticatorProtectionStatus.Unlocked -> { + Timber.asTree().sdk("Password authenticator is unlocked.") + CredentialProtectionInformation() + } + is PasswordAuthenticatorProtectionStatus.LastAttemptFailed -> { + Timber.asTree().sdk("Last attempt failed using the Password authenticator.") + Timber.asTree() + .sdk(String.format( + "Remaining tries: %d, cool down period: %d", + protectionStatus.remainingRetries(), + protectionStatus.coolDownTimeInSeconds()) + ) + CredentialProtectionInformation( + isLocked = protectionStatus.coolDownTimeInSeconds() > 0, + remainingRetries = protectionStatus.remainingRetries(), + coolDownTime = protectionStatus.coolDownTimeInSeconds(), + ) + } + is PasswordAuthenticatorProtectionStatus.LockedOut -> { + Timber.asTree().sdk("Password authenticator is locked.") + CredentialProtectionInformation( + isLocked = true, + message = protectionStatus.message(context) + ) + } + else -> throw IllegalStateException("Unsupported Password authenticator protection status.") + } + } + //endregion + + //region CancellableOperationViewModel + override fun cancelOperation() { + pinChangeHandler?.cancel() + pinChangeHandler = null + + pinEnrollmentHandler?.cancel() + pinEnrollmentHandler = null + + pinUserVerificationHandler?.cancel() + pinUserVerificationHandler = null + + passwordChangeHandler?.cancel() + passwordChangeHandler = null + + passwordEnrollmentHandler?.cancel() + passwordEnrollmentHandler = null + + passwordUserVerificationHandler?.cancel() + passwordUserVerificationHandler = null + } + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialProtectionInformation.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialProtectionInformation.kt new file mode 100644 index 0000000..407c15f --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialProtectionInformation.kt @@ -0,0 +1,32 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential.model + +/** + * Represents the protection related information of a credential that is used by the Credential view. + */ +data class CredentialProtectionInformation( + /** + * Specifies whether the given authenticator is locked. + */ + val isLocked: Boolean = false, + + /** + * The number of remaining retries available. + */ + val remainingRetries: Int? = null, + + /** + * The time that must be passed before the user can try to provide credentials again. + */ + val coolDownTime: Long = 0L, + + /** + * The protection related message. + */ + val message: String? = null +) diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewData.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewData.kt new file mode 100644 index 0000000..c83f11f --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewData.kt @@ -0,0 +1,131 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2023. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential.model + +import android.view.View +import ch.nevis.exampleapp.R +import ch.nevis.exampleapp.ui.base.model.ViewData +import ch.nevis.mobile.sdk.api.localdata.Authenticator +import ch.nevis.mobile.sdk.api.operation.RecoverableError +import kotlinx.parcelize.IgnoredOnParcel + +/** + * [ViewData] implementation for Credential view and its view model. The view model composes an instance + * of this [CredentialViewData] class and posts it to the Credential view to indicate that the view related + * data changed the view should be updated. + */ +data class CredentialViewData( + /** + * The mode, the Credential view intend to be used/initialized. + */ + val credentialViewMode: CredentialViewMode, + + /** + * The type of the credential. + */ + val credentialType: String, + + /** + * Authenticator protection information. + */ + val protectionInformation: CredentialProtectionInformation?, + + /** + * The last recoverable error. It exists only if there was already a failed PIN operation attempt. + */ + @IgnoredOnParcel + val lastRecoverableError: RecoverableError? = null +) : ViewData { + + /** + * The identifier of String resource that should be used as title on the Credential view. + * Its value depends on the actual [credentialViewMode] and [credentialType]. + */ + val title: Int + get() { + return when (credentialViewMode) { + CredentialViewMode.CHANGE -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_title_change + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_title_change + else -> throw IllegalStateException("Unsupported credential type.") + } + CredentialViewMode.ENROLLMENT -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_title_enrollment + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_title_enrollment + else -> throw IllegalStateException("Unsupported credential type.") + } + CredentialViewMode.VERIFICATION -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_title_verify + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_title_verify + else -> throw IllegalStateException("Unsupported credential type.") + } + } + } + + /** + * The identifier of String resource that should be used as description on Credential view. + * Its value depends on the actual [credentialViewMode] and [credentialType]. + */ + val description: Int + get() { + return when (credentialViewMode) { + CredentialViewMode.CHANGE -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_description_change + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_description_change + else -> throw IllegalStateException("Unsupported credential type.") + } + CredentialViewMode.ENROLLMENT -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_description_enrollment + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_description_enrollment + else -> throw IllegalStateException("Unsupported credential type.") + } + CredentialViewMode.VERIFICATION -> when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_description_verify + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_description_verify + else -> throw IllegalStateException("Unsupported credential type.") + } + } + } + + /** + * The identifier of String resource that should be used as hint of credential text field on the + * Credential view. Its value depends on the actual [credentialType]. + */ + val credentialTextFieldHint: Int + get() { + return when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_hint_pin + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_hint_password + else -> throw IllegalStateException("Unsupported credential type.") + } + } + + /** + * The identifier of String resource that should be used as hint of old credential text field on + * the Credential view. Its value depends on the actual [credentialType]. + */ + val oldCredentialTextFieldHint: Int + get() { + return when (credentialType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> R.string.pin_hint_old_pin + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> R.string.password_hint_old_password + else -> throw IllegalStateException("Unsupported credential type.") + } + } + + /** + * The visibility value that tells if the old credential text field should be displayed on the + * Credential view. Its value depends on the actual [credentialViewMode]. + */ + val oldCredentialVisibility: Int + get() { + return when (credentialViewMode) { + CredentialViewMode.CHANGE -> View.VISIBLE + else -> View.GONE + } + } +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewMode.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewMode.kt new file mode 100644 index 0000000..0622992 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/model/CredentialViewMode.kt @@ -0,0 +1,28 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2023. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential.model + +/** + * Enumeration of available Credential view modes. + */ +enum class CredentialViewMode { + + /** + * Change mode. + */ + CHANGE, + + /** + * Enrollment mode. + */ + ENROLLMENT, + + /** + * Verification mode. + */ + VERIFICATION +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/CredentialNavigationParameter.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/CredentialNavigationParameter.kt new file mode 100644 index 0000000..2c77192 --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/CredentialNavigationParameter.kt @@ -0,0 +1,24 @@ +package ch.nevis.exampleapp.ui.credential.parameter + +import ch.nevis.exampleapp.ui.base.model.NavigationParameter +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.mobile.sdk.api.operation.RecoverableError + +/** + * Interface for Credential view navigation parameter. + */ +abstract class CredentialNavigationParameter : NavigationParameter { + + //region Properties + /** + * The mode, the Credential view intend to be used/initialized. + */ + abstract val credentialViewMode: CredentialViewMode + + /** + * The last recoverable error. It exists only if there was already a failed PIN/Password operation + * attempt. + */ + abstract val lastRecoverableError: RecoverableError? + //endregion +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PasswordNavigationParameter.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PasswordNavigationParameter.kt new file mode 100644 index 0000000..2aec94f --- /dev/null +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PasswordNavigationParameter.kt @@ -0,0 +1,66 @@ +/** + * Nevis Mobile Authentication SDK Example App + * + * Copyright © 2024. Nevis Security AG. All rights reserved. + */ + +package ch.nevis.exampleapp.ui.credential.parameter + +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode +import ch.nevis.mobile.sdk.api.operation.RecoverableError +import ch.nevis.mobile.sdk.api.operation.password.PasswordAuthenticatorProtectionStatus +import ch.nevis.mobile.sdk.api.operation.password.PasswordChangeHandler +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnrollmentHandler +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerificationHandler +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +/** + * Navigation parameter of the Credential view in case of Password authenticator. + */ +@Parcelize +data class PasswordNavigationParameter( + /** + * The mode, the Credential view intend to be used/initialized. + */ + override val credentialViewMode: CredentialViewMode, + + /** + * The last recoverable error. It exists only if there was already a failed Password operation attempt. + */ + @IgnoredOnParcel + override val lastRecoverableError: RecoverableError? = null, + + /** + * Status object of the Password authenticator. + */ + @IgnoredOnParcel + val passwordAuthenticatorProtectionStatus: PasswordAuthenticatorProtectionStatus? = null, + + /** + * An instance of a [PasswordChangeHandler] in case a Password change operation is started and we + * navigate to Credential view to ask the user to enter the old and new passwords to be able to + * continue the operation. + * [PasswordNavigationParameter.credentialViewMode] must be [CredentialViewMode.CHANGE]. + */ + @IgnoredOnParcel + val passwordChangeHandler: PasswordChangeHandler? = null, + + /** + * An instance of a [PasswordEnrollmentHandler] in case a Password enrollment is started as part + * of a registration operation and we navigate to Credential view to ask the user to enter, define + * the password to be able to continue the operation. + * [PasswordNavigationParameter.credentialViewMode] must be [CredentialViewMode.ENROLLMENT]. + */ + @IgnoredOnParcel + val passwordEnrollmentHandler: PasswordEnrollmentHandler? = null, + + /** + * An instance of a [PasswordUserVerificationHandler] in case a Password verification is started + * as part of an authentication operation and we navigate to Credential view to ask the user to + * enter the password to be able to continue the operation. + * [PasswordNavigationParameter.credentialViewMode] must be [CredentialViewMode.VERIFICATION]. + */ + @IgnoredOnParcel + val passwordUserVerificationHandler: PasswordUserVerificationHandler? = null +) : CredentialNavigationParameter() diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/pin/parameter/PinNavigationParameter.kt b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PinNavigationParameter.kt similarity index 56% rename from app/src/main/java/ch/nevis/exampleapp/ui/pin/parameter/PinNavigationParameter.kt rename to app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PinNavigationParameter.kt index a8ef33f..d654fa9 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/pin/parameter/PinNavigationParameter.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/credential/parameter/PinNavigationParameter.kt @@ -4,10 +4,9 @@ * Copyright © 2023. Nevis Security AG. All rights reserved. */ -package ch.nevis.exampleapp.ui.pin.parameter +package ch.nevis.exampleapp.ui.credential.parameter -import ch.nevis.exampleapp.ui.base.model.NavigationParameter -import ch.nevis.exampleapp.ui.pin.model.PinViewMode +import ch.nevis.exampleapp.ui.credential.model.CredentialViewMode import ch.nevis.mobile.sdk.api.operation.RecoverableError import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus import ch.nevis.mobile.sdk.api.operation.pin.PinChangeHandler @@ -17,50 +16,50 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize /** - * Navigation parameter data class for PIN view. + * Navigation parameter of the Credential view in case of PIN authenticator. */ @Parcelize data class PinNavigationParameter( /** - * The mode, the PIN view intend to be used/initialized. + * The mode, the Credential view intend to be used/initialized. */ - val pinViewMode: PinViewMode, + override val credentialViewMode: CredentialViewMode, /** - * Status object of the PIN authenticator. + * The last recoverable error. It exists only if there was already a failed PIN operation attempt. */ @IgnoredOnParcel - val pinAuthenticatorProtectionStatus: PinAuthenticatorProtectionStatus? = null, + override val lastRecoverableError: RecoverableError? = null, /** - * The last recoverable error. It exists only if there was already a failed PIN operation attempt. + * Status object of the PIN authenticator. */ @IgnoredOnParcel - val lastRecoverableError: RecoverableError? = null, + val pinAuthenticatorProtectionStatus: PinAuthenticatorProtectionStatus? = null, /** - * An instance of a [PinChangeHandler] in case a PIN change operation is started and we navigate to - * PIN view to ask the user to enter the old and new PINs to be able to continue the operation. - * [PinNavigationParameter.pinViewMode] must be [PinViewMode.CHANGE_PIN]. + * An instance of a [PinChangeHandler] in case a PIN change operation is started and we navigate + * to Credential view to ask the user to enter the old and new PINs to be able to continue the + * operation. [PinNavigationParameter.credentialViewMode] must be [CredentialViewMode.CHANGE]. */ @IgnoredOnParcel val pinChangeHandler: PinChangeHandler? = null, /** * An instance of a [PinEnrollmentHandler] in case a PIN enrollment is started as part of a - * registration operation and we navigate to PIN view to ask the user to enter, define the PIN to - * be able to continue the operation. - * [PinNavigationParameter.pinViewMode] must be [PinViewMode.ENROLL_PIN]. + * registration operation and we navigate to Credential view to ask the user to enter, define the + * PIN to be able to continue the operation. + * [PinNavigationParameter.credentialViewMode] must be [CredentialViewMode.ENROLLMENT]. */ @IgnoredOnParcel val pinEnrollmentHandler: PinEnrollmentHandler? = null, /** * An instance of a [PinUserVerificationHandler] in case a PIN verification is started as part of - * an authentication operation and we navigate to PIN view to ask the user to enter the PIN to be able - * to continue the operation. - * [PinNavigationParameter.pinViewMode] must be [PinViewMode.VERIFY_PIN]. + * an authentication operation and we navigate to Credential view to ask the user to enter the PIN + * to be able to continue the operation. + * [PinNavigationParameter.credentialViewMode] must be [CredentialViewMode.VERIFICATION]. */ @IgnoredOnParcel val pinUserVerificationHandler: PinUserVerificationHandler? = null -) : NavigationParameter +) : CredentialNavigationParameter() diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeFragment.kt b/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeFragment.kt index b4d414f..d14eeb9 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeFragment.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeFragment.kt @@ -19,6 +19,7 @@ import ch.nevis.exampleapp.ui.base.BaseFragment import ch.nevis.exampleapp.ui.base.model.ViewData import ch.nevis.exampleapp.ui.home.model.HomeViewData import ch.nevis.exampleapp.ui.util.handleDispatchTokenResponse +import ch.nevis.mobile.sdk.api.localdata.Authenticator import dagger.hilt.android.AndroidEntryPoint /** @@ -69,7 +70,11 @@ class HomeFragment : BaseFragment() { } binding.changePinButton.setOnClickListener { - viewModel.changePin() + viewModel.changeCredential(Authenticator.PIN_AUTHENTICATOR_AAID) + } + + binding.changePasswordButton.setOnClickListener { + viewModel.changeCredential(Authenticator.PASSWORD_AUTHENTICATOR_AAID) } binding.changeDeviceInformationButton.setOnClickListener { @@ -120,4 +125,4 @@ class HomeFragment : BaseFragment() { } } //endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt index 0032f2e..955e46f 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/home/HomeViewModel.kt @@ -20,21 +20,25 @@ import ch.nevis.exampleapp.domain.interaction.OnErrorImpl import ch.nevis.exampleapp.domain.model.error.BusinessException import ch.nevis.exampleapp.domain.model.error.MobileAuthenticationClientException import ch.nevis.exampleapp.domain.model.operation.Operation +import ch.nevis.exampleapp.domain.util.isUserEnrolled import ch.nevis.exampleapp.ui.home.model.HomeViewData import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.outOfBand.OutOfBandViewModel import ch.nevis.exampleapp.ui.result.parameter.ResultNavigationParameter import ch.nevis.exampleapp.ui.selectAccount.parameter.SelectAccountNavigationParameter import ch.nevis.mobile.sdk.api.MobileAuthenticationClientInitializer -import ch.nevis.mobile.sdk.api.localdata.Account import ch.nevis.mobile.sdk.api.localdata.Authenticator import ch.nevis.mobile.sdk.api.operation.OperationError +import ch.nevis.mobile.sdk.api.operation.password.PasswordChanger +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller +import ch.nevis.mobile.sdk.api.operation.pin.PinChanger import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AccountSelector import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.DevicePasscodeUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -67,6 +71,16 @@ class HomeViewModel @Inject constructor( */ private val clientProvider: ClientProvider, + /** + * An instance of a [PinChanger] interface implementation. + */ + private val pinChanger: PinChanger, + + /** + * An instance of a [PasswordChanger] interface implementation. + */ + private val passwordChanger: PasswordChanger, + /** * An instance of a [NavigationDispatcher] interface implementation. */ @@ -104,11 +118,21 @@ class HomeViewModel @Inject constructor( */ pinEnroller: PinEnroller, + /** + * An instance of a [PasswordEnroller] interface implementation. + */ + passwordEnroller: PasswordEnroller, + /** * An instance of a [PinUserVerifier] interface implementation. */ pinUserVerifier: PinUserVerifier, + /** + * An instance of a [PasswordUserVerifier] interface implementation. + */ + passwordUserVerifier: PasswordUserVerifier, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -138,7 +162,9 @@ class HomeViewModel @Inject constructor( registrationAuthenticatorSelector, authenticationAuthenticatorSelector, pinEnroller, + passwordEnroller, pinUserVerifier, + passwordUserVerifier, biometricUserVerifier, devicePasscodeUserVerifier, fingerprintUserVerifier, @@ -186,32 +212,61 @@ class HomeViewModel @Inject constructor( } /** - * Starts a change PIN operation. + * Starts credential changing. + * + * @param authenticatorType The type of the authenticator. */ - fun changePin() { + fun changeCredential(authenticatorType: String) { try { val client = clientProvider.get() ?: throw BusinessException.clientNotInitialized() - var accounts: Set? = null - client.localData().authenticators().forEach { - if (it.aaid() == Authenticator.PIN_AUTHENTICATOR_AAID) { - accounts = it.registration().registeredAccounts() - } + val accounts = client.localData().accounts() + if (accounts.isEmpty()) { + throw BusinessException.accountsNotFound() } - if (accounts.isNullOrEmpty()) { - throw BusinessException.accountsNotFound() + val operation = when (authenticatorType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> Operation.CHANGE_PIN + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> Operation.CHANGE_PASSWORD + else -> throw BusinessException.invalidState() } - accounts?.also { - navigationDispatcher.requestNavigation( - NavigationGraphDirections.actionGlobalSelectAccountFragment( - SelectAccountNavigationParameter( - Operation.CHANGE_PIN, - it + val authenticatorNotFoundException = when (authenticatorType) { + Authenticator.PIN_AUTHENTICATOR_AAID -> BusinessException.pinAuthenticatorNotFound() + Authenticator.PASSWORD_AUTHENTICATOR_AAID -> BusinessException.passwordAuthenticatorNotFound() + else -> throw BusinessException.invalidState() + } + + val authenticators = client.localData().authenticators() + if (authenticators.isEmpty()) { + throw authenticatorNotFoundException + } + + val credentialAuthenticator = authenticators.firstOrNull { it.aaid() == authenticatorType } + if (credentialAuthenticator == null) { + throw authenticatorNotFoundException + } + + val eligibleAccounts = accounts.filter { + credentialAuthenticator.isUserEnrolled(it.username(), false) + }.toSet() + + when (eligibleAccounts.size) { + 0 -> throw BusinessException.accountsNotFound() + 1 -> { + when (operation) { + Operation.CHANGE_PIN -> startPinChange(eligibleAccounts.first().username()) + Operation.CHANGE_PASSWORD -> startPasswordChange(eligibleAccounts.first().username()) + else -> throw BusinessException.invalidState() + } + } + else -> { + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalSelectAccountFragment( + SelectAccountNavigationParameter(operation, eligibleAccounts) ) ) - ) - } ?: throw BusinessException.invalidState() + } + } } catch (exception: Exception) { errorHandler.handle(exception) } @@ -347,5 +402,48 @@ class HomeViewModel @Inject constructor( cancellableContinuation.resume(Unit) } } + + /** + * Starts the PIN change operation. + * + * @param username The username of the account whose PIN must be changed. + */ + private fun startPinChange(username: String) { + val client = clientProvider.get() ?: throw BusinessException.clientNotInitialized() + client.operations().pinChange() + .username(username) + .pinChanger(pinChanger) + .onSuccess { + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalResultFragment( + ResultNavigationParameter.forSuccessfulOperation(Operation.CHANGE_PIN) + ) + ) + } + .onError(OnErrorImpl(Operation.CHANGE_PIN, errorHandler)) + .execute() + } + + /** + * Starts the Password change operation. + * + * @param username The username of the account whose Password must be changed. + */ + private fun startPasswordChange(username: String) { + val client = clientProvider.get() ?: throw BusinessException.clientNotInitialized() + client.operations().passwordChange() + .username(username) + .passwordChanger(passwordChanger) + .onSuccess { + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalResultFragment( + ResultNavigationParameter.forSuccessfulOperation(Operation.CHANGE_PASSWORD) + ) + ) + } + .onError(OnErrorImpl(Operation.CHANGE_PASSWORD, errorHandler)) + .execute() + } + //endregion } diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/outOfBand/OutOfBandViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/outOfBand/OutOfBandViewModel.kt index ef49f75..f464350 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/outOfBand/OutOfBandViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/outOfBand/OutOfBandViewModel.kt @@ -20,12 +20,14 @@ import ch.nevis.exampleapp.ui.result.parameter.ResultNavigationParameter import ch.nevis.mobile.sdk.api.operation.outofband.OutOfBandAuthentication import ch.nevis.mobile.sdk.api.operation.outofband.OutOfBandPayload import ch.nevis.mobile.sdk.api.operation.outofband.OutOfBandRegistration +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AccountSelector import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.DevicePasscodeUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier /** @@ -74,11 +76,21 @@ abstract class OutOfBandViewModel( */ private val pinEnroller: PinEnroller, + /** + * An instance of a [PasswordEnroller] interface implementation. + */ + private val passwordEnroller: PasswordEnroller, + /** * An instance of a [PinUserVerifier] interface implementation. */ private val pinUserVerifier: PinUserVerifier, + /** + * An instance of a [PasswordUserVerifier] interface implementation. + */ + private val passwordUserVerifier: PasswordUserVerifier, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -157,6 +169,7 @@ abstract class OutOfBandViewModel( .accountSelector(accountSelector) .authenticatorSelector(authenticationAuthenticatorSelector) .pinUserVerifier(pinUserVerifier) + .passwordUserVerifier(passwordUserVerifier) .fingerprintUserVerifier(fingerprintUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) @@ -184,6 +197,7 @@ abstract class OutOfBandViewModel( .allowClass2Sensors(settings.allowClass2Sensors) .authenticatorSelector(registrationAuthenticatorSelector) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .fingerprintUserVerifier(fingerprintUserVerifier) diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinFragment.kt b/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinFragment.kt deleted file mode 100644 index fbe5672..0000000 --- a/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinFragment.kt +++ /dev/null @@ -1,205 +0,0 @@ -/** - * Nevis Mobile Authentication SDK Example App - * - * Copyright © 2023. Nevis Security AG. All rights reserved. - */ - -package ch.nevis.exampleapp.ui.pin - -import android.os.Bundle -import android.os.CountDownTimer -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.navArgs -import ch.nevis.exampleapp.R -import ch.nevis.exampleapp.databinding.FragmentPinBinding -import ch.nevis.exampleapp.ui.base.BaseFragment -import ch.nevis.exampleapp.ui.base.CancelOperationOnBackPressedCallback -import ch.nevis.exampleapp.ui.base.model.NavigationParameter -import ch.nevis.exampleapp.ui.base.model.ViewData -import ch.nevis.exampleapp.ui.pin.model.PinViewData -import ch.nevis.exampleapp.ui.pin.parameter.PinNavigationParameter -import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus -import dagger.hilt.android.AndroidEntryPoint - -/** - * [androidx.fragment.app.Fragment] implementation of PIN view where the user - * can enroll, change and verify PIN. - */ -@AndroidEntryPoint -class PinFragment : BaseFragment() { - - //region Properties - /** - * UI component bindings. - */ - private var _binding: FragmentPinBinding? = null - private val binding get() = _binding!! - - /** - * View model implementation of this view. - */ - override val viewModel: PinViewModel by viewModels() - - /** - * Safe Args navigation arguments. - */ - private val navigationArguments: PinFragmentArgs by navArgs() - - /** - * A [CountDownTimer] instance that is used to disable the screen for a cool down time period if necessary. - */ - private var timer: CountDownTimer? = null - - /** - * A boolean flag that tells whether the timer is started/running or not. - */ - private var isTimerRunning = false - //endregion - - //region Fragment - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentPinBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.updateViewModel(navigationArguments.parameter) - - binding.confirmButton.setOnClickListener { - viewModel.processPins( - binding.oldPinTextInputEditText.text?.toList()?.toCharArray() ?: charArrayOf(), - binding.pinTextInputEditText.text?.toList()?.toCharArray() ?: charArrayOf() - ) - } - - // Set on text changed listeners. - binding.oldPinTextInputEditText.addTextChangedListener(onTextChanged = { _, _, _, _ -> - clearErrors() - }) - - binding.pinTextInputEditText.addTextChangedListener(onTextChanged = { _, _, _, _ -> - clearErrors() - }) - - // Override back button handling. - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - CancelOperationOnBackPressedCallback(viewModel) - ) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - //endregion - - //region BaseFragment - override fun updateViewParameter(parameter: NavigationParameter): Boolean { - if (parameter is PinNavigationParameter) { - viewModel.updateViewModel(parameter) - return true - } - return false - } - - override fun updateView(viewData: ViewData) { - super.updateView(viewData) - - val pinViewData = viewData as? PinViewData ?: return - - binding.titleTextView.setText(pinViewData.title) - binding.messageTextView.setText(pinViewData.message) - binding.oldPinTextInputLayout.visibility = pinViewData.oldPinVisibility - binding.pinTextInputLayout.setHint(pinViewData.pinTextFieldHint) - - pinViewData.lastRecoverableError?.let { - binding.pinTextInputLayout.error = it.description() - } - - when (pinViewData.pinAuthenticatorProtectionStatus) { - // In case of cool down state the screen should be disabled for the specified cool down time period. - is PinAuthenticatorProtectionStatus.LastAttemptFailed -> { - setViewState( - false, - coolDownTime = pinViewData.pinAuthenticatorProtectionStatus.coolDownTimeInSeconds() - ) - isTimerRunning = true - timer = object : CountDownTimer( - pinViewData.pinAuthenticatorProtectionStatus.coolDownTimeInSeconds() * 1000, - 1000 - ) { - override fun onTick(millisUntilFinished: Long) { - binding.messageTextView.text = context?.getString( - R.string.pin_message_cool_down, - millisUntilFinished / 1000 - ) - } - - override fun onFinish() { - setViewState(true, pinViewData.message) - isTimerRunning = false - } - }.start() - } - // In case of locked state, the screen will be disabled forever. - is PinAuthenticatorProtectionStatus.LockedOut -> { - setViewState(false) - } - else -> { - // Do nothing. - } - } - } - //endregion - - //region Private Interface - /** - * Clears all error messages on screen. - */ - private fun clearErrors() { - binding.oldPinTextInputLayout.error = "" - binding.pinTextInputLayout.error = "" - } - - /** - * Enables or disables UI components of this view. - * - * @param isEnabled A flag that tells the UI components should be enabled or disabled. - * @param message String resource identifier that will be used as message text in case the view enabled. - * Message text will be an empty string if this parameter is null. Message text will be overridden with error, - * warning messages in case the view is disabled. - * @param coolDownTime The cool down time period the UI components are disabled for. Please note this method - * won't start a timer, this value is used only for information text composing. - */ - private fun setViewState( - isEnabled: Boolean, - message: Int? = null, - coolDownTime: Long? = null - ) { - binding.oldPinTextInputLayout.isEnabled = isEnabled - binding.pinTextInputLayout.isEnabled = isEnabled - binding.confirmButton.isEnabled = isEnabled - if (isEnabled) { - message?.also { - binding.messageTextView.setText(it) - } ?: run { binding.messageTextView.text = "" } - } else { - binding.messageTextView.text = if (coolDownTime != null) { - context?.getString(R.string.pin_message_cool_down, coolDownTime) - } else { - context?.getString(R.string.pin_message_locked_out) - } - } - } - //endregion -} \ No newline at end of file diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinViewModel.kt deleted file mode 100644 index 5fd925a..0000000 --- a/app/src/main/java/ch/nevis/exampleapp/ui/pin/PinViewModel.kt +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Nevis Mobile Authentication SDK Example App - * - * Copyright © 2023. Nevis Security AG. All rights reserved. - */ - -package ch.nevis.exampleapp.ui.pin - -import ch.nevis.exampleapp.common.error.ErrorHandler -import ch.nevis.exampleapp.domain.model.error.BusinessException -import ch.nevis.exampleapp.ui.base.CancellableOperationViewModel -import ch.nevis.exampleapp.ui.pin.model.PinViewData -import ch.nevis.exampleapp.ui.pin.model.PinViewMode -import ch.nevis.exampleapp.ui.pin.parameter.PinNavigationParameter -import ch.nevis.mobile.sdk.api.operation.pin.PinChangeHandler -import ch.nevis.mobile.sdk.api.operation.pin.PinEnrollmentHandler -import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerificationHandler -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -/** - * View model implementation of PIN view. - */ -@HiltViewModel -class PinViewModel @Inject constructor( - - /** - * An instance of an [ErrorHandler] interface implementation. Received errors will be passed to this error - * handler instance. - */ - private val errorHandler: ErrorHandler -) : CancellableOperationViewModel() { - - /** - * An instance of a [PinChangeHandler] in case a PIN change operation is started and we navigate to - * PIN view to ask the user to enter the old and new PINs to be able to continue the operation. - * [PinViewModel.pinViewMode] must be [PinViewMode.CHANGE_PIN]. - */ - private var pinChangeHandler: PinChangeHandler? = null - - /** - * An instance of a [PinEnrollmentHandler] in case a PIN enrollment is started as part of a - * registration operation and we navigate to PIN view to ask the user to enter, define the PIN to - * be able to continue the operation. - * [PinViewModel.pinViewMode] must be [PinViewMode.ENROLL_PIN]. - */ - private var pinEnrollmentHandler: PinEnrollmentHandler? = null - - /** - * An instance of a [PinUserVerificationHandler] in case a PIN verification is started as part of - * an authentication operation and we navigate to PIN view to ask the user to enter the PIN to be able - * to continue the operation. - * [PinViewModel.pinViewMode] must be [PinViewMode.VERIFY_PIN]. - */ - private var pinUserVerificationHandler: PinUserVerificationHandler? = null - - /** - * The mode, the PIN view intend to be used/initialized. - */ - private var pinViewMode: PinViewMode? = null - - //region Public Interface - /** - * Updates this view model instance based on the [PinNavigationParameter] that was received by - * the owner [PinFragment]. This method must be called by the owner fragment. - * - * @param parameter The [PinNavigationParameter] that was received by the owner [PinFragment]. - */ - fun updateViewModel(parameter: PinNavigationParameter) { - pinViewMode = parameter.pinViewMode - pinChangeHandler = parameter.pinChangeHandler - pinEnrollmentHandler = parameter.pinEnrollmentHandler - pinUserVerificationHandler = parameter.pinUserVerificationHandler - val pinViewData = PinViewData( - parameter.pinViewMode, - parameter.pinAuthenticatorProtectionStatus, - parameter.lastRecoverableError - ) - requestViewUpdate(pinViewData) - } - - /** - * Processes the given PINs based on the current [PinViewMode]. - * - * @param oldPin The text entered into old PIN text field. It is only used when the current [PinViewMode] - * is [PinViewMode.CHANGE_PIN] otherwise it will be ignored. - * @param pin The text entered into PIN text field. - */ - fun processPins(oldPin: CharArray, pin: CharArray) { - try { - when (pinViewMode) { - PinViewMode.CHANGE_PIN -> changePin(oldPin, pin) - PinViewMode.ENROLL_PIN -> enrollPin(pin) - PinViewMode.VERIFY_PIN -> verifyPin(pin) - else -> throw BusinessException.invalidState() - } - } catch (exception: Exception) { - errorHandler.handle(exception) - } - } - //endregion - - //region Private Interface - /** - * Changes the previously enrolled PIN. - * - * @param oldPin The old PIN to be verified before changing it. - * @param newPin The new PIN. - */ - private fun changePin(oldPin: CharArray, newPin: CharArray) { - val pinChangeHandler = - this.pinChangeHandler ?: throw BusinessException.invalidState() - pinChangeHandler.pins(oldPin, newPin) - this.pinChangeHandler = null - } - - /** - * Enrolls a new PIN during a registration operation. - * - * @param pin The PIN entered by the user. - */ - private fun enrollPin(pin: CharArray) { - val pinEnrollmentHandler = - this.pinEnrollmentHandler ?: throw BusinessException.invalidState() - pinEnrollmentHandler.pin(pin) - this.pinEnrollmentHandler = null - } - - /** - * Verifies the given PIN during a authentication operation. - * - * @param pin The PIN entered by the user. - */ - private fun verifyPin(pin: CharArray) { - val pinUserVerificationHandler = - this.pinUserVerificationHandler ?: throw BusinessException.invalidState() - pinUserVerificationHandler.verifyPin(pin) - this.pinUserVerificationHandler = null - } - //endregion - - //region CancellableOperationViewModel - override fun cancelOperation() { - pinChangeHandler?.cancel() - pinChangeHandler = null - - pinEnrollmentHandler?.cancel() - pinEnrollmentHandler = null - - pinUserVerificationHandler?.cancel() - pinUserVerificationHandler = null - } - //endregion -} \ No newline at end of file diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewData.kt b/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewData.kt deleted file mode 100644 index 436d3de..0000000 --- a/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewData.kt +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Nevis Mobile Authentication SDK Example App - * - * Copyright © 2023. Nevis Security AG. All rights reserved. - */ - -package ch.nevis.exampleapp.ui.pin.model - -import android.view.View -import ch.nevis.exampleapp.R -import ch.nevis.exampleapp.ui.base.model.ViewData -import ch.nevis.mobile.sdk.api.operation.RecoverableError -import ch.nevis.mobile.sdk.api.operation.pin.PinAuthenticatorProtectionStatus -import kotlinx.parcelize.IgnoredOnParcel - -/** - * [ViewData] implementation for PIN view and its view model. The view model composes an instance of - * this [PinViewData] class and posts it to the PIN view to indicate that the view related data changed - * the view should be updated. - */ -data class PinViewData( - /** - * The mode, the PIN view intend to be used/initialized. - */ - val pinViewMode: PinViewMode, - - /** - * Status object of the PIN authenticator. - */ - @IgnoredOnParcel - val pinAuthenticatorProtectionStatus: PinAuthenticatorProtectionStatus? = null, - - /** - * The last recoverable error. It exists only if there was already a failed PIN operation attempt. - */ - @IgnoredOnParcel - val lastRecoverableError: RecoverableError? = null -) : ViewData { - - /** - * The identifier of String resource that should be used as title on PIN view. - * Its value depends on the actual [PinViewData.pinViewMode]. - */ - val title: Int - get() { - return when (pinViewMode) { - PinViewMode.CHANGE_PIN -> R.string.pin_title_change_pin - PinViewMode.ENROLL_PIN -> R.string.pin_title_create_pin - PinViewMode.VERIFY_PIN -> R.string.pin_title_verify_pin - } - } - - /** - * The identifier of String resource that should be used as message on PIN view. - * Its value depends on the actual [PinViewData.pinViewMode]. - */ - val message: Int - get() { - return when (pinViewMode) { - PinViewMode.CHANGE_PIN -> R.string.pin_message_change_pin - PinViewMode.ENROLL_PIN -> R.string.pin_message_create_pin - PinViewMode.VERIFY_PIN -> R.string.pin_message_verify_pin - } - } - - /** - * The identifier of String resource that should be used as hint of PIN text field on PIN view. - * Its value depends on the actual [PinViewData.pinViewMode]. - */ - val pinTextFieldHint: Int - get() { - return when (pinViewMode) { - PinViewMode.CHANGE_PIN -> R.string.pin_hint_new_pin - else -> R.string.pin_hint_pin - } - } - - - /** - * The visibility value that tells if the old PIN text field should be displayed on PIN view or not. - * Its value depends on the actual [PinViewData.pinViewMode]. - */ - val oldPinVisibility: Int - get() { - return when (pinViewMode) { - PinViewMode.CHANGE_PIN -> View.VISIBLE - else -> View.GONE - } - } -} diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewMode.kt b/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewMode.kt deleted file mode 100644 index 5354cb3..0000000 --- a/app/src/main/java/ch/nevis/exampleapp/ui/pin/model/PinViewMode.kt +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Nevis Mobile Authentication SDK Example App - * - * Copyright © 2023. Nevis Security AG. All rights reserved. - */ - -package ch.nevis.exampleapp.ui.pin.model - -/** - * Enumeration of available PIN view modes. - */ -enum class PinViewMode { - - /** - * Change PIN mode - */ - CHANGE_PIN, - - /** - * Enroll PIN mode - */ - ENROLL_PIN, - - /** - * Verify PIN mode - */ - VERIFY_PIN -} \ No newline at end of file diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/qrReader/QrReaderViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/qrReader/QrReaderViewModel.kt index e690c77..68c8d5b 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/qrReader/QrReaderViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/qrReader/QrReaderViewModel.kt @@ -13,12 +13,14 @@ import ch.nevis.exampleapp.domain.client.ClientProvider import ch.nevis.exampleapp.domain.deviceInformation.DeviceInformationFactory import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.outOfBand.OutOfBandViewModel +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AccountSelector import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.DevicePasscodeUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -72,11 +74,21 @@ class QrReaderViewModel @Inject constructor( */ pinEnroller: PinEnroller, + /** + * An instance of a [PasswordEnroller] interface implementation. + */ + passwordEnroller: PasswordEnroller, + /** * An instance of a [PinUserVerifier] interface implementation. */ pinUserVerifier: PinUserVerifier, + /** + * An instance of a [PasswordUserVerifier] interface implementation. + */ + passwordUserVerifier: PasswordUserVerifier, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -106,7 +118,9 @@ class QrReaderViewModel @Inject constructor( registrationAuthenticatorSelector, authenticationAuthenticatorSelector, pinEnroller, + passwordEnroller, pinUserVerifier, + passwordUserVerifier, biometricUserVerifier, devicePasscodeUserVerifier, fingerprintUserVerifier, diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt index 0034fab..5856365 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/selectAccount/SelectAccountViewModel.kt @@ -17,12 +17,14 @@ import ch.nevis.exampleapp.ui.base.CancellableOperationViewModel import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.result.parameter.ResultNavigationParameter import ch.nevis.exampleapp.ui.selectAccount.parameter.SelectAccountNavigationParameter +import ch.nevis.mobile.sdk.api.operation.password.PasswordChanger import ch.nevis.mobile.sdk.api.operation.pin.PinChanger import ch.nevis.mobile.sdk.api.operation.selection.AccountSelectionHandler import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.DevicePasscodeUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.FingerprintUserVerifier +import ch.nevis.mobile.sdk.api.operation.userverification.PasswordUserVerifier import ch.nevis.mobile.sdk.api.operation.userverification.PinUserVerifier import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -55,6 +57,11 @@ class SelectAccountViewModel @Inject constructor( */ private val pinUserVerifier: PinUserVerifier, + /** + * An instance of a [PasswordUserVerifier] interface implementation. + */ + private val passwordUserVerifier: PasswordUserVerifier, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -75,6 +82,11 @@ class SelectAccountViewModel @Inject constructor( */ private val pinChanger: PinChanger, + /** + * An instance of a [PasswordChanger] interface implementation. + */ + private val passwordChanger: PasswordChanger, + /** * An instance of an [ErrorHandler] interface implementation. Received errors will be passed to this error * handler instance. @@ -114,6 +126,7 @@ class SelectAccountViewModel @Inject constructor( Operation.AUTHENTICATION -> inBandAuthentication(username) Operation.DEREGISTRATION -> inBandAuthenticationForDeregistration(username) Operation.CHANGE_PIN -> changePin(username) + Operation.CHANGE_PASSWORD -> changePassword(username) Operation.OUT_OF_BAND_AUTHENTICATION -> outOfBandAuthentication(username) else -> throw BusinessException.invalidState() } @@ -145,6 +158,27 @@ class SelectAccountViewModel @Inject constructor( .execute() } + /** + * Starts Password change. + * + * @param username The username that identifies the account the Password change must be started for. + */ + private fun changePassword(username: String) { + val client = clientProvider.get() ?: throw BusinessException.clientNotInitialized() + client.operations().passwordChange() + .username(username) + .passwordChanger(passwordChanger) + .onSuccess { + navigationDispatcher.requestNavigation( + NavigationGraphDirections.actionGlobalResultFragment( + ResultNavigationParameter.forSuccessfulOperation(Operation.CHANGE_PASSWORD) + ) + ) + } + .onError(OnErrorImpl(Operation.CHANGE_PASSWORD, errorHandler)) + .execute() + } + /** * Starts an in-band authentication. * @@ -155,10 +189,11 @@ class SelectAccountViewModel @Inject constructor( client.operations().authentication() .username(username) .authenticatorSelector(authenticatorSelector) + .pinUserVerifier(pinUserVerifier) + .passwordUserVerifier(passwordUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .fingerprintUserVerifier(fingerprintUserVerifier) - .pinUserVerifier(pinUserVerifier) .onSuccess { navigationDispatcher.requestNavigation( NavigationGraphDirections.actionGlobalResultFragment( @@ -180,10 +215,11 @@ class SelectAccountViewModel @Inject constructor( client.operations().authentication() .username(username) .authenticatorSelector(authenticatorSelector) + .pinUserVerifier(pinUserVerifier) + .passwordUserVerifier(passwordUserVerifier) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .fingerprintUserVerifier(fingerprintUserVerifier) - .pinUserVerifier(pinUserVerifier) .onSuccess { client.operations().deregistration() .username(username) diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/model/AuthenticatorItem.kt b/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/model/AuthenticatorItem.kt index 45e3d25..a72e54d 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/model/AuthenticatorItem.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/model/AuthenticatorItem.kt @@ -41,6 +41,10 @@ data class AuthenticatorItem( * The value is calculated based on [AuthenticatorItem.isPolicyCompliant] and [AuthenticatorItem.isUserEnrolled] flags. */ fun isEnabled(): Boolean { - return isPolicyCompliant && (aaid == Authenticator.PIN_AUTHENTICATOR_AAID || isUserEnrolled) + return isPolicyCompliant && ( + aaid == Authenticator.PIN_AUTHENTICATOR_AAID || + aaid == Authenticator.PASSWORD_AUTHENTICATOR_AAID || + isUserEnrolled + ) } } diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/parameter/SelectAuthenticatorNavigationParameter.kt b/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/parameter/SelectAuthenticatorNavigationParameter.kt index 6dd21de..2a1e89c 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/parameter/SelectAuthenticatorNavigationParameter.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/selectAuthenticator/parameter/SelectAuthenticatorNavigationParameter.kt @@ -6,7 +6,6 @@ package ch.nevis.exampleapp.ui.selectAuthenticator.parameter -import ch.nevis.exampleapp.domain.model.operation.Operation import ch.nevis.exampleapp.ui.base.model.NavigationParameter import ch.nevis.exampleapp.ui.selectAuthenticator.model.AuthenticatorItem import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelectionHandler diff --git a/app/src/main/java/ch/nevis/exampleapp/ui/userNamePasswordLogin/UserNamePasswordLoginViewModel.kt b/app/src/main/java/ch/nevis/exampleapp/ui/userNamePasswordLogin/UserNamePasswordLoginViewModel.kt index 1e61046..7b72100 100644 --- a/app/src/main/java/ch/nevis/exampleapp/ui/userNamePasswordLogin/UserNamePasswordLoginViewModel.kt +++ b/app/src/main/java/ch/nevis/exampleapp/ui/userNamePasswordLogin/UserNamePasswordLoginViewModel.kt @@ -22,6 +22,7 @@ import ch.nevis.exampleapp.ui.navigation.NavigationDispatcher import ch.nevis.exampleapp.ui.result.parameter.ResultNavigationParameter import ch.nevis.mobile.sdk.api.authorization.AuthorizationProvider.CookieAuthorizationProvider import ch.nevis.mobile.sdk.api.authorization.Cookie +import ch.nevis.mobile.sdk.api.operation.password.PasswordEnroller import ch.nevis.mobile.sdk.api.operation.pin.PinEnroller import ch.nevis.mobile.sdk.api.operation.selection.AuthenticatorSelector import ch.nevis.mobile.sdk.api.operation.userverification.BiometricUserVerifier @@ -76,6 +77,11 @@ class UserNamePasswordLoginViewModel @Inject constructor( */ private val pinEnroller: PinEnroller, + /** + * An instance of a [PasswordEnroller] interface implementation. + */ + private val passwordEnroller: PasswordEnroller, + /** * An instance of a [BiometricUserVerifier] interface implementation. */ @@ -155,6 +161,7 @@ class UserNamePasswordLoginViewModel @Inject constructor( .allowClass2Sensors(settings.allowClass2Sensors) .authenticatorSelector(authenticatorSelector) .pinEnroller(pinEnroller) + .passwordEnroller(passwordEnroller) .biometricUserVerifier(biometricUserVerifier) .devicePasscodeUserVerifier(devicePasscodeUserVerifier) .fingerprintUserVerifier(fingerprintUserVerifier) diff --git a/app/src/main/res/layout/fragment_pin.xml b/app/src/main/res/layout/fragment_credential.xml similarity index 72% rename from app/src/main/res/layout/fragment_pin.xml rename to app/src/main/res/layout/fragment_credential.xml index a2209b2..fcab4b5 100644 --- a/app/src/main/res/layout/fragment_pin.xml +++ b/app/src/main/res/layout/fragment_credential.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.pin.PinFragment"> + tools:context=".ui.credential.CredentialFragment"> + app:layout_constraintTop_toBottomOf="@id/descriptionTextView"> + android:layout_height="wrap_content" /> + app:layout_constraintTop_toBottomOf="@id/oldCredentialTextInputLayout"> + android:layout_height="wrap_content" /> + +