mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-23278] Upgrade user KDF settings to minimums (#5955)
Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
parent
44c373a354
commit
d98ff6478f
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk.util
|
||||
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.network.model.KdfJson
|
||||
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,23 @@ fun Kdf.toKdfTypeJson(): KdfTypeJson =
|
||||
is Kdf.Argon2id -> ARGON2_ID
|
||||
is Kdf.Pbkdf2 -> PBKDF2_SHA256
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a [Kdf] to [KdfJson]
|
||||
*/
|
||||
fun Kdf.toKdfRequestModel(): KdfJson =
|
||||
when (this) {
|
||||
is Kdf.Argon2id -> KdfJson(
|
||||
kdfType = toKdfTypeJson(),
|
||||
iterations = iterations.toInt(),
|
||||
memory = memory.toInt(),
|
||||
parallelism = parallelism.toInt(),
|
||||
)
|
||||
|
||||
is Kdf.Pbkdf2 -> KdfJson(
|
||||
kdfType = toKdfTypeJson(),
|
||||
iterations = iterations.toInt(),
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
|
||||
/**
|
||||
* An interface to manage the user KDF settings.
|
||||
*/
|
||||
interface KdfManager {
|
||||
|
||||
/**
|
||||
* Checks if user's current KDF settings are below the minimums and needs update
|
||||
*/
|
||||
fun needsKdfUpdateToMinimums(): Boolean
|
||||
|
||||
/**
|
||||
* Updates the user's KDF settings if below the minimums
|
||||
*/
|
||||
suspend fun updateKdfToMinimumsIfNeeded(password: String): UpdateKdfMinimumsResult
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.MasterPasswordAuthenticationDataJson
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
|
||||
import com.bitwarden.network.model.UpdateKdfJsonRequest
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toUserStateJsonKdfUpdatedMinimums
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import kotlin.collections.get
|
||||
|
||||
/**
|
||||
* Default implementation of [KdfManager].
|
||||
*/
|
||||
class KdfManagerImpl(
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val accountsService: AccountsService,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : KdfManager {
|
||||
|
||||
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
|
||||
|
||||
override fun needsKdfUpdateToMinimums(): Boolean {
|
||||
if (!featureFlagManager.getFeatureFlag(FlagKey.ForceUpdateKdfSettings)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val account = authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(activeUserId)
|
||||
?: return false
|
||||
|
||||
if (account.profile.userDecryptionOptions != null &&
|
||||
!account.profile.userDecryptionOptions.hasMasterPassword
|
||||
) {
|
||||
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
|
||||
|
||||
if (!needsKdfUpdateToMinimums()) {
|
||||
return UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
return vaultSdkSource
|
||||
.makeUpdateKdf(
|
||||
userId = userId,
|
||||
password = password,
|
||||
kdf = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()),
|
||||
)
|
||||
.flatMap {
|
||||
accountsService.updateKdf(createUpdateKdfRequest(it))
|
||||
}
|
||||
.fold(
|
||||
onSuccess = {
|
||||
authDiskSource.userState = authDiskSource.userState
|
||||
?.toUserStateJsonKdfUpdatedMinimums()
|
||||
UpdateKdfMinimumsResult.Success
|
||||
},
|
||||
onFailure = { UpdateKdfMinimumsResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUpdateKdfRequest(response: UpdateKdfResponse): UpdateKdfJsonRequest {
|
||||
val authData = response.masterPasswordAuthenticationData
|
||||
val oldAuthData = response.oldMasterPasswordAuthenticationData
|
||||
val unlockData = response.masterPasswordUnlockData
|
||||
|
||||
return UpdateKdfJsonRequest(
|
||||
authenticationData = MasterPasswordAuthenticationDataJson(
|
||||
kdf = authData.kdf.toKdfRequestModel(),
|
||||
masterPasswordAuthenticationHash = authData.masterPasswordAuthenticationHash,
|
||||
salt = authData.salt,
|
||||
),
|
||||
key = unlockData.masterKeyWrappedUserKey,
|
||||
masterPasswordHash = oldAuthData.masterPasswordAuthenticationHash,
|
||||
newMasterPasswordHash = authData.masterPasswordAuthenticationHash,
|
||||
unlockData = MasterPasswordUnlockDataJson(
|
||||
kdf = unlockData.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = unlockData.masterKeyWrappedUserKey,
|
||||
salt = unlockData.salt,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
@ -25,6 +27,7 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.CredentialExchangeRegistryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
@ -143,4 +146,18 @@ object AuthManagerModule {
|
||||
fun providesAuthTokenManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
): AuthTokenManager = AuthTokenManagerImpl(authDiskSource = authDiskSource)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesKdfManager(
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
accountsService: AccountsService,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): KdfManager = KdfManagerImpl(
|
||||
authDiskSource = authDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
accountsService = accountsService,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import com.bitwarden.network.model.TwoFactorDataModel
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserStateManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
@ -44,7 +45,11 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Provides an API for observing an modifying authentication state.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthRepository : AuthenticatorProvider, AuthRequestManager, UserStateManager {
|
||||
interface AuthRepository :
|
||||
AuthenticatorProvider,
|
||||
AuthRequestManager,
|
||||
KdfManager,
|
||||
UserStateManager {
|
||||
/**
|
||||
* Models the current auth state.
|
||||
*/
|
||||
|
||||
@ -53,6 +53,7 @@ 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.toKdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@ -79,6 +80,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
|
||||
@ -129,6 +131,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@ -158,11 +161,13 @@ class AuthRepositoryImpl(
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val policyManager: PolicyManager,
|
||||
private val userStateManager: UserStateManager,
|
||||
private val kdfManager: KdfManager,
|
||||
logsManager: LogsManager,
|
||||
pushManager: PushManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : AuthRepository,
|
||||
AuthRequestManager by authRequestManager,
|
||||
KdfManager by kdfManager,
|
||||
UserStateManager by userStateManager {
|
||||
/**
|
||||
* A scope intended for use when simply collecting multiple flows in order to combine them. The
|
||||
@ -1681,6 +1686,16 @@ class AuthRepositoryImpl(
|
||||
settingsRepository.hasUserLoggedInOrCreatedAccount = true
|
||||
|
||||
authDiskSource.userState = userStateJson
|
||||
password?.let {
|
||||
// Automatically update kdf to minimums after password unlock and userState update
|
||||
kdfManager
|
||||
.updateKdfToMinimumsIfNeeded(password = password)
|
||||
.also { result ->
|
||||
if (result is UpdateKdfMinimumsResult.Error) {
|
||||
Timber.e(result.error, message = "Failed to silent update KDF settings.")
|
||||
}
|
||||
}
|
||||
}
|
||||
loginResponse.key?.let {
|
||||
// Only set the value if it's present, since we may have set it already
|
||||
// when we completed the pending admin auth request.
|
||||
|
||||
@ -10,6 +10,7 @@ import com.bitwarden.network.service.OrganizationService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@ -67,6 +68,7 @@ object AuthRepositoryModule {
|
||||
policyManager: PolicyManager,
|
||||
logsManager: LogsManager,
|
||||
userStateManager: UserStateManager,
|
||||
kdfManager: KdfManager,
|
||||
): AuthRepository = AuthRepositoryImpl(
|
||||
clock = clock,
|
||||
accountsService = accountsService,
|
||||
@ -91,6 +93,7 @@ object AuthRepositoryModule {
|
||||
policyManager = policyManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
kdfManager = kdfManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
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()
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
@ -12,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
@ -123,6 +125,30 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the [UserStateJson] KDF settings to minimum requirements.
|
||||
*/
|
||||
fun UserStateJson.toUserStateJsonKdfUpdatedMinimums(): UserStateJson {
|
||||
val account = this.activeAccount
|
||||
val profile = account.profile
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
.copy(
|
||||
accounts = accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
replace(activeUserId, updatedAccount)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given [UserStateJson] to a [UserState] using the given [vaultState].
|
||||
*/
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -7,6 +7,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
|
||||
@ -608,4 +609,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,9 +17,11 @@ import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
|
||||
@ -64,6 +66,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.time.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@ -86,6 +89,7 @@ class VaultLockManagerImpl(
|
||||
private val appStateManager: AppStateManager,
|
||||
private val userLogoutManager: UserLogoutManager,
|
||||
private val trustedDeviceManager: TrustedDeviceManager,
|
||||
private val kdfManager: KdfManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
context: Context,
|
||||
) : VaultLockManager {
|
||||
@ -230,13 +234,12 @@ class VaultLockManagerImpl(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.also {
|
||||
if (it is VaultUnlockResult.Success) {
|
||||
clearInvalidUnlockCount(userId = userId)
|
||||
trustedDeviceManager
|
||||
.trustThisDeviceIfNecessary(userId = userId)
|
||||
.also { setVaultToUnlocked(userId = userId) }
|
||||
updateKdfIfNeeded(initUserCryptoMethod)
|
||||
setVaultToUnlocked(userId = userId)
|
||||
} else {
|
||||
incrementInvalidUnlockCount(userId = userId)
|
||||
}
|
||||
@ -673,6 +676,20 @@ class VaultLockManagerImpl(
|
||||
return (accounts.find { it.userId == userId }?.isLoggedIn) == false
|
||||
}
|
||||
|
||||
private suspend fun updateKdfIfNeeded(initUserCryptoMethod: InitUserCryptoMethod) {
|
||||
if (initUserCryptoMethod is InitUserCryptoMethod.Password) {
|
||||
kdfManager
|
||||
.updateKdfToMinimumsIfNeeded(
|
||||
password = initUserCryptoMethod.password,
|
||||
)
|
||||
.also { result ->
|
||||
if (result is UpdateKdfMinimumsResult.Error) {
|
||||
Timber.e(result.error, message = "Failed to silent update KDF settings.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom [BroadcastReceiver] that listens for when the screen is powered on and restarts the
|
||||
* vault timeout jobs to ensure they complete at the correct time.
|
||||
|
||||
@ -10,6 +10,7 @@ import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
@ -142,6 +143,7 @@ object VaultManagerModule {
|
||||
userLogoutManager: UserLogoutManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
trustedDeviceManager: TrustedDeviceManager,
|
||||
kdfManager: KdfManager,
|
||||
): VaultLockManager =
|
||||
VaultLockManagerImpl(
|
||||
context = context,
|
||||
@ -155,6 +157,7 @@ object VaultManagerModule {
|
||||
userLogoutManager = userLogoutManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
kdfManager = kdfManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@ -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,12 +29,20 @@ 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).
|
||||
*/
|
||||
@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,
|
||||
) {
|
||||
@ -42,14 +51,14 @@ fun BitwardenMasterPasswordDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.cancel),
|
||||
label = dismissButtonText,
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.testTag("DismissAlertButton"),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = BitwardenString.submit),
|
||||
BitwardenFilledButton(
|
||||
label = confirmButtonText,
|
||||
isEnabled = masterPassword.isNotEmpty(),
|
||||
onClick = { onConfirmClick(masterPassword) },
|
||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||
@ -57,7 +66,7 @@ fun BitwardenMasterPasswordDialog(
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = BitwardenString.password_confirmation),
|
||||
text = title,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier.testTag("AlertTitleText"),
|
||||
)
|
||||
@ -65,7 +74,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.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.review.AppReviewManager
|
||||
@ -458,6 +459,18 @@ 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,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,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.autofill.manager.browser.BrowserAutofillDialogManager
|
||||
@ -284,6 +285,10 @@ class VaultViewModel @Inject constructor(
|
||||
handleShareAllCipherDecryptionErrorsClick()
|
||||
}
|
||||
|
||||
is VaultAction.KdfUpdatePasswordRepromptSubmit -> {
|
||||
handleKdfUpdatePasswordRepromptSubmit(action)
|
||||
}
|
||||
|
||||
VaultAction.EnableThirdPartyAutofillClick -> handleEnableThirdPartyAutofillClick()
|
||||
VaultAction.DismissThirdPartyAutofillDialogClick -> {
|
||||
handleDismissThirdPartyAutofillDialogClick()
|
||||
@ -796,12 +801,53 @@ class VaultViewModel @Inject constructor(
|
||||
handleDecryptionErrorReceive(action)
|
||||
}
|
||||
|
||||
is VaultAction.Internal.UpdatedKdfToMinimumsReceived -> {
|
||||
handleUpdatedKdfToMinimumsReceived(action)
|
||||
}
|
||||
|
||||
is VaultAction.Internal.CredentialExchangeProtocolExportFlagUpdateReceive -> {
|
||||
handleCredentialExchangeProtocolExportFlagUpdateReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpdatedKdfToMinimumsReceived(
|
||||
action: VaultAction.Internal.UpdatedKdfToMinimumsReceived,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
|
||||
when (val result = action.result) {
|
||||
UpdateKdfMinimumsResult.ActiveAccountNotFound -> {
|
||||
showGenericError(
|
||||
message = BitwardenString.kdf_update_failed_active_account_not_found.asText(),
|
||||
)
|
||||
Timber.e(message = "Failed to update kdf to minimums: Active account not found")
|
||||
}
|
||||
|
||||
is UpdateKdfMinimumsResult.Error -> {
|
||||
showGenericError(
|
||||
message = BitwardenString
|
||||
.an_error_occurred_while_trying_to_update_your_kdf_settings
|
||||
.asText(),
|
||||
error = result.error,
|
||||
)
|
||||
Timber.e(result.error, message = "Failed to update kdf to minimums.")
|
||||
}
|
||||
|
||||
UpdateKdfMinimumsResult.Success -> {
|
||||
sendEvent(
|
||||
event = VaultEvent.ShowSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
message = BitwardenString.encryption_settings_updated.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCredentialExchangeProtocolExportFlagUpdateReceive(
|
||||
action: VaultAction.Internal.CredentialExchangeProtocolExportFlagUpdateReceive,
|
||||
) {
|
||||
@ -964,22 +1010,15 @@ 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,
|
||||
dialog = if (shouldShowDecryptionAlert ||
|
||||
state.dialog is VaultState.DialogState.VaultLoadCipherDecryptionError
|
||||
) {
|
||||
VaultState.DialogState.VaultLoadCipherDecryptionError(
|
||||
title = BitwardenString.decryption_error.asText(),
|
||||
cipherCount = vaultData.data.decryptCipherListResult.failures.size,
|
||||
)
|
||||
} else if (state.dialog is VaultState.DialogState.ThirdPartyBrowserAutofill) {
|
||||
state.dialog
|
||||
} else {
|
||||
null
|
||||
},
|
||||
dialog = getDialogVaultLoaded(
|
||||
shouldShowDecryptionAlert = shouldShowDecryptionAlert,
|
||||
vaultData = vaultData,
|
||||
),
|
||||
hasShownDecryptionFailureAlert = if (shouldShowDecryptionAlert) {
|
||||
true
|
||||
} else {
|
||||
@ -988,6 +1027,29 @@ class VaultViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDialogVaultLoaded(
|
||||
shouldShowDecryptionAlert: Boolean,
|
||||
vaultData: DataState.Loaded<VaultData>,
|
||||
): VaultState.DialogState? = if (authRepository.needsKdfUpdateToMinimums()) {
|
||||
VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = BitwardenString.update_your_encryption_settings.asText(),
|
||||
message = BitwardenString
|
||||
.the_new_recommended_encryption_settings_will_improve_your_account_desc_long
|
||||
.asText(),
|
||||
)
|
||||
} else if (shouldShowDecryptionAlert ||
|
||||
state.dialog is VaultState.DialogState.VaultLoadCipherDecryptionError
|
||||
) {
|
||||
VaultState.DialogState.VaultLoadCipherDecryptionError(
|
||||
title = BitwardenString.decryption_error.asText(),
|
||||
cipherCount = vaultData.data.decryptCipherListResult.failures.size,
|
||||
)
|
||||
} else if (state.dialog is VaultState.DialogState.ThirdPartyBrowserAutofill) {
|
||||
state.dialog
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private fun updateVaultState(
|
||||
vaultData: VaultData,
|
||||
dialog: VaultState.DialogState? = null,
|
||||
@ -1146,6 +1208,30 @@ class VaultViewModel @Inject constructor(
|
||||
|
||||
is GetCipherResult.Success -> result.cipherView
|
||||
}
|
||||
|
||||
private fun handleKdfUpdatePasswordRepromptSubmit(
|
||||
action: VaultAction.KdfUpdatePasswordRepromptSubmit,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.updateKdfToMinimumsIfNeeded(password = action.password)
|
||||
sendAction(action = VaultAction.Internal.UpdatedKdfToMinimumsReceived(result))
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGenericError(
|
||||
message: Text = BitwardenString.generic_error_message.asText(),
|
||||
error: Throwable? = null,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = message,
|
||||
error = error,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1531,6 +1617,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].
|
||||
*/
|
||||
@ -1784,6 +1879,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.
|
||||
*/
|
||||
@ -1943,6 +2045,13 @@ sealed class VaultAction {
|
||||
val error: Throwable?,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a result for updating the kdf has been received.
|
||||
*/
|
||||
data class UpdatedKdfToMinimumsReceived(
|
||||
val result: UpdateKdfMinimumsResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the Credential Exchange Protocol export flag has been updated.
|
||||
*/
|
||||
|
||||
@ -46,6 +46,7 @@ data class VaultHandlers(
|
||||
val dismissFlightRecorderSnackbar: () -> Unit,
|
||||
val onShareCipherDecryptionErrorClick: (selectedCipherId: String) -> Unit,
|
||||
val onShareAllCipherDecryptionErrorsClick: () -> Unit,
|
||||
val onKdfUpdatePasswordRepromptSubmit: (password: String) -> Unit,
|
||||
val onEnabledThirdPartyAutofillClick: () -> Unit,
|
||||
val onDismissThirdPartyAutofillDialogClick: () -> Unit,
|
||||
) {
|
||||
@ -137,6 +138,9 @@ data class VaultHandlers(
|
||||
onDismissThirdPartyAutofillDialogClick = {
|
||||
viewModel.trySendAction(VaultAction.DismissThirdPartyAutofillDialogClick)
|
||||
},
|
||||
onKdfUpdatePasswordRepromptSubmit = {
|
||||
viewModel.trySendAction(VaultAction.KdfUpdatePasswordRepromptSubmit(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,422 @@
|
||||
package com.x8bit.bitwarden.data.auth.manager
|
||||
|
||||
import com.bitwarden.core.MasterPasswordAuthenticationData
|
||||
import com.bitwarden.core.MasterPasswordUnlockData
|
||||
import com.bitwarden.core.UpdateKdfResponse
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class KdfManagerTest {
|
||||
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val vaultSdkSource: VaultSdkSource = mockk()
|
||||
private val accountsService: AccountsService = mockk()
|
||||
private val featureFlagManager: FeatureFlagManager = mockk {
|
||||
every { getFeatureFlag(FlagKey.ForceUpdateKdfSettings) } returns true
|
||||
}
|
||||
|
||||
private val manager: KdfManager = KdfManagerImpl(
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
accountsService = accountsService,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with no active user should return false`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = manager.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 = manager.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `needsKdfUpdateToMinimums with user decryption options and without password returns false`() =
|
||||
runTest {
|
||||
val account = ACCOUNT_1.copy(
|
||||
profile = ACCOUNT_1.profile.copy(
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1.copy(
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to account,
|
||||
),
|
||||
)
|
||||
|
||||
val result = manager.needsKdfUpdateToMinimums()
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `needsKdfUpdateToMinimums with PBKDF2 below minimum iterations should return true`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_2
|
||||
|
||||
val result = manager.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 = manager.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 = manager.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 = manager.needsKdfUpdateToMinimums()
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateKdfToMinimumsIfNeeded with no active user should return ActiveAccountNotFound`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = manager.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 = manager.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(
|
||||
UpdateKdfMinimumsResult.Success,
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateKdfToMinimumsIfNeeded with feature flag ForceUpdateKdfSettings to false return Success`() =
|
||||
runTest {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.ForceUpdateKdfSettings)
|
||||
} returns false
|
||||
|
||||
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 = 1000,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val result = manager.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 = manager.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 = manager.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(UpdateKdfMinimumsResult.Error(error = error), result)
|
||||
coVerify(exactly = 1) {
|
||||
accountsService.updateKdf(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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 = manager.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(UpdateKdfMinimumsResult.Success, result)
|
||||
coVerify(exactly = 1) {
|
||||
accountsService.updateKdf(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `updateKdfToMinimumsIfNeeded with PBKDF2 below minimums should update userState to minimums`() =
|
||||
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 = manager.updateKdfToMinimumsIfNeeded(password = PASSWORD)
|
||||
|
||||
assertEquals(UpdateKdfMinimumsResult.Success, result)
|
||||
|
||||
// Verify userState was updated with minimum KDF values
|
||||
val updatedUserState = fakeAuthDiskSource.userState
|
||||
val updatedProfile = updatedUserState?.accounts?.get(USER_ID_2)?.profile
|
||||
assertEquals(KdfTypeJson.PBKDF2_SHA256, updatedProfile?.kdfType)
|
||||
assertEquals(600000, updatedProfile?.kdfIterations)
|
||||
assertNull(updatedProfile?.kdfMemory)
|
||||
assertNull(updatedProfile?.kdfParallelism)
|
||||
}
|
||||
}
|
||||
|
||||
private const val EMAIL = "test@bitwarden.com"
|
||||
private const val EMAIL_2 = "test2@bitwarden.com"
|
||||
private const val PASSWORD = "password"
|
||||
private const val USER_ID_1 = "2a135b23-e1fb-42c9-bec3-573857bc8181"
|
||||
private const val USER_ID_2 = "b9d32ec0-6497-4582-9798-b350f53bfa02"
|
||||
private val PROFILE_1 = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = EMAIL,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
|
||||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = PROFILE_1,
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||
),
|
||||
)
|
||||
|
||||
private val ACCOUNT_2 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_2,
|
||||
email = EMAIL_2,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester 2",
|
||||
hasPremium = false,
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = 400000,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU,
|
||||
),
|
||||
)
|
||||
|
||||
private val SINGLE_USER_STATE_1 = UserStateJson(
|
||||
activeUserId = USER_ID_1,
|
||||
accounts = mapOf(
|
||||
USER_ID_1 to ACCOUNT_1,
|
||||
),
|
||||
)
|
||||
|
||||
private val SINGLE_USER_STATE_2 = UserStateJson(
|
||||
activeUserId = USER_ID_2,
|
||||
accounts = mapOf(
|
||||
USER_ID_2 to ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
|
||||
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",
|
||||
),
|
||||
)
|
||||
@ -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
|
||||
@ -74,6 +77,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_3
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength.LEVEL_4
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
@ -100,6 +104,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
|
||||
@ -266,7 +271,12 @@ class AuthRepositoryTest {
|
||||
val blockSlot = slot<suspend () -> LoginResult>()
|
||||
coEvery { userStateTransaction(capture(blockSlot)) } coAnswers { blockSlot.captured() }
|
||||
}
|
||||
|
||||
private val kdfManager: KdfManager = mockk {
|
||||
every { needsKdfUpdateToMinimums() } returns false
|
||||
coEvery {
|
||||
updateKdfToMinimumsIfNeeded(password = any())
|
||||
} returns UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
private val repository: AuthRepository = AuthRepositoryImpl(
|
||||
clock = FIXED_CLOCK,
|
||||
accountsService = accountsService,
|
||||
@ -291,6 +301,7 @@ class AuthRepositoryTest {
|
||||
policyManager = policyManager,
|
||||
logsManager = logsManager,
|
||||
userStateManager = userStateManager,
|
||||
kdfManager = kdfManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
@ -2127,6 +2138,18 @@ class AuthRepositoryTest {
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||
coEvery {
|
||||
vaultSdkSource.makeUpdateKdf(
|
||||
userId = any(),
|
||||
password = any(),
|
||||
kdf = any(),
|
||||
)
|
||||
} returns UPDATE_KDF_RESPONSE.asSuccess()
|
||||
coEvery {
|
||||
accountsService.updateKdf(
|
||||
body = any(),
|
||||
)
|
||||
} returns Unit.asSuccess()
|
||||
every {
|
||||
GET_TOKEN_WITH_ACCOUNT_KEYS_RESPONSE_SUCCESS.toUserState(
|
||||
previousUserState = SINGLE_USER_STATE_2,
|
||||
@ -7208,5 +7231,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",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import io.mockk.every
|
||||
@ -1681,6 +1682,192 @@ class UserStateJsonExtensionsTest {
|
||||
originalUserState.toUpdatedUserStateJson(syncResponse),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonKdfUpdatedMinimums should update KDF settings to minimum values`() {
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = "stamp",
|
||||
organizationId = null,
|
||||
avatarColorHex = "avatarColorHex",
|
||||
hasPremium = true,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(environmentUrlData = null),
|
||||
)
|
||||
val originalUserState = UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val result = originalUserState.toUserStateJsonKdfUpdatedMinimums()
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUserStateJsonKdfUpdatedMinimums should preserve other profile data while updating KDF`() {
|
||||
val userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "test@example.com",
|
||||
isEmailVerified = true,
|
||||
name = "Test User",
|
||||
stamp = "securityStamp",
|
||||
organizationId = "orgId",
|
||||
avatarColorHex = "#FF0000",
|
||||
hasPremium = false,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 100000,
|
||||
kdfMemory = 32,
|
||||
kdfParallelism = 8,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = ZonedDateTime.parse("2024-01-01T00:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(base = "https://vault.bitwarden.com"),
|
||||
),
|
||||
)
|
||||
val originalUserState = UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val result = originalUserState.toUserStateJsonKdfUpdatedMinimums()
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonKdfUpdatedMinimums should only update active user account`() {
|
||||
val activeProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "active@example.com",
|
||||
isEmailVerified = true,
|
||||
name = "Active User",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremium = true,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val inactiveProfile = AccountJson.Profile(
|
||||
userId = "inactiveUserId",
|
||||
email = "inactive@example.com",
|
||||
isEmailVerified = true,
|
||||
name = "Inactive User",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremium = false,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 500000,
|
||||
kdfMemory = 8,
|
||||
kdfParallelism = 2,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = ZonedDateTime.parse("2024-08-13T01:00:00.00Z"),
|
||||
)
|
||||
val activeAccount = AccountJson(
|
||||
profile = activeProfile,
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(environmentUrlData = null),
|
||||
)
|
||||
val inactiveAccount = AccountJson(
|
||||
profile = inactiveProfile,
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(environmentUrlData = null),
|
||||
)
|
||||
val originalUserState = UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to activeAccount,
|
||||
"inactiveUserId" to inactiveAccount,
|
||||
),
|
||||
)
|
||||
|
||||
val result = originalUserState.toUserStateJsonKdfUpdatedMinimums()
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to activeAccount.copy(
|
||||
profile = activeProfile.copy(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
kdfMemory = null,
|
||||
kdfParallelism = null,
|
||||
),
|
||||
),
|
||||
"inactiveUserId" to inactiveAccount, // Should remain unchanged
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val MOCK_MASTER_PASSWORD_UNLOCK_DATA = MasterPasswordUnlockDataJson(
|
||||
|
||||
@ -7,6 +7,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
|
||||
@ -1486,6 +1489,64 @@ 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>()
|
||||
val error = BitwardenException.Crypto(CryptoException.MissingKey("mockException"))
|
||||
coEvery {
|
||||
clientCrypto.makeUpdateKdf(
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
} throws error
|
||||
val result = vaultSdkSource.makeUpdateKdf(
|
||||
userId = "mockUserId",
|
||||
password = "mockPassword",
|
||||
kdf = kdf,
|
||||
)
|
||||
assertEquals(error.asFailure(), result)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_SIGNATURE = "0987654321ABCDEF"
|
||||
|
||||
@ -18,10 +18,12 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
|
||||
import com.x8bit.bitwarden.data.auth.manager.KdfManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.LogoutEvent
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UpdateKdfMinimumsResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppForegroundState
|
||||
@ -104,6 +106,12 @@ class VaultLockManagerTest {
|
||||
private val realtimeManager: RealtimeManager = mockk {
|
||||
every { elapsedRealtimeMs } returns FIXED_CLOCK.millis()
|
||||
}
|
||||
private val kdfManager: KdfManager = mockk {
|
||||
every { needsKdfUpdateToMinimums() } returns false
|
||||
coEvery {
|
||||
updateKdfToMinimumsIfNeeded(password = any())
|
||||
} returns UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
|
||||
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
|
||||
context = context,
|
||||
@ -117,6 +125,7 @@ class VaultLockManagerTest {
|
||||
userLogoutManager = userLogoutManager,
|
||||
trustedDeviceManager = trustedDeviceManager,
|
||||
dispatcherManager = fakeDispatcherManager,
|
||||
kdfManager = kdfManager,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
||||
@ -779,6 +779,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()
|
||||
|
||||
@ -22,6 +22,7 @@ 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.Organization
|
||||
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.autofill.manager.browser.BrowserAutofillDialogManager
|
||||
@ -148,6 +149,10 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
every { hasPendingAccountAddition = any() } just runs
|
||||
every { logout(userId = any(), reason = any()) } just runs
|
||||
every { switchAccount(any()) } answers { switchAccountResult }
|
||||
every { needsKdfUpdateToMinimums() } returns false
|
||||
coEvery {
|
||||
updateKdfToMinimumsIfNeeded(password = any())
|
||||
} returns UpdateKdfMinimumsResult.Success
|
||||
}
|
||||
|
||||
private var mutableFlightRecorderDataFlow =
|
||||
@ -2860,6 +2865,204 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `UpdatedKdfToMinimumsReceived with Success should clear dialog and send a ShowSnackbar event`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.Internal.UpdatedKdfToMinimumsReceived(
|
||||
result = UpdateKdfMinimumsResult.Success,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(dialog = null),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
assertEquals(
|
||||
VaultEvent.ShowSnackbar(BitwardenString.encryption_settings_updated.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UpdatedKdfToMinimumsReceived with ActiveAccountNotFound should show error dialog`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.Internal.UpdatedKdfToMinimumsReceived(
|
||||
result = UpdateKdfMinimumsResult.ActiveAccountNotFound,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString
|
||||
.kdf_update_failed_active_account_not_found
|
||||
.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UpdatedKdfToMinimumsReceived with Error should show error dialog`() = runTest {
|
||||
val testError = Exception("Test error")
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.Internal.UpdatedKdfToMinimumsReceived(
|
||||
result = UpdateKdfMinimumsResult.Error(testError),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
dialog = VaultState.DialogState.Error(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString
|
||||
.an_error_occurred_while_trying_to_update_your_kdf_settings
|
||||
.asText(),
|
||||
error = testError,
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `vaultDataStateFlow Loaded with needsKdfUpdateToMinimums true should show KdfUpdateRequired dialog`() =
|
||||
runTest {
|
||||
coEvery { authRepository.needsKdfUpdateToMinimums() } returns true
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
decryptCipherListResult = createMockDecryptCipherListResult(
|
||||
number = 1,
|
||||
successes = listOf(
|
||||
createMockCipherListView(
|
||||
number = 1,
|
||||
type = CipherListViewType.Login(
|
||||
createMockLoginListView(number = 1),
|
||||
),
|
||||
),
|
||||
createMockCipherListView(
|
||||
number = 2,
|
||||
type = CipherListViewType.Login(
|
||||
createMockLoginListView(number = 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
failures = emptyList(),
|
||||
),
|
||||
collectionViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
createMockVaultState(
|
||||
viewState = VaultState.ViewState.Content(
|
||||
loginItemsCount = 2,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
folderItems = listOf(),
|
||||
collectionItems = listOf(),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
totpItemsCount = 2,
|
||||
itemTypesCount = 5,
|
||||
sshKeyItemsCount = 0,
|
||||
showCardGroup = true,
|
||||
),
|
||||
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
|
||||
title = BitwardenString.update_your_encryption_settings.asText(),
|
||||
message = BitwardenString.the_new_recommended_encryption_settings_will_improve_your_account_desc_long.asText(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `vaultDataStateFlow Loaded with needsKdfUpdateToMinimums false should not show KdfUpdateRequired dialog`() =
|
||||
runTest {
|
||||
coEvery { authRepository.needsKdfUpdateToMinimums() } returns false
|
||||
mutableVaultDataStateFlow.value = DataState.Loaded(
|
||||
data = VaultData(
|
||||
decryptCipherListResult = createMockDecryptCipherListResult(
|
||||
number = 1,
|
||||
successes = listOf(
|
||||
createMockCipherListView(
|
||||
number = 1,
|
||||
type = CipherListViewType.Login(
|
||||
createMockLoginListView(number = 1),
|
||||
),
|
||||
),
|
||||
createMockCipherListView(
|
||||
number = 2,
|
||||
type = CipherListViewType.Login(
|
||||
createMockLoginListView(number = 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
failures = emptyList(),
|
||||
),
|
||||
collectionViewList = emptyList(),
|
||||
folderViewList = emptyList(),
|
||||
sendViewList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel()
|
||||
|
||||
assertEquals(
|
||||
createMockVaultState(
|
||||
viewState = VaultState.ViewState.Content(
|
||||
loginItemsCount = 2,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
folderItems = listOf(),
|
||||
collectionItems = listOf(),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
totpItemsCount = 2,
|
||||
itemTypesCount = 5,
|
||||
sshKeyItemsCount = 0,
|
||||
showCardGroup = true,
|
||||
),
|
||||
dialog = null,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on KdfUpdatePasswordRepromptSubmit should call updateKdfToMinimumsIfNeeded`() = runTest {
|
||||
val password = "mock_password"
|
||||
coEvery {
|
||||
authRepository.updateKdfToMinimumsIfNeeded(password)
|
||||
} returns UpdateKdfMinimumsResult.Success
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
action = VaultAction.KdfUpdatePasswordRepromptSubmit(password = password),
|
||||
)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.updateKdfToMinimumsIfNeeded(password)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CredentialExchangeProtocolExportFlagUpdateReceive should register for export when flag is enabled`() =
|
||||
|
||||
@ -32,6 +32,7 @@ sealed class FlagKey<out T : Any> {
|
||||
listOf(
|
||||
CredentialExchangeProtocolImport,
|
||||
CredentialExchangeProtocolExport,
|
||||
ForceUpdateKdfSettings,
|
||||
CipherKeyEncryption,
|
||||
)
|
||||
}
|
||||
@ -71,6 +72,14 @@ sealed class FlagKey<out T : Any> {
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Data object holding the feature flag key for the Force Update KDF Settings feature.
|
||||
*/
|
||||
data object ForceUpdateKdfSettings : FlagKey<Boolean>() {
|
||||
override val keyName: String = "pm-18021-force-update-kdf-settings"
|
||||
override val defaultValue: Boolean = false
|
||||
}
|
||||
|
||||
//region Dummy keys for testing
|
||||
/**
|
||||
* Data object holding the key for a [Boolean] flag to be used in tests.
|
||||
|
||||
@ -24,6 +24,10 @@ class FlagKeyTest {
|
||||
FlagKey.BitwardenAuthenticationEnabled.keyName,
|
||||
"bitwarden-authentication-enabled",
|
||||
)
|
||||
assertEquals(
|
||||
FlagKey.ForceUpdateKdfSettings.keyName,
|
||||
"pm-18021-force-update-kdf-settings",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -34,6 +38,7 @@ class FlagKeyTest {
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.BitwardenAuthenticationEnabled,
|
||||
FlagKey.ForceUpdateKdfSettings,
|
||||
).all {
|
||||
!it.defaultValue
|
||||
},
|
||||
|
||||
@ -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,19 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents the data used to authenticate with the master password.
|
||||
*/
|
||||
@Serializable
|
||||
data class MasterPasswordAuthenticationDataJson(
|
||||
@SerialName("Kdf")
|
||||
val kdf: KdfJson,
|
||||
|
||||
@SerialName("MasterPasswordAuthenticationHash")
|
||||
val masterPasswordAuthenticationHash: 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: MasterPasswordAuthenticationDataJson,
|
||||
|
||||
@SerialName("key")
|
||||
val key: String,
|
||||
|
||||
@SerialName("masterPasswordHash")
|
||||
val masterPasswordHash: String,
|
||||
|
||||
@SerialName("newMasterPasswordHash")
|
||||
val newMasterPasswordHash: String,
|
||||
|
||||
@SerialName("unlockData")
|
||||
val unlockData: MasterPasswordUnlockDataJson,
|
||||
)
|
||||
@ -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
|
||||
import com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
|
||||
@ -115,4 +116,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.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import com.bitwarden.network.model.VerifyOtpRequestJson
|
||||
@ -209,4 +210,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.KdfJson
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.KeyConnectorKeyRequestJson
|
||||
import com.bitwarden.network.model.KeyConnectorMasterKeyResponseJson
|
||||
import com.bitwarden.network.model.MasterPasswordAuthenticationDataJson
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
|
||||
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 com.bitwarden.network.model.VerificationCodeResponseJson
|
||||
import com.bitwarden.network.model.VerificationOtpResponseJson
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@ -291,6 +295,25 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
fun `resendNewDeviceOtp with 400 response is Error`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400).setBody(INVALID_JSON)
|
||||
server.enqueue(response)
|
||||
@ -318,3 +341,29 @@ private const val INVALID_JSON = """
|
||||
"validationErrors": null
|
||||
}
|
||||
"""
|
||||
|
||||
private val UPDATE_KDF_REQUEST = UpdateKdfJsonRequest(
|
||||
authenticationData = MasterPasswordAuthenticationDataJson(
|
||||
kdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = 7,
|
||||
memory = 1,
|
||||
parallelism = 2,
|
||||
),
|
||||
masterPasswordAuthenticationHash = "mockMasterPasswordHash",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
key = "mockKey",
|
||||
masterPasswordHash = "mockMasterPasswordHash",
|
||||
newMasterPasswordHash = "mockNewMasterPasswordHash",
|
||||
unlockData = MasterPasswordUnlockDataJson(
|
||||
kdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = 7,
|
||||
memory = 1,
|
||||
parallelism = 2,
|
||||
),
|
||||
masterKeyWrappedUserKey = "mockMasterPasswordKey",
|
||||
salt = "mockSalt",
|
||||
),
|
||||
)
|
||||
|
||||
@ -27,6 +27,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
|
||||
FlagKey.CredentialExchangeProtocolImport,
|
||||
FlagKey.CredentialExchangeProtocolExport,
|
||||
FlagKey.CipherKeyEncryption,
|
||||
FlagKey.ForceUpdateKdfSettings,
|
||||
-> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
BooleanFlagItem(
|
||||
@ -71,6 +72,7 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
|
||||
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
|
||||
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
|
||||
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
|
||||
FlagKey.ForceUpdateKdfSettings -> stringResource(BitwardenString.force_update_kdf_settings)
|
||||
FlagKey.BitwardenAuthenticationEnabled -> {
|
||||
stringResource(BitwardenString.bitwarden_authentication_enabled)
|
||||
}
|
||||
|
||||
@ -1091,6 +1091,10 @@ 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_desc_long">The new recommended encryption settings will improve your account security. Enter your master password to update now.</string>
|
||||
<string name="import_was_cancelled_in_the_selected_app">Credential import was cancelled in the selected app. No items have been imported.</string>
|
||||
<string name="import_cancelled">Import cancelled</string>
|
||||
<string name="no_items_imported">No items imported</string>
|
||||
@ -1128,5 +1132,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="passkeys">Passkeys</string>
|
||||
<string name="import_verb">Import</string>
|
||||
<string name="why_is_this_step_required">Why is this step required?</string>
|
||||
<string name="kdf_update_failed_active_account_not_found">Kdf update failed, active account not found. Please try again or contact us.</string>
|
||||
<string name="an_error_occurred_while_trying_to_update_your_kdf_settings">An error occurred while trying to update your kdf settings. Please try again or contact us.</string>
|
||||
<string name="the_import_request_could_not_be_processed">The import request could not be processed.</string>
|
||||
</resources>
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
<string name="import_format_label_2fas_json">2FAS (no password)</string>
|
||||
<string name="import_format_label_lastpass_json">LastPass (.json)</string>
|
||||
<string name="import_format_label_aegis_json">Aegis (.json)</string>
|
||||
<string name="force_update_kdf_settings">Force update KDF settings</string>
|
||||
|
||||
<!-- endregion Debug Menu -->
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user