mirror of
https://github.com/bitwarden/android.git
synced 2026-06-12 14:07:54 -05:00
Compare commits
9 Commits
optional-a
...
pm-25933/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59332ac8c7 | ||
|
|
7c5a6ac2d8 | ||
|
|
d07e8f9ae2 | ||
|
|
98d03614f4 | ||
|
|
6300d5d789 | ||
|
|
b200af36a0 | ||
|
|
0ff6bb4aeb | ||
|
|
a0bbc1a313 | ||
|
|
5ff44d6133 |
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk.util
|
||||
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.network.model.KdfJsonRequest
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.KdfTypeJson.ARGON2_ID
|
||||
import com.bitwarden.network.model.KdfTypeJson.PBKDF2_SHA256
|
||||
@@ -13,3 +14,22 @@ fun Kdf.toKdfTypeJson(): KdfTypeJson =
|
||||
is Kdf.Argon2id -> ARGON2_ID
|
||||
is Kdf.Pbkdf2 -> PBKDF2_SHA256
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a [Kdf] to [KdfJsonRequest]
|
||||
*/
|
||||
fun Kdf.toKdfRequestModel(): KdfJsonRequest =
|
||||
when (this) {
|
||||
is Kdf.Argon2id -> KdfJsonRequest(
|
||||
kdfType = toKdfTypeJson(),
|
||||
iterations = iterations.toInt(),
|
||||
memory = memory.toInt(),
|
||||
parallelism = parallelism.toInt(),
|
||||
)
|
||||
is Kdf.Pbkdf2 -> KdfJsonRequest(
|
||||
kdfType = toKdfTypeJson(),
|
||||
iterations = iterations.toInt(),
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
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.VerifiedOrganizationDomainSsoDetailsResult
|
||||
@@ -351,6 +352,16 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateM
|
||||
*/
|
||||
suspend fun getPasswordStrength(email: String? = null, password: String): PasswordStrengthResult
|
||||
|
||||
/**
|
||||
* Checks if their current settings are below the minimums and needs update
|
||||
*/
|
||||
suspend fun needsKdfUpdateToMinimums(): Boolean
|
||||
|
||||
/**
|
||||
* Updates the user's KDF settings if their current settings are below the minimums
|
||||
*/
|
||||
suspend fun updateKdfToMinimumsIfNeeded(password: String): UpdateKdfMinimumsResult
|
||||
|
||||
/**
|
||||
* Validates the master password for the current logged in user.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,9 @@ import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.bitwarden.network.model.DeleteAccountResponseJson
|
||||
import com.bitwarden.network.model.GetTokenResponseJson
|
||||
import com.bitwarden.network.model.IdentityTokenAuthModel
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.MasterPasswordAuthenticationDataJsonRequest
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJsonRequest
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.PasswordHintResponseJson
|
||||
import com.bitwarden.network.model.PolicyTypeJson
|
||||
@@ -33,6 +36,7 @@ import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.TwoFactorAuthMethod
|
||||
import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
import com.bitwarden.network.model.VerifyEmailTokenRequestJson
|
||||
import com.bitwarden.network.model.VerifyEmailTokenResponseJson
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
@@ -49,6 +53,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toInt
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
@@ -77,6 +82,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
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.VerifiedOrganizationDomainSsoDetailsResult
|
||||
@@ -1212,6 +1218,98 @@ class AuthRepositoryImpl(
|
||||
onFailure = { PasswordStrengthResult.Error(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun needsKdfUpdateToMinimums(): Boolean {
|
||||
val account = authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(activeUserId)
|
||||
?: return false
|
||||
|
||||
return account.profile.kdfType == KdfTypeJson.PBKDF2_SHA256 &&
|
||||
account.profile.kdfIterations != null &&
|
||||
account.profile.kdfIterations < DEFAULT_PBKDF2_ITERATIONS
|
||||
}
|
||||
|
||||
override suspend fun updateKdfToMinimumsIfNeeded(password: String): UpdateKdfMinimumsResult {
|
||||
val userId = activeUserId ?: return UpdateKdfMinimumsResult.ActiveAccountNotFound
|
||||
val account = authDiskSource.userState?.accounts?.get(userId)
|
||||
?: return UpdateKdfMinimumsResult.ActiveAccountNotFound
|
||||
account.profile
|
||||
|
||||
// Check if needs update kdf
|
||||
if (!needsKdfUpdateToMinimums()) {
|
||||
return UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
|
||||
// Generate updated KDF data
|
||||
val updateKdfResponse = vaultSdkSource.makeUpdateKdf(
|
||||
userId = userId,
|
||||
password = password,
|
||||
kdf = account.profile.toSdkParams(),
|
||||
).getOrElse { error ->
|
||||
return UpdateKdfMinimumsResult.Error(error = error)
|
||||
}
|
||||
|
||||
val authData = updateKdfResponse.masterPasswordAuthenticationData
|
||||
val oldAuthData = updateKdfResponse.oldMasterPasswordAuthenticationData
|
||||
val unlockData = updateKdfResponse.masterPasswordUnlockData
|
||||
// Send update to server
|
||||
val updateKdfRequest = UpdateKdfJsonRequest(
|
||||
authenticationData = MasterPasswordAuthenticationDataJsonRequest(
|
||||
kdf = authData.kdf.toKdfRequestModel(),
|
||||
masterPasswordAuthenticationHash =
|
||||
authData.masterPasswordAuthenticationHash,
|
||||
salt = authData.salt,
|
||||
),
|
||||
key = unlockData.masterKeyWrappedUserKey,
|
||||
masterPasswordHash = oldAuthData.masterPasswordAuthenticationHash,
|
||||
newMasterPasswordHash = authData.masterPasswordAuthenticationHash,
|
||||
unlockData = MasterPasswordUnlockDataJsonRequest(
|
||||
kdf = unlockData.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = unlockData.masterKeyWrappedUserKey,
|
||||
salt = unlockData.salt,
|
||||
),
|
||||
)
|
||||
|
||||
accountsService
|
||||
.updateKdf(body = updateKdfRequest)
|
||||
.getOrElse { error ->
|
||||
return UpdateKdfMinimumsResult.Error(error = error)
|
||||
}
|
||||
|
||||
// TODO CHECK IF WE NEED TO SAVE NEW VALUES TO STATE
|
||||
/**
|
||||
// Update local storage
|
||||
authDiskSource.storeMasterPasswordHash(
|
||||
userId = profile.userId,
|
||||
passwordHash = updateKdfResponse
|
||||
.masterPasswordAuthenticationData.masterPasswordAuthenticationHash,
|
||||
)
|
||||
authDiskSource.storeUserKey(
|
||||
userId = profile.userId,
|
||||
userKey = updateKdfResponse.masterPasswordUnlockData.masterKeyWrappedUserKey,
|
||||
)
|
||||
|
||||
// Update profile with new KDF parameters
|
||||
val updatedProfile = profile.copy(
|
||||
kdfType = authData.kdf.toKdfRequestModel().kdfType,
|
||||
kdfIterations = authData.kdf.toKdfRequestModel().iterations,
|
||||
kdfMemory = authData.kdf.toKdfRequestModel().memory,
|
||||
kdfParallelism = authData.kdf.toKdfRequestModel().parallelism,
|
||||
)
|
||||
|
||||
val updatedUserState = authDiskSource.userState?.copy(
|
||||
accounts = authDiskSource.userState!!.accounts.toMutableMap().apply {
|
||||
this[profile.userId] = account.copy(profile = updatedProfile)
|
||||
}
|
||||
)
|
||||
authDiskSource.userState = updatedUserState
|
||||
|
||||
**/
|
||||
|
||||
return UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
|
||||
override suspend fun validatePassword(password: String): ValidatePasswordResult {
|
||||
val userId = activeUserId ?: return ValidatePasswordResult.Error(NoActiveUserException())
|
||||
return authDiskSource
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of updating a user's kdf settings to minimums
|
||||
*/
|
||||
sealed class UpdateKdfMinimumsResult {
|
||||
/**
|
||||
* Active account was not found
|
||||
*/
|
||||
object ActiveAccountNotFound : UpdateKdfMinimumsResult()
|
||||
|
||||
/**
|
||||
* Account with userId was not found
|
||||
*/
|
||||
object AccountNotFound : UpdateKdfMinimumsResult()
|
||||
|
||||
/**
|
||||
* There was an error updating user to minimum kdf settings.
|
||||
*
|
||||
* @param error the error.
|
||||
*/
|
||||
data class Error(
|
||||
val error: Throwable?,
|
||||
) : UpdateKdfMinimumsResult()
|
||||
|
||||
/**
|
||||
* Updated user to minimum kdf settings successfully.
|
||||
*/
|
||||
object Success : UpdateKdfMinimumsResult()
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
@@ -491,4 +492,13 @@ interface VaultSdkSource {
|
||||
fido2CredentialStore: Fido2CredentialStore,
|
||||
relyingPartyId: String,
|
||||
): Result<List<Fido2CredentialAutofillView>>
|
||||
|
||||
/**
|
||||
* Updates the KDF settings for the user with the given [userId].
|
||||
*/
|
||||
suspend fun makeUpdateKdf(
|
||||
userId: String,
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
): Result<UpdateKdfResponse>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.crypto.TrustDeviceResponse
|
||||
@@ -474,7 +475,7 @@ class VaultSdkSourceImpl(
|
||||
): Result<UpdatePasswordResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.crypto()
|
||||
.updatePassword(newPassword = newPassword)
|
||||
.makeUpdatePassword(newPassword = newPassword)
|
||||
}
|
||||
|
||||
override suspend fun exportVaultDataToString(
|
||||
@@ -604,4 +605,14 @@ class VaultSdkSourceImpl(
|
||||
)
|
||||
.silentlyDiscoverCredentials(relyingPartyId)
|
||||
}
|
||||
|
||||
override suspend fun makeUpdateKdf(
|
||||
userId: String,
|
||||
password: String,
|
||||
kdf: Kdf,
|
||||
): Result<UpdateKdfResponse> = runCatchingWithLogs {
|
||||
getClient(userId = userId)
|
||||
.crypto()
|
||||
.makeUpdateKdf(password = password, kdf = kdf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
@@ -28,36 +29,55 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
/**
|
||||
* Represents a Bitwarden-styled dialog for entering your master password.
|
||||
*
|
||||
* @param title The title of the dialog.
|
||||
* @param message The message of the dialog.
|
||||
* @param confirmButtonText The text of the confirm button.
|
||||
* @param dismissButtonText The text of the dismiss button.
|
||||
* @param onConfirmClick called when the confirm button is clicked and emits the entered password.
|
||||
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
|
||||
* tapping outside of it).
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenMasterPasswordDialog(
|
||||
title: String = stringResource(id = BitwardenString.password_confirmation),
|
||||
message: String = stringResource(id = BitwardenString.password_confirmation_desc),
|
||||
confirmButtonText: String = stringResource(id = BitwardenString.submit),
|
||||
dismissButtonText: String = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick: (masterPassword: String) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
filledButtonStyle: Boolean = false,
|
||||
) {
|
||||
var masterPassword by remember { mutableStateOf("") }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.cancel),
|
||||
label = dismissButtonText,
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.testTag("DismissAlertButton"),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.submit),
|
||||
isEnabled = masterPassword.isNotEmpty(),
|
||||
onClick = { onConfirmClick(masterPassword) },
|
||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||
)
|
||||
if (filledButtonStyle) {
|
||||
BitwardenFilledButton(
|
||||
label = confirmButtonText,
|
||||
isEnabled = masterPassword.isNotEmpty(),
|
||||
onClick = { onConfirmClick(masterPassword) },
|
||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||
)
|
||||
} else {
|
||||
BitwardenTextButton(
|
||||
label = confirmButtonText,
|
||||
isEnabled = masterPassword.isNotEmpty(),
|
||||
onClick = { onConfirmClick(masterPassword) },
|
||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.password_confirmation),
|
||||
text = title,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier.testTag("AlertTitleText"),
|
||||
)
|
||||
@@ -65,7 +85,7 @@ fun BitwardenMasterPasswordDialog(
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.password_confirmation_desc),
|
||||
text = message,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier.testTag("AlertContentText"),
|
||||
)
|
||||
|
||||
@@ -59,6 +59,7 @@ import com.bitwarden.ui.platform.manager.IntentManager
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalAppReviewManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
@@ -404,6 +405,7 @@ private fun VaultScreenScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun VaultDialogs(
|
||||
dialogState: VaultState.DialogState?,
|
||||
@@ -462,6 +464,19 @@ private fun VaultDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
is VaultState.DialogState.VaultLoadKdfUpdateRequired -> {
|
||||
BitwardenMasterPasswordDialog(
|
||||
title = dialogState.title(),
|
||||
message = dialogState.message(),
|
||||
dismissButtonText = stringResource(BitwardenString.later),
|
||||
onConfirmClick = {
|
||||
vaultHandlers.onKdfUpdatePasswordRepromptSubmit(it)
|
||||
},
|
||||
onDismissRequest = vaultHandlers.dialogDismiss,
|
||||
filledButtonStyle = true,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.toast.ToastManager
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.util.persistentListOfNotNull
|
||||
import com.bitwarden.data.repository.util.baseIconUrl
|
||||
@@ -24,6 +25,7 @@ import com.bitwarden.vault.DecryptCipherListResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
|
||||
@@ -97,6 +99,7 @@ class VaultViewModel @Inject constructor(
|
||||
private val reviewPromptManager: ReviewPromptManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
private val toastManager: ToastManager,
|
||||
snackbarRelayManager: SnackbarRelayManager,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
initialState = run {
|
||||
@@ -256,6 +259,10 @@ class VaultViewModel @Inject constructor(
|
||||
VaultAction.ShareAllCipherDecryptionErrorsClick -> {
|
||||
handleShareAllCipherDecryptionErrorsClick()
|
||||
}
|
||||
|
||||
is VaultAction.KdfUpdatePasswordRepromptSubmit -> {
|
||||
handleKdfUpdatePasswordRepromptSubmit(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -909,7 +916,8 @@ class VaultViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
val shouldShowDecryptionAlert = !state.hasShownDecryptionFailureAlert &&
|
||||
vaultData.data.decryptCipherListResult.failures.isNotEmpty()
|
||||
vaultData.data.decryptCipherListResult.failures.isNotEmpty() &&
|
||||
state.dialog == null
|
||||
|
||||
updateVaultState(
|
||||
vaultData = vaultData.data,
|
||||
@@ -929,6 +937,21 @@ class VaultViewModel @Inject constructor(
|
||||
state.hasShownDecryptionFailureAlert
|
||||
},
|
||||
)
|
||||
|
||||
// Check if user needs to update kdf settings to minimums
|
||||
viewModelScope.launch {
|
||||
if (authRepository.needsKdfUpdateToMinimums()) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
@Suppress("MaxLineLength")
|
||||
currentState.copy(
|
||||
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = BitwardenString.update_your_encryption_settings.asText(),
|
||||
message = BitwardenString.the_new_recommended_encryption_settings_will_improve_your_account_security_desc_long.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVaultState(
|
||||
@@ -1089,6 +1112,48 @@ class VaultViewModel @Inject constructor(
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
|
||||
private fun handleKdfUpdatePasswordRepromptSubmit(
|
||||
action: VaultAction.KdfUpdatePasswordRepromptSubmit,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.updateKdfToMinimumsIfNeeded(action.password)
|
||||
when (result) {
|
||||
UpdateKdfMinimumsResult.AccountNotFound -> {
|
||||
showGenericError()
|
||||
Timber.e(message = "Failed to update kdf to minimums: Account not found")
|
||||
}
|
||||
UpdateKdfMinimumsResult.ActiveAccountNotFound -> {
|
||||
showGenericError()
|
||||
Timber.e(message = "Failed to update kdf to minimums: Active account not found")
|
||||
}
|
||||
is UpdateKdfMinimumsResult.Error -> {
|
||||
showGenericError(error = result.error)
|
||||
Timber.e(message = "Failed to update kdf to minimums: ${result.error}")
|
||||
}
|
||||
UpdateKdfMinimumsResult.Success -> {
|
||||
toastManager.show(messageId = BitwardenString.encryption_settings_updated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGenericError(
|
||||
error: Throwable? = null,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
BitwardenString.an_error_has_occurred.asText(),
|
||||
BitwardenString.generic_error_message.asText(),
|
||||
error = error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1466,6 +1531,15 @@ data class VaultState(
|
||||
val cipherCount: Int,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a dialog indicating that the user needs to update their kdf settings.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultLoadKdfUpdateRequired(
|
||||
val title: Text,
|
||||
val message: Text,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents an error dialog with the given [title] and [message].
|
||||
*/
|
||||
@@ -1715,6 +1789,13 @@ sealed class VaultAction {
|
||||
val selectedCipherId: String,
|
||||
) : VaultAction()
|
||||
|
||||
/**
|
||||
* Click to submit the update kdf password reprompt form.
|
||||
*/
|
||||
data class KdfUpdatePasswordRepromptSubmit(
|
||||
val password: String,
|
||||
) : VaultAction()
|
||||
|
||||
/**
|
||||
* Click to share all cipher decryption error details.
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,7 @@ data class VaultHandlers(
|
||||
val dismissFlightRecorderSnackbar: () -> Unit,
|
||||
val onShareCipherDecryptionErrorClick: (selectedCipherId: String) -> Unit,
|
||||
val onShareAllCipherDecryptionErrorsClick: () -> Unit,
|
||||
val onKdfUpdatePasswordRepromptSubmit: (password: String) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@@ -139,6 +140,12 @@ data class VaultHandlers(
|
||||
VaultAction.ShareAllCipherDecryptionErrorsClick,
|
||||
)
|
||||
},
|
||||
onKdfUpdatePasswordRepromptSubmit =
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultAction.KdfUpdatePasswordRepromptSubmit(it),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.MasterPasswordAuthenticationData
|
||||
import com.bitwarden.core.MasterPasswordUnlockData
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
@@ -99,6 +102,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
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.VerifiedOrganizationDomainSsoDetailsResult
|
||||
@@ -158,7 +162,8 @@ import java.time.ZonedDateTime
|
||||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class AuthRepositoryTest {
|
||||
class
|
||||
AuthRepositoryTest {
|
||||
|
||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||
private val accountsService: AccountsService = mockk()
|
||||
@@ -6888,6 +6893,212 @@ class AuthRepositoryTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with no active user should return false`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with kdfType null should return false`() = runTest {
|
||||
val nullKdfProfile = PROFILE_1.copy(
|
||||
kdfType = null,
|
||||
kdfIterations = null,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(profile = nullKdfProfile),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with PBKDF2 below minimum iterations should return true`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with PBKDF2 meeting minimum iterations should return false`() =
|
||||
runTest {
|
||||
val sufficientIterationsProfile = PROFILE_1.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 600000, // Meets minimum
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(profile = sufficientIterationsProfile),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with Argon2id below minimum parameters should return false`() =
|
||||
runTest {
|
||||
val lowArgon2idProfile = PROFILE_1.copy(
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 1, // Below minimum of 3
|
||||
kdfMemory = 16, // Below minimum of 64
|
||||
kdfParallelism = 1, // Below minimum of 4
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(profile = lowArgon2idProfile),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with Argon2id meeting minimum parameters should return false`() =
|
||||
runTest {
|
||||
val sufficientArgon2idProfile = PROFILE_1.copy(
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000, // Meets minimum
|
||||
kdfMemory = 64,
|
||||
kdfParallelism = 4,
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(profile = sufficientArgon2idProfile),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdfToMinimumsIfNeeded with no active user should return ActiveAccountNotFound`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = repository.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(
|
||||
UpdateKdfMinimumsResult.ActiveAccountNotFound,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdfToMinimumsIfNeeded with minimum Kdf iterations should return Success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1.copy(
|
||||
profile = PROFILE_1.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 600000,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = repository.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(
|
||||
UpdateKdfMinimumsResult.Success,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdfToMinimumsIfNeeded if sdk throws an error should return Error`() = runTest {
|
||||
val error = Throwable("Kdf update failed")
|
||||
coEvery {
|
||||
vaultSdkSource.makeUpdateKdf(
|
||||
userId = any(),
|
||||
password = any(),
|
||||
kdf = any(),
|
||||
)
|
||||
} returns error.asFailure()
|
||||
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
val result = repository.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(
|
||||
UpdateKdfMinimumsResult.Error(error = error),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateKdfToMinimumsIfNeeded with PBKDF2 below minimums and updateKdf API failure should return Error`() = runTest {
|
||||
val error = Throwable("API failed")
|
||||
coEvery {
|
||||
vaultSdkSource.makeUpdateKdf(
|
||||
userId = any(),
|
||||
password = any(),
|
||||
kdf = any(),
|
||||
)
|
||||
} returns UPDATE_KDF_RESPONSE.asSuccess()
|
||||
|
||||
coEvery {
|
||||
accountsService.updateKdf(any())
|
||||
} returns error.asFailure()
|
||||
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
val result = repository.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(UpdateKdfMinimumsResult.Error(error = error), result)
|
||||
coVerify(exactly = 1) {
|
||||
accountsService.updateKdf(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateKdfToMinimumsIfNeeded with PBKDF2 below minimums should return Success`() =
|
||||
runTest {
|
||||
coEvery {
|
||||
vaultSdkSource.makeUpdateKdf(
|
||||
userId = any(),
|
||||
password = any(),
|
||||
kdf = any(),
|
||||
)
|
||||
} returns UPDATE_KDF_RESPONSE.asSuccess()
|
||||
|
||||
coEvery {
|
||||
accountsService.updateKdf(any())
|
||||
} returns Unit.asSuccess()
|
||||
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
val result = repository.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(UpdateKdfMinimumsResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
accountsService.updateKdf(any())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
@@ -7132,5 +7343,23 @@ class AuthRepositoryTest {
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val UPDATE_KDF_RESPONSE = UpdateKdfResponse(
|
||||
masterPasswordAuthenticationData = MasterPasswordAuthenticationData(
|
||||
kdf = mockk<Kdf>(relaxed = true),
|
||||
salt = "mockSalt",
|
||||
masterPasswordAuthenticationHash = "mockHash",
|
||||
),
|
||||
masterPasswordUnlockData = MasterPasswordUnlockData(
|
||||
kdf = mockk<Kdf>(relaxed = true),
|
||||
masterKeyWrappedUserKey = "mockKey",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
oldMasterPasswordAuthenticationData = MasterPasswordAuthenticationData(
|
||||
kdf = mockk<Kdf>(relaxed = true),
|
||||
salt = "mockSalt",
|
||||
masterPasswordAuthenticationHash = "mockHash",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.MasterPasswordAuthenticationData
|
||||
import com.bitwarden.core.MasterPasswordUnlockData
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.UpdatePasswordResponse
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
@@ -1079,7 +1082,7 @@ class VaultSdkSourceTest {
|
||||
newKey = newKey,
|
||||
)
|
||||
coEvery {
|
||||
clientCrypto.updatePassword(
|
||||
clientCrypto.makeUpdatePassword(
|
||||
newPassword = newPassword,
|
||||
)
|
||||
} returns updatePasswordResponse
|
||||
@@ -1427,6 +1430,63 @@ class VaultSdkSourceTest {
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `makeUpdateKdf should return results when successful`() = runTest {
|
||||
val kdf = mockk<Kdf>()
|
||||
val updateKdfResponse = UpdateKdfResponse(
|
||||
masterPasswordAuthenticationData = MasterPasswordAuthenticationData(
|
||||
kdf = kdf,
|
||||
salt = "mockSalt",
|
||||
masterPasswordAuthenticationHash = "mockHash",
|
||||
),
|
||||
masterPasswordUnlockData = MasterPasswordUnlockData(
|
||||
kdf = kdf,
|
||||
masterKeyWrappedUserKey = "mockKey",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
oldMasterPasswordAuthenticationData = MasterPasswordAuthenticationData(
|
||||
kdf = kdf,
|
||||
salt = "mockSalt",
|
||||
masterPasswordAuthenticationHash = "mockHash",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
clientCrypto.makeUpdateKdf(
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
} returns updateKdfResponse
|
||||
|
||||
val result = vaultSdkSource.makeUpdateKdf(
|
||||
userId = "mockUserId",
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
updateKdfResponse.asSuccess(),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `makeUpdateKdf should return Failure when Bitwarden exception is thrown`() =
|
||||
runTest {
|
||||
val kdf = mockk<Kdf>()
|
||||
coEvery {
|
||||
clientCrypto.makeUpdateKdf(
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
} throws BitwardenException.E("mockException")
|
||||
val result = vaultSdkSource.makeUpdateKdf(
|
||||
userId = "mockUserId",
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_SIGNATURE = "0987654321ABCDEF"
|
||||
|
||||
@@ -784,6 +784,90 @@ class VaultScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `vault load KDF update required dialog should be shown or hidden according to the state`() {
|
||||
val dialogTitle = "Master Password Update"
|
||||
val dialogMessage = "Your master password does not meet the current security requirements."
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogTitle)
|
||||
.assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogMessage)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = dialogTitle.asText(),
|
||||
message = dialogMessage.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogTitle)
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogMessage)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `confirm button click on the VaultLoadKdfUpdateRequired dialog should send KdfUpdatePasswordRepromptSubmit`() {
|
||||
val dialogTitle = "Master Password Update"
|
||||
val dialogMessage = "Your master password does not meet the current security requirements."
|
||||
val testPassword = "test_password"
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = dialogTitle.asText(),
|
||||
message = dialogMessage.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Enter password in the input field
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password")
|
||||
.performTextInput(testPassword)
|
||||
|
||||
// Click confirm button
|
||||
composeTestRule
|
||||
.onNodeWithText("Submit")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.KdfUpdatePasswordRepromptSubmit(testPassword),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `later button click on the VaultLoadKdfUpdateRequired dialog should send DialogDismiss`() {
|
||||
val dialogTitle = "Master Password Update"
|
||||
val dialogMessage = "Your master password does not meet the current security requirements."
|
||||
val laterText = "Later"
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = dialogTitle.asText(),
|
||||
message = dialogMessage.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(laterText)
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultAction.DialogDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncing dialog should be displayed according to state`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.bitwarden.network.model.DeleteAccountRequestJson
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
import com.bitwarden.network.model.VerifyOtpRequestJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.HTTP
|
||||
@@ -36,6 +37,12 @@ internal interface AuthenticatedAccountsApi {
|
||||
@POST("/accounts/request-otp")
|
||||
suspend fun requestOtp(): NetworkResult<Unit>
|
||||
|
||||
/**
|
||||
* Update the KDF settings for the current account.
|
||||
*/
|
||||
@POST("/accounts/kdf")
|
||||
suspend fun updateKdf(@Body body: UpdateKdfJsonRequest): NetworkResult<Unit>
|
||||
|
||||
@POST("/accounts/verify-otp")
|
||||
suspend fun verifyOtp(
|
||||
@Body body: VerifyOtpRequestJson,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to create the kdf settings.
|
||||
*/
|
||||
@Serializable
|
||||
data class KdfJsonRequest(
|
||||
@SerialName("KdfType")
|
||||
val kdfType: KdfTypeJson,
|
||||
|
||||
@SerialName("Iterations")
|
||||
val iterations: Int,
|
||||
|
||||
@SerialName("Memory")
|
||||
val memory: Int?,
|
||||
|
||||
@SerialName("Parallelism")
|
||||
val parallelism: Int?,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to authenticate with the master password.
|
||||
*/
|
||||
@Serializable
|
||||
data class MasterPasswordAuthenticationDataJsonRequest(
|
||||
@SerialName("Kdf")
|
||||
val kdf: KdfJsonRequest,
|
||||
|
||||
@SerialName("MasterPasswordAuthenticationHash")
|
||||
val masterPasswordAuthenticationHash: String,
|
||||
|
||||
@SerialName("Salt")
|
||||
val salt: String,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to unlock with the master password.
|
||||
*/
|
||||
@Serializable
|
||||
data class MasterPasswordUnlockDataJsonRequest(
|
||||
@SerialName("Kdf")
|
||||
val kdf: KdfJsonRequest,
|
||||
|
||||
@SerialName("MasterKeyWrappedUserKey")
|
||||
val masterKeyWrappedUserKey: String,
|
||||
|
||||
@SerialName("Salt")
|
||||
val salt: String,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the request body used to update the user's kdf settings.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateKdfJsonRequest(
|
||||
@SerialName("authenticationData")
|
||||
val authenticationData: MasterPasswordAuthenticationDataJsonRequest,
|
||||
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String,
|
||||
|
||||
@SerialName("newMasterPasswordHash")
|
||||
val newMasterPasswordHash: String,
|
||||
|
||||
@SerialName("unlockData")
|
||||
val unlockData: MasterPasswordUnlockDataJsonRequest,
|
||||
)
|
||||
@@ -8,6 +8,7 @@ import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
|
||||
/**
|
||||
* Provides an API for querying accounts endpoints.
|
||||
@@ -109,4 +110,9 @@ interface AccountsService {
|
||||
accessToken: String,
|
||||
masterKey: String,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Update the KDF settings for the current account.
|
||||
*/
|
||||
suspend fun updateKdf(body: UpdateKdfJsonRequest): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
import com.bitwarden.network.model.VerifyOtpRequestJson
|
||||
import com.bitwarden.network.model.toBitwardenError
|
||||
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
@@ -183,4 +184,9 @@ internal class AccountsServiceImpl(
|
||||
body = KeyConnectorMasterKeyRequestJson(masterKey = masterKey),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun updateKdf(body: UpdateKdfJsonRequest): Result<Unit> =
|
||||
authenticatedAccountsApi
|
||||
.updateKdf(body)
|
||||
.toResult()
|
||||
}
|
||||
|
||||
@@ -6,15 +6,19 @@ import com.bitwarden.network.api.AuthenticatedKeyConnectorApi
|
||||
import com.bitwarden.network.api.UnauthenticatedAccountsApi
|
||||
import com.bitwarden.network.api.UnauthenticatedKeyConnectorApi
|
||||
import com.bitwarden.network.base.BaseServiceTest
|
||||
import com.bitwarden.network.model.KdfJsonRequest
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.KeyConnectorKeyRequestJson
|
||||
import com.bitwarden.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.bitwarden.network.model.MasterPasswordAuthenticationDataJsonRequest
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJsonRequest
|
||||
import com.bitwarden.network.model.PasswordHintResponseJson
|
||||
import com.bitwarden.network.model.RegisterRequestJson
|
||||
import com.bitwarden.network.model.ResendEmailRequestJson
|
||||
import com.bitwarden.network.model.ResendNewDeviceOtpRequestJson
|
||||
import com.bitwarden.network.model.ResetPasswordRequestJson
|
||||
import com.bitwarden.network.model.SetPasswordRequestJson
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -264,4 +268,50 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||
)
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdf success should return Success`() = runTest {
|
||||
val response = MockResponse().setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
|
||||
val result = service.updateKdf(body = UPDATE_KDF_REQUEST)
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdf failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
|
||||
val result = service.updateKdf(body = UPDATE_KDF_REQUEST)
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
private val UPDATE_KDF_REQUEST = UpdateKdfJsonRequest(
|
||||
authenticationData = MasterPasswordAuthenticationDataJsonRequest(
|
||||
kdf = KdfJsonRequest(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = 7,
|
||||
memory = 1,
|
||||
parallelism = 2,
|
||||
),
|
||||
masterPasswordAuthenticationHash = "mockMasterPasswordHash",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
key = "mockKey",
|
||||
masterPasswordHash = "mockMasterPasswordHash",
|
||||
newMasterPasswordHash = "mockNewMasterPasswordHash",
|
||||
unlockData = MasterPasswordUnlockDataJsonRequest(
|
||||
kdf = KdfJsonRequest(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = 7,
|
||||
memory = 1,
|
||||
parallelism = 2,
|
||||
),
|
||||
masterKeyWrappedUserKey = "mockMasterPasswordKey",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1084,4 +1084,8 @@ Do you want to switch to this account?</string>
|
||||
<string name="default_uri_match_detection_description_advanced_options">URI match detection controls how Bitwarden identifies autofill suggestions.\n<annotation emphasis="bold">Warning:</annotation> “Starts with” is an advanced option with increased risk of exposing credentials.</string>
|
||||
<string name="advanced_option_with_increased_risk_of_exposing_credentials">“Starts with” is an advanced option with increased risk of exposing credentials.</string>
|
||||
<string name="advanced_option_increased_risk_exposing_credentials_used_incorrectly">“Regular expression” is an advanced option with increased risk of exposing credentials if used incorrectly.</string>
|
||||
<string name="later">Later</string>
|
||||
<string name="encryption_settings_updated">Encryption settings updated</string>
|
||||
<string name="update_your_encryption_settings">Update your encryption settings</string>
|
||||
<string name="the_new_recommended_encryption_settings_will_improve_your_account_security_desc_long">The new recommended encryption settings will improve your account security. Enter your master password to update now.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user