Merge pull request #5600 from element-hq/feature/bma/deletePinCode

Delete pin code only when the last session is deleted
This commit is contained in:
Benoit Marty 2025-10-24 09:47:57 +02:00 committed by GitHub
commit f1b8f878de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 181 additions and 67 deletions

View file

@ -33,8 +33,7 @@ class DefaultSeenInvitesStore(
) : SeenInvitesStore {
init {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionCreated(userId: String) = Unit
override suspend fun onSessionDeleted(userId: String) {
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
if (sessionId.value == userId) {
clear()
}

View file

@ -35,12 +35,6 @@ interface LockScreenService {
fun isPinSetup(): Flow<Boolean>
}
/**
* Check if the app is currently locked.
*/
val LockScreenService.isLocked: Boolean
get() = lockState.value == LockScreenLockState.Locked
/**
* Makes sure the secure flag is set on the activity if the pin is setup.
* @param activity the activity to set the flag on.

View file

@ -73,15 +73,14 @@ class DefaultLockScreenService(
}
/**
* Makes sure to delete the pin code when the session is deleted.
* Makes sure to delete the pin code when the last session is deleted.
*/
private fun observeSessionsState() {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionCreated(userId: String) = Unit
override suspend fun onSessionDeleted(userId: String) {
// TODO handle multi session at some point
pinCodeManager.deletePinCode()
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
if (wasLastSession) {
pinCodeManager.deletePinCode()
}
}
})
}

View file

@ -0,0 +1,95 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.lockscreen.impl
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.biometric.BiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.biometric.FakeBiometricAuthenticatorManager
import io.element.android.features.lockscreen.impl.fixtures.aLockScreenConfig
import io.element.android.features.lockscreen.impl.pin.PinCodeManager
import io.element.android.features.lockscreen.impl.pin.createDefaultPinCodeManager
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import io.element.android.libraries.sessionstorage.test.observer.FakeSessionObserver
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultLockScreenServiceTest {
@Test
fun `when the pin is not mandatory and no pin is configured isSetupRequired emits false`() = runTest {
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = false)
)
sut.isSetupRequired().test {
assertThat(awaitItem()).isFalse()
}
}
@Test
fun `when the pin is mandatory, isSetupRequired emits true`() = runTest {
val lockScreenStore = InMemoryLockScreenStore()
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
lockScreenStore = lockScreenStore,
)
sut.isSetupRequired().test {
assertThat(awaitItem()).isTrue()
// When the user configures the pin code, the setup is not required anymore
lockScreenStore.saveEncryptedPinCode("encryptedCode")
assertThat(awaitItem()).isFalse()
// Users deletes the pin code
lockScreenStore.deleteEncryptedPinCode()
assertThat(awaitItem()).isTrue()
}
}
@Test
fun `when the last session is deleted, the pin code is removed`() = runTest {
val sessionObserver = FakeSessionObserver()
val lockScreenStore = InMemoryLockScreenStore()
val sut = createDefaultLockScreenService(
lockScreenConfig = aLockScreenConfig(isPinMandatory = true),
lockScreenStore = lockScreenStore,
sessionObserver = sessionObserver,
)
sut.isPinSetup().test {
assertThat(awaitItem()).isFalse()
// When the user configure the pin code, the setup is not required anymore
lockScreenStore.saveEncryptedPinCode("encryptedCode")
assertThat(awaitItem()).isTrue()
sessionObserver.onSessionDeleted("userId", wasLastSession = false)
expectNoEvents()
sessionObserver.onSessionDeleted("userId", wasLastSession = true)
assertThat(awaitItem()).isFalse()
}
}
}
private fun TestScope.createDefaultLockScreenService(
lockScreenConfig: LockScreenConfig = aLockScreenConfig(),
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
pinCodeManager: PinCodeManager = createDefaultPinCodeManager(
lockScreenStore = lockScreenStore,
),
sessionObserver: SessionObserver = FakeSessionObserver(),
appForegroundStateService: AppForegroundStateService = FakeAppForegroundStateService(),
biometricAuthenticatorManager: BiometricAuthenticatorManager = FakeBiometricAuthenticatorManager(),
) = DefaultLockScreenService(
lockScreenConfig = lockScreenConfig,
lockScreenStore = lockScreenStore,
pinCodeManager = pinCodeManager,
coroutineScope = backgroundScope,
sessionObserver = sessionObserver,
appForegroundStateService = appForegroundStateService,
biometricAuthenticatorManager = biometricAuthenticatorManager,
)

View file

@ -10,19 +10,18 @@ package io.element.android.features.lockscreen.impl.pin
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.lockscreen.impl.pin.storage.InMemoryLockScreenStore
import io.element.android.features.lockscreen.impl.storage.LockScreenStore
import io.element.android.libraries.cryptography.api.EncryptionDecryptionService
import io.element.android.libraries.cryptography.api.SecretKeyRepository
import io.element.android.libraries.cryptography.impl.AESEncryptionDecryptionService
import io.element.android.libraries.cryptography.test.SimpleSecretKeyRepository
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultPinCodeManagerTest {
private val lockScreenStore = InMemoryLockScreenStore()
private val secretKeyRepository = SimpleSecretKeyRepository()
private val encryptionDecryptionService = AESEncryptionDecryptionService()
private val pinCodeManager = DefaultPinCodeManager(secretKeyRepository, encryptionDecryptionService, lockScreenStore)
@Test
fun `given a pin code when create and delete assert no pin code left`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.hasPinCode().test {
assertThat(awaitItem()).isFalse()
pinCodeManager.createPinCode("1234")
@ -34,6 +33,7 @@ class DefaultPinCodeManagerTest {
@Test
fun `given a pin code when create and verify with the same pin succeed`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
val pinCode = "1234"
pinCodeManager.createPinCode(pinCode)
assertThat(pinCodeManager.verifyPinCode(pinCode)).isTrue()
@ -41,7 +41,18 @@ class DefaultPinCodeManagerTest {
@Test
fun `given a pin code when create and verify with a different pin fails`() = runTest {
val pinCodeManager = createDefaultPinCodeManager()
pinCodeManager.createPinCode("1234")
assertThat(pinCodeManager.verifyPinCode("1235")).isFalse()
}
}
fun createDefaultPinCodeManager(
lockScreenStore: LockScreenStore = InMemoryLockScreenStore(),
secretKeyRepository: SecretKeyRepository = SimpleSecretKeyRepository(),
encryptionDecryptionService: EncryptionDecryptionService = AESEncryptionDecryptionService(),
) = DefaultPinCodeManager(
lockScreenStore = lockScreenStore,
secretKeyRepository = secretKeyRepository,
encryptionDecryptionService = encryptionDecryptionService,
)