mirror of
https://github.com/bitwarden/android.git
synced 2024-11-25 02:46:00 +03:00
PM-9532: pt2. separate vault unlock logic and fail out on error during login. (#3609)
This commit is contained in:
parent
a090000826
commit
18cd66a34b
13 changed files with 749 additions and 182 deletions
|
@ -58,6 +58,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.toLoginErrorResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
|
@ -91,6 +92,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
|||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -509,6 +511,12 @@ class AuthRepositoryImpl(
|
|||
val userId = profile.userId
|
||||
val privateKey = authDiskSource.getPrivateKey(userId = userId)
|
||||
?: return LoginResult.Error(errorMessage = null)
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { error ->
|
||||
return error.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = profile.email,
|
||||
|
@ -521,6 +529,7 @@ class AuthRepositoryImpl(
|
|||
// We should already have the org keys from the login sync.
|
||||
organizationKeys = authDiskSource.getOrganizationKeys(userId = userId),
|
||||
)
|
||||
}
|
||||
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = asymmetricalKey)
|
||||
vaultRepository.syncIfNecessary()
|
||||
|
@ -947,9 +956,9 @@ class AuthRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
VaultUnlockResult.GenericError,
|
||||
-> {
|
||||
IllegalStateException("Failed to unlock vault").asFailure()
|
||||
}
|
||||
|
@ -1132,7 +1141,7 @@ class AuthRepositoryImpl(
|
|||
ValidatePinResult.Success(isValid = true)
|
||||
}
|
||||
|
||||
InitializeCryptoResult.AuthenticationError -> {
|
||||
is InitializeCryptoResult.AuthenticationError -> {
|
||||
ValidatePinResult.Success(isValid = false)
|
||||
}
|
||||
}
|
||||
|
@ -1350,28 +1359,41 @@ class AuthRepositoryImpl(
|
|||
deviceData: DeviceDataModel?,
|
||||
orgIdentifier: String?,
|
||||
): LoginResult = userStateTransaction {
|
||||
|
||||
val userStateJson = loginResponse.toUserState(
|
||||
previousUserState = authDiskSource.userState,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
password?.let {
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
vaultRepository.unlockVault(
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
val isDeviceUnlockAvailable = deviceData != null ||
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||
// if possible attempt to unlock the vault with trusted device data
|
||||
if (isDeviceUnlockAvailable) {
|
||||
unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
userKey = loginResponse.key,
|
||||
privateKey = loginResponse.privateKey,
|
||||
masterPassword = it,
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
userStateJson = userStateJson,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else {
|
||||
password?.let {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userId = userId,
|
||||
userStateJson = userStateJson,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
password?.let {
|
||||
// Save the master password hash.
|
||||
authSdkSource
|
||||
.hashPassword(
|
||||
|
@ -1391,48 +1413,6 @@ class AuthRepositoryImpl(
|
|||
passwordsToCheckMap.put(userId, it)
|
||||
}
|
||||
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
.masterPasswordHash
|
||||
?.let {
|
||||
AuthRequestMethod.MasterKey(
|
||||
protectedMasterKey = model.asymmetricalKey,
|
||||
authRequestKey = loginResponse.key,
|
||||
)
|
||||
}
|
||||
?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
// We are purposely not storing the master password hash here since it is not
|
||||
// formatted in in a manner that we can use. We will store it properly the next
|
||||
// time the user enters their master password and it is validated.
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Trusted Device Encryption flow
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions?.let { options ->
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
|
@ -1466,79 +1446,12 @@ class AuthRepositoryImpl(
|
|||
twoFactorResponse = null
|
||||
resendEmailRequestJson = null
|
||||
twoFactorDeviceData = null
|
||||
|
||||
settingsRepository.setDefaultsIfNecessary(userId = userId)
|
||||
vaultRepository.syncIfNecessary()
|
||||
hasPendingAccountAddition = false
|
||||
LoginResult.Success
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE.
|
||||
*/
|
||||
private suspend fun handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
|
||||
trustedDeviceDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
privateKey: String,
|
||||
) {
|
||||
val userId = userStateJson.activeUserId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
val pendingRequest = authDiskSource.getPendingAuthRequest(userId = userId) ?: return
|
||||
authRequestManager
|
||||
.getAuthRequestIfApproved(pendingRequest.requestId)
|
||||
.getOrNull()
|
||||
?.let { request ->
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = userKey)
|
||||
}
|
||||
authDiskSource.storePendingAuthRequest(
|
||||
userId = userId,
|
||||
pendingAuthRequest = null,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val encryptedPrivateKey = trustedDeviceDecryptionOptions.encryptedPrivateKey
|
||||
val encryptedUserKey = trustedDeviceDecryptionOptions.encryptedUserKey
|
||||
if (encryptedPrivateKey == null || encryptedUserKey == null) {
|
||||
// If we have a device key but server is missing private key and user key, we
|
||||
// need to clear the device key and let the user go through the TDE flow again.
|
||||
authDiskSource.storeDeviceKey(userId = userId, deviceKey = null)
|
||||
return
|
||||
}
|
||||
vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
deviceProtectedUserKey = encryptedUserKey,
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method that processes the [GetTokenResponseJson.TwoFactorRequired] when logging in.
|
||||
*/
|
||||
|
@ -1564,6 +1477,175 @@ class AuthRepositoryImpl(
|
|||
return LoginResult.TwoFactorRequired
|
||||
}
|
||||
|
||||
private suspend fun unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userId: String,
|
||||
userStateJson: UserStateJson,
|
||||
password: String?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with password if possible.
|
||||
val masterPassword = password ?: return null
|
||||
val privateKey = loginResponse.privateKey ?: return null
|
||||
val key = loginResponse.key ?: return null
|
||||
return vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
userKey = key,
|
||||
privateKey = privateKey,
|
||||
masterPassword = masterPassword,
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to unlock the vault with trusted device specific data.
|
||||
*/
|
||||
private suspend fun unlockVaultWithTdeOnLoginSuccess(
|
||||
loginResponse: GetTokenResponseJson.Success,
|
||||
userId: String,
|
||||
userStateJson: UserStateJson,
|
||||
deviceData: DeviceDataModel?,
|
||||
): VaultUnlockResult? {
|
||||
// Attempt to unlock the vault with auth request if possible.
|
||||
// These values will only be null during the Just-in-Time provisioning flow.
|
||||
if (loginResponse.privateKey != null && loginResponse.key != null) {
|
||||
deviceData?.let { model ->
|
||||
return vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = loginResponse.privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = model.privateKey,
|
||||
method = model
|
||||
.masterPasswordHash
|
||||
?.let {
|
||||
AuthRequestMethod.MasterKey(
|
||||
protectedMasterKey = model.asymmetricalKey,
|
||||
authRequestKey = loginResponse.key,
|
||||
)
|
||||
}
|
||||
?: AuthRequestMethod.UserKey(protectedUserKey = model.asymmetricalKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
// We are purposely not storing the master password hash here since it is not
|
||||
// formatted in in a manner that we can use. We will store it properly the next
|
||||
// time the user enters their master password and it is validated.
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the Trusted Device Encryption flow
|
||||
return loginResponse
|
||||
.userDecryptionOptions
|
||||
?.trustedDeviceUserDecryptionOptions
|
||||
?.let { options ->
|
||||
loginResponse.privateKey?.let { privateKey ->
|
||||
unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options = options,
|
||||
userStateJson = userStateJson,
|
||||
privateKey = privateKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method to handle the [TrustedDeviceUserDecryptionOptionsJson] specific to TDE
|
||||
* and store the necessary keys when appropriate.
|
||||
*/
|
||||
private suspend fun unlockVaultWithTrustedDeviceUserDecryptionOptionsAndStoreKeys(
|
||||
options: TrustedDeviceUserDecryptionOptionsJson,
|
||||
userStateJson: UserStateJson,
|
||||
privateKey: String,
|
||||
): VaultUnlockResult? {
|
||||
var vaultUnlockResult: VaultUnlockResult? = null
|
||||
val userId = userStateJson.activeUserId
|
||||
val deviceKey = authDiskSource.getDeviceKey(userId = userId)
|
||||
if (deviceKey == null) {
|
||||
// A null device key means this device is not trusted.
|
||||
val pendingRequest = authDiskSource
|
||||
.getPendingAuthRequest(userId = userId)
|
||||
?: return null
|
||||
authRequestManager
|
||||
.getAuthRequestIfApproved(pendingRequest.requestId)
|
||||
.getOrNull()
|
||||
?.let { request ->
|
||||
// For approved requests the key will always be present.
|
||||
val userKey = requireNotNull(request.key)
|
||||
vaultUnlockResult = vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = pendingRequest.requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = userKey),
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = userKey)
|
||||
}
|
||||
authDiskSource.storePendingAuthRequest(
|
||||
userId = userId,
|
||||
pendingAuthRequest = null,
|
||||
)
|
||||
return vaultUnlockResult
|
||||
}
|
||||
|
||||
val encryptedPrivateKey = options.encryptedPrivateKey
|
||||
val encryptedUserKey = options.encryptedUserKey
|
||||
|
||||
if (encryptedPrivateKey == null || encryptedUserKey == null) {
|
||||
// If we have a device key but server is missing private key and user key, we
|
||||
// need to clear the device key and let the user go through the TDE flow again.
|
||||
authDiskSource.storeDeviceKey(userId = userId, deviceKey = null)
|
||||
return null
|
||||
}
|
||||
|
||||
vaultUnlockResult = vaultRepository.unlockVault(
|
||||
userId = userId,
|
||||
email = userStateJson.activeAccount.profile.email,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.DeviceKey(
|
||||
deviceKey = deviceKey,
|
||||
protectedDevicePrivateKey = encryptedPrivateKey,
|
||||
deviceProtectedUserKey = encryptedUserKey,
|
||||
),
|
||||
// We can separately unlock vault for organization data after
|
||||
// receiving the sync response if this data is currently absent.
|
||||
organizationKeys = null,
|
||||
)
|
||||
|
||||
if (vaultUnlockResult is VaultUnlockResult.Success) {
|
||||
authDiskSource.storeUserKey(userId = userId, userKey = encryptedUserKey)
|
||||
}
|
||||
return vaultUnlockResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to check for a vault unlock related error when logging in.
|
||||
*
|
||||
* @param onVaultUnlockError a lambda function to be invoked in the event a [VaultUnlockError]
|
||||
* is produced via the passed in [block]
|
||||
* @param block a lambda representing logic which produces either a [VaultUnlockResult] which
|
||||
* is castable to [VaultUnlockError] or `null`
|
||||
*/
|
||||
private inline fun checkForVaultUnlockError(
|
||||
onVaultUnlockError: (VaultUnlockError) -> Unit,
|
||||
block: () -> VaultUnlockResult?,
|
||||
) {
|
||||
(block() as? VaultUnlockError)?.also(onVaultUnlockError)
|
||||
}
|
||||
|
||||
//endregion LoginCommon
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockError
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
|
||||
/**
|
||||
* Helper function to map a [VaultUnlockError] to a [LoginResult.Error] with
|
||||
* the necessary `message` if applicable.
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
}
|
|
@ -134,7 +134,7 @@ class VaultSdkSourceImpl(
|
|||
InitializeCryptoResult.Success
|
||||
} catch (exception: BitwardenException) {
|
||||
// The only truly expected error from the SDK is an incorrect key/password.
|
||||
InitializeCryptoResult.AuthenticationError
|
||||
InitializeCryptoResult.AuthenticationError(message = exception.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,7 +150,9 @@ class VaultSdkSourceImpl(
|
|||
InitializeCryptoResult.Success
|
||||
} catch (exception: BitwardenException) {
|
||||
// The only truly expected error from the SDK is for incorrect keys.
|
||||
InitializeCryptoResult.AuthenticationError
|
||||
InitializeCryptoResult.AuthenticationError(
|
||||
message = exception.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,5 +13,7 @@ sealed class InitializeCryptoResult {
|
|||
/**
|
||||
* Incorrect password or key(s) provided.
|
||||
*/
|
||||
data object AuthenticationError : InitializeCryptoResult()
|
||||
data class AuthenticationError(
|
||||
val message: String? = null,
|
||||
) : InitializeCryptoResult()
|
||||
}
|
||||
|
|
|
@ -13,15 +13,22 @@ sealed class VaultUnlockResult {
|
|||
/**
|
||||
* Incorrect password provided.
|
||||
*/
|
||||
data object AuthenticationError : VaultUnlockResult()
|
||||
data class AuthenticationError(
|
||||
val message: String? = null,
|
||||
) : VaultUnlockResult(), VaultUnlockError
|
||||
|
||||
/**
|
||||
* Unable to access user state information.
|
||||
*/
|
||||
data object InvalidStateError : VaultUnlockResult()
|
||||
data object InvalidStateError : VaultUnlockResult(), VaultUnlockError
|
||||
|
||||
/**
|
||||
* Generic error thrown by Bitwarden SDK.
|
||||
*/
|
||||
data object GenericError : VaultUnlockResult()
|
||||
data object GenericError : VaultUnlockResult(), VaultUnlockError
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed interface to denote that a [VaultUnlockResult] is an error result.
|
||||
*/
|
||||
sealed interface VaultUnlockError
|
||||
|
|
|
@ -8,6 +8,11 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
|||
*/
|
||||
fun InitializeCryptoResult.toVaultUnlockResult(): VaultUnlockResult =
|
||||
when (this) {
|
||||
InitializeCryptoResult.AuthenticationError -> VaultUnlockResult.AuthenticationError
|
||||
is InitializeCryptoResult.AuthenticationError -> {
|
||||
VaultUnlockResult.AuthenticationError(
|
||||
message = this.message,
|
||||
)
|
||||
}
|
||||
|
||||
InitializeCryptoResult.Success -> VaultUnlockResult.Success
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
|||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* The top level composable for the Reset Password screen.
|
||||
|
@ -230,6 +231,7 @@ private fun TrustedDeviceDialogs(
|
|||
@Preview
|
||||
@Composable
|
||||
private fun TrustedDeviceScaffold_preview() {
|
||||
BitwardenTheme {
|
||||
TrustedDeviceScaffold(
|
||||
state = TrustedDeviceState(
|
||||
dialogState = null,
|
||||
|
@ -253,3 +255,4 @@ private fun TrustedDeviceScaffold_preview() {
|
|||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -260,7 +260,7 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
action.vaultUnlockResult is VaultUnlockResult.Success
|
||||
|
||||
when (action.vaultUnlockResult) {
|
||||
VaultUnlockResult.AuthenticationError -> {
|
||||
is VaultUnlockResult.AuthenticationError -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
|
|
|
@ -1306,6 +1306,57 @@ class AuthRepositoryTest {
|
|||
assertEquals(LoginResult.Success, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completeTdeLogin where vault unlock fails should return LoginResult error`() = runTest {
|
||||
val requestPrivateKey = "requestPrivateKey"
|
||||
val asymmetricalKey = "asymmetricalKey"
|
||||
val privateKey = "privateKey"
|
||||
val orgKeys = mapOf("orgId" to "orgKey")
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
fakeAuthDiskSource.storePrivateKey(userId = USER_ID_1, privateKey = privateKey)
|
||||
fakeAuthDiskSource.storeOrganizationKeys(userId = USER_ID_1, organizationKeys = orgKeys)
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = SINGLE_USER_STATE_1.activeAccount.profile.email,
|
||||
kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
),
|
||||
organizationKeys = orgKeys,
|
||||
)
|
||||
} returns VaultUnlockResult.AuthenticationError(message = null)
|
||||
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||
|
||||
val result = repository.completeTdeLogin(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
asymmetricalKey = asymmetricalKey,
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = SINGLE_USER_STATE_1.activeAccount.profile.email,
|
||||
kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams(),
|
||||
privateKey = privateKey,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = requestPrivateKey,
|
||||
method = AuthRequestMethod.UserKey(protectedUserKey = asymmetricalKey),
|
||||
),
|
||||
organizationKeys = orgKeys,
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.syncIfNecessary()
|
||||
}
|
||||
|
||||
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null)
|
||||
assertEquals(LoginResult.Error(errorMessage = null), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login when pre login fails should return Error with no message`() = runTest {
|
||||
coEvery {
|
||||
|
@ -1471,6 +1522,94 @@ class AuthRepositoryTest {
|
|||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login should return Error result when get token succeeds but unlock vault fails`() =
|
||||
runTest {
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
val expectedErrorMessage = "crypto key failure"
|
||||
|
||||
coEvery {
|
||||
identityService.preLogin(email = EMAIL)
|
||||
} returns PRE_LOGIN_SUCCESS.asSuccess()
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns successResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
userKey = successResponse.key!!,
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
masterPassword = PASSWORD,
|
||||
)
|
||||
} returns VaultUnlockResult.AuthenticationError(expectedErrorMessage)
|
||||
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = null,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.Error(errorMessage = expectedErrorMessage), result)
|
||||
assertEquals(AuthState.Unauthenticated, repository.authStateFlow.value)
|
||||
coVerify { identityService.preLogin(email = EMAIL) }
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = null,
|
||||
)
|
||||
fakeAuthDiskSource.assertMasterPasswordHash(
|
||||
userId = USER_ID_1,
|
||||
passwordHash = null,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
userKey = successResponse.key!!,
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
masterPassword = PASSWORD,
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.syncIfNecessary()
|
||||
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
null,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login get token succeeds with null keys and hasMasterPassword false should not call unlockVault`() =
|
||||
|
@ -1796,6 +1935,97 @@ class AuthRepositoryTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login two factor should return Error result when get token succeeds but unlock vault fails`() = runTest {
|
||||
val twoFactorResponse = GetTokenResponseJson
|
||||
.TwoFactorRequired(
|
||||
authMethodsData = TWO_FACTOR_AUTH_METHODS_DATA,
|
||||
captchaToken = null,
|
||||
ssoToken = null,
|
||||
twoFactorProviders = null,
|
||||
)
|
||||
// Attempt a normal login with a two factor error first, so that the auth
|
||||
// data will be cached.
|
||||
coEvery { identityService.preLogin(EMAIL) } returns PRE_LOGIN_SUCCESS.asSuccess()
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns twoFactorResponse
|
||||
.asSuccess()
|
||||
val firstResult = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null)
|
||||
assertEquals(LoginResult.TwoFactorRequired, firstResult)
|
||||
coVerify { identityService.preLogin(email = EMAIL) }
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
}
|
||||
|
||||
// Login with two factor data.
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||
twoFactorToken = "twoFactorTokenToStore",
|
||||
)
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.MasterPassword(
|
||||
username = EMAIL,
|
||||
password = PASSWORD_HASH,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
twoFactorData = TWO_FACTOR_DATA,
|
||||
)
|
||||
} returns successResponse.asSuccess()
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
userKey = successResponse.key!!,
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
masterPassword = PASSWORD,
|
||||
)
|
||||
} returns VaultUnlockResult.InvalidStateError
|
||||
every {
|
||||
successResponse.toUserState(
|
||||
previousUserState = null,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
val finalResult = repository.login(
|
||||
email = EMAIL,
|
||||
password = PASSWORD,
|
||||
twoFactorData = TWO_FACTOR_DATA,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(LoginResult.Error(errorMessage = null), finalResult)
|
||||
assertEquals(twoFactorResponse, repository.twoFactorResponse)
|
||||
fakeAuthDiskSource.assertTwoFactorToken(
|
||||
email = EMAIL,
|
||||
twoFactorToken = null,
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.syncIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login uses remembered two factor tokens`() = runTest {
|
||||
fakeAuthDiskSource.storeTwoFactorToken(EMAIL, "storedTwoFactorToken")
|
||||
|
@ -1978,7 +2208,100 @@ class AuthRepositoryTest {
|
|||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync`() =
|
||||
fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with MasteryKey`() =
|
||||
runTest {
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.AuthRequest(
|
||||
username = EMAIL,
|
||||
authRequestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns successResponse.asSuccess()
|
||||
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = null,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
method = AuthRequestMethod.MasterKey(
|
||||
authRequestKey = successResponse.key!!,
|
||||
protectedMasterKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
),
|
||||
),
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
val result = repository.login(
|
||||
email = EMAIL,
|
||||
requestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
asymmetricalKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = PASSWORD_HASH,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(LoginResult.Success, result)
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = "privateKey",
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = "key",
|
||||
)
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.AuthRequest(
|
||||
username = EMAIL,
|
||||
authRequestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
vaultRepository.syncIfNecessary()
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
method = AuthRequestMethod.MasterKey(
|
||||
authRequestKey = successResponse.key!!,
|
||||
protectedMasterKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login with device should return Error result when get token succeeds but unlock vault fails`() =
|
||||
runTest {
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
|
@ -2410,6 +2733,97 @@ class AuthRepositoryTest {
|
|||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with UserKey`() =
|
||||
runTest {
|
||||
val successResponse = GET_TOKEN_RESPONSE_SUCCESS
|
||||
coEvery {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.AuthRequest(
|
||||
username = EMAIL,
|
||||
authRequestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
} returns successResponse.asSuccess()
|
||||
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||
every {
|
||||
GET_TOKEN_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = null,
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
)
|
||||
} returns SINGLE_USER_STATE_1
|
||||
coEvery {
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
method = AuthRequestMethod.UserKey(
|
||||
protectedUserKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
),
|
||||
),
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
val result = repository.login(
|
||||
email = EMAIL,
|
||||
requestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
asymmetricalKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
masterPasswordHash = null,
|
||||
captchaToken = null,
|
||||
)
|
||||
assertEquals(LoginResult.Success, result)
|
||||
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||
fakeAuthDiskSource.assertPrivateKey(
|
||||
userId = USER_ID_1,
|
||||
privateKey = "privateKey",
|
||||
)
|
||||
fakeAuthDiskSource.assertUserKey(
|
||||
userId = USER_ID_1,
|
||||
userKey = "key",
|
||||
)
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.AuthRequest(
|
||||
username = EMAIL,
|
||||
authRequestId = DEVICE_REQUEST_ID,
|
||||
accessCode = DEVICE_ACCESS_CODE,
|
||||
),
|
||||
captchaToken = null,
|
||||
uniqueAppId = UNIQUE_APP_ID,
|
||||
)
|
||||
vaultRepository.syncIfNecessary()
|
||||
vaultRepository.unlockVault(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||
privateKey = successResponse.privateKey!!,
|
||||
organizationKeys = null,
|
||||
initUserCryptoMethod = InitUserCryptoMethod.AuthRequest(
|
||||
requestPrivateKey = DEVICE_REQUEST_PRIVATE_KEY,
|
||||
method = AuthRequestMethod.UserKey(
|
||||
protectedUserKey = DEVICE_ASYMMETRICAL_KEY,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
assertEquals(
|
||||
SINGLE_USER_STATE_1,
|
||||
fakeAuthDiskSource.userState,
|
||||
)
|
||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `SSO login get token succeeds with trusted device key and no keys should return Success, clear device key, update AuthState, update stored keys, and sync`() =
|
||||
|
@ -2539,7 +2953,7 @@ class AuthRepositoryTest {
|
|||
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = encryptedUserKey)
|
||||
fakeAuthDiskSource.assertDeviceKey(userId = USER_ID_1, deviceKey = deviceKey)
|
||||
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||
coVerify(exactly = 1) {
|
||||
coVerify {
|
||||
identityService.getToken(
|
||||
email = EMAIL,
|
||||
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||
|
@ -4739,7 +5153,7 @@ class AuthRepositoryTest {
|
|||
userId = SINGLE_USER_STATE_1.activeUserId,
|
||||
request = any(),
|
||||
)
|
||||
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
|
||||
} returns InitializeCryptoResult.AuthenticationError().asSuccess()
|
||||
|
||||
val result = repository.validatePin(pin = pin)
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class LoginResultExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `VaultUnlockResult with error message maps to LoginResult Error with correct message`() {
|
||||
val errorMessage = "foo"
|
||||
val result = VaultUnlockResult.AuthenticationError(errorMessage).toLoginErrorResult()
|
||||
assertEquals(LoginResult.Error(errorMessage), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `VaultUnlockResult with null error message as default maps to LoginResult Error with null message`() {
|
||||
val result = VaultUnlockResult.AuthenticationError().toLoginErrorResult()
|
||||
assertEquals(LoginResult.Error(errorMessage = null), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `VaultUnlockResult with no message value are mapped to LoginResult with null error message`() {
|
||||
val invalidStateResult = VaultUnlockResult.InvalidStateError.toLoginErrorResult()
|
||||
val genericErrorResult = VaultUnlockResult.GenericError.toLoginErrorResult()
|
||||
val expectedResult = LoginResult.Error(errorMessage = null)
|
||||
|
||||
assertEquals(expectedResult, invalidStateResult)
|
||||
assertEquals(expectedResult, genericErrorResult)
|
||||
}
|
||||
}
|
|
@ -330,11 +330,13 @@ class VaultSdkSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `initializeUserCrypto with BitwardenException failure should return AuthenticationError`() =
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initializeUserCrypto with BitwardenException failure should return AuthenticationError with message`() =
|
||||
runBlocking {
|
||||
val userId = "userId"
|
||||
val mockInitCryptoRequest = mockk<InitUserCryptoRequest>()
|
||||
val expectedException = BitwardenException.E(message = "")
|
||||
val expectedErrorMessage = "Whoopsy"
|
||||
val expectedException = BitwardenException.E(message = expectedErrorMessage)
|
||||
coEvery {
|
||||
clientCrypto.initializeUserCrypto(
|
||||
req = mockInitCryptoRequest,
|
||||
|
@ -345,7 +347,7 @@ class VaultSdkSourceTest {
|
|||
request = mockInitCryptoRequest,
|
||||
)
|
||||
assertEquals(
|
||||
InitializeCryptoResult.AuthenticationError.asSuccess(),
|
||||
InitializeCryptoResult.AuthenticationError(expectedErrorMessage).asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
|
@ -409,11 +411,13 @@ class VaultSdkSourceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `initializeOrgCrypto with BitwardenException failure should return AuthenticationError`() =
|
||||
@Suppress("MaxLineLength")
|
||||
fun `initializeOrgCrypto with BitwardenException failure should return AuthenticationError with correct message`() =
|
||||
runBlocking {
|
||||
val userId = "userId"
|
||||
val mockInitCryptoRequest = mockk<InitOrgCryptoRequest>()
|
||||
val expectedException = BitwardenException.E(message = "")
|
||||
val expectedErrorMessage = "Whoopsy2"
|
||||
val expectedException = BitwardenException.E(message = expectedErrorMessage)
|
||||
coEvery {
|
||||
clientCrypto.initializeOrgCrypto(
|
||||
req = mockInitCryptoRequest,
|
||||
|
@ -424,7 +428,7 @@ class VaultSdkSourceTest {
|
|||
request = mockInitCryptoRequest,
|
||||
)
|
||||
assertEquals(
|
||||
InitializeCryptoResult.AuthenticationError.asSuccess(),
|
||||
InitializeCryptoResult.AuthenticationError(expectedErrorMessage).asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
|
|
|
@ -938,7 +938,7 @@ class VaultLockManagerTest {
|
|||
),
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
|
||||
} returns InitializeCryptoResult.AuthenticationError().asSuccess()
|
||||
|
||||
assertEquals(
|
||||
emptyList<VaultUnlockData>(),
|
||||
|
@ -961,7 +961,7 @@ class VaultLockManagerTest {
|
|||
organizationKeys = organizationKeys,
|
||||
)
|
||||
|
||||
assertEquals(VaultUnlockResult.AuthenticationError, result)
|
||||
assertEquals(VaultUnlockResult.AuthenticationError(), result)
|
||||
assertEquals(
|
||||
emptyList<VaultUnlockData>(),
|
||||
vaultLockManager.vaultUnlockDataStateFlow.value,
|
||||
|
@ -1015,7 +1015,7 @@ class VaultLockManagerTest {
|
|||
userId = USER_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
|
||||
} returns InitializeCryptoResult.AuthenticationError().asSuccess()
|
||||
|
||||
assertEquals(
|
||||
emptyList<VaultUnlockData>(),
|
||||
|
@ -1038,7 +1038,7 @@ class VaultLockManagerTest {
|
|||
organizationKeys = organizationKeys,
|
||||
)
|
||||
|
||||
assertEquals(VaultUnlockResult.AuthenticationError, result)
|
||||
assertEquals(VaultUnlockResult.AuthenticationError(), result)
|
||||
assertEquals(
|
||||
emptyList<VaultUnlockData>(),
|
||||
vaultLockManager.vaultUnlockDataStateFlow.value,
|
||||
|
|
|
@ -440,7 +440,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPassword(password)
|
||||
} returns VaultUnlockResult.AuthenticationError
|
||||
} returns VaultUnlockResult.AuthenticationError()
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
|
@ -577,7 +577,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithPin(pin)
|
||||
} returns VaultUnlockResult.AuthenticationError
|
||||
} returns VaultUnlockResult.AuthenticationError()
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
|
@ -726,7 +726,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createViewModel(state = initialState)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithBiometrics()
|
||||
} returns VaultUnlockResult.AuthenticationError
|
||||
} returns VaultUnlockResult.AuthenticationError()
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockSuccess(CIPHER))
|
||||
|
||||
|
|
Loading…
Reference in a new issue