Merge pull request #6784 from vector-im/fix/jorgem/lockscreen-device-locked

Fix lockscreen's 'device locked' crash on Android 12 and 12L devices
This commit is contained in:
Adam Brown 2022-08-09 15:18:28 +01:00 committed by GitHub
commit fe61fa844e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 194 additions and 227 deletions

1
changelog.d/6768.bugfix Normal file
View file

@ -0,0 +1 @@
Fix crash when biometric key is used when coming back to foreground and KeyStore reports that the device is still locked.

View file

@ -31,7 +31,6 @@ import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.TestBuildVersionSdkIntProvider import im.vector.app.TestBuildVersionSdkIntProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
@ -40,6 +39,7 @@ import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometric
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import io.mockk.mockkStatic import io.mockk.mockkStatic
@ -54,8 +54,10 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldThrow
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@ -239,36 +241,35 @@ class BiometricHelperTests {
@Test @Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // Due to some issues with mockk and CryptoObject initialization
fun authenticateCreatesSystemKeyIfNeededOnSuccessOnAndroidM() = runTest { fun enableAuthenticationDeletesSystemKeyOnFailure() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
every { lockScreenKeyRepository.isSystemKeyValid() } returns true
val mockAuthChannel = Channel<Boolean>(capacity = 1) val mockAuthChannel = Channel<Boolean>(capacity = 1)
val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) { val biometricUtils = spyk(createBiometricHelper(createDefaultConfiguration(isBiometricsEnabled = true))) {
every { createAuthChannel() } returns mockAuthChannel every { createAuthChannel() } returns mockAuthChannel
every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk() every { authenticateWithPromptInternal(any(), any(), any()) } returns mockk()
} }
justRun { lockScreenKeyRepository.deleteSystemKey() }
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java) val intent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, LockScreenTestActivity::class.java)
ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity -> ActivityScenario.launch<LockScreenTestActivity>(intent).onActivity { activity ->
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
val exception = IllegalStateException("Some error")
launch { launch {
mockAuthChannel.send(true) mockAuthChannel.close(exception)
mockAuthChannel.close()
} }
biometricUtils.authenticate(activity).collect() coInvoking { biometricUtils.enableAuthentication(activity).collect() } shouldThrow exception
latch.countDown() latch.countDown()
} }
} }
latch.await(1, TimeUnit.SECONDS) latch.await(1, TimeUnit.SECONDS)
verify { lockScreenKeyRepository.ensureSystemKey() } verify { lockScreenKeyRepository.deleteSystemKey() }
} }
private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper { private fun createBiometricHelper(configuration: LockScreenConfiguration): BiometricHelper {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
val configProvider = LockScreenConfiguratorProvider(configuration) return BiometricHelper(configuration, context, lockScreenKeyRepository, biometricManager, buildVersionSdkIntProvider)
return BiometricHelper(context, lockScreenKeyRepository, configProvider, biometricManager, buildVersionSdkIntProvider)
} }
private fun createDefaultConfiguration( private fun createDefaultConfiguration(

View file

@ -17,8 +17,6 @@
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.settings.VectorPreferences
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -44,8 +42,6 @@ class LockScreenKeyRepositoryTests {
} }
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
private val legacyPinCodeMigrator: LegacyPinCodeMigrator = mockk(relaxed = true)
private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
private val keyStore: KeyStore by lazy { private val keyStore: KeyStore by lazy {
KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) } KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }

View file

@ -24,6 +24,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.asMavericksArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
@ -33,7 +34,7 @@ import im.vector.app.databinding.FragmentPinBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.ui.AuthMethod import im.vector.app.features.pin.lockscreen.ui.AuthMethod
import im.vector.app.features.pin.lockscreen.ui.LockScreenFragment import im.vector.app.features.pin.lockscreen.ui.LockScreenFragment
@ -51,7 +52,7 @@ data class PinArgs(
class PinFragment @Inject constructor( class PinFragment @Inject constructor(
private val pinCodeStore: PinCodeStore, private val pinCodeStore: PinCodeStore,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val configuratorProvider: LockScreenConfiguratorProvider, private val defaultConfiguration: LockScreenConfiguration,
) : VectorBaseFragment<FragmentPinBinding>() { ) : VectorBaseFragment<FragmentPinBinding>() {
private val fragmentArgs: PinArgs by args() private val fragmentArgs: PinArgs by args()
@ -81,21 +82,17 @@ class PinFragment @Inject constructor(
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
} }
createFragment.arguments = defaultConfiguration.copy(
configuratorProvider.updateDefaultConfiguration { mode = LockScreenMode.CREATE,
copy( title = getString(R.string.create_pin_title),
mode = LockScreenMode.CREATE, needsNewCodeValidation = true,
title = getString(R.string.create_pin_title), newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title),
needsNewCodeValidation = true, ).asMavericksArgs()
newCodeConfirmationTitle = getString(R.string.create_pin_confirm_title),
)
}
replaceFragment(R.id.pinFragmentContainer, createFragment) replaceFragment(R.id.pinFragmentContainer, createFragment)
} }
private fun showAuthFragment() { private fun showAuthFragment() {
val authFragment = LockScreenFragment() val authFragment = LockScreenFragment()
val canUseBiometrics = vectorPreferences.useBiometricsToUnlock()
authFragment.onLeftButtonClickedListener = View.OnClickListener { displayForgotPinWarningDialog() } authFragment.onLeftButtonClickedListener = View.OnClickListener { displayForgotPinWarningDialog() }
authFragment.lockScreenListener = object : LockScreenListener { authFragment.lockScreenListener = object : LockScreenListener {
override fun onAuthenticationFailure(authMethod: AuthMethod) { override fun onAuthenticationFailure(authMethod: AuthMethod) {
@ -133,18 +130,12 @@ class PinFragment @Inject constructor(
.show() .show()
} }
} }
configuratorProvider.updateDefaultConfiguration { authFragment.arguments = defaultConfiguration.copy(
copy( mode = LockScreenMode.VERIFY,
mode = LockScreenMode.VERIFY, title = getString(R.string.auth_pin_title),
title = getString(R.string.auth_pin_title), leftButtonTitle = getString(R.string.auth_pin_forgot),
isStrongBiometricsEnabled = isStrongBiometricsEnabled && canUseBiometrics, clearCodeOnError = true,
isWeakBiometricsEnabled = isWeakBiometricsEnabled && canUseBiometrics, ).asMavericksArgs()
isDeviceCredentialUnlockEnabled = isDeviceCredentialUnlockEnabled && canUseBiometrics,
autoStartBiometric = canUseBiometrics,
leftButtonTitle = getString(R.string.auth_pin_forgot),
clearCodeOnError = true,
)
}
replaceFragment(R.id.pinFragmentContainer, authFragment) replaceFragment(R.id.pinFragmentContainer, authFragment)
} }

View file

@ -31,10 +31,12 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck import im.vector.app.features.pin.lockscreen.utils.DevicePromptCheck
@ -54,22 +56,24 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import java.security.KeyStore import java.security.KeyStore
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
* This is a helper to manage system authentication (biometric and other types) and the system key. * This is a helper to manage system authentication (biometric and other types) and the system key.
*/ */
class BiometricHelper @Inject constructor( class BiometricHelper @AssistedInject constructor(
@Assisted private val configuration: LockScreenConfiguration,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val lockScreenKeyRepository: LockScreenKeyRepository, private val lockScreenKeyRepository: LockScreenKeyRepository,
private val configurationProvider: LockScreenConfiguratorProvider,
private val biometricManager: BiometricManager, private val biometricManager: BiometricManager,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) { ) {
private var prompt: BiometricPrompt? = null private var prompt: BiometricPrompt? = null
private val configuration: LockScreenConfiguration get() = configurationProvider.currentConfiguration @AssistedFactory
interface BiometricHelperFactory {
fun create(configuration: LockScreenConfiguration): BiometricHelper
}
/** /**
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used. * Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
@ -174,16 +178,18 @@ class BiometricHelper @Inject constructor(
when (val exception = result.exceptionOrNull()) { when (val exception = result.exceptionOrNull()) {
null -> result.getOrNull()?.let { emit(it) } null -> result.getOrNull()?.let { emit(it) }
else -> { else -> {
// Exception found, stop collecting, throw it and remove the prompt reference // Exception found:
// 1. Stop collecting.
// 2. Remove the system key if we were creating it.
// 3. Throw the exception and remove the prompt reference
if (!checkSystemKeyExists) {
lockScreenKeyRepository.deleteSystemKey()
}
prompt = null prompt = null
throw exception throw exception
} }
} }
} }
// Generates the system key on successful authentication
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M) {
lockScreenKeyRepository.ensureSystemKey()
}
// Channel is closed, remove prompt reference // Channel is closed, remove prompt reference
prompt = null prompt = null
} }
@ -213,11 +219,11 @@ class BiometricHelper @Inject constructor(
.setAllowedAuthenticators(authenticators) .setAllowedAuthenticators(authenticators)
.build() .build()
return BiometricPrompt(activity, executor, callback).also { return BiometricPrompt(activity, executor, callback).also { prompt ->
showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) { showFallbackFragmentIfNeeded(activity, channel.receiveAsFlow(), executor.asCoroutineDispatcher()) {
// For some reason this seems to be needed unless we want to receive a fragment transaction exception // For some reason this seems to be needed unless we want to receive a fragment transaction exception
delay(1L) delay(1L)
it.authenticate(promptInfo, cryptoObject) prompt.authenticate(promptInfo, cryptoObject)
} }
} }
} }
@ -253,11 +259,9 @@ class BiometricHelper @Inject constructor(
): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() { ): BiometricPrompt.AuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {
private val scope = CoroutineScope(coroutineContext) private val scope = CoroutineScope(coroutineContext)
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
scope.launch { // Error is a terminal event, should close both the Channel and the CoroutineScope to free resources.
// Error is a terminal event, should close both the Channel and the CoroutineScope to free resources. channel.close(BiometricAuthError(errorCode, errString.toString()))
channel.close(BiometricAuthError(errorCode, errString.toString())) scope.cancel()
scope.cancel()
}
} }
override fun onAuthenticationFailed() { override fun onAuthenticationFailed() {
@ -274,10 +278,8 @@ class BiometricHelper @Inject constructor(
scope.cancel() scope.cancel()
} }
} else { } else {
scope.launch { channel.close(IllegalStateException("System key was not valid after authentication."))
channel.close(IllegalStateException("System key was not valid after authentication.")) scope.cancel()
scope.cancel()
}
} }
} }

View file

@ -16,9 +16,13 @@
package im.vector.app.features.pin.lockscreen.configuration package im.vector.app.features.pin.lockscreen.configuration
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/** /**
* Configuration to be used by the lockscreen feature. * Configuration to be used by the lockscreen feature.
*/ */
@Parcelize
data class LockScreenConfiguration( data class LockScreenConfiguration(
/** Which mode should the UI display, [LockScreenMode.VERIFY] or [LockScreenMode.CREATE]. */ /** Which mode should the UI display, [LockScreenMode.VERIFY] or [LockScreenMode.CREATE]. */
val mode: LockScreenMode, val mode: LockScreenMode,
@ -56,4 +60,4 @@ data class LockScreenConfiguration(
val biometricSubtitle: String? = null, val biometricSubtitle: String? = null,
/** Text for the cancel button of the Biometric prompt dialog. Optional. */ /** Text for the cancel button of the Biometric prompt dialog. Optional. */
val biometricCancelButtonTitle: String? = null, val biometricCancelButtonTitle: String? = null,
) ) : Parcelable

View file

@ -1,58 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.pin.lockscreen.configuration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* Class used to hold both the [defaultConfiguration] and an updated version in [currentConfiguration].
*/
@Singleton
class LockScreenConfiguratorProvider @Inject constructor(
/** Default [LockScreenConfiguration], any derived configuration created using [updateDefaultConfiguration] will use this as a base. */
val defaultConfiguration: LockScreenConfiguration,
) {
private val mutableConfigurationFlow = MutableStateFlow(defaultConfiguration)
/**
* A [Flow] that emits any changes in configuration.
*/
val configurationFlow: Flow<LockScreenConfiguration> = mutableConfigurationFlow
/**
* The current configuration to be read and used.
*/
val currentConfiguration get() = mutableConfigurationFlow.value
/**
* Applies the changes in [block] to the [defaultConfiguration] to generate a new [currentConfiguration].
*/
fun updateDefaultConfiguration(block: LockScreenConfiguration.() -> LockScreenConfiguration) {
mutableConfigurationFlow.value = defaultConfiguration.block()
}
/**
* Resets the [currentConfiguration] to the [defaultConfiguration].
*/
fun reset() {
mutableConfigurationFlow.value = defaultConfiguration
}
}

View file

@ -20,13 +20,13 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.UserNotAuthenticatedException
import android.util.Base64 import android.util.Base64
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import java.security.Key import java.security.Key
@ -113,14 +113,8 @@ class KeyStoreCrypto @AssistedInject constructor(
fun hasValidKey(): Boolean { fun hasValidKey(): Boolean {
val keyExists = hasKey() val keyExists = hasKey()
return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) { return if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && keyExists) {
try { val initializedKey = tryOrNull("Error validating lockscreen system key.") { ensureKey() }
ensureKey() initializedKey != null
true
} catch (e: KeyPermanentlyInvalidatedException) {
false
} catch (e: UserNotAuthenticatedException) {
false
}
} else { } else {
keyExists keyExists
} }

View file

@ -16,8 +16,10 @@
package im.vector.app.features.pin.lockscreen.di package im.vector.app.features.pin.lockscreen.di
import android.app.KeyguardManager
import android.content.Context import android.content.Context
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
import androidx.core.content.getSystemService
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -83,6 +85,9 @@ object LockScreenModule {
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider), SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider),
buildVersionSdkIntProvider, buildVersionSdkIntProvider,
) )
@Provides
fun provideKeyguardManager(context: Context): KeyguardManager = context.getSystemService()!!
} }
@Module @Module

View file

@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class LockScreenAction : VectorViewModelAction { sealed class LockScreenAction : VectorViewModelAction {
data class PinCodeEntered(val value: String) : LockScreenAction() data class PinCodeEntered(val value: String) : LockScreenAction()
data class ShowBiometricPrompt(val callingActivity: FragmentActivity) : LockScreenAction() data class ShowBiometricPrompt(val callingActivity: FragmentActivity) : LockScreenAction()
object OnUIReady : LockScreenAction()
} }

View file

@ -23,7 +23,6 @@ import android.view.ViewGroup
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -55,22 +54,7 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
handleEvent(it) handleEvent(it)
} }
withState(viewModel) { state -> viewModel.handle(LockScreenAction.OnUIReady)
if (state.lockScreenConfiguration.mode == LockScreenMode.CREATE) return@withState
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
if (state.canUseBiometricAuth && state.isBiometricKeyInvalidated) {
lockScreenListener?.onBiometricKeyInvalidated()
} else if (state.showBiometricPromptAutomatically) {
showBiometricPrompt()
}
}
}
}
override fun onDestroy() {
super.onDestroy()
viewModel.reset()
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
@ -83,6 +67,7 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
setupTitleView(views.titleTextView, false, state.lockScreenConfiguration) setupTitleView(views.titleTextView, false, state.lockScreenConfiguration)
} }
} }
renderDeleteOrFingerprintButtons(views, views.codeView.enteredDigits) renderDeleteOrFingerprintButtons(views, views.codeView.enteredDigits)
} }
@ -123,6 +108,8 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method) is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method) is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable) is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
is LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage -> lockScreenListener?.onBiometricKeyInvalidated()
is LockScreenViewEvent.ShowBiometricPromptAutomatically -> showBiometricPrompt()
} }
} }

View file

@ -24,4 +24,6 @@ sealed class LockScreenViewEvent : VectorViewEvents {
data class AuthSuccessful(val method: AuthMethod) : LockScreenViewEvent() data class AuthSuccessful(val method: AuthMethod) : LockScreenViewEvent()
data class AuthFailure(val method: AuthMethod) : LockScreenViewEvent() data class AuthFailure(val method: AuthMethod) : LockScreenViewEvent()
data class AuthError(val method: AuthMethod, val throwable: Throwable) : LockScreenViewEvent() data class AuthError(val method: AuthMethod, val throwable: Throwable) : LockScreenViewEvent()
object ShowBiometricKeyInvalidatedMessage : LockScreenViewEvent()
object ShowBiometricPromptAutomatically : LockScreenViewEvent()
} }

View file

@ -17,12 +17,11 @@
package im.vector.app.features.pin.lockscreen.ui package im.vector.app.features.pin.lockscreen.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.withState
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -31,26 +30,29 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError import im.vector.app.features.pin.lockscreen.biometrics.BiometricAuthError
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class LockScreenViewModel @AssistedInject constructor( class LockScreenViewModel @AssistedInject constructor(
@Assisted val initialState: LockScreenViewState, @Assisted val initialState: LockScreenViewState,
private val pinCodeHelper: PinCodeHelper, private val pinCodeHelper: PinCodeHelper,
private val biometricHelper: BiometricHelper, biometricHelperFactory: BiometricHelper.BiometricHelperFactory,
private val lockScreenKeysMigrator: LockScreenKeysMigrator, private val lockScreenKeysMigrator: LockScreenKeysMigrator,
private val configuratorProvider: LockScreenConfiguratorProvider, private val versionProvider: BuildVersionSdkIntProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val keyguardManager: KeyguardManager,
) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) { ) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) {
@AssistedFactory @AssistedFactory
@ -58,27 +60,9 @@ class LockScreenViewModel @AssistedInject constructor(
override fun create(initialState: LockScreenViewState): LockScreenViewModel override fun create(initialState: LockScreenViewState): LockScreenViewModel
} }
companion object : MavericksViewModelFactory<LockScreenViewModel, LockScreenViewState> by hiltMavericksViewModelFactory() { companion object : MavericksViewModelFactory<LockScreenViewModel, LockScreenViewState> by hiltMavericksViewModelFactory()
override fun initialState(viewModelContext: ViewModelContext): LockScreenViewState { private val biometricHelper = biometricHelperFactory.create(initialState.lockScreenConfiguration)
return LockScreenViewState(
lockScreenConfiguration = DUMMY_CONFIGURATION,
canUseBiometricAuth = false,
showBiometricPromptAutomatically = false,
pinCodeState = PinCodeState.Idle,
isBiometricKeyInvalidated = false,
)
}
private val DUMMY_CONFIGURATION = LockScreenConfiguration(
mode = LockScreenMode.VERIFY,
pinCodeLength = 4,
isStrongBiometricsEnabled = false,
isDeviceCredentialUnlockEnabled = false,
isWeakBiometricsEnabled = false,
needsNewCodeValidation = false,
)
}
private var firstEnteredCode: String? = null private var firstEnteredCode: String? = null
@ -86,18 +70,37 @@ class LockScreenViewModel @AssistedInject constructor(
private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false private var isSystemAuthTemporarilyDisabledByBiometricPrompt = false
init { init {
// We need this to run synchronously before we start reading the configurations viewModelScope.launch {
runBlocking { lockScreenKeysMigrator.migrateIfNeeded() } // Wait until the keyguard is unlocked before performing migrations, it might cause crashes otherwise on Android 12 and 12L
waitUntilKeyguardIsUnlocked()
// Migrate pin code / system keys if needed
lockScreenKeysMigrator.migrateIfNeeded()
// Update initial state with biometric info
updateStateWithBiometricInfo()
}
}
configuratorProvider.configurationFlow private fun observeStateChanges() {
.onEach { updateConfiguration(it) } // The first time the state allows it, show the biometric prompt
.launchIn(viewModelScope) viewModelScope.launch {
if (stateFlow.firstOrNull { it.showBiometricPromptAutomatically } != null) {
_viewEvents.post(LockScreenViewEvent.ShowBiometricPromptAutomatically)
}
}
// The first time the state allows it, react to biometric key being invalidated
viewModelScope.launch {
if (stateFlow.firstOrNull { it.isBiometricKeyInvalidated } != null) {
onBiometricKeyInvalidated()
}
}
} }
override fun handle(action: LockScreenAction) { override fun handle(action: LockScreenAction) {
when (action) { when (action) {
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value) is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity) is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity)
is LockScreenAction.OnUIReady -> observeStateChanges()
} }
} }
@ -141,13 +144,18 @@ class LockScreenViewModel @AssistedInject constructor(
private fun showBiometricPrompt(activity: FragmentActivity) = flow { private fun showBiometricPrompt(activity: FragmentActivity) = flow {
emitAll(biometricHelper.authenticate(activity)) emitAll(biometricHelper.authenticate(activity))
}.catch { error -> }.catch { error ->
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException) { when {
removeBiometricAuthentication() versionProvider.get() >= Build.VERSION_CODES.M && error is KeyPermanentlyInvalidatedException -> {
} else if (error is BiometricAuthError && error.isAuthDisabledError) { onBiometricKeyInvalidated()
isSystemAuthTemporarilyDisabledByBiometricPrompt = true }
updateStateWithBiometricInfo() else -> {
if (error is BiometricAuthError && error.isAuthDisabledError) {
isSystemAuthTemporarilyDisabledByBiometricPrompt = true
updateStateWithBiometricInfo()
}
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, error))
}
} }
_viewEvents.post(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, error))
}.onEach { success -> }.onEach { success ->
_viewEvents.post( _viewEvents.post(
if (success) LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS) if (success) LockScreenViewEvent.AuthSuccessful(AuthMethod.BIOMETRICS)
@ -155,24 +163,22 @@ class LockScreenViewModel @AssistedInject constructor(
) )
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
fun reset() { private suspend fun onBiometricKeyInvalidated() {
configuratorProvider.reset()
}
private fun removeBiometricAuthentication() {
biometricHelper.disableAuthentication() biometricHelper.disableAuthentication()
updateStateWithBiometricInfo() updateStateWithBiometricInfo()
_viewEvents.post(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
} }
private fun updateStateWithBiometricInfo() { @SuppressLint("NewApi")
val configuration = withState(this) { it.lockScreenConfiguration } private suspend fun updateStateWithBiometricInfo() {
val canUseBiometricAuth = configuration.mode == LockScreenMode.VERIFY && // This is a terrible hack, but I found no other way to ensure this would be called only after the device is considered unlocked on Android 12+
waitUntilKeyguardIsUnlocked()
setState {
val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid
val canUseBiometricAuth = lockScreenConfiguration.mode == LockScreenMode.VERIFY &&
!isSystemAuthTemporarilyDisabledByBiometricPrompt && !isSystemAuthTemporarilyDisabledByBiometricPrompt &&
biometricHelper.isSystemAuthEnabledAndValid biometricHelper.isSystemAuthEnabledAndValid
val isBiometricKeyInvalidated = biometricHelper.hasSystemKey && !biometricHelper.isSystemKeyValid val showBiometricPromptAutomatically = canUseBiometricAuth && lockScreenConfiguration.autoStartBiometric
val showBiometricPromptAutomatically = canUseBiometricAuth &&
configuration.autoStartBiometric
setState {
copy( copy(
canUseBiometricAuth = canUseBiometricAuth, canUseBiometricAuth = canUseBiometricAuth,
showBiometricPromptAutomatically = showBiometricPromptAutomatically, showBiometricPromptAutomatically = showBiometricPromptAutomatically,
@ -181,8 +187,18 @@ class LockScreenViewModel @AssistedInject constructor(
} }
} }
private fun updateConfiguration(configuration: LockScreenConfiguration) { /**
setState { copy(lockScreenConfiguration = configuration) } * Wait until the device is unlocked. There seems to be a behavior change on Android 12 that makes [KeyguardManager.isDeviceLocked] return `false` even
updateStateWithBiometricInfo() * after an Activity's `onResume` method. If we mix that with the system keys needing the device to be unlocked before they're used, we get crashes.
* See issue [#6768](https://github.com/vector-im/element-android/issues/6768).
*/
@SuppressLint("NewApi")
private suspend fun waitUntilKeyguardIsUnlocked() {
if (versionProvider.get() < Build.VERSION_CODES.S) return
withTimeoutOrNull(5.seconds) {
while (keyguardManager.isDeviceLocked) {
delay(50.milliseconds)
}
}
} }
} }

View file

@ -25,7 +25,11 @@ data class LockScreenViewState(
val showBiometricPromptAutomatically: Boolean, val showBiometricPromptAutomatically: Boolean,
val pinCodeState: PinCodeState, val pinCodeState: PinCodeState,
val isBiometricKeyInvalidated: Boolean, val isBiometricKeyInvalidated: Boolean,
) : MavericksState ) : MavericksState {
constructor(lockScreenConfiguration: LockScreenConfiguration) : this(
lockScreenConfiguration, false, false, PinCodeState.Idle, false
)
}
sealed class PinCodeState { sealed class PinCodeState {
object Idle : PinCodeState() object Idle : PinCodeState()

View file

@ -28,6 +28,8 @@ import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -38,12 +40,15 @@ class VectorSettingsPinFragment @Inject constructor(
private val pinCodeStore: PinCodeStore, private val pinCodeStore: PinCodeStore,
private val navigator: Navigator, private val navigator: Navigator,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val biometricHelper: BiometricHelper, biometricHelperFactory: BiometricHelper.BiometricHelperFactory,
defaultLockScreenConfiguration: LockScreenConfiguration,
) : VectorSettingsBaseFragment() { ) : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_security_application_protection_screen_title override var titleRes = R.string.settings_security_application_protection_screen_title
override val preferenceXmlRes = R.xml.vector_settings_pin override val preferenceXmlRes = R.xml.vector_settings_pin
private val biometricHelper = biometricHelperFactory.create(defaultLockScreenConfiguration.copy(mode = LockScreenMode.CREATE))
private val usePinCodePref by lazy { private val usePinCodePref by lazy {
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!! findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!!
} }
@ -102,9 +107,10 @@ class VectorSettingsPinFragment @Inject constructor(
}.onFailure { }.onFailure {
showEnableBiometricErrorMessage() showEnableBiometricErrorMessage()
} }
updateBiometricPrefState(isPinCodeChecked = usePinCodePref.isChecked) updateBiometricPrefState(isPinCodeChecked = usePinCodePref.isChecked)
} }
false true
} else { } else {
disableBiometricAuthentication() disableBiometricAuthentication()
true true

View file

@ -16,6 +16,7 @@
package im.vector.app.features.pin.lockscreen.fragment package im.vector.app.features.pin.lockscreen.fragment
import android.app.KeyguardManager
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@ -23,7 +24,6 @@ import com.airbnb.mvrx.test.MvRxTestRule
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper import im.vector.app.features.pin.lockscreen.biometrics.BiometricHelper
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguration
import im.vector.app.features.pin.lockscreen.configuration.LockScreenConfiguratorProvider
import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
@ -42,6 +42,7 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
@ -57,7 +58,15 @@ class LockScreenViewModelTests {
private val pinCodeHelper = mockk<PinCodeHelper>(relaxed = true) private val pinCodeHelper = mockk<PinCodeHelper>(relaxed = true)
private val biometricHelper = mockk<BiometricHelper>(relaxed = true) private val biometricHelper = mockk<BiometricHelper>(relaxed = true)
private val biometricHelperFactory = object : BiometricHelper.BiometricHelperFactory {
override fun create(configuration: LockScreenConfiguration): BiometricHelper {
return biometricHelper
}
}
private val keysMigrator = mockk<LockScreenKeysMigrator>(relaxed = true) private val keysMigrator = mockk<LockScreenKeysMigrator>(relaxed = true)
private val keyguardManager = mockk<KeyguardManager>(relaxed = true) {
every { isDeviceLocked } returns false
}
private val versionProvider = TestBuildVersionSdkIntProvider() private val versionProvider = TestBuildVersionSdkIntProvider()
@Before @Before
@ -68,19 +77,36 @@ class LockScreenViewModelTests {
@Test @Test
fun `init migrates old keys to new ones if needed`() { fun `init migrates old keys to new ones if needed`() {
val initialState = createViewState() val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coVerify { keysMigrator.migrateIfNeeded() } coVerify { keysMigrator.migrateIfNeeded() }
} }
@Test
fun `init updates the initial state with biometric info`() = runTest {
every { biometricHelper.isSystemAuthEnabledAndValid } returns true
val initialState = createViewState()
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
advanceUntilIdle()
val newState = viewModel.awaitState()
newState shouldNotBeEqualTo initialState
}
@Test
fun `Updating the initial state with biometric info waits until device is unlocked on Android 12+`() = runTest {
val initialState = createViewState()
versionProvider.value = Build.VERSION_CODES.S
LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
advanceUntilIdle()
verify { keyguardManager.isDeviceLocked }
}
@Test @Test
fun `when ViewModel is instantiated initialState is updated with biometric info`() { fun `when ViewModel is instantiated initialState is updated with biometric info`() {
val initialState = createViewState() val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
// This should set canUseBiometricAuth to true // This should set canUseBiometricAuth to true
every { biometricHelper.isSystemAuthEnabledAndValid } returns true every { biometricHelper.isSystemAuthEnabledAndValid } returns true
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val newState = withState(viewModel) { it } val newState = withState(viewModel) { it }
initialState shouldNotBeEqualTo newState initialState shouldNotBeEqualTo newState
} }
@ -88,8 +114,7 @@ class LockScreenViewModelTests {
@Test @Test
fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest { fun `when onPinCodeEntered is called in VERIFY mode, the code is verified and the result is emitted as a ViewEvent`() = runTest {
val initialState = createViewState() val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coEvery { pinCodeHelper.verifyPinCode(any()) } returns true coEvery { pinCodeHelper.verifyPinCode(any()) } returns true
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
@ -113,8 +138,7 @@ class LockScreenViewModelTests {
fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest { fun `when onPinCodeEntered is called in CREATE mode with no confirmation needed it creates the pin code`() = runTest {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = false)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration)
val configProvider = LockScreenConfiguratorProvider(configuration) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -128,9 +152,8 @@ class LockScreenViewModelTests {
@Test @Test
fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest { fun `when onPinCodeEntered is called twice in CREATE mode with confirmation needed it verifies and creates the pin code`() = runTest {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val configProvider = LockScreenConfiguratorProvider(configuration)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -149,8 +172,7 @@ class LockScreenViewModelTests {
fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest { fun `when onPinCodeEntered is called in CREATE mode with incorrect confirmation it clears the pin code`() = runTest {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration)
val configProvider = LockScreenConfiguratorProvider(configuration) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -170,8 +192,7 @@ class LockScreenViewModelTests {
@Test @Test
fun `onPinCodeEntered handles exceptions`() = runTest { fun `onPinCodeEntered handles exceptions`() = runTest {
val initialState = createViewState() val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val exception = IllegalStateException("Something went wrong") val exception = IllegalStateException("Something went wrong")
coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception coEvery { pinCodeHelper.verifyPinCode(any()) } throws exception
@ -187,39 +208,34 @@ class LockScreenViewModelTests {
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest { fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
versionProvider.value = Build.VERSION_CODES.M versionProvider.value = Build.VERSION_CODES.M
every { biometricHelper.isSystemAuthEnabledAndValid } returns true every { biometricHelper.isSystemKeyValid } returns false
every { biometricHelper.isSystemKeyValid } returns true
val exception = KeyPermanentlyInvalidatedException() val exception = KeyPermanentlyInvalidatedException()
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception
coEvery { biometricHelper.disableAuthentication() } coAnswers {
every { biometricHelper.isSystemAuthEnabledAndValid } returns false
}
val configuration = createDefaultConfiguration(mode = LockScreenMode.VERIFY, needsNewCodeValidation = true, isBiometricsEnabled = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.VERIFY, needsNewCodeValidation = true, isBiometricsEnabled = true)
val configProvider = LockScreenConfiguratorProvider(configuration)
val initialState = createViewState( val initialState = createViewState(
canUseBiometricAuth = true, canUseBiometricAuth = true,
isBiometricKeyInvalidated = false, isBiometricKeyInvalidated = false,
lockScreenConfiguration = configuration lockScreenConfiguration = configuration
) )
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk())) viewModel.handle(LockScreenAction.ShowBiometricPrompt(mockk()))
events.assertValues(LockScreenViewEvent.AuthError(AuthMethod.BIOMETRICS, exception)) events.assertValues(LockScreenViewEvent.ShowBiometricKeyInvalidatedMessage)
verify { biometricHelper.disableAuthentication() } verify { biometricHelper.disableAuthentication() }
// System key was deleted, biometric auth should be disabled // System key was deleted, biometric auth should be disabled
every { biometricHelper.isSystemAuthEnabledAndValid } returns false
val newState = viewModel.awaitState() val newState = viewModel.awaitState()
newState.canUseBiometricAuth.shouldBeFalse() newState.canUseBiometricAuth.shouldBeFalse()
} }
@Test @Test
fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest { fun `when showBiometricPrompt receives an event it propagates it as a ViewEvent`() = runTest {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true) coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } returns flowOf(false, true)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
@ -232,8 +248,7 @@ class LockScreenViewModelTests {
@Test @Test
fun `showBiometricPrompt handles exceptions`() = runTest { fun `showBiometricPrompt handles exceptions`() = runTest {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelperFactory, keysMigrator, versionProvider, keyguardManager)
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val exception = IllegalStateException("Something went wrong") val exception = IllegalStateException("Something went wrong")
coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception coEvery { biometricHelper.authenticate(any<FragmentActivity>()) } throws exception