mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 18:06:08 +03:00
Add ability to set a PIN to SettingsRepository (#631)
This commit is contained in:
parent
224395004f
commit
ca517c88c4
9 changed files with 331 additions and 31 deletions
|
@ -94,8 +94,15 @@ interface AuthDiskSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a pin-protected user key for the given [userId].
|
* Stores a pin-protected user key for the given [userId].
|
||||||
|
*
|
||||||
|
* When [inMemoryOnly] is `true`, the value will only be available via a call to
|
||||||
|
* [getPinProtectedUserKey] during the current app session.
|
||||||
*/
|
*/
|
||||||
fun storePinProtectedUserKey(userId: String, pinProtectedUserKey: String?)
|
fun storePinProtectedUserKey(
|
||||||
|
userId: String,
|
||||||
|
pinProtectedUserKey: String?,
|
||||||
|
inMemoryOnly: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an encrypted PIN for the given [userId].
|
* Retrieves an encrypted PIN for the given [userId].
|
||||||
|
|
|
@ -40,6 +40,7 @@ class AuthDiskSourceImpl(
|
||||||
sharedPreferences = sharedPreferences,
|
sharedPreferences = sharedPreferences,
|
||||||
),
|
),
|
||||||
AuthDiskSource {
|
AuthDiskSource {
|
||||||
|
private val inMemoryPinProtectedUserKeys = mutableMapOf<String, String?>()
|
||||||
private val mutableOrganizationsFlowMap =
|
private val mutableOrganizationsFlowMap =
|
||||||
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Profile.Organization>?>>()
|
||||||
|
|
||||||
|
@ -132,12 +133,16 @@ class AuthDiskSourceImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPinProtectedUserKey(userId: String): String? =
|
override fun getPinProtectedUserKey(userId: String): String? =
|
||||||
getString(key = "${PIN_PROTECTED_USER_KEY_KEY}_$userId")
|
inMemoryPinProtectedUserKeys[userId]
|
||||||
|
?: getString(key = "${PIN_PROTECTED_USER_KEY_KEY}_$userId")
|
||||||
|
|
||||||
override fun storePinProtectedUserKey(
|
override fun storePinProtectedUserKey(
|
||||||
userId: String,
|
userId: String,
|
||||||
pinProtectedUserKey: String?,
|
pinProtectedUserKey: String?,
|
||||||
|
inMemoryOnly: Boolean,
|
||||||
) {
|
) {
|
||||||
|
inMemoryPinProtectedUserKeys[userId] = pinProtectedUserKey
|
||||||
|
if (inMemoryOnly) return
|
||||||
putString(
|
putString(
|
||||||
key = "${PIN_PROTECTED_USER_KEY_KEY}_$userId",
|
key = "${PIN_PROTECTED_USER_KEY_KEY}_$userId",
|
||||||
value = pinProtectedUserKey,
|
value = pinProtectedUserKey,
|
||||||
|
|
|
@ -36,6 +36,11 @@ interface SettingsRepository {
|
||||||
*/
|
*/
|
||||||
var vaultTimeoutAction: VaultTimeoutAction
|
var vaultTimeoutAction: VaultTimeoutAction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not PIN unlocking is enabled for the current user.
|
||||||
|
*/
|
||||||
|
val isUnlockWithPinEnabled: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all the settings data for the given user.
|
* Clears all the settings data for the given user.
|
||||||
*/
|
*/
|
||||||
|
@ -85,4 +90,21 @@ interface SettingsRepository {
|
||||||
* Stores the given [isPullToRefreshEnabled] for the active user.
|
* Stores the given [isPullToRefreshEnabled] for the active user.
|
||||||
*/
|
*/
|
||||||
fun storePullToRefreshEnabled(isPullToRefreshEnabled: Boolean)
|
fun storePullToRefreshEnabled(isPullToRefreshEnabled: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the given PIN, allowing it to be used to unlock the current user's vault.
|
||||||
|
*
|
||||||
|
* When [shouldRequireMasterPasswordOnRestart] is `true`, the user's master password is required
|
||||||
|
* on app startup but they may use their PIN to unlock their vault if it becomes locked while
|
||||||
|
* the app is still open.
|
||||||
|
*/
|
||||||
|
fun storeUnlockPin(
|
||||||
|
pin: String,
|
||||||
|
shouldRequireMasterPasswordOnRestart: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears any previously set unlock PIN for the current user.
|
||||||
|
*/
|
||||||
|
fun clearUnlockPin()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -12,6 +13,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary implementation of [SettingsRepository].
|
* Primary implementation of [SettingsRepository].
|
||||||
|
@ -20,6 +22,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
class SettingsRepositoryImpl(
|
class SettingsRepositoryImpl(
|
||||||
private val authDiskSource: AuthDiskSource,
|
private val authDiskSource: AuthDiskSource,
|
||||||
private val settingsDiskSource: SettingsDiskSource,
|
private val settingsDiskSource: SettingsDiskSource,
|
||||||
|
private val vaultSdkSource: VaultSdkSource,
|
||||||
private val dispatcherManager: DispatcherManager,
|
private val dispatcherManager: DispatcherManager,
|
||||||
) : SettingsRepository {
|
) : SettingsRepository {
|
||||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||||
|
@ -77,6 +80,10 @@ class SettingsRepositoryImpl(
|
||||||
vaultTimeoutAction = value,
|
vaultTimeoutAction = value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
override val isUnlockWithPinEnabled: Boolean
|
||||||
|
get() = activeUserId
|
||||||
|
?.let { authDiskSource.getEncryptedPin(userId = it) != null }
|
||||||
|
?: false
|
||||||
|
|
||||||
override fun clearData(userId: String) {
|
override fun clearData(userId: String) {
|
||||||
settingsDiskSource.clearData(userId = userId)
|
settingsDiskSource.clearData(userId = userId)
|
||||||
|
@ -159,6 +166,54 @@ class SettingsRepositoryImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun storeUnlockPin(
|
||||||
|
pin: String,
|
||||||
|
shouldRequireMasterPasswordOnRestart: Boolean,
|
||||||
|
) {
|
||||||
|
val userId = activeUserId ?: return
|
||||||
|
unconfinedScope.launch {
|
||||||
|
vaultSdkSource
|
||||||
|
.derivePinKey(
|
||||||
|
userId = userId,
|
||||||
|
pin = pin,
|
||||||
|
)
|
||||||
|
.fold(
|
||||||
|
onSuccess = { derivePinKeyResponse ->
|
||||||
|
authDiskSource.apply {
|
||||||
|
storeEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = derivePinKeyResponse.encryptedPin,
|
||||||
|
)
|
||||||
|
storePinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = derivePinKeyResponse.pinProtectedUserKey,
|
||||||
|
inMemoryOnly = shouldRequireMasterPasswordOnRestart,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
// PIN derivation should only fail when the user's vault is locked. This
|
||||||
|
// should not be a concern when this method is actually called so we should
|
||||||
|
// be able to safely ignore this.
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearUnlockPin() {
|
||||||
|
val userId = activeUserId ?: return
|
||||||
|
authDiskSource.apply {
|
||||||
|
storeEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = null,
|
||||||
|
)
|
||||||
|
authDiskSource.storePinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -39,11 +40,13 @@ object PlatformRepositoryModule {
|
||||||
fun provideSettingsRepository(
|
fun provideSettingsRepository(
|
||||||
authDiskSource: AuthDiskSource,
|
authDiskSource: AuthDiskSource,
|
||||||
settingsDiskSource: SettingsDiskSource,
|
settingsDiskSource: SettingsDiskSource,
|
||||||
|
vaultSdkSource: VaultSdkSource,
|
||||||
dispatcherManager: DispatcherManager,
|
dispatcherManager: DispatcherManager,
|
||||||
): SettingsRepository =
|
): SettingsRepository =
|
||||||
SettingsRepositoryImpl(
|
SettingsRepositoryImpl(
|
||||||
authDiskSource = authDiskSource,
|
authDiskSource = authDiskSource,
|
||||||
settingsDiskSource = settingsDiskSource,
|
settingsDiskSource = settingsDiskSource,
|
||||||
|
vaultSdkSource = vaultSdkSource,
|
||||||
dispatcherManager = dispatcherManager,
|
dispatcherManager = dispatcherManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
||||||
isApproveLoginRequestsEnabled = false,
|
isApproveLoginRequestsEnabled = false,
|
||||||
isUnlockWithBiometricsEnabled = false,
|
isUnlockWithBiometricsEnabled = false,
|
||||||
isUnlockWithPinEnabled = false,
|
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||||
vaultTimeout = settingsRepository.vaultTimeout,
|
vaultTimeout = settingsRepository.vaultTimeout,
|
||||||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||||
),
|
),
|
||||||
|
@ -194,9 +194,17 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
// TODO: Complete implementation (BIT-465)
|
// TODO: Complete implementation (BIT-465)
|
||||||
when (action) {
|
when (action) {
|
||||||
AccountSecurityAction.UnlockWithPinToggle.PendingEnabled -> Unit
|
AccountSecurityAction.UnlockWithPinToggle.PendingEnabled -> Unit
|
||||||
AccountSecurityAction.UnlockWithPinToggle.Disabled,
|
AccountSecurityAction.UnlockWithPinToggle.Disabled -> {
|
||||||
is AccountSecurityAction.UnlockWithPinToggle.Enabled,
|
settingsRepository.clearUnlockPin()
|
||||||
-> {
|
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
|
||||||
|
}
|
||||||
|
|
||||||
|
is AccountSecurityAction.UnlockWithPinToggle.Enabled -> {
|
||||||
|
settingsRepository.storeUnlockPin(
|
||||||
|
pin = action.pin,
|
||||||
|
shouldRequireMasterPasswordOnRestart =
|
||||||
|
action.shouldRequireMasterPasswordOnRestart,
|
||||||
|
)
|
||||||
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
|
sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with pin.".asText()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||||
private val storedUserKeys = mutableMapOf<String, String?>()
|
private val storedUserKeys = mutableMapOf<String, String?>()
|
||||||
private val storedPrivateKeys = mutableMapOf<String, String?>()
|
private val storedPrivateKeys = mutableMapOf<String, String?>()
|
||||||
private val storedUserAutoUnlockKeys = mutableMapOf<String, String?>()
|
private val storedUserAutoUnlockKeys = mutableMapOf<String, String?>()
|
||||||
private val storedPinProtectedUserKeys = mutableMapOf<String, String?>()
|
private val storedPinProtectedUserKeys = mutableMapOf<String, Pair<String?, Boolean>>()
|
||||||
private val storedEncryptedPins = mutableMapOf<String, String?>()
|
private val storedEncryptedPins = mutableMapOf<String, String?>()
|
||||||
private val storedOrganizations =
|
private val storedOrganizations =
|
||||||
mutableMapOf<String, List<SyncResponseJson.Profile.Organization>?>()
|
mutableMapOf<String, List<SyncResponseJson.Profile.Organization>?>()
|
||||||
|
@ -81,10 +81,14 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPinProtectedUserKey(userId: String): String? =
|
override fun getPinProtectedUserKey(userId: String): String? =
|
||||||
storedPinProtectedUserKeys[userId]
|
storedPinProtectedUserKeys[userId]?.first
|
||||||
|
|
||||||
override fun storePinProtectedUserKey(userId: String, pinProtectedUserKey: String?) {
|
override fun storePinProtectedUserKey(
|
||||||
storedPinProtectedUserKeys[userId] = pinProtectedUserKey
|
userId: String,
|
||||||
|
pinProtectedUserKey: String?,
|
||||||
|
isInMemoryOnly: Boolean,
|
||||||
|
) {
|
||||||
|
storedPinProtectedUserKeys[userId] = pinProtectedUserKey to isInMemoryOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEncryptedPin(userId: String): String? =
|
override fun getEncryptedPin(userId: String): String? =
|
||||||
|
@ -157,6 +161,24 @@ class FakeAuthDiskSource : AuthDiskSource {
|
||||||
assertEquals(userAutoUnlockKey, storedUserAutoUnlockKeys[userId])
|
assertEquals(userAutoUnlockKey, storedUserAutoUnlockKeys[userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the [encryptedPin] was stored successfully using the [userId].
|
||||||
|
*/
|
||||||
|
fun assertEncryptedPin(userId: String, encryptedPin: String?) {
|
||||||
|
assertEquals(encryptedPin, storedEncryptedPins[userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the [pinProtectedUserKey] was stored successfully using the [userId].
|
||||||
|
*/
|
||||||
|
fun assertPinProtectedUserKey(
|
||||||
|
userId: String,
|
||||||
|
pinProtectedUserKey: String?,
|
||||||
|
inMemoryOnly: Boolean = false,
|
||||||
|
) {
|
||||||
|
assertEquals(pinProtectedUserKey to inMemoryOnly, storedPinProtectedUserKeys[userId])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert the the [organizationKeys] was stored successfully using the [userId].
|
* Assert the the [organizationKeys] was stored successfully using the [userId].
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package com.x8bit.bitwarden.data.platform.repository
|
package com.x8bit.bitwarden.data.platform.repository
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
import com.bitwarden.core.DerivePinKeyResponse
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||||
|
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
@ -19,12 +22,14 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class SettingsRepositoryTest {
|
class SettingsRepositoryTest {
|
||||||
private val authDiskSource: AuthDiskSource = mockk()
|
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||||
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||||
|
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||||
|
|
||||||
private val settingsRepository = SettingsRepositoryImpl(
|
private val settingsRepository = SettingsRepositoryImpl(
|
||||||
authDiskSource = authDiskSource,
|
authDiskSource = fakeAuthDiskSource,
|
||||||
settingsDiskSource = fakeSettingsDiskSource,
|
settingsDiskSource = fakeSettingsDiskSource,
|
||||||
|
vaultSdkSource = vaultSdkSource,
|
||||||
dispatcherManager = FakeDispatcherManager(),
|
dispatcherManager = FakeDispatcherManager(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,14 +132,14 @@ class SettingsRepositoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultTimeout should pull from and update SettingsDiskSource for the current user`() {
|
fun `vaultTimeout should pull from and update SettingsDiskSource for the current user`() {
|
||||||
every { authDiskSource.userState?.activeUserId } returns null
|
fakeAuthDiskSource.userState = null
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultTimeout.Never,
|
VaultTimeout.Never,
|
||||||
settingsRepository.vaultTimeout,
|
settingsRepository.vaultTimeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
val userId = "userId"
|
val userId = "userId"
|
||||||
every { authDiskSource.userState?.activeUserId } returns userId
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
|
||||||
// Updates to the disk source change the repository value
|
// Updates to the disk source change the repository value
|
||||||
VAULT_TIMEOUT_MAP.forEach { (vaultTimeout, vaultTimeoutInMinutes) ->
|
VAULT_TIMEOUT_MAP.forEach { (vaultTimeout, vaultTimeoutInMinutes) ->
|
||||||
|
@ -160,14 +165,14 @@ class SettingsRepositoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultTimeoutAction should pull from and update SettingsDiskSource`() {
|
fun `vaultTimeoutAction should pull from and update SettingsDiskSource`() {
|
||||||
every { authDiskSource.userState?.activeUserId } returns null
|
fakeAuthDiskSource.userState = null
|
||||||
assertEquals(
|
assertEquals(
|
||||||
VaultTimeoutAction.LOCK,
|
VaultTimeoutAction.LOCK,
|
||||||
settingsRepository.vaultTimeoutAction,
|
settingsRepository.vaultTimeoutAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
val userId = "userId"
|
val userId = "userId"
|
||||||
every { authDiskSource.userState?.activeUserId } returns userId
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
|
||||||
// Updates to the disk source change the repository value
|
// Updates to the disk source change the repository value
|
||||||
VAULT_TIMEOUT_ACTIONS.forEach { vaultTimeoutAction ->
|
VAULT_TIMEOUT_ACTIONS.forEach { vaultTimeoutAction ->
|
||||||
|
@ -287,13 +292,28 @@ class SettingsRepositoryTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `isUnlockWithPinEnabled should return a value that tracks the existence of an encrypted PIN for the current user`() {
|
||||||
|
val userId = "userId"
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
fakeAuthDiskSource.storeEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = null,
|
||||||
|
)
|
||||||
|
assertFalse(settingsRepository.isUnlockWithPinEnabled)
|
||||||
|
|
||||||
|
fakeAuthDiskSource.storeEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = "encryptedPin",
|
||||||
|
)
|
||||||
|
assertTrue(settingsRepository.isUnlockWithPinEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
|
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
|
||||||
val userId = "userId"
|
val userId = "userId"
|
||||||
val userState = mockk<UserStateJson> {
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
every { activeUserId } returns userId
|
|
||||||
}
|
|
||||||
coEvery { authDiskSource.userState } returns userState
|
|
||||||
settingsRepository
|
settingsRepository
|
||||||
.getPullToRefreshEnabledFlow()
|
.getPullToRefreshEnabledFlow()
|
||||||
.test {
|
.test {
|
||||||
|
@ -314,12 +334,134 @@ class SettingsRepositoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `storePullToRefreshEnabled should properly update SettingsDiskSource`() {
|
fun `storePullToRefreshEnabled should properly update SettingsDiskSource`() {
|
||||||
val userId = "userId"
|
val userId = "userId"
|
||||||
every { authDiskSource.userState?.activeUserId } returns userId
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
settingsRepository.storePullToRefreshEnabled(true)
|
settingsRepository.storePullToRefreshEnabled(true)
|
||||||
assertEquals(true, fakeSettingsDiskSource.getPullToRefreshEnabled(userId = userId))
|
assertEquals(true, fakeSettingsDiskSource.getPullToRefreshEnabled(userId = userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `storeUnlockPin when the master password on restart is required should only save an encrypted PIN to disk`() {
|
||||||
|
val userId = "userId"
|
||||||
|
val pin = "1234"
|
||||||
|
val encryptedPin = "encryptedPin"
|
||||||
|
val pinProtectedUserKey = "pinProtectedUserKey"
|
||||||
|
val derivePinKeyResponse = DerivePinKeyResponse(
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
encryptedPin = encryptedPin,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.derivePinKey(
|
||||||
|
userId = userId,
|
||||||
|
pin = pin,
|
||||||
|
)
|
||||||
|
} returns derivePinKeyResponse.asSuccess()
|
||||||
|
|
||||||
|
settingsRepository.storeUnlockPin(
|
||||||
|
pin = pin,
|
||||||
|
shouldRequireMasterPasswordOnRestart = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
fakeAuthDiskSource.apply {
|
||||||
|
assertEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = encryptedPin,
|
||||||
|
)
|
||||||
|
assertPinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
inMemoryOnly = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
coVerify {
|
||||||
|
vaultSdkSource.derivePinKey(
|
||||||
|
userId = userId,
|
||||||
|
pin = pin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `storeUnlockPin when the master password on restart is not required should save all PIN data to disk`() {
|
||||||
|
val userId = "userId"
|
||||||
|
val pin = "1234"
|
||||||
|
val encryptedPin = "encryptedPin"
|
||||||
|
val pinProtectedUserKey = "pinProtectedUserKey"
|
||||||
|
val derivePinKeyResponse = DerivePinKeyResponse(
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
encryptedPin = encryptedPin,
|
||||||
|
)
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.derivePinKey(
|
||||||
|
userId = userId,
|
||||||
|
pin = pin,
|
||||||
|
)
|
||||||
|
} returns derivePinKeyResponse.asSuccess()
|
||||||
|
|
||||||
|
settingsRepository.storeUnlockPin(
|
||||||
|
pin = pin,
|
||||||
|
shouldRequireMasterPasswordOnRestart = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
fakeAuthDiskSource.apply {
|
||||||
|
assertEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = encryptedPin,
|
||||||
|
)
|
||||||
|
assertPinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = pinProtectedUserKey,
|
||||||
|
inMemoryOnly = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
coVerify {
|
||||||
|
vaultSdkSource.derivePinKey(
|
||||||
|
userId = userId,
|
||||||
|
pin = pin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `clearUnlockPin should clear any previously stored PIN-related values for the current user`() {
|
||||||
|
val userId = "userId"
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
fakeAuthDiskSource.apply {
|
||||||
|
storeEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = "encryptedPin",
|
||||||
|
)
|
||||||
|
storePinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = "pinProtectedUserKey",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsRepository.clearUnlockPin()
|
||||||
|
|
||||||
|
fakeAuthDiskSource.apply {
|
||||||
|
assertEncryptedPin(
|
||||||
|
userId = userId,
|
||||||
|
encryptedPin = null,
|
||||||
|
)
|
||||||
|
assertPinProtectedUserKey(
|
||||||
|
userId = userId,
|
||||||
|
pinProtectedUserKey = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MOCK_USER_STATE =
|
||||||
|
UserStateJson(
|
||||||
|
activeUserId = "userId",
|
||||||
|
accounts = mapOf("userId" to mockk()),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all [VaultTimeoutAction].
|
* A list of all [VaultTimeoutAction].
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,11 +22,28 @@ import org.junit.jupiter.api.Test
|
||||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct`() {
|
fun `initial state should be correct when saved state is set`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel(initialState = DEFAULT_STATE)
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when saved state is not set`() {
|
||||||
|
val settingsRepository: SettingsRepository = mockk {
|
||||||
|
every { isUnlockWithPinEnabled } returns true
|
||||||
|
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||||
|
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
||||||
|
}
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = null,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on AccountFingerprintPhraseClick should show the fingerprint phrase dialog`() = runTest {
|
fun `on AccountFingerprintPhraseClick should show the fingerprint phrase dialog`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
@ -210,12 +227,18 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on UnlockWithPinToggle Disabled should set pin unlock to false and emit ShowToast`() =
|
fun `on UnlockWithPinToggle Disabled should set pin unlock to false, clear the PIN in settings, and emit ShowToast`() =
|
||||||
runTest {
|
runTest {
|
||||||
val initialState = DEFAULT_STATE.copy(
|
val initialState = DEFAULT_STATE.copy(
|
||||||
isUnlockWithPinEnabled = true,
|
isUnlockWithPinEnabled = true,
|
||||||
)
|
)
|
||||||
val viewModel = createViewModel(initialState = initialState)
|
val settingsRepository: SettingsRepository = mockk() {
|
||||||
|
every { clearUnlockPin() } just runs
|
||||||
|
}
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = initialState,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
)
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
AccountSecurityAction.UnlockWithPinToggle.Disabled,
|
AccountSecurityAction.UnlockWithPinToggle.Disabled,
|
||||||
|
@ -229,11 +252,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
initialState.copy(isUnlockWithPinEnabled = false),
|
initialState.copy(isUnlockWithPinEnabled = false),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
verify { settingsRepository.clearUnlockPin() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on UnlockWithPinToggle Enabled should set pin unlock to true`() {
|
fun `on UnlockWithPinToggle PendingEnabled should set pin unlock to true`() {
|
||||||
val initialState = DEFAULT_STATE.copy(
|
val initialState = DEFAULT_STATE.copy(
|
||||||
isUnlockWithPinEnabled = false,
|
isUnlockWithPinEnabled = false,
|
||||||
)
|
)
|
||||||
|
@ -249,12 +273,18 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on UnlockWithPinToggle Enabled should set pin unlock to true and emit ShowToast`() =
|
fun `on UnlockWithPinToggle Enabled should set pin unlock to true, set the PIN in settings, and emit ShowToast`() =
|
||||||
runTest {
|
runTest {
|
||||||
val initialState = DEFAULT_STATE.copy(
|
val initialState = DEFAULT_STATE.copy(
|
||||||
isUnlockWithPinEnabled = false,
|
isUnlockWithPinEnabled = false,
|
||||||
)
|
)
|
||||||
val viewModel = createViewModel(initialState = initialState)
|
val settingsRepository: SettingsRepository = mockk() {
|
||||||
|
every { storeUnlockPin(any(), any()) } just runs
|
||||||
|
}
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
initialState = initialState,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
)
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(
|
viewModel.trySendAction(
|
||||||
AccountSecurityAction.UnlockWithPinToggle.Enabled(
|
AccountSecurityAction.UnlockWithPinToggle.Enabled(
|
||||||
|
@ -271,6 +301,12 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
initialState.copy(isUnlockWithPinEnabled = true),
|
initialState.copy(isUnlockWithPinEnabled = true),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
verify {
|
||||||
|
settingsRepository.storeUnlockPin(
|
||||||
|
pin = "1234",
|
||||||
|
shouldRequireMasterPasswordOnRestart = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -302,7 +338,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createViewModel(
|
private fun createViewModel(
|
||||||
initialState: AccountSecurityState = DEFAULT_STATE,
|
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||||
authRepository: AuthRepository = mockk(relaxed = true),
|
authRepository: AuthRepository = mockk(relaxed = true),
|
||||||
vaultRepository: VaultRepository = mockk(relaxed = true),
|
vaultRepository: VaultRepository = mockk(relaxed = true),
|
||||||
settingsRepository: SettingsRepository = mockk(relaxed = true),
|
settingsRepository: SettingsRepository = mockk(relaxed = true),
|
||||||
|
|
Loading…
Reference in a new issue