mirror of
https://github.com/bitwarden/android.git
synced 2026-05-29 07:56:39 -05:00
BIT-1165: Log out the current user after maximum invalid unlock attempts (#642)
This commit is contained in:
@@ -772,7 +772,7 @@ class VaultLockManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault with initializeCrypto success for a Never VaultTimeout should return Success and save the auto-unlock key`() =
|
||||
fun `unlockVault with initializeCrypto success for a Never VaultTimeout should return Success, save the auto-unlock key, and clear invalid unlock attempts`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
@@ -813,10 +813,16 @@ class VaultLockManagerTest {
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
mutableVaultTimeoutStateFlow.value = VaultTimeout.Never
|
||||
fakeAuthDiskSource.storeUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.apply {
|
||||
storeUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = null,
|
||||
)
|
||||
storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 4,
|
||||
)
|
||||
}
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
@@ -839,10 +845,16 @@ class VaultLockManagerTest {
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
fakeAuthDiskSource.apply {
|
||||
assertUserAutoUnlockKey(
|
||||
userId = userId,
|
||||
userAutoUnlockKey = userAutoUnlockKey,
|
||||
)
|
||||
assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = null,
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
@@ -870,7 +882,7 @@ class VaultLockManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault with initializeCrypto authentication failure for users should return AuthenticationError`() =
|
||||
fun `unlockVault with initializeCrypto authentication failure for users should return AuthenticationError and increment invalid unlock attempts`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
@@ -901,6 +913,11 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 1,
|
||||
)
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = kdf,
|
||||
@@ -921,6 +938,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 2,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
@@ -939,7 +960,7 @@ class VaultLockManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault with initializeCrypto authentication failure for orgs should return AuthenticationError`() =
|
||||
fun `unlockVault with initializeCrypto authentication failure for orgs should return AuthenticationError and increment invalid unlock attempts`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
@@ -976,6 +997,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 1,
|
||||
)
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
@@ -997,6 +1022,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 2,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
@@ -1019,8 +1048,9 @@ class VaultLockManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault with initializeCrypto failure for users should return GenericError`() =
|
||||
fun `unlockVault with initializeCrypto failure for users should return GenericError and increment invalid unlock attempts`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
@@ -1050,6 +1080,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 1,
|
||||
)
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
@@ -1071,6 +1105,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 2,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
@@ -1087,8 +1125,9 @@ class VaultLockManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault with initializeCrypto failure for orgs should return GenericError`() =
|
||||
fun `unlockVault with initializeCrypto failure for orgs should return GenericError and increment invalid unlock attempts`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
@@ -1124,6 +1163,10 @@ class VaultLockManagerTest {
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 1,
|
||||
)
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
@@ -1137,6 +1180,101 @@ class VaultLockManagerTest {
|
||||
organizationKeys = organizationKeys,
|
||||
)
|
||||
|
||||
assertEquals(VaultUnlockResult.GenericError, result)
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 2,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = kdf,
|
||||
email = email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = userKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `unlockVault error when reaching the maximum number of invalid unlock attempts should log out the user`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
val kdf = MOCK_PROFILE.toSdkParams()
|
||||
val email = MOCK_PROFILE.email
|
||||
val masterPassword = "drowssap"
|
||||
val userKey = "12345"
|
||||
val privateKey = "54321"
|
||||
val organizationKeys = mapOf("orgId1" to "orgKey1")
|
||||
coEvery {
|
||||
vaultSdkSource.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
kdfParams = kdf,
|
||||
email = email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = userKey,
|
||||
),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
assertEquals(
|
||||
VaultState(
|
||||
unlockedVaultUserIds = emptySet(),
|
||||
unlockingVaultUserIds = emptySet(),
|
||||
),
|
||||
vaultLockManager.vaultStateFlow.value,
|
||||
)
|
||||
fakeAuthDiskSource.storeInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 4,
|
||||
)
|
||||
|
||||
val result = vaultLockManager.unlockVault(
|
||||
userId = userId,
|
||||
kdf = kdf,
|
||||
email = email,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.Password(
|
||||
password = masterPassword,
|
||||
userKey = userKey,
|
||||
),
|
||||
privateKey = privateKey,
|
||||
organizationKeys = organizationKeys,
|
||||
)
|
||||
|
||||
fakeAuthDiskSource.assertInvalidUnlockAttempts(
|
||||
userId = userId,
|
||||
invalidUnlockAttempts = 5,
|
||||
)
|
||||
verify { userLogoutManager.logout(userId = userId) }
|
||||
|
||||
assertEquals(VaultUnlockResult.GenericError, result)
|
||||
assertEquals(
|
||||
VaultState(
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
@@ -24,6 +25,8 @@ import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -33,6 +36,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val environmentRepository = FakeEnvironmentRepository()
|
||||
private val authRepository = mockk<AuthRepository>() {
|
||||
every { activeUserId } answers { mutableUserStateFlow.value?.activeUserId }
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
every { specialCircumstance } returns null
|
||||
every { specialCircumstance = any() } just runs
|
||||
@@ -347,6 +351,43 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for password unlock should clear dialog when user has changed`() {
|
||||
val password = "abcd1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val resultFlow = bufferedMutableSharedFlow<VaultUnlockResult>()
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
} coAnswers { resultFlow.first() }
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
val updatedUserId = "updatedUserId"
|
||||
mutableUserStateFlow.update {
|
||||
it?.copy(
|
||||
activeUserId = updatedUserId,
|
||||
accounts = listOf(DEFAULT_ACCOUNT.copy(userId = updatedUserId)),
|
||||
)
|
||||
}
|
||||
resultFlow.tryEmit(VaultUnlockResult.GenericError)
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(dialog = null),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithMasterPasswordAndSync(password)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should display error dialog on AuthenticationError`() {
|
||||
val pin = "1234"
|
||||
@@ -447,6 +488,43 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should clear dialog when user has changed`() {
|
||||
val pin = "1234"
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = pin,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val resultFlow = bufferedMutableSharedFlow<VaultUnlockResult>()
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
} coAnswers { resultFlow.first() }
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
val updatedUserId = "updatedUserId"
|
||||
mutableUserStateFlow.update {
|
||||
it?.copy(
|
||||
activeUserId = updatedUserId,
|
||||
accounts = listOf(DEFAULT_ACCOUNT.copy(userId = updatedUserId)),
|
||||
)
|
||||
}
|
||||
resultFlow.tryEmit(VaultUnlockResult.GenericError)
|
||||
|
||||
assertEquals(
|
||||
initialState.copy(dialog = null),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithPinAndSync(pin)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: VaultUnlockState? = DEFAULT_STATE,
|
||||
environmentRepo: EnvironmentRepository = environmentRepository,
|
||||
@@ -481,19 +559,19 @@ private val DEFAULT_STATE: VaultUnlockState = VaultUnlockState(
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
|
||||
private val DEFAULT_ACCOUNT = UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
environment = Environment.Us,
|
||||
avatarColorHex = "#aa00aa",
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE = UserState(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "activeUserId",
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
environment = Environment.Us,
|
||||
avatarColorHex = "#aa00aa",
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
accounts = listOf(DEFAULT_ACCOUNT),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user