mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Add validity checks to ensure that changes to biometrics require a master password or pin to continue (#839)
This commit is contained in:
parent
2623fc3cbe
commit
9a8aca9fe1
15 changed files with 344 additions and 2 deletions
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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? =
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue