diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index ed5a90cfa51..3f49b5135df 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -172,6 +172,16 @@ interface AuthDiskSource { pendingAuthRequest: PendingAuthRequestJson?, ) + /** + * Gets the biometrics initialization vector for the given [userId]. + */ + fun getUserBiometricInitVector(userId: String): ByteArray? + + /** + * Stores the biometrics initialization vector for the given [userId]. + */ + fun storeUserBiometricInitVector(userId: String, iv: ByteArray?) + /** * Gets the biometrics key for the given [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 302a2ed5744..811f3c61de7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -23,6 +23,7 @@ import java.util.UUID private const val ACCOUNT_TOKENS_KEY = "accountTokens" private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetric" private const val AUTHENTICATOR_SYNC_UNLOCK_KEY = "authenticatorSyncUnlock" +private const val BIOMETRICS_INIT_VECTOR_KEY = "biometricInitializationVector" private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock" private const val USER_AUTO_UNLOCK_KEY_KEY = "userKeyAutoUnlock" private const val DEVICE_KEY_KEY = "deviceKey" @@ -145,6 +146,7 @@ class AuthDiskSourceImpl( storePrivateKey(userId = userId, privateKey = null) storeOrganizationKeys(userId = userId, organizationKeys = null) storeOrganizations(userId = userId, organizations = null) + storeUserBiometricInitVector(userId = userId, iv = null) storeUserBiometricUnlockKey(userId = userId, biometricsKey = null) storeMasterPasswordHash(userId = userId, passwordHash = null) storePolicies(userId = userId, policies = null) @@ -280,6 +282,17 @@ class AuthDiskSourceImpl( ) } + override fun getUserBiometricInitVector(userId: String): ByteArray? = + getEncryptedString(key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId)) + ?.toByteArray(Charsets.ISO_8859_1) + + override fun storeUserBiometricInitVector(userId: String, iv: ByteArray?) { + putEncryptedString( + key = BIOMETRICS_INIT_VECTOR_KEY.appendIdentifier(userId), + value = iv?.toString(Charsets.ISO_8859_1), + ) + } + override fun getUserBiometricUnlockKey(userId: String): String? = getEncryptedString(key = BIOMETRICS_UNLOCK_KEY.appendIdentifier(userId)) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManager.kt index 8eb391fc2a7..ee7f5ccc43e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManager.kt @@ -20,12 +20,6 @@ interface BiometricsEncryptionManager { userId: String, ): Cipher? - /** - * Sets up biometrics to ensure future integrity checks work properly. If this method has never - * been called [isBiometricIntegrityValid] will return false. - */ - fun setupBiometrics(userId: String) - /** * Checks to verify that the biometrics integrity is still valid. This returns `true` if the * biometrics data has not changed since the app setup biometrics; `false` will be returned if diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManagerImpl.kt index 83387786f0e..bd1527e3d79 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/BiometricsEncryptionManagerImpl.kt @@ -4,9 +4,9 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties import com.x8bit.bitwarden.BuildConfig +import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource -import java.io.IOException import java.security.InvalidAlgorithmParameterException import java.security.InvalidKeyException import java.security.KeyStore @@ -15,12 +15,12 @@ import java.security.NoSuchAlgorithmException import java.security.NoSuchProviderException import java.security.ProviderException import java.security.UnrecoverableKeyException -import java.security.cert.CertificateException import java.util.UUID import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.NoSuchPaddingException import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec /** * Default implementation of [BiometricsEncryptionManager] for managing Android keystore encryption @@ -28,6 +28,7 @@ import javax.crypto.SecretKey */ @OmitFromCoverage class BiometricsEncryptionManagerImpl( + private val authDiskSource: AuthDiskSource, private val settingsDiskSource: SettingsDiskSource, ) : BiometricsEncryptionManager { private val keystore = KeyStore @@ -50,7 +51,7 @@ class BiometricsEncryptionManagerImpl( val secretKey: SecretKey = generateKeyOrNull() ?: run { // user removed all biometrics from the device - settingsDiskSource.systemBiometricIntegritySource = null + destroyBiometrics(userId = userId) return null } val cipher = try { @@ -60,37 +61,27 @@ class BiometricsEncryptionManagerImpl( } catch (_: NoSuchPaddingException) { return null } + // Instantiate integrity values. + createIntegrityValues(userId = userId) // This should never fail to initialize / return false because the cipher is newly generated - initializeCipher( - userId = userId, - cipher = cipher, - secretKey = secretKey, - ) + cipher.initializeCipher(userId = userId, secretKey = secretKey) return cipher } override fun getOrCreateCipher(userId: String): Cipher? { - val secretKey = getSecretKeyOrNull() + val secretKey: SecretKey = getSecretKeyOrNull() ?: generateKeyOrNull() ?: run { // user removed all biometrics from the device - settingsDiskSource.systemBiometricIntegritySource = null + destroyBiometrics(userId = userId) return null } val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION) - val isCipherInitialized = initializeCipher( - userId = userId, - cipher = cipher, - secretKey = secretKey, - ) + val isCipherInitialized = cipher.initializeCipher(userId = userId, secretKey = secretKey) return cipher?.takeIf { isCipherInitialized } } - override fun setupBiometrics(userId: String) { - createIntegrityValues(userId) - } - override fun isBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean = isSystemBiometricIntegrityValid(userId, cipher) && isAccountBiometricIntegrityValid(userId) @@ -112,10 +103,7 @@ class BiometricsEncryptionManagerImpl( */ private fun generateKeyOrNull(): SecretKey? { val keyGen = try { - KeyGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_AES, - ENCRYPTION_KEYSTORE_NAME, - ) + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME) } catch (_: NoSuchAlgorithmException) { return null } catch (_: NoSuchProviderException) { @@ -124,40 +112,24 @@ class BiometricsEncryptionManagerImpl( return null } - try { + return try { keyGen.init(keyGenParameterSpec) keyGen.generateKey() } catch (_: InvalidAlgorithmParameterException) { - return null + null } catch (_: ProviderException) { - return null + null } - - return getSecretKeyOrNull() } /** * Returns the [SecretKey] stored in the keystore, or null if there isn't one. */ - private fun getSecretKeyOrNull(): SecretKey? { + private fun getSecretKeyOrNull(): SecretKey? = try { - keystore.load(null) - } catch (_: IllegalArgumentException) { - // keystore could not be loaded because [param] is unrecognized. - return null - } catch (_: IOException) { - // keystore data format is invalid or the password is incorrect. - return null - } catch (_: NoSuchAlgorithmException) { - // keystore integrity could not be checked due to missing algorithm. - return null - } catch (_: CertificateException) { - // keystore certificates could not be loaded - return null - } - - return try { - keystore.getKey(ENCRYPTION_KEY_NAME, null) as? SecretKey + keystore + .getKey(ENCRYPTION_KEY_NAME, null) + ?.let { it as SecretKey } } catch (_: KeyStoreException) { // keystore was not loaded null @@ -168,30 +140,31 @@ class BiometricsEncryptionManagerImpl( // key could not be recovered null } - } /** * Initialize a [Cipher] and return a boolean indicating whether it is valid. */ - private fun initializeCipher( + private fun Cipher.initializeCipher( userId: String, - cipher: Cipher, secretKey: SecretKey, ): Boolean = try { - cipher.init(Cipher.ENCRYPT_MODE, secretKey) + authDiskSource + .getUserBiometricInitVector(userId = userId) + ?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) } + ?: init(Cipher.ENCRYPT_MODE, secretKey) true } catch (_: KeyPermanentlyInvalidatedException) { // Biometric has changed - settingsDiskSource.systemBiometricIntegritySource = null + destroyBiometrics(userId = userId) false } catch (_: UnrecoverableKeyException) { // Biometric was disabled and re-enabled - settingsDiskSource.systemBiometricIntegritySource = null + destroyBiometrics(userId = userId) false } catch (_: InvalidKeyException) { - // Fallback for old Bitwarden users without a key - createIntegrityValues(userId) + // User has no key + destroyBiometrics(userId = userId) true } @@ -201,11 +174,7 @@ class BiometricsEncryptionManagerImpl( private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean { val secretKey = getSecretKeyOrNull() return if (cipher != null && secretKey != null) { - initializeCipher( - userId = userId, - cipher = cipher, - secretKey = secretKey, - ) + cipher.initializeCipher(userId = userId, secretKey = secretKey) } else { false } @@ -215,7 +184,6 @@ class BiometricsEncryptionManagerImpl( * Creates the initial values to be used for biometrics, including the key from which the * master [Cipher] will be generated. */ - @Suppress("TooGenericExceptionCaught") private fun createIntegrityValues(userId: String) { val systemBiometricIntegritySource = settingsDiskSource .systemBiometricIntegritySource @@ -226,10 +194,20 @@ class BiometricsEncryptionManagerImpl( systemBioIntegrityState = systemBiometricIntegritySource, value = true, ) + } - // Ignore result so biometrics function on devices that are in a state where key generation - // is not functioning - createCipherOrNull(userId) + private fun destroyBiometrics(userId: String) { + settingsDiskSource.systemBiometricIntegritySource?.let { systemBioIntegrityState -> + settingsDiskSource.storeAccountBiometricIntegrityValidity( + userId = userId, + systemBioIntegrityState = systemBioIntegrityState, + value = null, + ) + } + settingsDiskSource.systemBiometricIntegritySource = null + authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null) + authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null) + keystore.deleteEntry(ENCRYPTION_KEY_NAME) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index 497d04a51e7..4cda838ffe8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -141,8 +141,10 @@ object PlatformManagerModule { @Provides @Singleton fun provideBiometricsEncryptionManager( + authDiskSource: AuthDiskSource, settingsDiskSource: SettingsDiskSource, ): BiometricsEncryptionManager = BiometricsEncryptionManagerImpl( + authDiskSource = authDiskSource, settingsDiskSource = settingsDiskSource, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 2891d63a028..3fc66076af6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.time.Instant +import javax.crypto.Cipher /** * Provides an API for observing and modifying settings state. @@ -234,7 +235,7 @@ interface SettingsRepository { * Stores the encrypted user key for biometrics, allowing it to be used to unlock the current * user's vault. */ - suspend fun setupBiometricsKey(): BiometricsKeyResult + suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult /** * Stores the given PIN, allowing it to be used to unlock the current user's vault. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index 79df7d8d3d4..d30332d00a5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -10,7 +10,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.policyInformation import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource -import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.Instant +import javax.crypto.Cipher private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG @@ -50,7 +50,6 @@ class SettingsRepositoryImpl( private val authDiskSource: AuthDiskSource, private val settingsDiskSource: SettingsDiskSource, private val vaultSdkSource: VaultSdkSource, - private val biometricsEncryptionManager: BiometricsEncryptionManager, accessibilityEnabledManager: AccessibilityEnabledManager, policyManager: PolicyManager, dispatcherManager: DispatcherManager, @@ -482,13 +481,18 @@ class SettingsRepositoryImpl( } } - override suspend fun setupBiometricsKey(): BiometricsKeyResult { + override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult { val userId = activeUserId ?: return BiometricsKeyResult.Error - biometricsEncryptionManager.setupBiometrics(userId) return vaultSdkSource .getUserEncryptionKey(userId = userId) - .onSuccess { - authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = it) + .onSuccess { biometricsKey -> + authDiskSource.storeUserBiometricUnlockKey( + userId = userId, + biometricsKey = cipher + .doFinal(biometricsKey.encodeToByteArray()) + .toString(Charsets.ISO_8859_1), + ) + authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv) } .fold( onSuccess = { BiometricsKeyResult.Success }, @@ -498,6 +502,7 @@ class SettingsRepositoryImpl( override fun clearBiometricsKey() { val userId = activeUserId ?: return + authDiskSource.storeUserBiometricInitVector(userId = userId, iv = null) authDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt index 13022f5a69c..a2374122e46 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/di/PlatformRepositoryModule.kt @@ -10,7 +10,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigService -import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository @@ -92,7 +91,6 @@ object PlatformRepositoryModule { authDiskSource: AuthDiskSource, settingsDiskSource: SettingsDiskSource, vaultSdkSource: VaultSdkSource, - encryptionManager: BiometricsEncryptionManager, accessibilityEnabledManager: AccessibilityEnabledManager, dispatcherManager: DispatcherManager, policyManager: PolicyManager, @@ -103,7 +101,6 @@ object PlatformRepositoryModule { authDiskSource = authDiskSource, settingsDiskSource = settingsDiskSource, vaultSdkSource = vaultSdkSource, - biometricsEncryptionManager = encryptionManager, accessibilityEnabledManager = accessibilityEnabledManager, dispatcherManager = dispatcherManager, policyManager = policyManager, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 1a011a045cf..14d3ee0adee 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import javax.crypto.Cipher /** * Responsible for managing vault data inside the network layer. @@ -189,7 +190,7 @@ interface VaultRepository : CipherManager, VaultLockManager { /** * Attempt to unlock the vault using the stored biometric key for the currently active user. */ - suspend fun unlockVaultWithBiometrics(): VaultUnlockResult + suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult /** * Attempt to unlock the vault with the given [masterPassword] and for the currently active diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 54c257d28d8..e9a278d60cd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -116,6 +116,7 @@ import kotlinx.coroutines.launch import retrofit2.HttpException import java.time.Clock import java.time.temporal.ChronoUnit +import javax.crypto.Cipher /** * A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the @@ -542,19 +543,36 @@ class VaultRepositoryImpl( ), ) - override suspend fun unlockVaultWithBiometrics(): VaultUnlockResult { + override suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult { val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError val biometricsKey = authDiskSource .getUserBiometricUnlockKey(userId = userId) ?: return VaultUnlockResult.InvalidStateError - return unlockVaultForUser( - userId = userId, - initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey( - decryptedUserKey = biometricsKey, - ), - ) + val iv = authDiskSource.getUserBiometricInitVector(userId = userId) + return this + .unlockVaultForUser( + userId = userId, + initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey( + decryptedUserKey = iv + ?.let { + cipher + .doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1)) + .decodeToString() + } + ?: biometricsKey, + ), + ) .also { if (it is VaultUnlockResult.Success) { + if (iv == null) { + authDiskSource.storeUserBiometricUnlockKey( + userId = userId, + biometricsKey = cipher + .doFinal(biometricsKey.encodeToByteArray()) + .toString(Charsets.ISO_8859_1), + ) + authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv) + } deriveTemporaryPinProtectedUserKeyIfNecessary(userId = userId) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt index beb44227f02..bafabb3629a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt @@ -75,7 +75,7 @@ fun SetupUnlockScreen( showBiometricsPrompt = true biometricsManager.promptBiometrics( onSuccess = { - handler.unlockWithBiometricToggle() + handler.unlockWithBiometricToggle(it) showBiometricsPrompt = false }, onCancel = { showBiometricsPrompt = false }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt index 458e2a45476..ee5f4aa6d05 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt @@ -65,8 +65,12 @@ class SetupUnlockViewModel @Inject constructor( SetupUnlockAction.EnableBiometricsClick -> handleEnableBiometricsClick() SetupUnlockAction.SetUpLaterClick -> handleSetUpLaterClick() SetupUnlockAction.DismissDialog -> handleDismissDialog() - is SetupUnlockAction.UnlockWithBiometricToggle -> { - handleUnlockWithBiometricToggle(action) + SetupUnlockAction.UnlockWithBiometricToggleDisabled -> { + handleUnlockWithBiometricToggleDisabled() + } + + is SetupUnlockAction.UnlockWithBiometricToggleEnabled -> { + handleUnlockWithBiometricToggleEnabled(action) } is SetupUnlockAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action) @@ -127,23 +131,23 @@ class SetupUnlockViewModel @Inject constructor( } } - private fun handleUnlockWithBiometricToggle( - action: SetupUnlockAction.UnlockWithBiometricToggle, + private fun handleUnlockWithBiometricToggleDisabled() { + settingsRepository.clearBiometricsKey() + mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) } + } + + private fun handleUnlockWithBiometricToggleEnabled( + action: SetupUnlockAction.UnlockWithBiometricToggleEnabled, ) { - if (action.isEnabled) { - mutableStateFlow.update { - it.copy( - dialogState = SetupUnlockState.DialogState.Loading(R.string.saving.asText()), - isUnlockWithBiometricsEnabled = true, - ) - } - viewModelScope.launch { - val result = settingsRepository.setupBiometricsKey() - sendAction(SetupUnlockAction.Internal.BiometricsKeyResultReceive(result)) - } - } else { - settingsRepository.clearBiometricsKey() - mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) } + mutableStateFlow.update { + it.copy( + dialogState = SetupUnlockState.DialogState.Loading(R.string.saving.asText()), + isUnlockWithBiometricsEnabled = true, + ) + } + viewModelScope.launch { + val result = settingsRepository.setupBiometricsKey(cipher = action.cipher) + sendAction(SetupUnlockAction.Internal.BiometricsKeyResultReceive(result = result)) } } @@ -272,10 +276,15 @@ sealed class SetupUnlockEvent { */ sealed class SetupUnlockAction { /** - * User toggled the unlock with biometrics switch. + * User toggled the unlock with biometrics switch to off. */ - data class UnlockWithBiometricToggle( - val isEnabled: Boolean, + data object UnlockWithBiometricToggleDisabled : SetupUnlockAction() + + /** + * User toggled the unlock with biometrics switch to on. + */ + data class UnlockWithBiometricToggleEnabled( + val cipher: Cipher, ) : SetupUnlockAction() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/handlers/SetupUnlockHandler.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/handlers/SetupUnlockHandler.kt index 27c0985a5b1..5c24c86a982 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/handlers/SetupUnlockHandler.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/handlers/SetupUnlockHandler.kt @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockAction import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockViewModel import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState +import javax.crypto.Cipher /** * A collection of handler functions for managing actions within the context of the Setup Unlock @@ -14,7 +15,7 @@ data class SetupUnlockHandler( val onUnlockWithPinToggle: (UnlockWithPinState) -> Unit, val onContinueClick: () -> Unit, val onSetUpLaterClick: () -> Unit, - val unlockWithBiometricToggle: () -> Unit, + val unlockWithBiometricToggle: (cipher: Cipher) -> Unit, ) { @Suppress("UndocumentedPublicClass") companion object { @@ -25,9 +26,7 @@ data class SetupUnlockHandler( fun create(viewModel: SetupUnlockViewModel): SetupUnlockHandler = SetupUnlockHandler( onDisableBiometrics = { - viewModel.trySendAction( - SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false), - ) + viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggleDisabled) }, onEnableBiometrics = { viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick) @@ -39,7 +38,7 @@ data class SetupUnlockHandler( onSetUpLaterClick = { viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) }, unlockWithBiometricToggle = { viewModel.trySendAction( - SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = true), + SetupUnlockAction.UnlockWithBiometricToggleEnabled(cipher = it), ) }, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 77d7fe2d922..539e55c7b68 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -89,7 +89,7 @@ fun VaultUnlockScreen( } } - val onBiometricsUnlockSuccess: (cipher: Cipher?) -> Unit = remember(viewModel) { + val onBiometricsUnlockSuccess: (cipher: Cipher) -> Unit = remember(viewModel) { { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(it)) } } val onBiometricsLockOut: () -> Unit = remember(viewModel) { 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 9dfaae1e152..fa1687ea36b 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 @@ -231,13 +231,9 @@ class VaultUnlockViewModel @Inject constructor( private fun handleBiometricsUnlockSuccess(action: VaultUnlockAction.BiometricsUnlockSuccess) { val activeUserId = authRepository.activeUserId ?: return - if (!biometricsEncryptionManager.isBiometricIntegrityValid(activeUserId, action.cipher)) { - mutableStateFlow.update { it.copy(isBiometricsValid = false) } - return - } mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) } viewModelScope.launch { - val vaultUnlockResult = vaultRepo.unlockVaultWithBiometrics() + val vaultUnlockResult = vaultRepo.unlockVaultWithBiometrics(cipher = action.cipher) sendAction( VaultUnlockAction.Internal.ReceiveVaultUnlockResult( userId = activeUserId, @@ -345,9 +341,6 @@ class VaultUnlockViewModel @Inject constructor( VaultUnlockResult.Success -> { mutableStateFlow.update { it.copy(dialog = null) } - if (state.isBiometricEnabled && !state.isBiometricsValid) { - biometricsEncryptionManager.setupBiometrics(action.userId) - } // Don't do anything, we'll navigate to the right place. } } @@ -586,7 +579,7 @@ sealed class VaultUnlockAction { * The user has received a successful response from the biometrics call. */ data class BiometricsUnlockSuccess( - val cipher: Cipher?, + val cipher: Cipher, ) : VaultUnlockAction() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 8bf284ba27d..86c7df8afa7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -67,6 +67,7 @@ import com.x8bit.bitwarden.ui.platform.util.displayLabel import com.x8bit.bitwarden.ui.platform.util.minutes import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern import java.time.LocalTime +import javax.crypto.Cipher private const val MINUTES_PER_HOUR = 60 @@ -89,12 +90,10 @@ fun AccountSecurityScreen( val context = LocalContext.current val resources = context.resources var showBiometricsPrompt by rememberSaveable { mutableStateOf(false) } - val unlockWithBiometricToggle: () -> Unit = remember(viewModel) { + val unlockWithBiometricToggle: (cipher: Cipher) -> Unit = remember(viewModel) { { viewModel.trySendAction( - action = AccountSecurityAction.UnlockWithBiometricToggle( - enabled = true, - ), + action = AccountSecurityAction.UnlockWithBiometricToggleEnabled(cipher = it), ) } } @@ -126,7 +125,7 @@ fun AccountSecurityScreen( showBiometricsPrompt = true biometricsManager.promptBiometrics( onSuccess = { - unlockWithBiometricToggle() + unlockWithBiometricToggle(it) showBiometricsPrompt = false }, onCancel = { showBiometricsPrompt = false }, @@ -234,9 +233,7 @@ fun AccountSecurityScreen( onDisableBiometrics = remember(viewModel) { { viewModel.trySendAction( - AccountSecurityAction.UnlockWithBiometricToggle( - enabled = false, - ), + AccountSecurityAction.UnlockWithBiometricToggleDisabled, ) } }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 3ab0c70b682..9edb0b65fc5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -172,8 +172,12 @@ class AccountSecurityViewModel @Inject constructor( is AccountSecurityAction.CustomVaultTimeoutSelect -> handleCustomVaultTimeoutSelect(action) is AccountSecurityAction.VaultTimeoutActionSelect -> handleVaultTimeoutActionSelect(action) AccountSecurityAction.TwoStepLoginClick -> handleTwoStepLoginClick() - is AccountSecurityAction.UnlockWithBiometricToggle -> { - handleUnlockWithBiometricToggle(action) + AccountSecurityAction.UnlockWithBiometricToggleDisabled -> { + handleUnlockWithBiometricToggleDisabled() + } + + is AccountSecurityAction.UnlockWithBiometricToggleEnabled -> { + handleUnlockWithBiometricToggleEnabled(action) } is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action) @@ -313,24 +317,24 @@ class AccountSecurityViewModel @Inject constructor( sendEvent(AccountSecurityEvent.NavigateToTwoStepLogin(webSettingsUrl)) } - private fun handleUnlockWithBiometricToggle( - action: AccountSecurityAction.UnlockWithBiometricToggle, + private fun handleUnlockWithBiometricToggleDisabled() { + settingsRepository.clearBiometricsKey() + mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) } + validateVaultTimeoutAction() + } + + private fun handleUnlockWithBiometricToggleEnabled( + action: AccountSecurityAction.UnlockWithBiometricToggleEnabled, ) { - if (action.enabled) { - mutableStateFlow.update { - it.copy( - dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), - isUnlockWithBiometricsEnabled = true, - ) - } - viewModelScope.launch { - val result = settingsRepository.setupBiometricsKey() - sendAction(AccountSecurityAction.Internal.BiometricsKeyResultReceive(result)) - } - } else { - settingsRepository.clearBiometricsKey() - mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) } - validateVaultTimeoutAction() + mutableStateFlow.update { + it.copy( + dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), + isUnlockWithBiometricsEnabled = true, + ) + } + viewModelScope.launch { + val result = settingsRepository.setupBiometricsKey(cipher = action.cipher) + sendAction(AccountSecurityAction.Internal.BiometricsKeyResultReceive(result = result)) } } @@ -717,10 +721,15 @@ sealed class AccountSecurityAction { data object TwoStepLoginClick : AccountSecurityAction() /** - * User toggled the unlock with biometrics switch. + * User toggled the unlock with biometrics switch to off. */ - data class UnlockWithBiometricToggle( - val enabled: Boolean, + data object UnlockWithBiometricToggleDisabled : AccountSecurityAction() + + /** + * User toggled the unlock with biometrics switch to on. + */ + data class UnlockWithBiometricToggleEnabled( + val cipher: Cipher, ) : AccountSecurityAction() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManager.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManager.kt index d5ae7a6ab85..d0ff4a898c0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManager.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManager.kt @@ -29,7 +29,7 @@ interface BiometricsManager { * Display a prompt for setting up or verifying biometrics. */ fun promptBiometrics( - onSuccess: (cipher: Cipher?) -> Unit, + onSuccess: (cipher: Cipher) -> Unit, onCancel: () -> Unit, onLockOut: () -> Unit, onError: () -> Unit, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManagerImpl.kt index 8ecdc17f543..b7ec1dbe1c8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManagerImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/biometrics/BiometricsManagerImpl.kt @@ -43,14 +43,14 @@ class BiometricsManagerImpl( } override fun promptBiometrics( - onSuccess: (cipher: Cipher?) -> Unit, + onSuccess: (cipher: Cipher) -> Unit, onCancel: () -> Unit, onLockOut: () -> Unit, onError: () -> Unit, cipher: Cipher, ) { configureAndDisplayPrompt( - onSuccess = onSuccess, + onSuccess = { it?.let(block = onSuccess) ?: onError() }, onCancel = onCancel, onLockOut = onLockOut, onError = onError, diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 40e51ecb5b0..e85e7cec0a9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -263,6 +263,7 @@ class AuthDiskSourceTest { authDiskSource.storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = true) val deviceKey = "deviceKey" authDiskSource.storeDeviceKey(userId = userId, deviceKey = deviceKey) + authDiskSource.storeUserBiometricInitVector(userId = userId, iv = byteArrayOf()) authDiskSource.storeUserBiometricUnlockKey( userId = userId, biometricsKey = "1234-9876-0192", @@ -324,6 +325,7 @@ class AuthDiskSourceTest { ) // These should be cleared + assertNull(authDiskSource.getUserBiometricInitVector(userId = userId)) assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId)) assertNull(authDiskSource.getPinProtectedUserKey(userId = userId)) assertNull(authDiskSource.getInvalidUnlockAttempts(userId = userId)) @@ -667,6 +669,33 @@ class AuthDiskSourceTest { assertEquals(biometricsKey, actual) } + @Test + fun `storeUserBiometricInitVector for non-null values should update SharedPreferences`() { + val biometricsInitVectorBaseKey = "bwSecureStorage:biometricInitializationVector" + val mockUserId = "mockUserId" + val biometricsInitVectorKey = "${biometricsInitVectorBaseKey}_$mockUserId" + val initVector = byteArrayOf(1, 2) + authDiskSource.storeUserBiometricInitVector(userId = mockUserId, iv = initVector) + val actual = fakeEncryptedSharedPreferences.getString( + key = biometricsInitVectorKey, + defaultValue = null, + ) + assertEquals(initVector.toString(Charsets.ISO_8859_1), actual) + } + + @Test + fun `storeUserBiometricInitVector for null values should clear SharedPreferences`() { + val biometricsInitVectorBaseKey = "bwSecureStorage:biometricInitializationVector" + val mockUserId = "mockUserId" + val biometricsInitVectorKey = "${biometricsInitVectorBaseKey}_$mockUserId" + val initVector = "1234" + fakeEncryptedSharedPreferences.edit { + putString(biometricsInitVectorKey, initVector) + } + authDiskSource.storeUserBiometricInitVector(userId = mockUserId, iv = null) + assertFalse(fakeEncryptedSharedPreferences.contains(biometricsInitVectorKey)) + } + @Test fun `storeUserBiometricUnlockKey for non-null values should update SharedPreferences`() { val biometricsKeyBaseKey = "bwSecureStorage:userKeyBiometricUnlock" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index 479a3809bba..3115bf96a43 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -56,6 +56,7 @@ class FakeAuthDiskSource : AuthDiskSource { private val storedAccountTokens = mutableMapOf() private val storedDeviceKey = mutableMapOf() private val storedPendingAuthRequests = mutableMapOf() + private val storedBiometricInitVectors = mutableMapOf() private val storedBiometricKeys = mutableMapOf() private val storedMasterPasswordHashes = mutableMapOf() private val storedAuthenticationSyncKeys = mutableMapOf() @@ -84,6 +85,7 @@ class FakeAuthDiskSource : AuthDiskSource { storedOrganizations.remove(userId) storedPolicies.remove(userId) storedAccountTokens.remove(userId) + storedBiometricInitVectors.remove(userId) storedBiometricKeys.remove(userId) storedOrganizationKeys.remove(userId) @@ -224,6 +226,13 @@ class FakeAuthDiskSource : AuthDiskSource { storedPendingAuthRequests[userId] = pendingAuthRequest } + override fun getUserBiometricInitVector(userId: String): ByteArray? = + storedBiometricInitVectors[userId] + + override fun storeUserBiometricInitVector(userId: String, iv: ByteArray?) { + storedBiometricInitVectors[userId] = iv + } + override fun getUserBiometricUnlockKey(userId: String): String? = storedBiometricKeys[userId] @@ -421,6 +430,13 @@ class FakeAuthDiskSource : AuthDiskSource { assertEquals(pendingAuthRequest, storedPendingAuthRequests[userId]) } + /** + * Assert that the [iv] was stored successfully using the [userId]. + */ + fun assertBiometricInitVector(userId: String, iv: ByteArray?) { + assertEquals(iv, storedBiometricInitVectors[userId]) + } + /** * Assert that the [biometricsKey] was stored successfully using the [userId]. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 5fc94ed780c..41925f53abf 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -17,7 +17,6 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource -import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency @@ -48,6 +47,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.time.Instant import java.time.ZonedDateTime +import javax.crypto.Cipher @Suppress("LargeClass") class SettingsRepositoryTest { @@ -59,7 +59,6 @@ class SettingsRepositoryTest { private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeSettingsDiskSource = FakeSettingsDiskSource() private val vaultSdkSource: VaultSdkSource = mockk() - private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk() private val mutableActivePolicyFlow = bufferedMutableSharedFlow>() private val policyManager: PolicyManager = mockk { every { @@ -73,7 +72,6 @@ class SettingsRepositoryTest { authDiskSource = fakeAuthDiskSource, settingsDiskSource = fakeSettingsDiskSource, vaultSdkSource = vaultSdkSource, - biometricsEncryptionManager = biometricsEncryptionManager, accessibilityEnabledManager = fakeAccessibilityEnabledManager, dispatcherManager = FakeDispatcherManager(), policyManager = policyManager, @@ -802,21 +800,22 @@ class SettingsRepositoryTest { @Test fun `clearBiometricsKey should remove the stored biometrics key`() { fakeAuthDiskSource.userState = MOCK_USER_STATE + fakeAuthDiskSource.storeUserBiometricUnlockKey(userId = USER_ID, "fake key") + fakeAuthDiskSource.storeUserBiometricInitVector(userId = USER_ID, byteArrayOf(1)) settingsRepository.clearBiometricsKey() - fakeAuthDiskSource.assertBiometricsKey( - userId = USER_ID, - biometricsKey = null, - ) + fakeAuthDiskSource.assertBiometricsKey(userId = USER_ID, biometricsKey = null) + fakeAuthDiskSource.assertBiometricInitVector(userId = USER_ID, iv = null) } @Test fun `setupBiometricsKey with missing user state should return BiometricsKeyResult Error`() = runTest { + val cipher = mockk() fakeAuthDiskSource.userState = null - val result = settingsRepository.setupBiometricsKey() + val result = settingsRepository.setupBiometricsKey(cipher = cipher) assertEquals(BiometricsKeyResult.Error, result) coVerify(exactly = 0) { @@ -829,17 +828,14 @@ class SettingsRepositoryTest { fun `setupBiometricsKey with getUserEncryptionKey failure should return BiometricsKeyResult Error`() = runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE - every { biometricsEncryptionManager.setupBiometrics(USER_ID) } just runs + val cipher = mockk() coEvery { vaultSdkSource.getUserEncryptionKey(userId = USER_ID) } returns Throwable("Fail").asFailure() - val result = settingsRepository.setupBiometricsKey() + val result = settingsRepository.setupBiometricsKey(cipher = cipher) assertEquals(BiometricsKeyResult.Error, result) - verify(exactly = 1) { - biometricsEncryptionManager.setupBiometrics(USER_ID) - } coVerify(exactly = 1) { vaultSdkSource.getUserEncryptionKey(userId = USER_ID) } @@ -851,18 +847,24 @@ class SettingsRepositoryTest { runTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val encryptedKey = "asdf1234" - every { biometricsEncryptionManager.setupBiometrics(USER_ID) } just runs + val encryptedBytes = byteArrayOf(1, 1) + val initVector = byteArrayOf(2, 2) + val cipher = mockk { + every { doFinal(any()) } returns encryptedBytes + every { iv } returns initVector + } coEvery { vaultSdkSource.getUserEncryptionKey(userId = USER_ID) } returns encryptedKey.asSuccess() - val result = settingsRepository.setupBiometricsKey() + val result = settingsRepository.setupBiometricsKey(cipher = cipher) assertEquals(BiometricsKeyResult.Success, result) - fakeAuthDiskSource.assertBiometricsKey(userId = USER_ID, biometricsKey = encryptedKey) - verify(exactly = 1) { - biometricsEncryptionManager.setupBiometrics(USER_ID) - } + fakeAuthDiskSource.assertBiometricsKey( + userId = USER_ID, + biometricsKey = encryptedBytes.toString(Charsets.ISO_8859_1), + ) + fakeAuthDiskSource.assertBiometricInitVector(userId = USER_ID, iv = initVector) coVerify(exactly = 1) { vaultSdkSource.getUserEncryptionKey(userId = USER_ID) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 91d2c4c4d1a..ecad08d6f91 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -134,6 +134,7 @@ import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.temporal.ChronoUnit +import javax.crypto.Cipher @Suppress("LargeClass") class VaultRepositoryTest { @@ -1148,12 +1149,13 @@ class VaultRepositoryTest { fun `unlockVaultWithBiometrics with missing user state should return InvalidStateError`() = runTest { fakeAuthDiskSource.userState = null + val cipher = mockk() assertEquals( emptyList(), vaultRepository.vaultUnlockDataStateFlow.value, ) - val result = vaultRepository.unlockVaultWithBiometrics() + val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) assertEquals(VaultUnlockResult.InvalidStateError, result) assertEquals( @@ -1170,10 +1172,11 @@ class VaultRepositoryTest { vaultRepository.vaultUnlockDataStateFlow.value, ) fakeAuthDiskSource.userState = MOCK_USER_STATE + val cipher = mockk() val userId = MOCK_USER_STATE.activeUserId fakeAuthDiskSource.storeUserBiometricUnlockKey(userId = userId, biometricsKey = null) - val result = vaultRepository.unlockVaultWithBiometrics() + val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) assertEquals(VaultUnlockResult.InvalidStateError, result) assertEquals( @@ -1190,6 +1193,12 @@ class VaultRepositoryTest { val privateKey = "mockPrivateKey-1" val biometricsKey = "asdf1234" fakeAuthDiskSource.userState = MOCK_USER_STATE + val encryptedBytes = byteArrayOf(1, 1) + val initVector = byteArrayOf(2, 2) + val cipher = mockk { + every { doFinal(any()) } returns encryptedBytes + every { iv } returns initVector + } coEvery { vaultLockManager.unlockVault( userId = userId, @@ -1203,11 +1212,12 @@ class VaultRepositoryTest { ) } returns VaultUnlockResult.Success fakeAuthDiskSource.apply { + storeUserBiometricInitVector(userId = userId, iv = null) storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey) storePrivateKey(userId = userId, privateKey = privateKey) } - val result = vaultRepository.unlockVaultWithBiometrics() + val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) assertEquals(VaultUnlockResult.Success, result) coVerify { @@ -1225,6 +1235,64 @@ class VaultRepositoryTest { coVerify(exactly = 0) { vaultSdkSource.derivePinProtectedUserKey(any(), any()) } + fakeAuthDiskSource.apply { + assertBiometricsKey( + userId = userId, + biometricsKey = encryptedBytes.toString(Charsets.ISO_8859_1), + ) + assertBiometricInitVector(userId = userId, iv = initVector) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultWithBiometrics with an IV and VaultLockManager Success should store the updated key and IV and unlock for the current user and return Success`() = + runTest { + val userId = MOCK_USER_STATE.activeUserId + val privateKey = "mockPrivateKey-1" + val biometricsKey = "asdf1234" + fakeAuthDiskSource.userState = MOCK_USER_STATE + val encryptedBytes = byteArrayOf(1, 1) + val initVector = byteArrayOf(2, 2) + val cipher = mockk { + every { doFinal(any()) } returns encryptedBytes + } + coEvery { + vaultLockManager.unlockVault( + userId = userId, + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey( + decryptedUserKey = encryptedBytes.toString(Charsets.ISO_8859_1), + ), + organizationKeys = null, + ) + } returns VaultUnlockResult.Success + fakeAuthDiskSource.apply { + storeUserBiometricInitVector(userId = userId, iv = initVector) + storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey) + storePrivateKey(userId = userId, privateKey = privateKey) + } + + val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) + + assertEquals(VaultUnlockResult.Success, result) + coVerify(exactly = 1) { + vaultLockManager.unlockVault( + userId = userId, + kdf = MOCK_PROFILE.toSdkParams(), + email = "email", + privateKey = privateKey, + initUserCryptoMethod = InitUserCryptoMethod.DecryptedKey( + decryptedUserKey = encryptedBytes.toString(Charsets.ISO_8859_1), + ), + organizationKeys = null, + ) + } + coVerify(exactly = 0) { + vaultSdkSource.derivePinProtectedUserKey(any(), any()) + } } @Suppress("MaxLineLength") @@ -1237,6 +1305,12 @@ class VaultRepositoryTest { val pinProtectedUserKey = "pinProtectedUserkey" val biometricsKey = "asdf1234" fakeAuthDiskSource.userState = MOCK_USER_STATE + val encryptedBytes = byteArrayOf(1, 1) + val initVector = byteArrayOf(2, 2) + val cipher = mockk { + every { doFinal(any()) } returns encryptedBytes + every { iv } returns initVector + } coEvery { vaultSdkSource.derivePinProtectedUserKey( userId = userId, @@ -1256,6 +1330,7 @@ class VaultRepositoryTest { ) } returns VaultUnlockResult.Success fakeAuthDiskSource.apply { + storeUserBiometricInitVector(userId = userId, iv = null) storeUserBiometricUnlockKey(userId = userId, biometricsKey = biometricsKey) storePrivateKey(userId = userId, privateKey = privateKey) storeEncryptedPin(userId = userId, encryptedPin = encryptedPin) @@ -1266,7 +1341,7 @@ class VaultRepositoryTest { ) } - val result = vaultRepository.unlockVaultWithBiometrics() + val result = vaultRepository.unlockVaultWithBiometrics(cipher = cipher) assertEquals(VaultUnlockResult.Success, result) fakeAuthDiskSource.assertPinProtectedUserKey( @@ -1292,6 +1367,13 @@ class VaultRepositoryTest { encryptedPin = encryptedPin, ) } + fakeAuthDiskSource.apply { + assertBiometricsKey( + userId = userId, + biometricsKey = encryptedBytes.toString(Charsets.ISO_8859_1), + ) + assertBiometricInitVector(userId = userId, iv = initVector) + } } @Test diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt index 8715e174422..bb4cdb770d8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt @@ -112,7 +112,7 @@ class SetupUnlockScreenTest : BaseComposeTest() { } @Test - fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle`() { + fun `on unlock with biometrics toggle should send UnlockWithBiometricToggleDisabled`() { mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = true) } composeTestRule .onNodeWithText(text = "Unlock with Biometrics") @@ -120,7 +120,7 @@ class SetupUnlockScreenTest : BaseComposeTest() { .assertIsOn() .performClick() verify(exactly = 1) { - viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false)) + viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggleDisabled) } } @@ -187,8 +187,9 @@ class SetupUnlockScreenTest : BaseComposeTest() { } } + @Suppress("MaxLineLength") @Test - fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle on success`() { + fun `on unlock with biometrics toggle should send UnlockWithBiometricToggleEnabled on success`() { composeTestRule .onNodeWithText(text = "Unlock with Biometrics") .performScrollTo() @@ -204,7 +205,7 @@ class SetupUnlockScreenTest : BaseComposeTest() { .performScrollTo() .assertIsOff() verify(exactly = 1) { - viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = true)) + viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggleEnabled(CIPHER)) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt index cabde2dbb2b..3969b0f2867 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt @@ -148,15 +148,16 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle false should call clearBiometricsKey and update the state`() { + fun `on UnlockWithBiometricToggleDisabled should call clearBiometricsKey and update the state`() { val initialState = DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true) every { settingsRepository.isUnlockWithBiometricsEnabled } returns true every { settingsRepository.clearBiometricsKey() } just runs val viewModel = createViewModel(initialState) assertEquals(initialState, viewModel.stateFlow.value) - viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false)) + viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggleDisabled) assertEquals( initialState.copy(isUnlockWithBiometricsEnabled = false), @@ -169,15 +170,17 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle true and setupBiometricsKey error should update the state accordingly`() = + fun `on UnlockWithBiometricToggleEnabled and setupBiometricsKey error should update the state accordingly`() = runTest { - coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Error + coEvery { + settingsRepository.setupBiometricsKey(CIPHER) + } returns BiometricsKeyResult.Error val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction( - SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = true), + SetupUnlockAction.UnlockWithBiometricToggleEnabled(cipher = CIPHER), ) assertEquals( DEFAULT_STATE.copy( @@ -197,21 +200,23 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { ) } coVerify(exactly = 1) { - settingsRepository.setupBiometricsKey() + settingsRepository.setupBiometricsKey(cipher = CIPHER) } } @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle true and setupBiometricsKey success should call update the state accordingly`() = + fun `on UnlockWithBiometricToggleEnabled and setupBiometricsKey success should call update the state accordingly`() = runTest { - coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Success + coEvery { + settingsRepository.setupBiometricsKey(cipher = CIPHER) + } returns BiometricsKeyResult.Success val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction( - SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = true), + SetupUnlockAction.UnlockWithBiometricToggleEnabled(cipher = CIPHER), ) assertEquals( DEFAULT_STATE.copy( @@ -231,7 +236,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { ) } coVerify(exactly = 1) { - settingsRepository.setupBiometricsKey() + settingsRepository.setupBiometricsKey(cipher = CIPHER) } } 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 aa44e183946..39942495c31 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 @@ -1041,7 +1041,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultWithBiometrics() + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } returns VaultUnlockResult.AuthenticationError() viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER)) @@ -1055,8 +1055,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) - coVerify { - vaultRepository.unlockVaultWithBiometrics() + coVerify(exactly = 1) { + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } } @@ -1069,7 +1069,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultWithBiometrics() + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } returns VaultUnlockResult.GenericError viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER)) @@ -1083,8 +1083,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) - coVerify { - vaultRepository.unlockVaultWithBiometrics() + coVerify(exactly = 1) { + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } } @@ -1097,7 +1097,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultWithBiometrics() + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } returns VaultUnlockResult.InvalidStateError viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER)) @@ -1111,8 +1111,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ), viewModel.stateFlow.value, ) - coVerify { - vaultRepository.unlockVaultWithBiometrics() + coVerify(exactly = 1) { + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } } @@ -1124,7 +1124,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultWithBiometrics() + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } returns VaultUnlockResult.Success viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER)) @@ -1133,8 +1133,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { initialState.copy(dialog = null), viewModel.stateFlow.value, ) - coVerify { - vaultRepository.unlockVaultWithBiometrics() + coVerify(exactly = 1) { + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } } @@ -1147,7 +1147,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val resultFlow = bufferedMutableSharedFlow() val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultWithBiometrics() + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } coAnswers { resultFlow.first() } viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER)) @@ -1165,36 +1165,11 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { } resultFlow.tryEmit(VaultUnlockResult.GenericError) assertEquals(initialState.copy(dialog = null), viewModel.stateFlow.value) - coVerify { - vaultRepository.unlockVaultWithBiometrics() + coVerify(exactly = 1) { + vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER) } } - @Test - fun `on BiometricsUnlockSuccess should set isBiometricsValid to false with null cipher`() { - val initialState = DEFAULT_STATE.copy( - isBiometricEnabled = true, - isBiometricsValid = true, - ) - mutableUserStateFlow.value = DEFAULT_USER_STATE.copy( - accounts = listOf(DEFAULT_ACCOUNT.copy(isBiometricsEnabled = true)), - ) - val viewModel = createViewModel(state = initialState) - coEvery { - vaultRepository.unlockVaultWithBiometrics() - } returns VaultUnlockResult.Success - - viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(cipher = null)) - - assertEquals( - initialState.copy( - dialog = null, - isBiometricsValid = false, - ), - viewModel.stateFlow.value, - ) - } - @Suppress("MaxLineLength") @Test fun `on ReceiveVaultUnlockResult should set FIDO 2 user verification state to verified when result is Success`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt index 09b2ba04003..fb48fcf1d9d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt @@ -134,7 +134,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { } @Test - fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle`() { + fun `on unlock with biometrics toggle should send UnlockWithBiometricToggleDisabled`() { mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = true) } composeTestRule .onNodeWithText("Unlock with Biometrics") @@ -145,7 +145,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { .performScrollTo() .performClick() verify(exactly = 1) { - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(false)) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggleDisabled) } } @@ -212,8 +212,9 @@ class AccountSecurityScreenTest : BaseComposeTest() { } } + @Suppress("MaxLineLength") @Test - fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle on success`() { + fun `on unlock with biometrics toggle should send UnlockWithBiometricToggleEnabled on success`() { composeTestRule .onNodeWithText("Unlock with Biometrics") .performScrollTo() @@ -229,7 +230,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { .performScrollTo() .assertIsOff() verify(exactly = 1) { - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggleEnabled(CIPHER)) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index 6790c130c7b..7632d81ad02 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -385,7 +385,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { viewModel.trySendAction(AccountSecurityAction.EnableBiometricsClick) assertEquals( - AccountSecurityEvent.ShowBiometricsPrompt(CIPHER), + AccountSecurityEvent.ShowBiometricsPrompt(cipher = CIPHER), awaitItem(), ) } @@ -426,8 +426,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle false should call clearBiometricsKey and update the state`() = + fun `on UnlockWithBiometricToggleDisabled should call clearBiometricsKey and update the state`() = runTest { val initialState = DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true) every { settingsRepository.isUnlockWithBiometricsEnabled } returns true @@ -435,7 +436,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(initialState) assertEquals(initialState, viewModel.stateFlow.value) - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(false)) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggleDisabled) assertEquals( initialState.copy(isUnlockWithBiometricsEnabled = false), @@ -448,7 +449,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle false should call clearBiometricsKey, reset the vaultTimeoutAction, and update the state`() = + fun `on UnlockWithBiometricToggleDisabled should call clearBiometricsKey, reset the vaultTimeoutAction, and update the state`() = runTest { val initialState = DEFAULT_STATE.copy( isUnlockWithPasswordEnabled = false, @@ -460,7 +461,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { val viewModel = createViewModel(initialState) assertEquals(initialState, viewModel.stateFlow.value) - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(false)) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggleDisabled) assertEquals( initialState.copy( @@ -477,14 +478,18 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle true and setupBiometricsKey error should call update the state accordingly`() = + fun `on UnlockWithBiometricToggleEnabled and setupBiometricsKey error should call update the state accordingly`() = runTest { - coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Error + coEvery { + settingsRepository.setupBiometricsKey(cipher = CIPHER) + } returns BiometricsKeyResult.Error val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + viewModel.trySendAction( + AccountSecurityAction.UnlockWithBiometricToggleEnabled(cipher = CIPHER), + ) assertEquals( DEFAULT_STATE.copy( dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), @@ -501,20 +506,24 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { ) } coVerify(exactly = 1) { - settingsRepository.setupBiometricsKey() + settingsRepository.setupBiometricsKey(cipher = CIPHER) } } @Suppress("MaxLineLength") @Test - fun `on UnlockWithBiometricToggle true and setupBiometricsKey success should call update the state accordingly`() = + fun `on UnlockWithBiometricToggleEnabled and setupBiometricsKey success should call update the state accordingly`() = runTest { - coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Success + coEvery { + settingsRepository.setupBiometricsKey(cipher = CIPHER) + } returns BiometricsKeyResult.Success val viewModel = createViewModel() viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + viewModel.trySendAction( + AccountSecurityAction.UnlockWithBiometricToggleEnabled(cipher = CIPHER), + ) assertEquals( DEFAULT_STATE.copy( dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), @@ -531,7 +540,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { ) } coVerify(exactly = 1) { - settingsRepository.setupBiometricsKey() + settingsRepository.setupBiometricsKey(cipher = CIPHER) } }