Improve lock screen implementation.

This commit is contained in:
Jorge Martín 2022-07-12 12:10:07 +02:00 committed by Jorge Martin Espinosa
parent 067a030f19
commit b468a9da33
22 changed files with 571 additions and 176 deletions

1
changelog.d/6522.feature Normal file
View File

@ -0,0 +1 @@
Improve lock screen implementation with extra security measures

View File

@ -180,11 +180,11 @@ class SecretStoringUtils @Inject constructor(
is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
else -> throw IllegalStateException("Unknown KeyEntry type.") else -> throw IllegalStateException("Unknown KeyEntry type.")
} }
val cipherMode = when { val cipherAlgorithm = when {
buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
else -> RSA_MODE else -> RSA_MODE
} }
val cipher = Cipher.getInstance(cipherMode) val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, key) cipher.init(Cipher.ENCRYPT_MODE, key)
return cipher return cipher
} }
@ -204,13 +204,17 @@ class SecretStoringUtils @Inject constructor(
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(128) .setKeySize(128)
.setUserAuthenticationRequired(keyNeedsUserAuthentication)
.apply { .apply {
setUserAuthenticationRequired(keyNeedsUserAuthentication) if (keyNeedsUserAuthentication) {
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) { buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.N) {
setInvalidatedByBiometricEnrollment(true) setInvalidatedByBiometricEnrollment(true)
}
buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.P) {
setUnlockedDeviceRequired(true)
}
} }
} }
.setUserAuthenticationRequired(keyNeedsUserAuthentication)
.build() .build()
generator.init(keyGenSpec) generator.init(keyGenSpec)
return generator.generateKey() return generator.generateKey()

View File

@ -21,4 +21,14 @@ interface BuildVersionSdkIntProvider {
* Return the current version of the Android SDK. * Return the current version of the Android SDK.
*/ */
fun get(): Int fun get(): Int
/**
* Checks the if the current OS version is equal or greater than [version].
* @return A `non-null` result if true, `null` otherwise.
*/
fun <T> whenAtLeast(version: Int, result: () -> T): T? {
return if (get() >= version) {
result()
} else null
}
} }

View File

@ -24,6 +24,7 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
import androidx.biometric.BiometricPrompt
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -31,6 +32,7 @@ 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.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.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity import im.vector.app.features.pin.lockscreen.tests.LockScreenTestActivity
import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment import im.vector.app.features.pin.lockscreen.ui.fallbackprompt.FallbackBiometricDialogFragment
@ -56,6 +58,9 @@ import org.amshove.kluent.shouldBeTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
import java.security.KeyStore
import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -64,6 +69,13 @@ class BiometricHelperTests {
private val biometricManager = mockk<BiometricManager>(relaxed = true) private val biometricManager = mockk<BiometricManager>(relaxed = true)
private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true) private val lockScreenKeyRepository = mockk<LockScreenKeyRepository>(relaxed = true)
private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
private val keyStore = KeyStore.getInstance(LockScreenCryptoConstants.ANDROID_KEY_STORE).also { it.load(null) }
private val secretStoringUtils = SecretStoringUtils(
InstrumentationRegistry.getInstrumentation().targetContext,
keyStore,
buildVersionSdkIntProvider,
false,
)
@Before @Before
fun setup() { fun setup() {
@ -190,6 +202,7 @@ class BiometricHelperTests {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest { fun authenticateInDeviceWithIssuesShowsFallbackPromptDialog() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
mockkStatic("kotlinx.coroutines.flow.FlowKt") mockkStatic("kotlinx.coroutines.flow.FlowKt")
val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) { val mockAuthChannel: Channel<Boolean> = mockk(relaxed = true) {
// Empty flow to keep the dialog open // Empty flow to keep the dialog open
@ -201,6 +214,9 @@ class BiometricHelperTests {
mockkObject(DevicePromptCheck) mockkObject(DevicePromptCheck)
every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true every { DevicePromptCheck.isDeviceWithNoBiometricUI } returns true
every { lockScreenKeyRepository.isSystemKeyValid() } returns true every { lockScreenKeyRepository.isSystemKeyValid() } returns true
val keyAlias = UUID.randomUUID().toString()
every { biometricUtils.getAuthCryptoObject() } returns BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(keyAlias))
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)
with(ActivityScenario.launch<LockScreenTestActivity>(intent)) { with(ActivityScenario.launch<LockScreenTestActivity>(intent)) {
@ -214,6 +230,7 @@ class BiometricHelperTests {
} }
} }
latch.await(1, TimeUnit.SECONDS) latch.await(1, TimeUnit.SECONDS)
keyStore.deleteEntry(keyAlias)
unmockkObject(DevicePromptCheck) unmockkObject(DevicePromptCheck)
unmockkStatic("kotlinx.coroutines.flow.FlowKt") unmockkStatic("kotlinx.coroutines.flow.FlowKt")
} }

View File

@ -43,7 +43,9 @@ class KeyStoreCryptoTests {
private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M } private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider)) private val secretStoringUtils = spyk(SecretStoringUtils(context, keyStore, versionProvider))
private val keyStoreCrypto = spyk( private val keyStoreCrypto = spyk(
KeyStoreCrypto(alias, false, context, versionProvider, keyStore, secretStoringUtils) KeyStoreCrypto(alias, false, context, versionProvider, keyStore).also {
it.secretStoringUtils = secretStoringUtils
}
) )
@After @After
@ -146,10 +148,10 @@ class KeyStoreCryptoTests {
@Test @Test
fun getCryptoObjectUsesCipherFromSecretStoringUtils() { fun getCryptoObjectUsesCipherFromSecretStoringUtils() {
keyStoreCrypto.getCryptoObject() keyStoreCrypto.getAuthCryptoObject()
verify { secretStoringUtils.getEncryptCipher(any()) } verify { secretStoringUtils.getEncryptCipher(any()) }
every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException() every { secretStoringUtils.getEncryptCipher(any()) } throws KeyPermanentlyInvalidatedException()
invoking { keyStoreCrypto.getCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class invoking { keyStoreCrypto.getAuthCryptoObject() } shouldThrow KeyPermanentlyInvalidatedException::class
} }
} }

View File

@ -16,21 +16,16 @@
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto
import android.security.keystore.KeyPermanentlyInvalidatedException
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 im.vector.app.features.settings.VectorPreferences
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.coInvoking
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldNotThrow
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -49,7 +44,7 @@ class LockScreenKeyRepositoryTests {
} }
private lateinit var lockScreenKeyRepository: LockScreenKeyRepository private lateinit var lockScreenKeyRepository: LockScreenKeyRepository
private val pinCodeMigrator: PinCodeMigrator = mockk(relaxed = true) private val legacyPinCodeMigrator: LegacyPinCodeMigrator = mockk(relaxed = true)
private val vectorPreferences: VectorPreferences = mockk(relaxed = true) private val vectorPreferences: VectorPreferences = mockk(relaxed = true)
private val keyStore: KeyStore by lazy { private val keyStore: KeyStore by lazy {
@ -58,7 +53,7 @@ class LockScreenKeyRepositoryTests {
@Before @Before
fun setup() { fun setup() {
lockScreenKeyRepository = spyk(LockScreenKeyRepository("base", pinCodeMigrator, vectorPreferences, keyStoreCryptoFactory)) lockScreenKeyRepository = spyk(LockScreenKeyRepository("base.pin_code", "base.system", keyStoreCryptoFactory))
} }
@After @After
@ -141,44 +136,4 @@ class LockScreenKeyRepositoryTests {
lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse() lockScreenKeyRepository.hasPinCodeKey().shouldBeFalse()
} }
@Test
fun migrateKeysIfNeededReturnsEarlyIfNotNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false
lockScreenKeyRepository.migrateKeysIfNeeded()
coVerify(exactly = 0) { pinCodeMigrator.migrate(any()) }
}
@Test
fun migrateKeysIfNeededWillMigratePinCodeAndKeys() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
lockScreenKeyRepository.migrateKeysIfNeeded()
coVerify { pinCodeMigrator.migrate(any()) }
}
@Test
fun migrateKeysIfNeededWillCreateSystemKeyIfNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
every { vectorPreferences.useBiometricsToUnlock() } returns true
every { lockScreenKeyRepository.ensureSystemKey() } returns mockk()
lockScreenKeyRepository.migrateKeysIfNeeded()
verify { lockScreenKeyRepository.ensureSystemKey() }
}
@Test
fun migrateKeysIfNeededWillHandleKeyPermanentlyInvalidatedException() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns true
every { vectorPreferences.useBiometricsToUnlock() } returns true
every { lockScreenKeyRepository.ensureSystemKey() } throws KeyPermanentlyInvalidatedException()
coInvoking { lockScreenKeyRepository.migrateKeysIfNeeded() } shouldNotThrow KeyPermanentlyInvalidatedException::class
verify { lockScreenKeyRepository.ensureSystemKey() }
}
} }

View File

@ -16,7 +16,7 @@
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto.migrations
import android.os.Build import android.os.Build
import android.security.KeyPairGeneratorSpec import android.security.KeyPairGeneratorSpec
@ -57,7 +57,7 @@ import javax.crypto.spec.PSource
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
import kotlin.math.abs import kotlin.math.abs
class PinCodeMigratorTests { class LegacyPinCodeMigratorTests {
private val alias = UUID.randomUUID().toString() private val alias = UUID.randomUUID().toString()
@ -72,7 +72,9 @@ class PinCodeMigratorTests {
private val secretStoringUtils: SecretStoringUtils = spyk( private val secretStoringUtils: SecretStoringUtils = spyk(
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider) SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
) )
private val pinCodeMigrator = spyk(PinCodeMigrator(pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)) private val legacyPinCodeMigrator = spyk(
LegacyPinCodeMigrator(alias, pinCodeStore, keyStore, secretStoringUtils, buildVersionSdkIntProvider)
)
@After @After
fun tearDown() { fun tearDown() {
@ -87,21 +89,21 @@ class PinCodeMigratorTests {
@Test @Test
fun isMigrationNeededReturnsTrueIfLegacyKeyExists() { fun isMigrationNeededReturnsTrueIfLegacyKeyExists() {
pinCodeMigrator.isMigrationNeeded() shouldBe false legacyPinCodeMigrator.isMigrationNeeded() shouldBe false
generateLegacyKey() generateLegacyKey()
pinCodeMigrator.isMigrationNeeded() shouldBe true legacyPinCodeMigrator.isMigrationNeeded() shouldBe true
} }
@Test @Test
fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest { fun migrateWillReturnEarlyIfPinCodeDoesNotExist() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
coEvery { pinCodeStore.getPinCode() } returns null coEvery { pinCodeStore.getPinCode() } returns null
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } coVerify(exactly = 0) { legacyPinCodeMigrator.getDecryptedPinCode() }
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -109,13 +111,13 @@ class PinCodeMigratorTests {
@Test @Test
fun migrateWillReturnEarlyIfIsNotNeeded() = runTest { fun migrateWillReturnEarlyIfIsNotNeeded() = runTest {
every { pinCodeMigrator.isMigrationNeeded() } returns false every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
coEvery { pinCodeMigrator.getDecryptedPinCode() } returns "1234" coEvery { legacyPinCodeMigrator.getDecryptedPinCode() } returns "1234"
every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0) every { secretStoringUtils.securelyStoreBytes(any(), any()) } returns ByteArray(0)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify(exactly = 0) { pinCodeMigrator.getDecryptedPinCode() } coVerify(exactly = 0) { legacyPinCodeMigrator.getDecryptedPinCode() }
verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) } verify(exactly = 0) { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) } coVerify(exactly = 0) { pinCodeStore.savePinCode(any()) }
verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify(exactly = 0) { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -126,9 +128,9 @@ class PinCodeMigratorTests {
val pinCode = "1234" val pinCode = "1234"
saveLegacyPinCode(pinCode) saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() } coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) } verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) } coVerify { pinCodeStore.savePinCode(any()) }
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }
@ -145,9 +147,9 @@ class PinCodeMigratorTests {
every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP every { buildVersionSdkIntProvider.get() } returns Build.VERSION_CODES.LOLLIPOP
saveLegacyPinCode(pinCode) saveLegacyPinCode(pinCode)
pinCodeMigrator.migrate(alias) legacyPinCodeMigrator.migrate()
coVerify { pinCodeMigrator.getDecryptedPinCode() } coVerify { legacyPinCodeMigrator.getDecryptedPinCode() }
verify { secretStoringUtils.securelyStoreBytes(any(), any()) } verify { secretStoringUtils.securelyStoreBytes(any(), any()) }
coVerify { pinCodeStore.savePinCode(any()) } coVerify { pinCodeStore.savePinCode(any()) }
verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) } verify { keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) }

View File

@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch 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.inject.Inject import javax.inject.Inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -74,22 +75,19 @@ class BiometricHelper @Inject constructor(
* 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.
*/ */
val canUseWeakBiometricAuth: Boolean val canUseWeakBiometricAuth: Boolean
get() = get() = configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
/** /**
* Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used. * Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
*/ */
val canUseStrongBiometricAuth: Boolean val canUseStrongBiometricAuth: Boolean
get() = get() = configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
/** /**
* Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.). * Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.).
*/ */
val canUseDeviceCredentialsAuth: Boolean val canUseDeviceCredentialsAuth: Boolean
get() = get() = configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
/** /**
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used. * Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
@ -120,7 +118,7 @@ class BiometricHelper @Inject constructor(
*/ */
@MainThread @MainThread
fun enableAuthentication(activity: FragmentActivity): Flow<Boolean> { fun enableAuthentication(activity: FragmentActivity): Flow<Boolean> {
return authenticateInternal(activity, checkSystemKeyExists = false, cryptoObject = null) return authenticateInternal(activity, checkSystemKeyExists = false, cryptoObject = getAuthCryptoObject())
} }
/** /**
@ -140,7 +138,7 @@ class BiometricHelper @Inject constructor(
*/ */
@MainThread @MainThread
fun authenticate(activity: FragmentActivity): Flow<Boolean> { fun authenticate(activity: FragmentActivity): Flow<Boolean> {
return authenticateInternal(activity, checkSystemKeyExists = true, cryptoObject = null) return authenticateInternal(activity, checkSystemKeyExists = true, cryptoObject = getAuthCryptoObject())
} }
/** /**
@ -157,9 +155,9 @@ class BiometricHelper @Inject constructor(
@SuppressLint("NewApi") @SuppressLint("NewApi")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun authenticateInternal( private fun authenticateInternal(
activity: FragmentActivity, activity: FragmentActivity,
checkSystemKeyExists: Boolean, checkSystemKeyExists: Boolean,
cryptoObject: BiometricPrompt.CryptoObject? = null, cryptoObject: BiometricPrompt.CryptoObject,
): Flow<Boolean> { ): Flow<Boolean> {
if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false) if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false)
@ -193,9 +191,9 @@ class BiometricHelper @Inject constructor(
@VisibleForTesting(otherwise = PRIVATE) @VisibleForTesting(otherwise = PRIVATE)
internal fun authenticateWithPromptInternal( internal fun authenticateWithPromptInternal(
activity: FragmentActivity, activity: FragmentActivity,
cryptoObject: BiometricPrompt.CryptoObject? = null, cryptoObject: BiometricPrompt.CryptoObject,
channel: Channel<Boolean>, channel: Channel<Boolean>,
): BiometricPrompt { ): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(context) val executor = ContextCompat.getMainExecutor(context)
val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher()) val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher())
@ -214,15 +212,12 @@ class BiometricHelper @Inject constructor(
} }
.setAllowedAuthenticators(authenticators) .setAllowedAuthenticators(authenticators)
.build() .build()
return BiometricPrompt(activity, executor, callback).also { return BiometricPrompt(activity, executor, callback).also {
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)
if (cryptoObject != null) { it.authenticate(promptInfo, cryptoObject)
it.authenticate(promptInfo, cryptoObject)
} else {
it.authenticate(promptInfo)
}
} }
} }
} }
@ -270,13 +265,28 @@ class BiometricHelper @Inject constructor(
} }
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
scope.launch { val cipher = result.cryptoObject?.cipher
channel.send(true) if (isCipherValid(cipher)) {
// Success is a terminal event, should close both the Channel and the CoroutineScope to free resources. scope.launch {
channel.close() channel.send(true)
scope.cancel() // Success is a terminal event, should close both the Channel and the CoroutineScope to free resources.
channel.close()
scope.cancel()
}
} else {
scope.launch {
channel.close(IllegalStateException("System key was not valid after authentication."))
scope.cancel()
}
} }
} }
private fun isCipherValid(cipher: Cipher?): Boolean {
if (cipher == null) return false
return runCatching {
cipher.doFinal("biometric_challenge".toByteArray())
}.isSuccess
}
} }
/** /**
@ -321,6 +331,9 @@ class BiometricHelper @Inject constructor(
@VisibleForTesting(otherwise = PRIVATE) @VisibleForTesting(otherwise = PRIVATE)
internal fun createAuthChannel(): Channel<Boolean> = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) internal fun createAuthChannel(): Channel<Boolean> = Channel(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
@VisibleForTesting(otherwise = PRIVATE)
internal fun getAuthCryptoObject(): BiometricPrompt.CryptoObject = lockScreenKeyRepository.getSystemKeyAuthCryptoObject()
companion object { companion object {
private const val FALLBACK_BIOMETRIC_FRAGMENT_TAG = "fragment.biometric_fallback" private const val FALLBACK_BIOMETRIC_FRAGMENT_TAG = "fragment.biometric_fallback"
} }

View File

@ -18,11 +18,11 @@ package im.vector.app.features.pin.lockscreen.crypto
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.hardware.biometrics.BiometricPrompt
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.util.Base64 import android.util.Base64
import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting
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
@ -40,8 +40,6 @@ class KeyStoreCrypto @AssistedInject constructor(
context: Context, context: Context,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
private val keyStore: KeyStore, private val keyStore: KeyStore,
// It's easier to test it this way
private val secretStoringUtils: SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider, keyNeedsUserAuthentication)
) { ) {
@AssistedFactory @AssistedFactory
@ -49,6 +47,9 @@ class KeyStoreCrypto @AssistedInject constructor(
fun provide(alias: String, keyNeedsUserAuthentication: Boolean): KeyStoreCrypto fun provide(alias: String, keyNeedsUserAuthentication: Boolean): KeyStoreCrypto
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var secretStoringUtils: SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider, keyNeedsUserAuthentication)
/** /**
* Ensures a [Key] for the [alias] exists and validates it. * Ensures a [Key] for the [alias] exists and validates it.
* @throws KeyPermanentlyInvalidatedException if key is not valid. * @throws KeyPermanentlyInvalidatedException if key is not valid.
@ -137,6 +138,5 @@ class KeyStoreCrypto @AssistedInject constructor(
* @throws KeyPermanentlyInvalidatedException if key is invalidated. * @throws KeyPermanentlyInvalidatedException if key is invalidated.
*/ */
@Throws(KeyPermanentlyInvalidatedException::class) @Throws(KeyPermanentlyInvalidatedException::class)
@RequiresApi(Build.VERSION_CODES.P) fun getAuthCryptoObject() = BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(alias))
fun getCryptoObject() = BiometricPrompt.CryptoObject(secretStoringUtils.getEncryptCipher(alias))
} }

View File

@ -19,22 +19,22 @@ package im.vector.app.features.pin.lockscreen.crypto
import android.os.Build import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import im.vector.app.features.settings.VectorPreferences import androidx.biometric.BiometricPrompt
import timber.log.Timber import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
import im.vector.app.features.pin.lockscreen.di.PinCodeKeyAlias
import javax.inject.Inject
import javax.inject.Singleton
/** /**
* Class in charge of managing both the PIN code key and the system authentication keys. * Class in charge of managing both the PIN code key and the system authentication keys.
*/ */
class LockScreenKeyRepository( @Singleton
baseName: String, class LockScreenKeyRepository @Inject constructor(
private val pinCodeMigrator: PinCodeMigrator, @PinCodeKeyAlias private val pinCodeKeyAlias: String,
private val vectorPreferences: VectorPreferences, @BiometricKeyAlias private val systemKeyAlias: String,
private val keyStoreCryptoFactory: KeyStoreCrypto.Factory, private val keyStoreCryptoFactory: KeyStoreCrypto.Factory,
) { ) {
private val pinCodeKeyAlias = "$baseName.pin_code"
private val systemKeyAlias = "$baseName.system"
private val pinCodeCrypto: KeyStoreCrypto by lazy { private val pinCodeCrypto: KeyStoreCrypto by lazy {
keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false) keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false)
} }
@ -86,19 +86,7 @@ class LockScreenKeyRepository(
fun isSystemKeyValid() = systemKeyCrypto.hasValidKey() fun isSystemKeyValid() = systemKeyCrypto.hasValidKey()
/** /**
* Migrates the PIN code key and encrypted value to use a more secure cipher, also creates a new system key if needed. * Returns a [BiometricPrompt.CryptoObject] for the system key.
*/ */
suspend fun migrateKeysIfNeeded() { fun getSystemKeyAuthCryptoObject() = systemKeyCrypto.getAuthCryptoObject()
if (pinCodeMigrator.isMigrationNeeded()) {
pinCodeMigrator.migrate(pinCodeKeyAlias)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
try {
ensureSystemKey()
} catch (e: KeyPermanentlyInvalidatedException) {
Timber.e("Could not automatically create biometric key.", e)
}
}
}
}
} }

View File

@ -0,0 +1,50 @@
/*
* 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.crypto
import android.annotation.SuppressLint
import android.os.Build
import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.pin.lockscreen.crypto.migrations.MissingSystemKeyMigrator
import im.vector.app.features.pin.lockscreen.crypto.migrations.SystemKeyV1Migrator
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import javax.inject.Inject
/**
* Performs all migrations needed for the lock screen keys.
*/
class LockScreenKeysMigrator @Inject constructor(
private val legacyPinCodeMigrator: LegacyPinCodeMigrator,
private val missingSystemKeyMigrator: MissingSystemKeyMigrator,
private val systemKeyV1Migrator: SystemKeyV1Migrator,
private val versionProvider: BuildVersionSdkIntProvider,
) {
/**
* Performs any needed migrations in order.
*/
@SuppressLint("NewApi")
suspend fun migrateIfNeeded() {
if (legacyPinCodeMigrator.isMigrationNeeded()) {
legacyPinCodeMigrator.migrate()
missingSystemKeyMigrator.migrate()
}
if (systemKeyV1Migrator.isMigrationNeeded() && versionProvider.get() >= Build.VERSION_CODES.M) {
systemKeyV1Migrator.migrate()
}
}
}

View File

@ -14,13 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.pin.lockscreen.crypto package im.vector.app.features.pin.lockscreen.crypto.migrations
import android.os.Build import android.os.Build
import android.util.Base64 import android.util.Base64
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS import im.vector.app.features.pin.lockscreen.crypto.LockScreenCryptoConstants.LEGACY_PIN_CODE_KEY_ALIAS
import im.vector.app.features.pin.lockscreen.di.PinCodeKeyAlias
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
@ -31,7 +32,8 @@ import javax.inject.Inject
/** /**
* Used to migrate from the old PIN code key ciphers to a more secure ones. * Used to migrate from the old PIN code key ciphers to a more secure ones.
*/ */
class PinCodeMigrator @Inject constructor( class LegacyPinCodeMigrator @Inject constructor(
@PinCodeKeyAlias private val pinCodeKeyAlias: String,
private val pinCodeStore: PinCodeStore, private val pinCodeStore: PinCodeStore,
private val keyStore: KeyStore, private val keyStore: KeyStore,
private val secretStoringUtils: SecretStoringUtils, private val secretStoringUtils: SecretStoringUtils,
@ -41,13 +43,13 @@ class PinCodeMigrator @Inject constructor(
private val legacyKey: Key get() = keyStore.getKey(LEGACY_PIN_CODE_KEY_ALIAS, null) private val legacyKey: Key get() = keyStore.getKey(LEGACY_PIN_CODE_KEY_ALIAS, null)
/** /**
* Migrates from the old ciphers and [LEGACY_PIN_CODE_KEY_ALIAS] to the [newAlias]. * Migrates from the old ciphers and renames [LEGACY_PIN_CODE_KEY_ALIAS] to [pinCodeKeyAlias].
*/ */
suspend fun migrate(newAlias: String) { suspend fun migrate() {
if (!keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return if (!keyStore.containsAlias(LEGACY_PIN_CODE_KEY_ALIAS)) return
val pinCode = getDecryptedPinCode() ?: return val pinCode = getDecryptedPinCode() ?: return
val encryptedBytes = secretStoringUtils.securelyStoreBytes(pinCode.toByteArray(), newAlias) val encryptedBytes = secretStoringUtils.securelyStoreBytes(pinCode.toByteArray(), pinCodeKeyAlias)
val encryptedPinCode = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP) val encryptedPinCode = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP)
pinCodeStore.savePinCode(encryptedPinCode) pinCodeStore.savePinCode(encryptedPinCode)
keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS) keyStore.deleteEntry(LEGACY_PIN_CODE_KEY_ALIAS)

View File

@ -0,0 +1,52 @@
/*
* 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.crypto.migrations
import android.annotation.SuppressLint
import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
import timber.log.Timber
import javax.inject.Inject
/**
* Creates a new system/biometric key when migrating from the old PFLockScreen implementation.
*/
class MissingSystemKeyMigrator @Inject constructor(
@BiometricKeyAlias private val systemKeyAlias: String,
private val keystoreCryptoFactory: KeyStoreCrypto.Factory,
private val vectorPreferences: VectorPreferences,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) {
/**
* If user had biometric auth enabled, ensure system key exists, creating one if needed.
*/
@SuppressLint("NewApi")
fun migrate() {
if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M && vectorPreferences.useBiometricsToUnlock()) {
try {
keystoreCryptoFactory.provide(systemKeyAlias, true).ensureKey()
} catch (e: KeyPermanentlyInvalidatedException) {
Timber.e("Could not automatically create biometric key.", e)
}
}
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.crypto.migrations
import android.os.Build
import androidx.annotation.RequiresApi
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
import im.vector.app.features.pin.lockscreen.di.BiometricKeyAlias
import java.security.KeyStore
import javax.inject.Inject
/**
* Migrates from the v1 of the biometric/system key to the new format, adding extra security measures to the new key.
*/
class SystemKeyV1Migrator @Inject constructor(
@BiometricKeyAlias private val systemKeyAlias: String,
private val keyStore: KeyStore,
private val keystoreCryptoFactory: KeyStoreCrypto.Factory,
) {
/**
* Removes the old v1 system key and creates a new system key.
*/
@RequiresApi(Build.VERSION_CODES.M)
fun migrate() {
keyStore.deleteEntry(SYSTEM_KEY_ALIAS_V1)
val systemKeyStoreCrypto = keystoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true)
systemKeyStoreCrypto.ensureKey()
}
/**
* Checks if an entry with [SYSTEM_KEY_ALIAS_V1] exists in the [keyStore].
*/
fun isMigrationNeeded() = keyStore.containsAlias(SYSTEM_KEY_ALIAS_V1)
companion object {
internal const val SYSTEM_KEY_ALIAS_V1 = "vector.system"
}
}

View File

@ -30,35 +30,32 @@ import im.vector.app.core.di.MavericksViewModelKey
import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.PinCodeStore
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.LockScreenMode import im.vector.app.features.pin.lockscreen.configuration.LockScreenMode
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto import im.vector.app.features.pin.lockscreen.crypto.migrations.LegacyPinCodeMigrator
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository
import im.vector.app.features.pin.lockscreen.crypto.PinCodeMigrator
import im.vector.app.features.pin.lockscreen.pincode.EncryptedPinCodeStorage import im.vector.app.features.pin.lockscreen.pincode.EncryptedPinCodeStorage
import im.vector.app.features.pin.lockscreen.ui.LockScreenViewModel import im.vector.app.features.pin.lockscreen.ui.LockScreenViewModel
import im.vector.app.features.settings.VectorPreferences
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 org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
import java.security.KeyStore import java.security.KeyStore
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object LockScreenModule { object LockScreenModule {
@Provides
@PinCodeKeyAlias
fun providePinCodeKeyAlias(): String = "vector.pin_code"
@Provides
@BiometricKeyAlias
fun provideSystemKeyAlias(): String = "vector.system_v2"
@Provides @Provides
fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
@Provides @Provides
fun provideBuildVersionSdkIntProvider(): BuildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider() fun provideBuildVersionSdkIntProvider(): BuildVersionSdkIntProvider = DefaultBuildVersionSdkIntProvider()
@Provides
fun provideSecretStoringUtils(
@ApplicationContext context: Context,
keyStore: KeyStore,
buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
@Provides @Provides
fun provideLockScreenConfig() = LockScreenConfiguration( fun provideLockScreenConfig() = LockScreenConfiguration(
mode = LockScreenMode.VERIFY, mode = LockScreenMode.VERIFY,
@ -70,20 +67,22 @@ object LockScreenModule {
) )
@Provides @Provides
@Singleton fun provideBiometricManager(@ApplicationContext context: Context) = BiometricManager.from(context)
fun provideKeyRepository(
pinCodeMigrator: PinCodeMigrator,
vectorPreferences: VectorPreferences,
keyStoreCryptoFactory: KeyStoreCrypto.Factory,
) = LockScreenKeyRepository(
baseName = "vector",
pinCodeMigrator,
vectorPreferences,
keyStoreCryptoFactory,
)
@Provides @Provides
fun provideBiometricManager(@ApplicationContext context: Context) = BiometricManager.from(context) fun provideLegacyPinCodeMigrator(
@PinCodeKeyAlias pinCodeKeyAlias: String,
context: Context,
pinCodeStore: PinCodeStore,
keyStore: KeyStore,
buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) = LegacyPinCodeMigrator(
pinCodeKeyAlias,
pinCodeStore,
keyStore,
SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider),
buildVersionSdkIntProvider,
)
} }
@Module @Module

View File

@ -0,0 +1,27 @@
/*
* 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.di
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PinCodeKeyAlias
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BiometricKeyAlias

View File

@ -55,11 +55,4 @@ class PinCodeHelper @Inject constructor(
encryptedStorage.deletePinCode() encryptedStorage.deletePinCode()
lockScreenKeyRepository.deletePinCodeKey() lockScreenKeyRepository.deletePinCodeKey()
} }
/**
* Migrates the PIN code key and encrypted value to use a more secure cipher.
*/
suspend fun migratePinCodeIfNeeded() {
lockScreenKeyRepository.migrateKeysIfNeeded()
}
} }

View File

@ -34,6 +34,7 @@ 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.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.pincode.PinCodeHelper import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
@ -47,6 +48,7 @@ class LockScreenViewModel @AssistedInject constructor(
@Assisted val initialState: LockScreenViewState, @Assisted val initialState: LockScreenViewState,
private val pinCodeHelper: PinCodeHelper, private val pinCodeHelper: PinCodeHelper,
private val biometricHelper: BiometricHelper, private val biometricHelper: BiometricHelper,
private val lockScreenKeysMigrator: LockScreenKeysMigrator,
private val configuratorProvider: LockScreenConfiguratorProvider, private val configuratorProvider: LockScreenConfiguratorProvider,
private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) { ) : VectorViewModel<LockScreenViewState, LockScreenAction, LockScreenViewEvent>(initialState) {
@ -85,7 +87,7 @@ class LockScreenViewModel @AssistedInject constructor(
init { init {
// We need this to run synchronously before we start reading the configurations // We need this to run synchronously before we start reading the configurations
runBlocking { pinCodeHelper.migratePinCodeIfNeeded() } runBlocking { lockScreenKeysMigrator.migrateIfNeeded() }
configuratorProvider.configurationFlow configuratorProvider.configurationFlow
.onEach { updateConfiguration(it) } .onEach { updateConfiguration(it) }

View File

@ -0,0 +1,81 @@
/*
* 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.crypto.migrations
import android.os.Build
import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeysMigrator
import im.vector.app.test.TestBuildVersionSdkIntProvider
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.Test
class LockScreenTestMigratorTests {
private val legacyPinCodeMigrator = mockk<LegacyPinCodeMigrator>(relaxed = true)
private val missingSystemKeyMigrator = mockk<MissingSystemKeyMigrator>(relaxed = true)
private val systemKeyV1Migrator = mockk<SystemKeyV1Migrator>(relaxed = true)
private val versionProvider = TestBuildVersionSdkIntProvider()
private val migrator = LockScreenKeysMigrator(legacyPinCodeMigrator, missingSystemKeyMigrator, systemKeyV1Migrator, versionProvider)
@Test
fun `When legacy pin code migration is needed, both legacyPinCodeMigrator and missingSystemKeyMigrator will be run`() {
// When no migration is needed
every { legacyPinCodeMigrator.isMigrationNeeded() } returns false
runBlocking { migrator.migrateIfNeeded() }
coVerify(exactly = 0) { legacyPinCodeMigrator.migrate() }
verify(exactly = 0) { missingSystemKeyMigrator.migrate() }
// When migration is needed
every { legacyPinCodeMigrator.isMigrationNeeded() } returns true
runBlocking { migrator.migrateIfNeeded() }
coVerify { legacyPinCodeMigrator.migrate() }
verify { missingSystemKeyMigrator.migrate() }
}
@Test
fun `System key from v1 migration will not be run for versions that don't support biometrics`() {
versionProvider.value = Build.VERSION_CODES.LOLLIPOP
every { systemKeyV1Migrator.isMigrationNeeded() } returns true
runBlocking { migrator.migrateIfNeeded() }
verify(exactly = 0) { systemKeyV1Migrator.migrate() }
}
@Test
fun `When system key from v1 migration is needed it will be run`() {
versionProvider.value = Build.VERSION_CODES.M
every { systemKeyV1Migrator.isMigrationNeeded() } returns false
runBlocking { migrator.migrateIfNeeded() }
verify(exactly = 0) { systemKeyV1Migrator.migrate() }
every { systemKeyV1Migrator.isMigrationNeeded() } returns true
runBlocking { migrator.migrateIfNeeded() }
verify { systemKeyV1Migrator.migrate() }
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.crypto.migrations
import android.os.Build
import android.security.keystore.KeyPermanentlyInvalidatedException
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.test.TestBuildVersionSdkIntProvider
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.amshove.kluent.invoking
import org.amshove.kluent.shouldNotThrow
import org.junit.Test
class MissingSystemKeyMigratorTests {
private val keyStoreCryptoFactory = mockk<KeyStoreCrypto.Factory>()
private val vectorPreferences = mockk<VectorPreferences>(relaxed = true)
private val versionProvider = TestBuildVersionSdkIntProvider().also { it.value = Build.VERSION_CODES.M }
private val missingSystemKeyMigrator = MissingSystemKeyMigrator("vector.system", keyStoreCryptoFactory, vectorPreferences, versionProvider)
@Test
fun migrateEnsuresSystemKeyExistsIfBiometricAuthIsEnabledAndSupported() {
val keyStoreCryptoMock = mockk<KeyStoreCrypto> {
every { ensureKey() } returns mockk()
}
every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
every { vectorPreferences.useBiometricsToUnlock() } returns true
missingSystemKeyMigrator.migrate()
verify { keyStoreCryptoMock.ensureKey() }
}
@Test
fun migrateHandlesKeyPermanentlyInvalidatedExceptions() {
val keyStoreCryptoMock = mockk<KeyStoreCrypto> {
every { ensureKey() } throws KeyPermanentlyInvalidatedException()
}
every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
every { vectorPreferences.useBiometricsToUnlock() } returns true
invoking { missingSystemKeyMigrator.migrate() } shouldNotThrow KeyPermanentlyInvalidatedException::class
}
@Test
fun migrateReturnsEarlyIfBiometricAuthIsDisabled() {
val keyStoreCryptoMock = mockk<KeyStoreCrypto> {
every { ensureKey() } returns mockk()
}
every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
every { vectorPreferences.useBiometricsToUnlock() } returns false
missingSystemKeyMigrator.migrate()
verify(exactly = 0) { keyStoreCryptoMock.ensureKey() }
}
@Test
fun migrateReturnsEarlyIfAndroidVersionCantHandleBiometrics() {
versionProvider.value = Build.VERSION_CODES.LOLLIPOP
val keyStoreCryptoMock = mockk<KeyStoreCrypto> {
every { ensureKey() } returns mockk()
}
every { keyStoreCryptoFactory.provide(any(), any()) } returns keyStoreCryptoMock
every { vectorPreferences.useBiometricsToUnlock() } returns false
missingSystemKeyMigrator.migrate()
verify(exactly = 0) { keyStoreCryptoMock.ensureKey() }
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.crypto.migrations
import im.vector.app.features.pin.lockscreen.crypto.KeyStoreCrypto
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.amshove.kluent.shouldBe
import org.junit.Test
import java.security.KeyStore
class SystemKeyV1MigratorTests {
private val keyStoreCryptoFactory = mockk<KeyStoreCrypto.Factory>()
private val keyStore = mockk<KeyStore>(relaxed = true)
private val systemKeyV1Migrator = SystemKeyV1Migrator("vector.system_new", keyStore, keyStoreCryptoFactory)
@Test
fun isMigrationNeededReturnsTrueIfV1KeyExists() {
every { keyStore.containsAlias(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) } returns true
systemKeyV1Migrator.isMigrationNeeded() shouldBe true
every { keyStore.containsAlias(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) } returns false
systemKeyV1Migrator.isMigrationNeeded() shouldBe false
}
@Test
fun migrateDeletesOldEntryAndEnsuresNewKey() {
val keyStoreCryptoMock = mockk<KeyStoreCrypto> {
every { ensureKey() } returns mockk()
}
every { keyStoreCryptoFactory.provide("vector.system_new", any()) } returns keyStoreCryptoMock
systemKeyV1Migrator.migrate()
verify { keyStore.deleteEntry(SystemKeyV1Migrator.SYSTEM_KEY_ALIAS_V1) }
verify { keyStoreCryptoMock.ensureKey() }
}
}

View File

@ -25,6 +25,7 @@ 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.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.pincode.PinCodeHelper import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper
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.LockScreenAction import im.vector.app.features.pin.lockscreen.ui.LockScreenAction
@ -56,7 +57,8 @@ 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 buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider() private val keysMigrator = mockk<LockScreenKeysMigrator>(relaxed = true)
private val versionProvider = TestBuildVersionSdkIntProvider()
@Before @Before
fun setup() { fun setup() {
@ -67,9 +69,9 @@ class LockScreenViewModelTests {
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()) val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
coVerify { pinCodeHelper.migratePinCodeIfNeeded() } coVerify { keysMigrator.migrateIfNeeded() }
} }
@Test @Test
@ -78,7 +80,7 @@ class LockScreenViewModelTests {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) 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, configProvider, buildVersionSdkIntProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val newState = withState(viewModel) { it } val newState = withState(viewModel) { it }
initialState shouldNotBeEqualTo newState initialState shouldNotBeEqualTo newState
} }
@ -87,7 +89,7 @@ class LockScreenViewModelTests {
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 configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) 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
@ -112,7 +114,7 @@ class LockScreenViewModelTests {
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 configProvider = LockScreenConfiguratorProvider(configuration)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -128,7 +130,7 @@ class LockScreenViewModelTests {
val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true) val configuration = createDefaultConfiguration(mode = LockScreenMode.CREATE, needsNewCodeValidation = true)
val configProvider = LockScreenConfiguratorProvider(configuration) val configProvider = LockScreenConfiguratorProvider(configuration)
val initialState = createViewState(lockScreenConfiguration = configuration) val initialState = createViewState(lockScreenConfiguration = configuration)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -148,7 +150,7 @@ class LockScreenViewModelTests {
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 configProvider = LockScreenConfiguratorProvider(configuration)
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -169,7 +171,7 @@ class LockScreenViewModelTests {
fun `onPinCodeEntered handles exceptions`() = runTest { fun `onPinCodeEntered handles exceptions`() = runTest {
val initialState = createViewState() val initialState = createViewState()
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) 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
@ -183,7 +185,7 @@ class LockScreenViewModelTests {
@Test @Test
fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest { fun `when showBiometricPrompt catches a KeyPermanentlyInvalidatedException it disables biometric authentication`() = runTest {
buildVersionSdkIntProvider.value = Build.VERSION_CODES.M versionProvider.value = Build.VERSION_CODES.M
every { biometricHelper.isSystemAuthEnabledAndValid } returns true every { biometricHelper.isSystemAuthEnabledAndValid } returns true
every { biometricHelper.isSystemKeyValid } returns true every { biometricHelper.isSystemKeyValid } returns true
@ -199,7 +201,7 @@ class LockScreenViewModelTests {
isBiometricKeyInvalidated = false, isBiometricKeyInvalidated = false,
lockScreenConfiguration = configuration lockScreenConfiguration = configuration
) )
val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) val viewModel = LockScreenViewModel(initialState, pinCodeHelper, biometricHelper, keysMigrator, configProvider, versionProvider)
val events = viewModel.test().viewEvents val events = viewModel.test().viewEvents
events.assertNoValues() events.assertNoValues()
@ -217,7 +219,7 @@ class LockScreenViewModelTests {
@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 configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) 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
@ -231,7 +233,7 @@ class LockScreenViewModelTests {
@Test @Test
fun `showBiometricPrompt handles exceptions`() = runTest { fun `showBiometricPrompt handles exceptions`() = runTest {
val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration()) val configProvider = LockScreenConfiguratorProvider(createDefaultConfiguration())
val viewModel = LockScreenViewModel(createViewState(), pinCodeHelper, biometricHelper, configProvider, buildVersionSdkIntProvider) 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