Add validity checks to ensure that changes to biometrics require a master password or pin to continue (#839)

This commit is contained in:
David Perez 2024-01-28 22:31:17 -06:00 committed by Álison Fernandes
parent 2623fc3cbe
commit 9a8aca9fe1
15 changed files with 344 additions and 2 deletions

View file

@ -113,6 +113,14 @@ abstract class BaseDiskSource(
value: String?,
): Unit = sharedPreferences.edit { putString(key, value) }
protected fun removeWithPrefix(prefix: String) {
sharedPreferences
.all
.keys
.filter { it.startsWith(prefix) }
.forEach { sharedPreferences.edit { remove(it) } }
}
companion object {
const val BASE_KEY: String = "bwPreferencesStorage"
}

View file

@ -28,6 +28,11 @@ interface SettingsDiskSource {
*/
val appThemeFlow: Flow<AppTheme>
/**
* The currently persisted biometric integrity source for the system.
*/
var systemBiometricIntegritySource: String?
/**
* The currently persisted setting for getting login item icons (or `null` if not set).
*/
@ -43,6 +48,24 @@ interface SettingsDiskSource {
*/
fun clearData(userId: String)
/**
* Retrieves the biometric integrity validity for the given [userId] and
* [systemBioIntegrityState].
*/
fun getAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
): Boolean?
/**
* Stores the biometric integrity validity for the given [userId] and [systemBioIntegrityState].
*/
fun storeAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
value: Boolean?,
)
/**
* Gets the last time the app synced the vault data for a given [userId] (or `null` if the
* vault has never been synced).

View file

@ -27,6 +27,8 @@ private const val DISABLE_AUTOFILL_SAVE_PROMPT_KEY = "$BASE_KEY:autofillDisableS
private const val DISABLE_ICON_LOADING_KEY = "$BASE_KEY:disableFavicon"
private const val APPROVE_PASSWORDLESS_LOGINS_KEY = "$BASE_KEY:approvePasswordlessLogins"
private const val SCREEN_CAPTURE_ALLOW_KEY = "$BASE_KEY:screenCaptureAllowed"
private const val SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY = "$BASE_KEY:biometricIntegritySource"
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "$BASE_KEY:accountBiometricIntegrityValid"
/**
* Primary implementation of [SettingsDiskSource].
@ -69,6 +71,12 @@ class SettingsDiskSourceImpl(
)
}
override var systemBiometricIntegritySource: String?
get() = getString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY)
set(value) {
putString(key = SYSTEM_BIOMETRIC_INTEGRITY_SOURCE_KEY, value = value)
}
override var appTheme: AppTheme
get() = getString(key = APP_THEME_KEY)
?.let { storedValue ->
@ -112,6 +120,26 @@ class SettingsDiskSourceImpl(
)
storeLastSyncTime(userId = userId, lastSyncTime = null)
storeScreenCaptureAllowed(userId = userId, isScreenCaptureAllowed = null)
removeWithPrefix(prefix = "${ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY}_$userId")
}
override fun getAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
): Boolean? =
getBoolean(
key = "${ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY}_${userId}_$systemBioIntegrityState",
)
override fun storeAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
value: Boolean?,
) {
putBoolean(
key = "${ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY}_${userId}_$systemBioIntegrityState",
value = value,
)
}
override fun getLastSyncTime(userId: String): Instant? =

View file

@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.platform.manager
/**
* Responsible for managing Android keystore encryption and decryption.
*/
interface BiometricsEncryptionManager {
/**
* 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 change since the app setup biometrics, `false` will be returned if
* it has changed.
*/
fun isBiometricIntegrityValid(userId: String): Boolean
}

View file

@ -0,0 +1,111 @@
package com.x8bit.bitwarden.data.platform.manager
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.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import java.security.InvalidKeyException
import java.security.KeyStore
import java.security.UnrecoverableKeyException
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
/**
* Default implementation of [BiometricsEncryptionManager] for managing Android keystore encryption
* and decryption.
*/
@OmitFromCoverage
class BiometricsEncryptionManagerImpl(
private val settingsDiskSource: SettingsDiskSource,
) : BiometricsEncryptionManager {
private val keystore = KeyStore
.getInstance(ENCRYPTION_KEYSTORE_NAME)
.also { it.load(null) }
private val keyGenParameterSpec: KeyGenParameterSpec
get() = KeyGenParameterSpec
.Builder(
ENCRYPTION_KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
override fun setupBiometrics(userId: String) {
createIntegrityValues(userId)
}
override fun isBiometricIntegrityValid(userId: String): Boolean =
isSystemBiometricIntegrityValid(userId) && isAccountBiometricIntegrityValid(userId)
private fun isAccountBiometricIntegrityValid(userId: String): Boolean {
val systemBioIntegrityState = settingsDiskSource
.systemBiometricIntegritySource
?: return false
return settingsDiskSource
.getAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBioIntegrityState,
)
?: false
}
private fun isSystemBiometricIntegrityValid(userId: String): Boolean =
try {
keystore.load(null)
keystore
.getKey(ENCRYPTION_KEY_NAME, null)
?.let { Cipher.getInstance(CIPHER_TRANSFORMATION).init(Cipher.ENCRYPT_MODE, it) }
true
} catch (e: KeyPermanentlyInvalidatedException) {
// Biometric has changed
settingsDiskSource.systemBiometricIntegritySource = null
false
} catch (e: UnrecoverableKeyException) {
// Biometric was disabled and re-enabled
settingsDiskSource.systemBiometricIntegritySource = null
false
} catch (e: InvalidKeyException) {
// Fallback for old bitwarden users without a key
createIntegrityValues(userId)
true
}
@Suppress("TooGenericExceptionCaught")
private fun createIntegrityValues(userId: String) {
val systemBiometricIntegritySource = settingsDiskSource
.systemBiometricIntegritySource
?: UUID.randomUUID().toString()
settingsDiskSource.systemBiometricIntegritySource = systemBiometricIntegritySource
settingsDiskSource.storeAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBiometricIntegritySource,
value = true,
)
try {
val keyGen = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ENCRYPTION_KEYSTORE_NAME,
)
keyGen.init(keyGenParameterSpec)
keyGen.generateKey()
} catch (e: Exception) {
// Catch silently to allow biometrics to function on devices that are in
// a state where key generation is not functioning
}
}
}
private const val ENCRYPTION_KEYSTORE_NAME: String = "AndroidKeyStore"
private const val ENCRYPTION_KEY_NAME: String = "${BuildConfig.APPLICATION_ID}.biometric_integrity"
private const val CIPHER_TRANSFORMATION =
KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +
KeyProperties.ENCRYPTION_PADDING_PKCS7

View file

@ -5,12 +5,15 @@ import android.content.Context
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.service.PushService
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManagerImpl
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManager
import com.x8bit.bitwarden.data.platform.manager.NetworkConfigManagerImpl
import com.x8bit.bitwarden.data.platform.manager.NetworkConnectionManager
@ -49,6 +52,14 @@ object PlatformManagerModule {
@Singleton
fun provideClock(): Clock = Clock.systemDefaultZone()
@Provides
@Singleton
fun provideBiometricsEncryptionManager(
settingsDiskSource: SettingsDiskSource,
): BiometricsEncryptionManager = BiometricsEncryptionManagerImpl(
settingsDiskSource = settingsDiskSource,
)
@Provides
@Singleton
fun provideBitwardenClipboardManager(

View file

@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@ -35,13 +36,14 @@ private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG
/**
* Primary implementation of [SettingsRepository].
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class SettingsRepositoryImpl(
private val autofillManager: AutofillManager,
private val appForegroundManager: AppForegroundManager,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val dispatcherManager: DispatcherManager,
) : SettingsRepository {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
@ -352,6 +354,7 @@ class SettingsRepositoryImpl(
override suspend fun setupBiometricsKey(): BiometricsKeyResult {
val userId = activeUserId ?: return BiometricsKeyResult.Error
biometricsEncryptionManager.setupBiometrics(userId)
return vaultSdkSource
.getUserEncryptionKey(userId = userId)
.onSuccess {

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
@ -45,6 +46,7 @@ object PlatformRepositoryModule {
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
vaultSdkSource: VaultSdkSource,
encryptionManager: BiometricsEncryptionManager,
dispatcherManager: DispatcherManager,
): SettingsRepository =
SettingsRepositoryImpl(
@ -53,6 +55,7 @@ object PlatformRepositoryModule {
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
vaultSdkSource = vaultSdkSource,
biometricsEncryptionManager = encryptionManager,
dispatcherManager = dispatcherManager,
)
}

View file

@ -192,7 +192,7 @@ fun VaultUnlockScreen(
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
if (state.isBiometricEnabled) {
if (state.showBiometricLogin && biometricsManager.isBiometricsSupported) {
BitwardenOutlinedButton(
label = stringResource(id = R.string.use_biometrics_to_unlock),
onClick = {

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@ -39,12 +40,16 @@ class VaultUnlockViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val vaultRepo: VaultRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
environmentRepo: EnvironmentRepository,
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val userState = requireNotNull(authRepository.userStateFlow.value)
val accountSummaries = userState.toAccountSummaries()
val activeAccountSummary = userState.toActiveAccountSummary()
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
userId = userState.activeUserId,
)
VaultUnlockState(
accountSummaries = accountSummaries,
avatarColorString = activeAccountSummary.avatarColorHex,
@ -54,6 +59,7 @@ class VaultUnlockViewModel @Inject constructor(
environmentUrl = environmentRepo.environment.label,
input = "",
isBiometricEnabled = userState.activeAccount.isBiometricsEnabled,
isBiometricsValid = isBiometricsValid,
vaultUnlockType = userState.activeAccount.vaultUnlockType,
)
},
@ -130,6 +136,10 @@ class VaultUnlockViewModel @Inject constructor(
private fun handleBiometricsUnlockClick() {
val activeUserId = authRepository.activeUserId ?: return
if (!biometricsEncryptionManager.isBiometricIntegrityValid(activeUserId)) {
mutableStateFlow.update { it.copy(isBiometricsValid = false) }
return
}
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
viewModelScope.launch {
val vaultUnlockResult = vaultRepo.unlockVaultWithBiometrics()
@ -221,6 +231,9 @@ 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.
}
}
@ -263,6 +276,7 @@ data class VaultUnlockState(
val environmentUrl: String,
val dialog: VaultUnlockDialog?,
val input: String,
val isBiometricsValid: Boolean,
val isBiometricEnabled: Boolean,
val vaultUnlockType: VaultUnlockType,
) : Parcelable {
@ -272,6 +286,11 @@ data class VaultUnlockState(
*/
val avatarColor: Color get() = avatarColorString.hexToColor()
/**
* Indicates if we should display the button login with biometrics.
*/
val showBiometricLogin: Boolean get() = isBiometricEnabled && isBiometricsValid
/**
* Represents the various dialogs the vault unlock screen can display.
*/

View file

@ -65,6 +65,32 @@ class SettingsDiskSourceTest {
)
}
@Test
fun `systemBiometricIntegritySource should pull from SharedPreferences`() {
val biometricIntegritySource = "bwPreferencesStorage:biometricIntegritySource"
val expected = "mockBiometricIntegritySource"
// Verify initial value is null and disk source matches shared preferences.
assertNull(fakeSharedPreferences.getString(biometricIntegritySource, null))
assertNull(settingsDiskSource.systemBiometricIntegritySource)
// Updating the shared preferences should update disk source.
fakeSharedPreferences.edit {
putString(biometricIntegritySource, expected)
}
val actual = settingsDiskSource.systemBiometricIntegritySource
assertEquals(expected, actual)
}
@Test
fun `setting systemBiometricIntegritySource should update SharedPreferences`() {
val biometricIntegritySource = "bwPreferencesStorage:biometricIntegritySource"
val expected = "mockBiometricIntegritySource"
settingsDiskSource.systemBiometricIntegritySource = expected
val actual = fakeSharedPreferences.getString(biometricIntegritySource, null)
assertEquals(expected, actual)
}
@Test
fun `clearData should clear all necessary data for the given user`() {
val userId = "userId"
@ -108,6 +134,12 @@ class SettingsDiskSourceTest {
userId = userId,
isScreenCaptureAllowed = true,
)
val systemBioIntegrityState = "system_biometrics_integrity_state"
settingsDiskSource.storeAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBioIntegrityState,
value = true,
)
settingsDiskSource.clearData(userId = userId)
@ -121,6 +153,51 @@ class SettingsDiskSourceTest {
assertNull(settingsDiskSource.getApprovePasswordlessLoginsEnabled(userId = userId))
assertNull(settingsDiskSource.getLastSyncTime(userId = userId))
assertNull(settingsDiskSource.getScreenCaptureAllowed(userId = userId))
assertNull(
settingsDiskSource.getAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBioIntegrityState,
),
)
}
@Suppress("MaxLineLength")
@Test
fun `getAccountBiometricIntegrityValidity should pull from and update SharedPreferences`() {
val userId = "userId-1234"
val systemBiometricIntegritySource = "systemValidity"
val accountBioIntegrityValid =
"bwPreferencesStorage:accountBiometricIntegrityValid_${userId}_$systemBiometricIntegritySource"
val isValid = true
// Assert that the default value in disk source is null
assertNull(
settingsDiskSource.getAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBiometricIntegritySource,
),
)
// Updating the shared preferences should update disk source.
fakeSharedPreferences.edit { putBoolean(accountBioIntegrityValid, isValid) }
assertEquals(
isValid,
settingsDiskSource.getAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBiometricIntegritySource,
),
)
// Updating the disk source updates the shared preferences
settingsDiskSource.storeAccountBiometricIntegrityValidity(
userId = userId,
systemBioIntegrityState = systemBiometricIntegritySource,
value = isValid,
)
assertEquals(
fakeSharedPreferences.getBoolean(accountBioIntegrityValid, false),
isValid,
)
}
@Test

View file

@ -48,6 +48,8 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private var storedIsIconLoadingDisabled: Boolean? = null
private val storedApprovePasswordLoginsEnabled = mutableMapOf<String, Boolean?>()
private val storedScreenCaptureAllowed = mutableMapOf<String, Boolean?>()
private var storedSystemBiometricIntegritySource: String? = null
private val storedAccountBiometricIntegrityValidity = mutableMapOf<String, Boolean?>()
override var appLanguage: AppLanguage? = null
@ -63,6 +65,12 @@ class FakeSettingsDiskSource : SettingsDiskSource {
emit(appTheme)
}
override var systemBiometricIntegritySource: String?
get() = storedSystemBiometricIntegritySource
set(value) {
storedSystemBiometricIntegritySource = value
}
override var isIconLoadingDisabled: Boolean?
get() = storedIsIconLoadingDisabled
set(value) {
@ -75,6 +83,19 @@ class FakeSettingsDiskSource : SettingsDiskSource {
emit(isIconLoadingDisabled)
}
override fun getAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
): Boolean? = storedAccountBiometricIntegrityValidity["${userId}_$systemBioIntegrityState"]
override fun storeAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,
value: Boolean?,
) {
storedAccountBiometricIntegrityValidity["${userId}_$systemBioIntegrityState"] = value
}
override fun clearData(userId: String) {
storedVaultTimeoutActions.remove(userId)
storedVaultTimeoutInMinutes.remove(userId)

View file

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
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.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@ -50,6 +51,7 @@ class SettingsRepositoryTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val vaultSdkSource: VaultSdkSource = mockk()
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk()
private var isAutofillEnabledAndSupported = false
@ -59,6 +61,7 @@ class SettingsRepositoryTest {
authDiskSource = fakeAuthDiskSource,
settingsDiskSource = fakeSettingsDiskSource,
vaultSdkSource = vaultSdkSource,
biometricsEncryptionManager = biometricsEncryptionManager,
dispatcherManager = FakeDispatcherManager(),
)
@ -687,6 +690,7 @@ class SettingsRepositoryTest {
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = MOCK_USER_STATE.activeUserId
every { biometricsEncryptionManager.setupBiometrics(userId) } just runs
coEvery {
vaultSdkSource.getUserEncryptionKey(userId = userId)
} returns Throwable("Fail").asFailure()
@ -694,6 +698,9 @@ class SettingsRepositoryTest {
val result = settingsRepository.setupBiometricsKey()
assertEquals(BiometricsKeyResult.Error, result)
verify(exactly = 1) {
biometricsEncryptionManager.setupBiometrics(userId)
}
coVerify(exactly = 1) {
vaultSdkSource.getUserEncryptionKey(userId = userId)
}
@ -706,6 +713,7 @@ class SettingsRepositoryTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = MOCK_USER_STATE.activeUserId
val encryptedKey = "asdf1234"
every { biometricsEncryptionManager.setupBiometrics(userId) } just runs
coEvery {
vaultSdkSource.getUserEncryptionKey(userId = userId)
} returns encryptedKey.asSuccess()
@ -714,6 +722,9 @@ class SettingsRepositoryTest {
assertEquals(BiometricsKeyResult.Success, result)
fakeAuthDiskSource.assertBiometricsKey(userId = userId, biometricsKey = encryptedKey)
verify(exactly = 1) {
biometricsEncryptionManager.setupBiometrics(userId)
}
coVerify(exactly = 1) {
vaultSdkSource.getUserEncryptionKey(userId = userId)
}

View file

@ -421,6 +421,7 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
environmentUrl = DEFAULT_ENVIRONMENT_URL,
initials = "AU",
input = "",
isBiometricsValid = true,
isBiometricEnabled = true,
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
)

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
@ -46,6 +47,9 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
every { lockVault(any()) } just runs
}
private val encryptionManager: BiometricsEncryptionManager = mockk {
every { isBiometricIntegrityValid(userId = DEFAULT_USER_STATE.activeUserId) } returns true
}
@Test
fun `initial state should be correct when not set`() {
@ -678,11 +682,13 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
state: VaultUnlockState? = DEFAULT_STATE,
environmentRepo: EnvironmentRepository = environmentRepository,
vaultRepo: VaultRepository = vaultRepository,
biometricsEncryptionManager: BiometricsEncryptionManager = encryptionManager,
): VaultUnlockViewModel = VaultUnlockViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
authRepository = authRepository,
vaultRepo = vaultRepo,
environmentRepo = environmentRepo,
biometricsEncryptionManager = biometricsEncryptionManager,
)
}
@ -705,6 +711,7 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
dialog = null,
environmentUrl = Environment.Us.label,
input = "",
isBiometricsValid = true,
isBiometricEnabled = false,
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
)