Move BiometricsEncryptionManager into the AuthRepository (#6249)

This commit is contained in:
David Perez authored and GitHub committed 2025-12-09 15:32:25 +00:00
1 parent 2eb8ad4221
commit cd27fe339d
17 files changed
+87 -182

No files matched your search

@@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.AuthenticatorProvider
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.StateFlow
interface AuthRepository :
AuthenticatorProvider,
AuthRequestManager,
BiometricsEncryptionManager,
KdfManager,
UserStateManager {
/**
@@ -102,6 +102,7 @@ import com.x8bit.bitwarden.data.auth.util.YubiKeyResult
import com.x8bit.bitwarden.data.auth.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
@@ -157,6 +158,7 @@ class AuthRepositoryImpl(
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val authRequestManager: AuthRequestManager,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val keyConnectorManager: KeyConnectorManager,
private val trustedDeviceManager: TrustedDeviceManager,
private val userLogoutManager: UserLogoutManager,
@@ -168,6 +170,7 @@ class AuthRepositoryImpl(
dispatcherManager: DispatcherManager,
) : AuthRepository,
AuthRequestManager by authRequestManager,
BiometricsEncryptionManager by biometricsEncryptionManager,
KdfManager by kdfManager,
UserStateManager by userStateManager {
/**
@@ -19,6 +19,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserStateManagerImpl
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.LogsManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
@@ -60,6 +61,7 @@ object AuthRepositoryModule {
environmentRepository: EnvironmentRepository,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
biometricsEncryptionManager: BiometricsEncryptionManager,
keyConnectorManager: KeyConnectorManager,
authRequestManager: AuthRequestManager,
trustedDeviceManager: TrustedDeviceManager,
@@ -85,6 +87,7 @@ object AuthRepositoryModule {
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
biometricsEncryptionManager = biometricsEncryptionManager,
keyConnectorManager = keyConnectorManager,
authRequestManager = authRequestManager,
trustedDeviceManager = trustedDeviceManager,
@@ -11,10 +11,10 @@ import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.vault.CipherListView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.util.login
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
/**
* Primary implementation of [CredentialEntryBuilder].
@@ -22,7 +22,7 @@ import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
class CredentialEntryBuilderImpl(
private val context: Context,
private val pendingIntentManager: CredentialManagerPendingIntentManager,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val authRepository: AuthRepository,
) : CredentialEntryBuilder {
override fun buildPublicKeyCredentialEntries(
@@ -82,7 +82,7 @@ class CredentialEntryBuilderImpl(
.also { builder ->
if (!isUserVerified) {
builder.setBiometricPromptDataIfSupported(
cipher = biometricsEncryptionManager.getOrCreateCipher(userId),
cipher = authRepository.getOrCreateCipher(userId),
)
}
}
@@ -113,8 +113,7 @@ class CredentialEntryBuilderImpl(
.apply {
if (!isUserVerified) {
setBiometricPromptDataIfSupported(
cipher = biometricsEncryptionManager
.getOrCreateCipher(userId),
cipher = authRepository.getOrCreateCipher(userId),
)
}
}
@@ -25,7 +25,6 @@ import com.x8bit.bitwarden.data.credentials.repository.PrivilegedAppRepositoryIm
import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizer
import com.x8bit.bitwarden.data.credentials.sanitizer.PasskeyAttestationOptionsSanitizerImpl
import com.x8bit.bitwarden.data.platform.manager.AssetManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -54,7 +53,6 @@ object CredentialProviderModule {
bitwardenCredentialManager: BitwardenCredentialManager,
dispatcherManager: DispatcherManager,
pendingIntentManager: CredentialManagerPendingIntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
clock: Clock,
): CredentialProviderProcessor =
CredentialProviderProcessorImpl(
@@ -63,7 +61,6 @@ object CredentialProviderModule {
bitwardenCredentialManager = bitwardenCredentialManager,
pendingIntentManager = pendingIntentManager,
clock = clock,
biometricsEncryptionManager = biometricsEncryptionManager,
dispatcherManager = dispatcherManager,
)
@@ -108,11 +105,11 @@ object CredentialProviderModule {
fun provideCredentialEntryBuilder(
@ApplicationContext context: Context,
pendingIntentManager: CredentialManagerPendingIntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
authRepository: AuthRepository,
): CredentialEntryBuilder = CredentialEntryBuilderImpl(
context = context,
pendingIntentManager = pendingIntentManager,
biometricsEncryptionManager = biometricsEncryptionManager,
authRepository = authRepository,
)
@Provides
@@ -34,7 +34,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -53,7 +52,6 @@ class CredentialProviderProcessorImpl(
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val pendingIntentManager: CredentialManagerPendingIntentManager,
private val clock: Clock,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
dispatcherManager: DispatcherManager,
) : CredentialProviderProcessor {
@@ -203,7 +201,7 @@ class CredentialProviderProcessorImpl(
.setAutoSelectAllowed(true)
if (isVaultUnlocked) {
biometricsEncryptionManager
authRepository
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
@@ -251,7 +249,7 @@ class CredentialProviderProcessorImpl(
.setAutoSelectAllowed(true)
if (isVaultUnlocked) {
biometricsEncryptionManager
authRepository
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
@@ -32,7 +32,6 @@ interface BiometricsEncryptionManager {
*/
fun isBiometricIntegrityValid(
userId: String,
cipher: Cipher?,
): Boolean
/**
@@ -90,8 +90,8 @@ class BiometricsEncryptionManagerImpl(
return cipher?.takeIf { isCipherInitialized }
}
override fun isBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean =
isSystemBiometricIntegrityValid(userId, cipher) && isAccountBiometricIntegrityValid(userId)
override fun isBiometricIntegrityValid(userId: String): Boolean =
isSystemBiometricIntegrityValid(userId) && isAccountBiometricIntegrityValid(userId)
override fun isAccountBiometricIntegrityValid(userId: String): Boolean {
val systemBioIntegrityState = settingsDiskSource
@@ -203,11 +203,13 @@ class BiometricsEncryptionManagerImpl(
}
/**
* Validates the keystore key and decrypts it using the user-provided [cipher].
* Validates the keystore key and decrypts it, if decryption is successful `true` is returned,
* `false` otherwise.
*/
private fun isSystemBiometricIntegrityValid(userId: String, cipher: Cipher?): Boolean {
private fun isSystemBiometricIntegrityValid(userId: String): Boolean {
// Attempt to get the user scoped key. If that fails, we check to see if a legacy key
// is available.
val cipher = getOrCreateCipher(userId = userId)
val secretKey = getSecretKeyOrNull(userId = userId) ?: getSecretKeyOrNull(userId = null)
return if (cipher != null && secretKey != null) {
cipher.initializeCipher(userId = userId, secretKey = secretKey)
@@ -10,7 +10,6 @@ import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
@@ -33,17 +32,12 @@ class SetupUnlockViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val settingsRepository: SettingsRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val firstTimeActionManager: FirstTimeActionManager,
private val browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager,
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
userId = userId,
cipher = biometricsEncryptionManager.getOrCreateCipher(userId = userId),
)
// whether or not the user has completed the initial setup prior to this.
val isInitialSetup = savedStateHandle.toSetupUnlockArgs().isInitialSetup
SetupUnlockState(
@@ -55,7 +49,7 @@ class SetupUnlockViewModel @Inject constructor(
?.hasMasterPassword != false,
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
isBiometricsValid,
authRepository.isBiometricIntegrityValid(userId = userId),
dialogState = null,
isInitialSetup = isInitialSetup,
)
@@ -100,7 +94,7 @@ class SetupUnlockViewModel @Inject constructor(
}
private fun handleEnableBiometricsClick() {
biometricsEncryptionManager
authRepository
.createCipherOrNull(userId = state.userId)
?.let {
sendEvent(
@@ -134,7 +128,7 @@ class SetupUnlockViewModel @Inject constructor(
}
private fun handleUnlockWithBiometricToggleDisabled() {
biometricsEncryptionManager.clearBiometrics(userId = state.userId)
authRepository.clearBiometrics(userId = state.userId)
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) }
}
@@ -21,7 +21,6 @@ import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.util.toCreateCredentialRequestOrNull
@@ -55,7 +54,6 @@ private const val KEY_STATE = "state"
class VaultUnlockViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val vaultRepo: VaultRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val appResumeManager: AppResumeManager,
@@ -68,10 +66,6 @@ class VaultUnlockViewModel @Inject constructor(
val activeAccount = userState.activeAccount
val accountSummaries = userState.toAccountSummaries()
val activeAccountSummary = userState.toActiveAccountSummary()
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
userId = userState.activeUserId,
cipher = biometricsEncryptionManager.getOrCreateCipher(userState.activeUserId),
)
val vaultUnlockType = activeAccount.vaultUnlockType
val hasNoMasterPassword = !activeAccount.hasMasterPassword
if (!activeAccount.hasManualUnlockMechanism) {
@@ -82,7 +76,6 @@ class VaultUnlockViewModel @Inject constructor(
}
val specialCircumstance = specialCircumstanceManager.specialCircumstance
val showAccountMenu =
savedStateHandle.toVaultUnlockArgs().unlockType == UnlockType.STANDARD &&
(specialCircumstance !is SpecialCircumstance.ProviderGetCredentials &&
@@ -97,7 +90,7 @@ class VaultUnlockViewModel @Inject constructor(
environmentUrl = activeAccount.environment.label,
input = "",
isBiometricEnabled = activeAccount.isBiometricsEnabled,
isBiometricsValid = isBiometricsValid,
isBiometricsValid = authRepository.isBiometricIntegrityValid(userState.activeUserId),
showAccountMenu = showAccountMenu,
showBiometricInvalidatedMessage = false,
vaultUnlockType = vaultUnlockType,
@@ -233,7 +226,7 @@ class VaultUnlockViewModel @Inject constructor(
}
private fun handleBiometricsUnlockClick() {
val cipher = biometricsEncryptionManager.getOrCreateCipher(state.userId)
val cipher = authRepository.getOrCreateCipher(state.userId)
if (cipher != null) {
sendEvent(
event = VaultUnlockEvent.PromptForBiometrics(
@@ -244,7 +237,7 @@ class VaultUnlockViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
isBiometricsValid = false,
showBiometricInvalidatedMessage = !biometricsEncryptionManager
showBiometricInvalidatedMessage = !authRepository
.isAccountBiometricIntegrityValid(state.userId),
)
}
@@ -351,7 +344,7 @@ class VaultUnlockViewModel @Inject constructor(
}
is VaultUnlockResult.BiometricDecodingError -> {
biometricsEncryptionManager.clearBiometrics(userId = state.userId)
authRepository.clearBiometrics(userId = state.userId)
mutableStateFlow.update {
it.copy(
isBiometricsValid = false,
@@ -433,13 +426,9 @@ class VaultUnlockViewModel @Inject constructor(
}
private fun promptForBiometricsIfAvailable() {
val cipher = biometricsEncryptionManager.getOrCreateCipher(state.userId)
val cipher = authRepository.getOrCreateCipher(state.userId)
if (state.showBiometricLogin && cipher != null && !state.isFromLockFlow) {
sendEvent(
VaultUnlockEvent.PromptForBiometrics(
cipher = cipher,
),
)
sendEvent(VaultUnlockEvent.PromptForBiometrics(cipher = cipher))
}
}
}
@@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
@@ -51,28 +50,20 @@ class AccountSecurityViewModel @Inject constructor(
private val vaultRepository: VaultRepository,
private val settingsRepository: SettingsRepository,
private val environmentRepository: EnvironmentRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val firstTimeActionManager: FirstTimeActionManager,
policyManager: PolicyManager,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
userId = userId,
cipher = biometricsEncryptionManager.getOrCreateCipher(userId),
)
val userState = requireNotNull(authRepository.userStateFlow.value)
val userId = userState.activeUserId
AccountSecurityState(
dialog = null,
fingerprintPhrase = "".asText(), // This will be filled in dynamically
isAuthenticatorSyncChecked = settingsRepository.isAuthenticatorSyncEnabled,
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
isBiometricsValid,
isUnlockWithPasswordEnabled = authRepository
.userStateFlow
.value
?.activeAccount
?.hasMasterPassword != false,
authRepository.isBiometricIntegrityValid(userId = userId),
isUnlockWithPasswordEnabled = userState.activeAccount.hasMasterPassword,
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
shouldShowEnableAuthenticatorSync = isBuildVersionAtLeast(Build.VERSION_CODES.S),
userId = userId,
@@ -230,7 +221,7 @@ class AccountSecurityViewModel @Inject constructor(
}
private fun handleEnableBiometricsClick() {
biometricsEncryptionManager
authRepository
.createCipherOrNull(userId = state.userId)
?.let {
sendEvent(
@@ -319,7 +310,7 @@ class AccountSecurityViewModel @Inject constructor(
}
private fun handleUnlockWithBiometricToggleDisabled() {
biometricsEncryptionManager.clearBiometrics(userId = state.userId)
authRepository.clearBiometrics(userId = state.userId)
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) }
validateVaultTimeoutAction()
}
@@ -280,6 +280,7 @@ class AuthRepositoryTest {
updateKdfToMinimumsIfNeeded(password = any())
} returns UpdateKdfMinimumsResult.Success
}
private val repository: AuthRepository = AuthRepositoryImpl(
clock = FIXED_CLOCK,
accountsService = accountsService,
@@ -296,6 +297,7 @@ class AuthRepositoryTest {
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
authRequestManager = authRequestManager,
biometricsEncryptionManager = mockk(),
keyConnectorManager = keyConnectorManager,
trustedDeviceManager = trustedDeviceManager,
userLogoutManager = userLogoutManager,
@@ -12,8 +12,8 @@ import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherListViewType
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.util.mockBuilder
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
@@ -53,13 +53,13 @@ class CredentialEntryBuilderTest {
)
} returns mockGetPasswordCredentialIntent
}
private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
private val mockAuthRepository = mockk<AuthRepository>()
private val mockBeginGetPublicKeyOption = mockk<BeginGetPublicKeyCredentialOption>()
private val mockBeginGetPasswordOption = mockk<BeginGetPasswordOption>()
private val credentialEntryBuilder = CredentialEntryBuilderImpl(
context = mockContext,
pendingIntentManager = mockPendingIntentManager,
biometricsEncryptionManager = mockBiometricsEncryptionManager,
authRepository = mockAuthRepository,
)
private val mockPublicKeyCredentialEntry = mockk<PublicKeyCredentialEntry>(relaxed = true)
private val mockPasswordCredentialEntry = mockk<PasswordCredentialEntry>(relaxed = true)
@@ -136,9 +136,7 @@ class CredentialEntryBuilderTest {
createMockFido2CredentialAutofillView(number = 1),
)
every {
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
} returns null
every { mockAuthRepository.getOrCreateCipher("userId") } returns null
val result = credentialEntryBuilder
.buildPublicKeyCredentialEntries(
@@ -172,9 +170,7 @@ class CredentialEntryBuilderTest {
// Verify biometric prompt data is not set when buildVersion is at least 35
// and cipher is null.
every {
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
} returns null
every { mockAuthRepository.getOrCreateCipher("userId") } returns null
every { isBuildVersionAtLeast(any()) } returns true
credentialEntryBuilder
@@ -216,9 +212,7 @@ class CredentialEntryBuilderTest {
}
// Verify biometric prompt data is not set when user is verified
every {
mockBiometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk(relaxed = true)
every { mockAuthRepository.getOrCreateCipher(any()) } returns mockk(relaxed = true)
credentialEntryBuilder
.buildPublicKeyCredentialEntries(
userId = "userId",
@@ -305,9 +299,7 @@ class CredentialEntryBuilderTest {
),
),
)
every {
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
} returns null
every { mockAuthRepository.getOrCreateCipher("userId") } returns null
val result = credentialEntryBuilder
.buildPasswordCredentialEntries(
@@ -347,9 +339,7 @@ class CredentialEntryBuilderTest {
),
)
every {
mockBiometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk(relaxed = true)
every { mockAuthRepository.getOrCreateCipher(any()) } returns mockk(relaxed = true)
every { isBuildVersionAtLeast(any()) } returns true
// Verify biometric prompt data is set when buildVersion is >= 35, cipher is
@@ -32,7 +32,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.manager.CredentialManagerPendingIntentManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import io.mockk.coEvery
import io.mockk.every
@@ -71,7 +70,6 @@ class CredentialProviderProcessorTest {
}
private val pendingIntentManager: CredentialManagerPendingIntentManager = mockk()
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk()
private val cancellationSignal: CancellationSignal = mockk()
private val clock = FIXED_CLOCK
@@ -84,7 +82,6 @@ class CredentialProviderProcessorTest {
bitwardenCredentialManager = bitwardenCredentialManager,
pendingIntentManager = pendingIntentManager,
clock = clock,
biometricsEncryptionManager = biometricsEncryptionManager,
dispatcherManager = dispatcherManager,
)
@@ -167,9 +164,7 @@ class CredentialProviderProcessorTest {
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { authRepository.getOrCreateCipher(userId = any()) } returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
@@ -208,9 +203,7 @@ class CredentialProviderProcessorTest {
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk<Cipher>()
every { authRepository.getOrCreateCipher(any()) } returns mockk<Cipher>()
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
@@ -249,7 +242,7 @@ class CredentialProviderProcessorTest {
) { "Expected all entries to have BIOMETRIC_STRONG authenticators." }
// Verify entries have no biometric prompt data when cipher is null
every { biometricsEncryptionManager.getOrCreateCipher(any()) } returns null
every { authRepository.getOrCreateCipher(any()) } returns null
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
@@ -279,9 +272,7 @@ class CredentialProviderProcessorTest {
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { authRepository.getOrCreateCipher(userId = any()) } returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns false
@@ -332,7 +323,7 @@ class CredentialProviderProcessorTest {
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { biometricsEncryptionManager.getOrCreateCipher(any()) }
verify(exactly = 0) { authRepository.getOrCreateCipher(any()) }
// Verify entries have no biometric prompt data when vault is locked
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData == null }) {
@@ -416,9 +407,7 @@ class CredentialProviderProcessorTest {
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { authRepository.getOrCreateCipher(userId = any()) } returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { request.candidateQueryData } returns candidateQueryData
every {
@@ -464,9 +453,7 @@ class CredentialProviderProcessorTest {
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk<Cipher>()
every { authRepository.getOrCreateCipher(any()) } returns mockk<Cipher>()
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
@@ -505,7 +492,7 @@ class CredentialProviderProcessorTest {
) { "Expected all entries to have BIOMETRIC_STRONG authenticators." }
// Verify entries have no biometric prompt data when cipher is null
every { biometricsEncryptionManager.getOrCreateCipher(any()) } returns null
every { authRepository.getOrCreateCipher(any()) } returns null
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
@@ -10,7 +10,6 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
@@ -40,6 +39,8 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(status = any()) } just runs
every { isBiometricIntegrityValid(userId = DEFAULT_USER_ID) } returns false
every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER
}
private val mutableAutofillEnabledStateFlow = MutableStateFlow(false)
@@ -53,13 +54,6 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
every { firstTimeStateFlow } returns mutableFirstTimeStateFlow
every { storeShowUnlockSettingBadge(any()) } just runs
}
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
every { getOrCreateCipher(userId = DEFAULT_USER_ID) } returns CIPHER
every {
isBiometricIntegrityValid(userId = DEFAULT_USER_ID, cipher = CIPHER)
} returns false
every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER
}
private val thirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager = mockk {
every {
browserThirdPartyAutofillStatus
@@ -180,7 +174,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
fun `on UnlockWithBiometricToggleDisabled should call clearBiometricsKey and update the state`() {
val initialState = DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true)
every { settingsRepository.isUnlockWithBiometricsEnabled } returns true
every { biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID) } just runs
every { authRepository.clearBiometrics(userId = DEFAULT_USER_ID) } just runs
val viewModel = createViewModel(initialState)
assertEquals(initialState, viewModel.stateFlow.value)
@@ -191,7 +185,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
verify(exactly = 1) {
biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID)
authRepository.clearBiometrics(userId = DEFAULT_USER_ID)
}
}
@@ -340,25 +334,21 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
verify {
biometricsEncryptionManager.getOrCreateCipher(DEFAULT_USER_ID)
}
viewModel.eventFlow.test {
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
assertEquals(
SetupUnlockEvent.ShowBiometricsPrompt(CIPHER),
awaitItem(),
)
}
verify {
authRepository.createCipherOrNull(DEFAULT_USER_ID)
}
}
@Test
fun `EnableBiometricsClick actin should show error dialog when cipher is null`() {
every {
biometricsEncryptionManager.createCipherOrNull(DEFAULT_USER_ID)
} returns null
every { authRepository.createCipherOrNull(DEFAULT_USER_ID) } returns null
val viewModel = createViewModel()
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
@@ -411,7 +401,6 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
},
authRepository = authRepository,
settingsRepository = settingsRepository,
biometricsEncryptionManager = biometricsEncryptionManager,
firstTimeActionManager = firstTimeActionManager,
browserThirdPartyAutofillEnabledManager = thirdPartyAutofillEnabledManager,
)
@@ -18,7 +18,6 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.model.createMockFido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsRequest
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
@@ -60,25 +59,12 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
every { logout(reason = any()) } just runs
every { logout(userId = any(), reason = any()) } just runs
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
every { getOrCreateCipher(USER_ID) } returns CIPHER
every { isBiometricIntegrityValid(userId = DEFAULT_USER_STATE.activeUserId) } returns true
}
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
every { lockVault(any(), any()) } just runs
}
private val encryptionManager: BiometricsEncryptionManager = mockk {
every { getOrCreateCipher(USER_ID) } returns CIPHER
every {
isBiometricIntegrityValid(
userId = DEFAULT_USER_STATE.activeUserId,
cipher = CIPHER,
)
} returns true
every {
isBiometricIntegrityValid(
userId = DEFAULT_USER_STATE.activeUserId,
cipher = null,
)
} returns false
}
private val bitwardenCredentialManager: BitwardenCredentialManager = mockk {
every { isUserVerified } returns true
every { isUserVerified = any() } just runs
@@ -124,7 +110,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
fun `initial state should be correct when not set`() {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
verify { encryptionManager.getOrCreateCipher(USER_ID) }
verify { authRepository.getOrCreateCipher(USER_ID) }
}
@Test
@@ -506,7 +492,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick)
assertEquals(VaultUnlockEvent.PromptForBiometrics(CIPHER), awaitItem())
}
verify { encryptionManager.getOrCreateCipher(USER_ID) }
verify { authRepository.getOrCreateCipher(USER_ID) }
}
@Test
@@ -533,8 +519,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
fun `on BiometricsUnlockClick should disable isBiometricsValid and show message when cipher is null and integrity check returns false`() {
val initialState = DEFAULT_STATE.copy(isBiometricsValid = true)
val viewModel = createViewModel(state = initialState)
every { encryptionManager.getOrCreateCipher(USER_ID) } returns null
every { encryptionManager.isAccountBiometricIntegrityValid(USER_ID) } returns false
every { authRepository.getOrCreateCipher(USER_ID) } returns null
every { authRepository.isAccountBiometricIntegrityValid(USER_ID) } returns false
viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick)
assertEquals(
@@ -544,7 +530,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
verify { encryptionManager.getOrCreateCipher(USER_ID) }
verify { authRepository.getOrCreateCipher(USER_ID) }
}
@Suppress("MaxLineLength")
@@ -552,8 +538,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
fun `on BiometricsUnlockClick should disable isBiometricsValid and not show message when cipher is null and integrity check returns true`() {
val initialState = DEFAULT_STATE.copy(isBiometricsValid = true)
val viewModel = createViewModel(state = initialState)
every { encryptionManager.getOrCreateCipher(USER_ID) } returns null
every { encryptionManager.isAccountBiometricIntegrityValid(USER_ID) } returns true
every { authRepository.getOrCreateCipher(USER_ID) } returns null
every { authRepository.isAccountBiometricIntegrityValid(USER_ID) } returns true
viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick)
assertEquals(
@@ -563,7 +549,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
verify { encryptionManager.getOrCreateCipher(USER_ID) }
verify { authRepository.getOrCreateCipher(USER_ID) }
}
@Test
@@ -718,7 +704,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
}
// The initial state causes this to be called as well as the change.
verify(exactly = 2) {
encryptionManager.getOrCreateCipher(USER_ID)
authRepository.getOrCreateCipher(USER_ID)
}
}
@@ -745,7 +731,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
}
// Only the call for the initial state should be called.
verify(exactly = 1) {
encryptionManager.getOrCreateCipher(USER_ID)
authRepository.getOrCreateCipher(USER_ID)
}
}
@@ -1168,7 +1154,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
coEvery {
vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER)
} returns VaultUnlockResult.BiometricDecodingError(error = Throwable("Fail"))
every { encryptionManager.clearBiometrics(userId = USER_ID) } just runs
every { authRepository.clearBiometrics(userId = USER_ID) } just runs
viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER))
@@ -1183,7 +1169,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
coVerify(exactly = 1) {
encryptionManager.clearBiometrics(userId = USER_ID)
authRepository.clearBiometrics(userId = USER_ID)
vaultRepository.unlockVaultWithBiometrics(cipher = CIPHER)
}
}
@@ -1341,7 +1327,6 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
state: VaultUnlockState? = null,
unlockType: UnlockType = UnlockType.STANDARD,
vaultRepo: VaultRepository = vaultRepository,
biometricsEncryptionManager: BiometricsEncryptionManager = encryptionManager,
lockManager: VaultLockManager = vaultLockManager,
): VaultUnlockViewModel = VaultUnlockViewModel(
savedStateHandle = SavedStateHandle().apply {
@@ -1350,7 +1335,6 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
},
authRepository = authRepository,
vaultRepo = vaultRepo,
biometricsEncryptionManager = biometricsEncryptionManager,
bitwardenCredentialManager = bitwardenCredentialManager,
specialCircumstanceManager = specialCircumstanceManager,
appResumeManager = appResumeManager,
@@ -21,7 +21,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
@@ -62,6 +61,8 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository: AuthRepository = mockk(relaxed = true) {
every { userStateFlow } returns mutableUserStateFlow
every { createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
every { isBiometricIntegrityValid(userId = DEFAULT_USER_STATE.activeUserId) } returns true
}
private val vaultRepository: VaultRepository = mockk(relaxed = true)
private val mutableBiometricsUnlockEnabledFlow = bufferedMutableSharedFlow<Boolean>()
@@ -84,16 +85,6 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
}
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<Policy>>()
private val mutableRemovePinPolicyFlow = bufferedMutableSharedFlow<List<Policy>>()
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
every { createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
every { getOrCreateCipher(DEFAULT_USER_STATE.activeUserId) } returns CIPHER
every {
isBiometricIntegrityValid(
userId = DEFAULT_USER_STATE.activeUserId,
cipher = CIPHER,
)
} returns true
}
private val policyManager: PolicyManager = mockk {
every {
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
@@ -123,16 +114,9 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct when saved state is not set`() {
val viewModel = createViewModel(initialState = null)
assertEquals(
DEFAULT_STATE,
viewModel.stateFlow.value,
)
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
verify {
biometricsEncryptionManager.getOrCreateCipher(DEFAULT_USER_STATE.activeUserId)
biometricsEncryptionManager.isBiometricIntegrityValid(
userId = DEFAULT_USER_STATE.activeUserId,
cipher = CIPHER,
)
settingsRepository.isUnlockWithBiometricsEnabled
}
coVerify { settingsRepository.getUserFingerprint() }
}
@@ -510,9 +494,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
@Test
fun `on EnableBiometricsClick should show Error dialog when cipher is null`() {
every {
biometricsEncryptionManager.createCipherOrNull(DEFAULT_USER_STATE.activeUserId)
} returns null
every { authRepository.createCipherOrNull(DEFAULT_USER_STATE.activeUserId) } returns null
val viewModel = createViewModel()
viewModel.trySendAction(AccountSecurityAction.EnableBiometricsClick)
@@ -534,9 +516,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
runTest {
val initialState = DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true)
every { settingsRepository.isUnlockWithBiometricsEnabled } returns true
every {
biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID)
} just runs
every { authRepository.clearBiometrics(userId = DEFAULT_USER_ID) } just runs
val viewModel = createViewModel(initialState)
assertEquals(initialState, viewModel.stateFlow.value)
@@ -547,7 +527,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
verify(exactly = 1) {
biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID)
authRepository.clearBiometrics(userId = DEFAULT_USER_ID)
}
}
@@ -560,9 +540,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
isUnlockWithBiometricsEnabled = true,
)
every { settingsRepository.isUnlockWithBiometricsEnabled } returns true
every {
biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID)
} just runs
every { authRepository.clearBiometrics(userId = DEFAULT_USER_ID) } just runs
every { settingsRepository.vaultTimeoutAction = VaultTimeoutAction.LOGOUT } just runs
val viewModel = createViewModel(initialState)
assertEquals(initialState, viewModel.stateFlow.value)
@@ -577,7 +555,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
verify(exactly = 1) {
biometricsEncryptionManager.clearBiometrics(userId = DEFAULT_USER_ID)
authRepository.clearBiometrics(userId = DEFAULT_USER_ID)
settingsRepository.vaultTimeoutAction = VaultTimeoutAction.LOGOUT
}
}
@@ -903,14 +881,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
vaultRepository: VaultRepository = this.vaultRepository,
environmentRepository: EnvironmentRepository = this.fakeEnvironmentRepository,
settingsRepository: SettingsRepository = this.settingsRepository,
biometricsEncryptionManager: BiometricsEncryptionManager = this.biometricsEncryptionManager,
policyManager: PolicyManager = this.policyManager,
): AccountSecurityViewModel = AccountSecurityViewModel(
authRepository = authRepository,
vaultRepository = vaultRepository,
settingsRepository = settingsRepository,
environmentRepository = environmentRepository,
biometricsEncryptionManager = biometricsEncryptionManager,
policyManager = policyManager,
savedStateHandle = SavedStateHandle().apply {
set("state", initialState)