From 18cd66a34b15735e1e124da04794ce60956ea5a2 Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:42:05 -0400 Subject: [PATCH] PM-9532: pt2. separate vault unlock logic and fail out on error during login. (#3609) --- .../auth/repository/AuthRepositoryImpl.kt | 354 +++++++++------ .../repository/model/LoginResultExtensions.kt | 15 + .../datasource/sdk/VaultSdkSourceImpl.kt | 6 +- .../sdk/model/InitializeCryptoResult.kt | 4 +- .../repository/model/VaultUnlockResult.kt | 13 +- .../util/VaultUnlockResultExtensions.kt | 7 +- .../trusteddevice/TrustedDeviceScreen.kt | 47 +- .../vaultunlock/VaultUnlockViewModel.kt | 2 +- .../auth/repository/AuthRepositoryTest.kt | 420 +++++++++++++++++- .../model/LoginResultExtensionsTest.kt | 33 ++ .../datasource/sdk/VaultSdkSourceTest.kt | 16 +- .../vault/manager/VaultLockManagerTest.kt | 8 +- .../vaultunlock/VaultUnlockViewModelTest.kt | 6 +- 13 files changed, 749 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensions.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensionsTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index ce8b93d2a..feb8c118a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -58,6 +58,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult +import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult @@ -91,6 +92,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -509,18 +511,25 @@ class AuthRepositoryImpl( val userId = profile.userId val privateKey = authDiskSource.getPrivateKey(userId = userId) ?: return LoginResult.Error(errorMessage = null) - vaultRepository.unlockVault( - userId = userId, - email = profile.email, - kdf = profile.toSdkParams(), - privateKey = privateKey, - initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( - requestPrivateKey = requestPrivateKey, - method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey), - ), - // We should already have the org keys from the login sync. - organizationKeys = authDiskSource.getOrganizationKeys(userId = userId), - ) + + checkForVaultUnlockError( + onVaultUnlockError = { error -> + return error.toLoginErrorResult() + }, + ) { + vaultRepository.unlockVault( + userId = userId, + email = profile.email, + kdf = profile.toSdkParams(), + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = requestPrivateKey, + method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey), + ), + // We should already have the org keys from the login sync. + organizationKeys = authDiskSource.getOrganizationKeys(userId = userId), + ) + } authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey) vaultRepository.syncIfNecessary() @@ -947,9 +956,9 @@ class AuthRepositoryImpl( ) } - VaultUnlockResult.AuthenticationError, - VaultUnlockResult.GenericError, + is VaultUnlockResult.AuthenticationError, VaultUnlockResult.InvalidStateError, + VaultUnlockResult.GenericError, -> { IllegalStateException("Failed to unlock vault").asFailure() } @@ -1132,7 +1141,7 @@ class AuthRepositoryImpl( ValidatePinResult.Success(isValid = true) } - InitializeCryptoResult.AuthenticationError -> { + is InitializeCryptoResult.AuthenticationError -> { ValidatePinResult.Success(isValid = false) } } @@ -1350,28 +1359,41 @@ class AuthRepositoryImpl( deviceData: DeviceDataModel?, orgIdentifier: String?, ): LoginResult = userStateTransaction { + val userStateJson = loginResponse.toUserState( previousUserState = authDiskSource.userState, environmentUrlData = environmentRepository.environment.environmentUrlData, ) val userId = userStateJson.activeUserId - // Attempt to unlock the vault with password if possible. - password?.let { - if (loginResponse.privateKey != null && loginResponse.key != null) { - vaultRepository.unlockVault( + checkForVaultUnlockError( + onVaultUnlockError = { vaultUnlockError -> + return@userStateTransaction vaultUnlockError.toLoginErrorResult() + }, + ) { + val isDeviceUnlockAvailable = deviceData != null || + loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null + // if possible attempt to unlock the vault with trusted device data + if (isDeviceUnlockAvailable) { + unlockVaultWithTdeOnLoginSuccess( + loginResponse = loginResponse, userId = userId, - email = userStateJson.activeAccount.profile.email, - kdf = userStateJson.activeAccount.profile.toSdkParams(), - userKey = loginResponse.key, - privateKey = loginResponse.privateKey, - masterPassword = it, - // We can separately unlock vault for organization data after - // receiving the sync response if this data is currently absent. - organizationKeys = null, + userStateJson = userStateJson, + deviceData = deviceData, ) + } else { + password?.let { + unlockVaultWithPasswordOnLoginSuccess( + loginResponse = loginResponse, + userId = userId, + userStateJson = userStateJson, + password = password, + ) + } } + } + password?.let { // Save the master password hash. authSdkSource .hashPassword( @@ -1391,48 +1413,6 @@ class AuthRepositoryImpl( passwordsToCheckMap.put(userId, it) } - // Attempt to unlock the vault with auth request if possible. - // These values will only be null during the Just-in-Time provisioning flow. - if (loginResponse.privateKey != null && loginResponse.key != null) { - deviceData?.let { model -> - vaultRepository.unlockVault( - userId = userId, - email = userStateJson.activeAccount.profile.email, - kdf = userStateJson.activeAccount.profile.toSdkParams(), - privateKey = loginResponse.privateKey, - initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( - requestPrivateKey = model.privateKey, - method = model - .masterPasswordHash - ?.let { - AuthRequestMethod.MasterKey( - protectedMasterKey = model.asymmetricalKey, - authRequestKey = loginResponse.key, - ) - } - ?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey), - ), - // We can separately unlock vault for organization data after - // receiving the sync response if this data is currently absent. - organizationKeys = null, - ) - // We are purposely not storing the master password hash here since it is not - // formatted in in a manner that we can use. We will store it properly the next - // time the user enters their master password and it is validated. - } - } - - // Handle the Trusted Device Encryption flow - loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions?.let { options -> - loginResponse.privateKey?.let { privateKey -> - handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions( - trustedDeviceDecryptionOptions = options, - userStateJson = userStateJson, - privateKey = privateKey, - ) - } - } - authDiskSource.storeAccountTokens( userId = userId, accountTokens = AccountTokensJson( @@ -1466,79 +1446,12 @@ class AuthRepositoryImpl( twoFactorResponse = null resendEmailRequestJson = null twoFactorDeviceData = null - settingsRepository.setDefaultsIfNecessary(userId = userId) vaultRepository.syncIfNecessary() hasPendingAccountAddition = false LoginResult.Success } - /** - * A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE. - */ - private suspend fun handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions( - trustedDeviceDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson, - userStateJson: UserStateJson, - privateKey: String, - ) { - val userId = userStateJson.activeUserId - val deviceKey = authDiskSource.getDeviceKey(userId = userId) - if (deviceKey == null) { - // A null device key means this device is not trusted. - val pendingRequest = authDiskSource.getPendingAuthRequest(userId = userId) ?: return - authRequestManager - .getAuthRequestIfApproved(pendingRequest.requestId) - .getOrNull() - ?.let { request -> - // For approved requests the key will always be present. - val userKey = requireNotNull(request.key) - vaultRepository.unlockVault( - userId = userId, - email = userStateJson.activeAccount.profile.email, - kdf = userStateJson.activeAccount.profile.toSdkParams(), - privateKey = privateKey, - initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( - requestPrivateKey = pendingRequest.requestPrivateKey, - method = AuthRequestMethod.UserKey(protectedUserKey = userKey), - ), - // We can separately unlock vault for organization data after - // receiving the sync response if this data is currently absent. - organizationKeys = null, - ) - authDiskSource.storeUserKey(userId = userId, userKey = userKey) - } - authDiskSource.storePendingAuthRequest( - userId = userId, - pendingAuthRequest = null, - ) - return - } - - val encryptedPrivateKey = trustedDeviceDecryptionOptions.encryptedPrivateKey - val encryptedUserKey = trustedDeviceDecryptionOptions.encryptedUserKey - if (encryptedPrivateKey == null || encryptedUserKey == null) { - // If we have a device key but server is missing private key and user key, we - // need to clear the device key and let the user go through the TDE flow again. - authDiskSource.storeDeviceKey(userId = userId, deviceKey = null) - return - } - vaultRepository.unlockVault( - userId = userId, - email = userStateJson.activeAccount.profile.email, - kdf = userStateJson.activeAccount.profile.toSdkParams(), - privateKey = privateKey, - initUserCryptoMethod = InitUserCryptoMethod.DeviceKey( - deviceKey = deviceKey, - protectedDevicePrivateKey = encryptedPrivateKey, - deviceProtectedUserKey = encryptedUserKey, - ), - // We can separately unlock vault for organization data after - // receiving the sync response if this data is currently absent. - organizationKeys = null, - ) - authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey) - } - /** * A helper method that processes the [GetTokenResponseJson.TwoFactorRequired] when logging in. */ @@ -1564,6 +1477,175 @@ class AuthRepositoryImpl( return LoginResult.TwoFactorRequired } + private suspend fun unlockVaultWithPasswordOnLoginSuccess( + loginResponse: GetTokenResponseJson.Success, + userId: String, + userStateJson: UserStateJson, + password: String?, + ): VaultUnlockResult? { + // Attempt to unlock the vault with password if possible. + val masterPassword = password ?: return null + val privateKey = loginResponse.privateKey ?: return null + val key = loginResponse.key ?: return null + return vaultRepository.unlockVault( + userId = userId, + email = userStateJson.activeAccount.profile.email, + kdf = userStateJson.activeAccount.profile.toSdkParams(), + userKey = key, + privateKey = privateKey, + masterPassword = masterPassword, + // We can separately unlock vault for organization data after + // receiving the sync response if this data is currently absent. + organizationKeys = null, + ) + } + + /** + * Attempt to unlock the vault with trusted device specific data. + */ + private suspend fun unlockVaultWithTdeOnLoginSuccess( + loginResponse: GetTokenResponseJson.Success, + userId: String, + userStateJson: UserStateJson, + deviceData: DeviceDataModel?, + ): VaultUnlockResult? { + // Attempt to unlock the vault with auth request if possible. + // These values will only be null during the Just-in-Time provisioning flow. + if (loginResponse.privateKey != null && loginResponse.key != null) { + deviceData?.let { model -> + return vaultRepository.unlockVault( + userId = userId, + email = userStateJson.activeAccount.profile.email, + kdf = userStateJson.activeAccount.profile.toSdkParams(), + privateKey = loginResponse.privateKey, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = model.privateKey, + method = model + .masterPasswordHash + ?.let { + AuthRequestMethod.MasterKey( + protectedMasterKey = model.asymmetricalKey, + authRequestKey = loginResponse.key, + ) + } + ?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey), + ), + // We can separately unlock vault for organization data after + // receiving the sync response if this data is currently absent. + organizationKeys = null, + ) + // We are purposely not storing the master password hash here since it is not + // formatted in in a manner that we can use. We will store it properly the next + // time the user enters their master password and it is validated. + } + } + + // Handle the Trusted Device Encryption flow + return loginResponse + .userDecryptionOptions + ?.trustedDeviceUserDecryptionOptions + ?.let { options -> + loginResponse.privateKey?.let { privateKey -> + unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys( + options = options, + userStateJson = userStateJson, + privateKey = privateKey, + ) + } + } + } + + /** + * A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE + * and store the necessary keys when appropriate. + */ + private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys( + options: TrustedDeviceUserDecryptionOptionsJson, + userStateJson: UserStateJson, + privateKey: String, + ): VaultUnlockResult? { + var vaultUnlockResult: VaultUnlockResult? = null + val userId = userStateJson.activeUserId + val deviceKey = authDiskSource.getDeviceKey(userId = userId) + if (deviceKey == null) { + // A null device key means this device is not trusted. + val pendingRequest = authDiskSource + .getPendingAuthRequest(userId = userId) + ?: return null + authRequestManager + .getAuthRequestIfApproved(pendingRequest.requestId) + .getOrNull() + ?.let { request -> + // For approved requests the key will always be present. + val userKey = requireNotNull(request.key) + vaultUnlockResult = vaultRepository.unlockVault( + userId = userId, + email = userStateJson.activeAccount.profile.email, + kdf = userStateJson.activeAccount.profile.toSdkParams(), + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = pendingRequest.requestPrivateKey, + method = AuthRequestMethod.UserKey(protectedUserKey = userKey), + ), + // We can separately unlock vault for organization data after + // receiving the sync response if this data is currently absent. + organizationKeys = null, + ) + authDiskSource.storeUserKey(userId = userId, userKey = userKey) + } + authDiskSource.storePendingAuthRequest( + userId = userId, + pendingAuthRequest = null, + ) + return vaultUnlockResult + } + + val encryptedPrivateKey = options.encryptedPrivateKey + val encryptedUserKey = options.encryptedUserKey + + if (encryptedPrivateKey == null || encryptedUserKey == null) { + // If we have a device key but server is missing private key and user key, we + // need to clear the device key and let the user go through the TDE flow again. + authDiskSource.storeDeviceKey(userId = userId, deviceKey = null) + return null + } + + vaultUnlockResult = vaultRepository.unlockVault( + userId = userId, + email = userStateJson.activeAccount.profile.email, + kdf = userStateJson.activeAccount.profile.toSdkParams(), + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.DeviceKey( + deviceKey = deviceKey, + protectedDevicePrivateKey = encryptedPrivateKey, + deviceProtectedUserKey = encryptedUserKey, + ), + // We can separately unlock vault for organization data after + // receiving the sync response if this data is currently absent. + organizationKeys = null, + ) + + if (vaultUnlockResult is VaultUnlockResult.Success) { + authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey) + } + return vaultUnlockResult + } + + /** + * A helper function to check for a vault unlock related error when logging in. + * + * @param onVaultUnlockError a lambda function to be invoked in the event a [VaultUnlockError] + * is produced via the passed in [block] + * @param block a lambda representing logic which produces either a [VaultUnlockResult] which + * is castable to [VaultUnlockError] or `null` + */ + private inline fun checkForVaultUnlockError( + onVaultUnlockError: (VaultUnlockError) -> Unit, + block: () -> VaultUnlockResult?, + ) { + (block() as? VaultUnlockError)?.also(onVaultUnlockError) + } + //endregion LoginCommon /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensions.kt new file mode 100644 index 000000000..ec2baf01b --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensions.kt @@ -0,0 +1,15 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult + +/** + * Helper function to map a [VaultUnlockError] to a [LoginResult.Error] with + * the necessary `message` if applicable. + */ +fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) { + is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message) + VaultUnlockResult.GenericError, + VaultUnlockResult.InvalidStateError, + -> LoginResult.Error(errorMessage = null) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt index 797294b83..659963dc7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt @@ -134,7 +134,7 @@ class VaultSdkSourceImpl( InitializeCryptoResult.Success } catch (exception: BitwardenException) { // The only truly expected error from the SDK is an incorrect key/password. - InitializeCryptoResult.AuthenticationError + InitializeCryptoResult.AuthenticationError(message = exception.message) } } @@ -150,7 +150,9 @@ class VaultSdkSourceImpl( InitializeCryptoResult.Success } catch (exception: BitwardenException) { // The only truly expected error from the SDK is for incorrect keys. - InitializeCryptoResult.AuthenticationError + InitializeCryptoResult.AuthenticationError( + message = exception.message, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt index 197fe3d32..54a8486e1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt @@ -13,5 +13,7 @@ sealed class InitializeCryptoResult { /** * Incorrect password or key(s) provided. */ - data object AuthenticationError : InitializeCryptoResult() + data class AuthenticationError( + val message: String? = null, + ) : InitializeCryptoResult() } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt index 43be52a83..5d2b28ed6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt @@ -13,15 +13,22 @@ sealed class VaultUnlockResult { /** * Incorrect password provided. */ - data object AuthenticationError : VaultUnlockResult() + data class AuthenticationError( + val message: String? = null, + ) : VaultUnlockResult(), VaultUnlockError /** * Unable to access user state information. */ - data object InvalidStateError : VaultUnlockResult() + data object InvalidStateError : VaultUnlockResult(), VaultUnlockError /** * Generic error thrown by Bitwarden SDK. */ - data object GenericError : VaultUnlockResult() + data object GenericError : VaultUnlockResult(), VaultUnlockError } + +/** + * Sealed interface to denote that a [VaultUnlockResult] is an error result. + */ +sealed interface VaultUnlockError diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt index 9e55ace45..763bf754e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt @@ -8,6 +8,11 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult */ fun InitializeCryptoResult.toVaultUnlockResult(): VaultUnlockResult = when (this) { - InitializeCryptoResult.AuthenticationError -> VaultUnlockResult.AuthenticationError + is InitializeCryptoResult.AuthenticationError -> { + VaultUnlockResult.AuthenticationError( + message = this.message, + ) + } + InitializeCryptoResult.Success -> VaultUnlockResult.Success } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt index d1cff2206..6699c72a2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt @@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** * The top level composable for the Reset Password screen. @@ -230,26 +231,28 @@ private fun TrustedDeviceDialogs( @Preview @Composable private fun TrustedDeviceScaffold_preview() { - TrustedDeviceScaffold( - state = TrustedDeviceState( - dialogState = null, - isRemembered = false, - emailAddress = "email@bitwarden.com", - environmentLabel = "vault.bitwarden.pw", - showContinueButton = false, - showOtherDeviceButton = true, - showRequestAdminButton = true, - showMasterPasswordButton = true, - ), - handlers = TrustedDeviceHandlers( - onBackClick = {}, - onDismissDialog = {}, - onRememberToggle = {}, - onContinueClick = {}, - onApproveWithAdminClick = {}, - onApproveWithDeviceClick = {}, - onApproveWithPasswordClick = {}, - onNotYouButtonClick = {}, - ), - ) + BitwardenTheme { + TrustedDeviceScaffold( + state = TrustedDeviceState( + dialogState = null, + isRemembered = false, + emailAddress = "email@bitwarden.com", + environmentLabel = "vault.bitwarden.pw", + showContinueButton = false, + showOtherDeviceButton = true, + showRequestAdminButton = true, + showMasterPasswordButton = true, + ), + handlers = TrustedDeviceHandlers( + onBackClick = {}, + onDismissDialog = {}, + onRememberToggle = {}, + onContinueClick = {}, + onApproveWithAdminClick = {}, + onApproveWithDeviceClick = {}, + onApproveWithPasswordClick = {}, + onNotYouButtonClick = {}, + ), + ) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 964d09753..8ea530b18 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -260,7 +260,7 @@ class VaultUnlockViewModel @Inject constructor( action.vaultUnlockResult is VaultUnlockResult.Success when (action.vaultUnlockResult) { - VaultUnlockResult.AuthenticationError -> { + is VaultUnlockResult.AuthenticationError -> { mutableStateFlow.update { it.copy( dialog = VaultUnlockState.VaultUnlockDialog.Error( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 0ab3ac95c..989385d36 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -1306,6 +1306,57 @@ class AuthRepositoryTest { assertEquals(LoginResult.Success, result) } + @Test + fun `completeTdeLogin where vault unlock fails should return LoginResult error`() = runTest { + val requestPrivateKey = "requestPrivateKey" + val asymmetricalKey = "asymmetricalKey" + val privateKey = "privateKey" + val orgKeys = mapOf("orgId" to "orgKey") + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + fakeAuthDiskSource.storePrivateKey(userId = USER_ID_1, privateKey = privateKey) + fakeAuthDiskSource.storeOrganizationKeys(userId = USER_ID_1, organizationKeys = orgKeys) + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = SINGLE_USER_STATE_1.activeAccount.profile.email, + kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams(), + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = requestPrivateKey, + method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey), + ), + organizationKeys = orgKeys, + ) + } returns VaultUnlockResult.AuthenticationError(message = null) + coEvery { vaultRepository.syncIfNecessary() } just runs + + val result = repository.completeTdeLogin( + requestPrivateKey = requestPrivateKey, + asymmetricalKey = asymmetricalKey, + ) + + coVerify(exactly = 1) { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = SINGLE_USER_STATE_1.activeAccount.profile.email, + kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams(), + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = requestPrivateKey, + method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey), + ), + organizationKeys = orgKeys, + ) + } + + coVerify(exactly = 0) { + vaultRepository.syncIfNecessary() + } + + fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null) + assertEquals(LoginResult.Error(errorMessage = null), result) + } + @Test fun `login when pre login fails should return Error with no message`() = runTest { coEvery { @@ -1471,6 +1522,94 @@ class AuthRepositoryTest { verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } } + @Test + @Suppress("MaxLineLength") + fun `login should return Error result when get token succeeds but unlock vault fails`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + val expectedErrorMessage = "crypto key failure" + + coEvery { + identityService.preLogin(email = EMAIL) + } returns PRE_LOGIN_SUCCESS.asSuccess() + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key!!, + privateKey = successResponse.privateKey!!, + organizationKeys = null, + masterPassword = PASSWORD, + ) + } returns VaultUnlockResult.AuthenticationError(expectedErrorMessage) + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.Error(errorMessage = expectedErrorMessage), result) + assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value) + coVerify { identityService.preLogin(email = EMAIL) } + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = null, + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = null, + ) + fakeAuthDiskSource.assertMasterPasswordHash( + userId = USER_ID_1, + passwordHash = null, + ) + coVerify(exactly = 1) { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key!!, + privateKey = successResponse.privateKey!!, + organizationKeys = null, + masterPassword = PASSWORD, + ) + } + + coVerify(exactly = 0) { + vaultRepository.syncIfNecessary() + settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) + } + + assertEquals( + null, + fakeAuthDiskSource.userState, + ) + } + @Test @Suppress("MaxLineLength") fun `login get token succeeds with null keys and hasMasterPassword false should not call unlockVault`() = @@ -1796,6 +1935,97 @@ class AuthRepositoryTest { ) } + @Test + @Suppress("MaxLineLength") + fun `login two factor should return Error result when get token succeeds but unlock vault fails`() = runTest { + val twoFactorResponse = GetTokenResponseJson + .TwoFactorRequired( + authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA, + captchaToken = null, + ssoToken = null, + twoFactorProviders = null, + ) + // Attempt a normal login with a two factor error first, so that the auth + // data will be cached. + coEvery { identityService.preLogin(EMAIL) } returns PRE_LOGIN_SUCCESS.asSuccess() + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns twoFactorResponse + .asSuccess() + val firstResult = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.TwoFactorRequired, firstResult) + coVerify { identityService.preLogin(email = EMAIL) } + coVerify { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } + + // Login with two factor data. + val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy( + twoFactorToken = "twoFactorTokenToStore", + ) + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + twoFactorData = TWO_FACTOR_DATA, + ) + } returns successResponse.asSuccess() + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key!!, + privateKey = successResponse.privateKey!!, + organizationKeys = null, + masterPassword = PASSWORD, + ) + } returns VaultUnlockResult.InvalidStateError + every { + successResponse.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + val finalResult = repository.login( + email = EMAIL, + password = PASSWORD, + twoFactorData = TWO_FACTOR_DATA, + captchaToken = null, + ) + assertEquals(LoginResult.Error(errorMessage = null), finalResult) + assertEquals(twoFactorResponse, repository.twoFactorResponse) + fakeAuthDiskSource.assertTwoFactorToken( + email = EMAIL, + twoFactorToken = null, + ) + + coVerify(exactly = 0) { + vaultRepository.syncIfNecessary() + } + } + @Test fun `login uses remembered two factor tokens`() = runTest { fakeAuthDiskSource.storeTwoFactorToken(EMAIL, "storedTwoFactorToken") @@ -1978,7 +2208,100 @@ class AuthRepositoryTest { @Test @Suppress("MaxLineLength") - fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync`() = + fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with MasteryKey`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.AuthRequest( + username = EMAIL, + authRequestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + method = AuthRequestMethod.MasterKey( + authRequestKey = successResponse.key!!, + protectedMasterKey = DEVICE_ASYMMETRICAL_KEY, + ), + ), + ) + } returns VaultUnlockResult.Success + val result = repository.login( + email = EMAIL, + requestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + asymmetricalKey = DEVICE_ASYMMETRICAL_KEY, + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + masterPasswordHash = PASSWORD_HASH, + captchaToken = null, + ) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", + ) + coVerify { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.AuthRequest( + username = EMAIL, + authRequestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + vaultRepository.syncIfNecessary() + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + method = AuthRequestMethod.MasterKey( + authRequestKey = successResponse.key!!, + protectedMasterKey = DEVICE_ASYMMETRICAL_KEY, + ), + ), + ) + } + assertEquals( + SINGLE_USER_STATE_1, + fakeAuthDiskSource.userState, + ) + verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } + } + + @Test + @Suppress("MaxLineLength") + fun `login with device should return Error result when get token succeeds but unlock vault fails`() = runTest { val successResponse = GET_TOKEN_RESPONSE_SUCCESS coEvery { @@ -2410,6 +2733,97 @@ class AuthRepositoryTest { verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } } + @Test + @Suppress("MaxLineLength") + fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with UserKey`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.AuthRequest( + username = EMAIL, + authRequestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + method = AuthRequestMethod.UserKey( + protectedUserKey = DEVICE_ASYMMETRICAL_KEY, + ), + ), + ) + } returns VaultUnlockResult.Success + val result = repository.login( + email = EMAIL, + requestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + asymmetricalKey = DEVICE_ASYMMETRICAL_KEY, + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + masterPasswordHash = null, + captchaToken = null, + ) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", + ) + coVerify { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.AuthRequest( + username = EMAIL, + authRequestId = DEVICE_REQUEST_ID, + accessCode = DEVICE_ACCESS_CODE, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + vaultRepository.syncIfNecessary() + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + initUserCryptoMethod = InitUserCryptoMethod.AuthRequest( + requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY, + method = AuthRequestMethod.UserKey( + protectedUserKey = DEVICE_ASYMMETRICAL_KEY, + ), + ), + ) + } + assertEquals( + SINGLE_USER_STATE_1, + fakeAuthDiskSource.userState, + ) + verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } + } + @Test @Suppress("MaxLineLength") fun `SSO login get token succeeds with trusted device key and no keys should return Success, clear device key, update AuthState, update stored keys, and sync`() = @@ -2539,7 +2953,7 @@ class AuthRepositoryTest { fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = encryptedUserKey) fakeAuthDiskSource.assertDeviceKey(userId = USER_ID_1, deviceKey = deviceKey) assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState) - coVerify(exactly = 1) { + coVerify { identityService.getToken( email = EMAIL, authModel = IdentityTokenAuthModel.SingleSignOn( @@ -4739,7 +5153,7 @@ class AuthRepositoryTest { userId = SINGLE_USER_STATE_1.activeUserId, request = any(), ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() + } returns InitializeCryptoResult.AuthenticationError().asSuccess() val result = repository.validatePin(pin = pin) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensionsTest.kt new file mode 100644 index 000000000..db4d4d4a8 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/model/LoginResultExtensionsTest.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class LoginResultExtensionsTest { + + @Test + fun `VaultUnlockResult with error message maps to LoginResult Error with correct message`() { + val errorMessage = "foo" + val result = VaultUnlockResult.AuthenticationError(errorMessage).toLoginErrorResult() + assertEquals(LoginResult.Error(errorMessage), result) + } + + @Test + @Suppress("MaxLineLength") + fun `VaultUnlockResult with null error message as default maps to LoginResult Error with null message`() { + val result = VaultUnlockResult.AuthenticationError().toLoginErrorResult() + assertEquals(LoginResult.Error(errorMessage = null), result) + } + + @Test + @Suppress("MaxLineLength") + fun `VaultUnlockResult with no message value are mapped to LoginResult with null error message`() { + val invalidStateResult = VaultUnlockResult.InvalidStateError.toLoginErrorResult() + val genericErrorResult = VaultUnlockResult.GenericError.toLoginErrorResult() + val expectedResult = LoginResult.Error(errorMessage = null) + + assertEquals(expectedResult, invalidStateResult) + assertEquals(expectedResult, genericErrorResult) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt index 27ab5bce0..628c19c71 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt @@ -330,11 +330,13 @@ class VaultSdkSourceTest { } @Test - fun `initializeUserCrypto with BitwardenException failure should return AuthenticationError`() = + @Suppress("MaxLineLength") + fun `initializeUserCrypto with BitwardenException failure should return AuthenticationError with message`() = runBlocking { val userId = "userId" val mockInitCryptoRequest = mockk() - val expectedException = BitwardenException.E(message = "") + val expectedErrorMessage = "Whoopsy" + val expectedException = BitwardenException.E(message = expectedErrorMessage) coEvery { clientCrypto.initializeUserCrypto( req = mockInitCryptoRequest, @@ -345,7 +347,7 @@ class VaultSdkSourceTest { request = mockInitCryptoRequest, ) assertEquals( - InitializeCryptoResult.AuthenticationError.asSuccess(), + InitializeCryptoResult.AuthenticationError(expectedErrorMessage).asSuccess(), result, ) coVerify { @@ -409,11 +411,13 @@ class VaultSdkSourceTest { } @Test - fun `initializeOrgCrypto with BitwardenException failure should return AuthenticationError`() = + @Suppress("MaxLineLength") + fun `initializeOrgCrypto with BitwardenException failure should return AuthenticationError with correct message`() = runBlocking { val userId = "userId" val mockInitCryptoRequest = mockk() - val expectedException = BitwardenException.E(message = "") + val expectedErrorMessage = "Whoopsy2" + val expectedException = BitwardenException.E(message = expectedErrorMessage) coEvery { clientCrypto.initializeOrgCrypto( req = mockInitCryptoRequest, @@ -424,7 +428,7 @@ class VaultSdkSourceTest { request = mockInitCryptoRequest, ) assertEquals( - InitializeCryptoResult.AuthenticationError.asSuccess(), + InitializeCryptoResult.AuthenticationError(expectedErrorMessage).asSuccess(), result, ) coVerify { diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt index e50a02657..faf54539e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt @@ -938,7 +938,7 @@ class VaultLockManagerTest { ), ), ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() + } returns InitializeCryptoResult.AuthenticationError().asSuccess() assertEquals( emptyList(), @@ -961,7 +961,7 @@ class VaultLockManagerTest { organizationKeys = organizationKeys, ) - assertEquals(VaultUnlockResult.AuthenticationError, result) + assertEquals(VaultUnlockResult.AuthenticationError(), result) assertEquals( emptyList(), vaultLockManager.vaultUnlockDataStateFlow.value, @@ -1015,7 +1015,7 @@ class VaultLockManagerTest { userId = USER_ID, request = InitOrgCryptoRequest(organizationKeys = organizationKeys), ) - } returns InitializeCryptoResult.AuthenticationError.asSuccess() + } returns InitializeCryptoResult.AuthenticationError().asSuccess() assertEquals( emptyList(), @@ -1038,7 +1038,7 @@ class VaultLockManagerTest { organizationKeys = organizationKeys, ) - assertEquals(VaultUnlockResult.AuthenticationError, result) + assertEquals(VaultUnlockResult.AuthenticationError(), result) assertEquals( emptyList(), vaultLockManager.vaultUnlockDataStateFlow.value, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index e95ad1112..95a9d6890 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -440,7 +440,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = initialState) coEvery { vaultRepository.unlockVaultWithMasterPassword(password) - } returns VaultUnlockResult.AuthenticationError + } returns VaultUnlockResult.AuthenticationError() viewModel.trySendAction(VaultUnlockAction.UnlockClick) assertEquals( @@ -577,7 +577,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = initialState) coEvery { vaultRepository.unlockVaultWithPin(pin) - } returns VaultUnlockResult.AuthenticationError + } returns VaultUnlockResult.AuthenticationError() viewModel.trySendAction(VaultUnlockAction.UnlockClick) assertEquals( @@ -726,7 +726,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(state = initialState) coEvery { vaultRepository.unlockVaultWithBiometrics() - } returns VaultUnlockResult.AuthenticationError + } returns VaultUnlockResult.AuthenticationError() viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER))