mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
Add the continue button flow for TDE (#1248)
This commit is contained in:
parent
403cfc94f0
commit
0a63d85457
9 changed files with 691 additions and 7 deletions
|
@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
|
@ -123,6 +124,12 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
|
|||
*/
|
||||
suspend fun deleteAccount(password: String): DeleteAccountResult
|
||||
|
||||
/**
|
||||
* Attempt to create a new user via SSO and log them into their account. Upon success the new
|
||||
* user will also have the vault automatically unlocked for them.
|
||||
*/
|
||||
suspend fun createNewSsoUser(): NewSsoUserResult
|
||||
|
||||
/**
|
||||
* Attempt to complete the trusted device login with the given [requestPrivateKey] and
|
||||
* [asymmetricalKey]. This will unlock the vault and finish trusting the device.
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
|
@ -332,6 +333,61 @@ class AuthRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun createNewSsoUser(): NewSsoUserResult {
|
||||
val account = authDiskSource.userState?.activeAccount ?: return NewSsoUserResult.Failure
|
||||
val orgIdentifier = rememberedOrgIdentifier ?: return NewSsoUserResult.Failure
|
||||
val userId = account.profile.userId
|
||||
return organizationService
|
||||
.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
.flatMap { orgAutoEnrollStatus ->
|
||||
organizationService
|
||||
.getOrganizationKeys(orgAutoEnrollStatus.organizationId)
|
||||
.flatMap { organizationKeys ->
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = userId,
|
||||
orgPublicKey = organizationKeys.publicKey,
|
||||
rememberDevice = authDiskSource.shouldTrustDevice,
|
||||
)
|
||||
}
|
||||
.flatMap { keys ->
|
||||
accountsService
|
||||
.createAccountKeys(
|
||||
publicKey = keys.publicKey,
|
||||
encryptedPrivateKey = keys.privateKey,
|
||||
)
|
||||
.map { keys }
|
||||
}
|
||||
.flatMap { keys ->
|
||||
organizationService
|
||||
.organizationResetPasswordEnroll(
|
||||
organizationId = orgAutoEnrollStatus.organizationId,
|
||||
userId = userId,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = keys.adminReset,
|
||||
)
|
||||
.map { keys }
|
||||
}
|
||||
.onSuccess { keys ->
|
||||
authDiskSource.storePrivateKey(
|
||||
userId = userId,
|
||||
privateKey = keys.privateKey,
|
||||
)
|
||||
keys.deviceKey?.let { trustDeviceResponse ->
|
||||
trustedDeviceManager.trustThisDevice(
|
||||
userId = userId,
|
||||
trustDeviceResponse = trustDeviceResponse,
|
||||
)
|
||||
}
|
||||
vaultRepository.syncVaultState(userId = userId)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { NewSsoUserResult.Success },
|
||||
onFailure = { NewSsoUserResult.Failure },
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun completeTdeLogin(
|
||||
requestPrivateKey: String,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of creating a new user via SSO.
|
||||
*/
|
||||
sealed class NewSsoUserResult {
|
||||
/**
|
||||
* A new user has successfully been created.
|
||||
*/
|
||||
data object Success : NewSsoUserResult()
|
||||
|
||||
/**
|
||||
* There was an error while truing to create the new user.
|
||||
*/
|
||||
data object Failure : NewSsoUserResult()
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.manager
|
|||
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.sdk.ClientAuth
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -55,4 +56,14 @@ interface VaultLockManager {
|
|||
* Suspends until the vault for the given [userId] is unlocked.
|
||||
*/
|
||||
suspend fun waitUntilUnlocked(userId: String)
|
||||
|
||||
/**
|
||||
* This will check the vault lock state for the given user and ensure that the
|
||||
* [vaultUnlockDataStateFlow] is up-to-date.
|
||||
*
|
||||
* This is only meant to be used when the SDK unlocks the vault as a side-effect of some other
|
||||
* function, such as [ClientAuth.makeRegisterTdeKeys]. When using the regular [unlockVault]
|
||||
* functions, this is not necessary.
|
||||
*/
|
||||
suspend fun syncVaultState(userId: String)
|
||||
}
|
||||
|
|
|
@ -187,6 +187,18 @@ class VaultLockManagerImpl(
|
|||
.first { unlockedUserIds -> userId in unlockedUserIds }
|
||||
}
|
||||
|
||||
override suspend fun syncVaultState(userId: String) {
|
||||
// There is no proper way to query if the vault is actually unlocked or not but we can
|
||||
// attempt to retrieve the user encryption key. If it fails, then the vault is locked and
|
||||
// if it succeeds, then the vault is unlocked.
|
||||
vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.fold(
|
||||
onFailure = { setVaultToLocked(userId = userId) },
|
||||
onSuccess = { setVaultToUnlocked(userId = userId) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the stored invalid unlock count for the given [userId] and automatically logs out
|
||||
* if this new value is greater than [MAXIMUM_INVALID_UNLOCK_ATTEMPTS].
|
||||
|
|
|
@ -2,13 +2,17 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -17,6 +21,7 @@ private const val KEY_STATE = "state"
|
|||
/**
|
||||
* Manages application state for the Trusted Device screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class TrustedDeviceViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
@ -52,6 +57,37 @@ class TrustedDeviceViewModel @Inject constructor(
|
|||
TrustedDeviceAction.ApproveWithDeviceClick -> handleApproveWithDeviceClick()
|
||||
TrustedDeviceAction.ApproveWithPasswordClick -> handleApproveWithPasswordClick()
|
||||
TrustedDeviceAction.NotYouClick -> handleNotYouClick()
|
||||
is TrustedDeviceAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: TrustedDeviceAction.Internal) {
|
||||
when (action) {
|
||||
is TrustedDeviceAction.Internal.ReceiveNewSsoUserResult -> {
|
||||
handleReceiveNewSsoUserResult(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReceiveNewSsoUserResult(
|
||||
action: TrustedDeviceAction.Internal.ReceiveNewSsoUserResult,
|
||||
) {
|
||||
when (action.result) {
|
||||
NewSsoUserResult.Failure -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TrustedDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NewSsoUserResult.Success -> {
|
||||
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||
// Should automatically navigate to a logged in state.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +104,16 @@ class TrustedDeviceViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = TrustedDeviceState.DialogState.Loading(R.string.loading.asText()),
|
||||
)
|
||||
}
|
||||
authRepository.shouldTrustDevice = state.isRemembered
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.createNewSsoUser()
|
||||
sendAction(TrustedDeviceAction.Internal.ReceiveNewSsoUserResult(result))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleApproveWithAdminClick() {
|
||||
|
@ -196,4 +241,16 @@ sealed class TrustedDeviceAction {
|
|||
* Indicates that the "Not you?" text was clicked.
|
||||
*/
|
||||
data object NotYouClick : TrustedDeviceAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
sealed class Internal : TrustedDeviceAction() {
|
||||
/**
|
||||
* Indicates a new SSO user result has been received.
|
||||
*/
|
||||
data class ReceiveNewSsoUserResult(
|
||||
val result: NewSsoUserResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,12 @@ import com.bitwarden.core.AuthRequestMethod
|
|||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.RsaKeyPair
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
|
||||
|
@ -55,6 +57,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LoginResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.OrganizationDomainSsoDetailsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
|
@ -733,6 +736,430 @@ class AuthRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when no active user returns failure`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when remembered org identifier returns failure`() = runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = null
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when getOrganizationAutoEnrollStatus fails returns failure`() = runTest {
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when getOrganizationKeys fails returns failure`() = runTest {
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery { organizationService.getOrganizationKeys(orgId) } returns Throwable().asFailure()
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when makeRegisterTdeKeysAndUnlockVault fails returns failure`() =
|
||||
runTest {
|
||||
val shouldTrustDevice = false
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgPublicKey = "organizationPublicKey"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
val orgKeysResponse = OrganizationKeysResponseJson(
|
||||
privateKey = "privateKey",
|
||||
publicKey = orgPublicKey,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery {
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
} returns orgKeysResponse.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when createAccountKeys fails returns failure`() = runTest {
|
||||
val shouldTrustDevice = false
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgPublicKey = "organizationPublicKey"
|
||||
val userPrivateKey = "userPrivateKey"
|
||||
val userPublicKey = "userPublicKey"
|
||||
val userAdminReset = "userAdminReset"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
val orgKeysResponse = OrganizationKeysResponseJson(
|
||||
privateKey = "privateKey",
|
||||
publicKey = orgPublicKey,
|
||||
)
|
||||
val registerTdeKeyResponse = RegisterTdeKeyResponse(
|
||||
privateKey = userPrivateKey,
|
||||
publicKey = userPublicKey,
|
||||
adminReset = userAdminReset,
|
||||
deviceKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery {
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
} returns orgKeysResponse.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
} returns registerTdeKeyResponse.asSuccess()
|
||||
coEvery {
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when organizationResetPasswordEnroll fails returns failure`() = runTest {
|
||||
val shouldTrustDevice = false
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgPublicKey = "organizationPublicKey"
|
||||
val userPrivateKey = "userPrivateKey"
|
||||
val userPublicKey = "userPublicKey"
|
||||
val userAdminReset = "userAdminReset"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
val orgKeysResponse = OrganizationKeysResponseJson(
|
||||
privateKey = "privateKey",
|
||||
publicKey = orgPublicKey,
|
||||
)
|
||||
val registerTdeKeyResponse = RegisterTdeKeyResponse(
|
||||
privateKey = userPrivateKey,
|
||||
publicKey = userPublicKey,
|
||||
adminReset = userAdminReset,
|
||||
deviceKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery {
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
} returns orgKeysResponse.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
} returns registerTdeKeyResponse.asSuccess()
|
||||
coEvery {
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery {
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
assertEquals(NewSsoUserResult.Failure, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createNewSsoUser when shouldTrustDevice false should not trust the device returns Success`() =
|
||||
runTest {
|
||||
val shouldTrustDevice = false
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgPublicKey = "organizationPublicKey"
|
||||
val userPrivateKey = "userPrivateKey"
|
||||
val userPublicKey = "userPublicKey"
|
||||
val userAdminReset = "userAdminReset"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
val orgKeysResponse = OrganizationKeysResponseJson(
|
||||
privateKey = "privateKey",
|
||||
publicKey = orgPublicKey,
|
||||
)
|
||||
val registerTdeKeyResponse = RegisterTdeKeyResponse(
|
||||
privateKey = userPrivateKey,
|
||||
publicKey = userPublicKey,
|
||||
adminReset = userAdminReset,
|
||||
deviceKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery {
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
} returns orgKeysResponse.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
} returns registerTdeKeyResponse.asSuccess()
|
||||
coEvery {
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery {
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery { vaultRepository.syncVaultState(userId = USER_ID_1) } just runs
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
|
||||
assertEquals(NewSsoUserResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
vaultRepository.syncVaultState(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createNewSsoUser when shouldTrustDevice true should trust the device returns Success`() =
|
||||
runTest {
|
||||
val shouldTrustDevice = true
|
||||
val orgIdentifier = "rememberedOrgIdentifier"
|
||||
val orgId = "organizationId"
|
||||
val orgPublicKey = "organizationPublicKey"
|
||||
val userPrivateKey = "userPrivateKey"
|
||||
val userPublicKey = "userPublicKey"
|
||||
val userAdminReset = "userAdminReset"
|
||||
val orgAutoEnrollStatusResponse = OrganizationAutoEnrollStatusResponseJson(
|
||||
organizationId = orgId,
|
||||
isResetPasswordEnabled = false,
|
||||
)
|
||||
val orgKeysResponse = OrganizationKeysResponseJson(
|
||||
privateKey = "privateKey",
|
||||
publicKey = orgPublicKey,
|
||||
)
|
||||
val trustDeviceResponse = mockk<TrustDeviceResponse>()
|
||||
val registerTdeKeyResponse = RegisterTdeKeyResponse(
|
||||
privateKey = userPrivateKey,
|
||||
publicKey = userPublicKey,
|
||||
adminReset = userAdminReset,
|
||||
deviceKey = trustDeviceResponse,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.rememberedOrgIdentifier = orgIdentifier
|
||||
fakeAuthDiskSource.shouldTrustDevice = shouldTrustDevice
|
||||
coEvery {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
} returns orgAutoEnrollStatusResponse.asSuccess()
|
||||
coEvery {
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
} returns orgKeysResponse.asSuccess()
|
||||
coEvery {
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
} returns registerTdeKeyResponse.asSuccess()
|
||||
coEvery {
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery {
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery {
|
||||
trustedDeviceManager.trustThisDevice(
|
||||
userId = USER_ID_1,
|
||||
trustDeviceResponse = trustDeviceResponse,
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
coEvery { vaultRepository.syncVaultState(userId = USER_ID_1) } just runs
|
||||
|
||||
val result = repository.createNewSsoUser()
|
||||
|
||||
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = userPrivateKey)
|
||||
assertEquals(NewSsoUserResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
organizationService.getOrganizationAutoEnrollStatus(orgIdentifier)
|
||||
organizationService.getOrganizationKeys(orgId)
|
||||
authSdkSource.makeRegisterTdeKeysAndUnlockVault(
|
||||
userId = USER_ID_1,
|
||||
orgPublicKey = orgPublicKey,
|
||||
rememberDevice = shouldTrustDevice,
|
||||
)
|
||||
accountsService.createAccountKeys(
|
||||
publicKey = userPublicKey,
|
||||
encryptedPrivateKey = userPrivateKey,
|
||||
)
|
||||
organizationService.organizationResetPasswordEnroll(
|
||||
organizationId = orgId,
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
resetPasswordKey = userAdminReset,
|
||||
)
|
||||
trustedDeviceManager.trustThisDevice(
|
||||
userId = USER_ID_1,
|
||||
trustDeviceResponse = trustDeviceResponse,
|
||||
)
|
||||
vaultRepository.syncVaultState(userId = USER_ID_1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completeTdeLogin without active user fails`() = runTest {
|
||||
val requestPrivateKey = "requestPrivateKey"
|
||||
|
|
|
@ -1285,6 +1285,46 @@ class VaultLockManagerTest {
|
|||
assertTrue(vaultLockManager.isVaultUnlocked(userId = USER_ID))
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncVaultState with getUserEncryptionKey failure should update the users vault state to locked`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
|
||||
} returns Throwable().asFailure()
|
||||
|
||||
// Begin in a locked state
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
|
||||
|
||||
vaultLockManager.syncVaultState(userId = USER_ID)
|
||||
|
||||
// Confirm the vault is still locked
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `syncVaultState with getUserEncryptionKey success should update the users vault state to unlocked`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
|
||||
} returns "UserEncryptionKey".asSuccess()
|
||||
|
||||
// Begin in a locked state
|
||||
assertFalse(vaultLockManager.isVaultUnlocked(userId = USER_ID))
|
||||
|
||||
vaultLockManager.syncVaultState(userId = USER_ID)
|
||||
|
||||
// Confirm the vault is unlocked
|
||||
assertTrue(vaultLockManager.isVaultUnlocked(userId = USER_ID))
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the verification call count for the given [mock] while leaving all other mocked
|
||||
* behavior in place.
|
||||
|
|
|
@ -2,13 +2,17 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
|
|||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
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.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -77,14 +81,68 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on ContinueClick emits ShowToast`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
fun `on ContinueClick with createNewSsoUser failure should display the error dialog state`() =
|
||||
runTest {
|
||||
every { authRepository.shouldTrustDevice = true } just runs
|
||||
coEvery { authRepository.createNewSsoUser() } returns NewSsoUserResult.Failure
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
|
||||
assertEquals(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()), awaitItem())
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = TrustedDeviceState.DialogState.Loading(
|
||||
message = R.string.loading.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = TrustedDeviceState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.shouldTrustDevice = true
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.createNewSsoUser()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ContinueClick with createNewSsoUser success should display the loading dialog state`() =
|
||||
runTest {
|
||||
every { authRepository.shouldTrustDevice = true } just runs
|
||||
coEvery { authRepository.createNewSsoUser() } returns NewSsoUserResult.Success
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(TrustedDeviceAction.ContinueClick)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = TrustedDeviceState.DialogState.Loading(
|
||||
message = R.string.loading.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.shouldTrustDevice = true
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.createNewSsoUser()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ApproveWithAdminClick emits NavigateToApproveWithAdmin`() = runTest {
|
||||
|
|
Loading…
Add table
Reference in a new issue