Skip to content

Commit 0143f93

Browse files
authored
[PM-35117] fix: Getting updated values from KDF before displaying update KDF prompt (#6802)
1 parent 7d2bfe1 commit 0143f93

7 files changed

Lines changed: 299 additions & 24 deletions

File tree

app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fun UserStateJson.toUpdatedUserStateJson(
5858
val userId = syncProfile.id
5959
val account = this.accounts[userId] ?: return this
6060
val profile = account.profile
61+
val masterPasswordUnlockKdf = syncResponse.userDecryption?.masterPasswordUnlock?.kdf
6162
val userDecryptionOptions = syncResponse
6263
.userDecryption
6364
?.let { syncUserDecryption ->
@@ -83,6 +84,14 @@ fun UserStateJson.toUpdatedUserStateJson(
8384
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
8485
creationDate = syncProfile.creationDate,
8586
userDecryptionOptions = userDecryptionOptions,
87+
kdfType = masterPasswordUnlockKdf?.kdfType
88+
?: profile.kdfType,
89+
kdfIterations = masterPasswordUnlockKdf?.iterations
90+
?: profile.kdfIterations,
91+
kdfMemory = masterPasswordUnlockKdf?.memory
92+
?: profile.kdfMemory,
93+
kdfParallelism = masterPasswordUnlockKdf?.parallelism
94+
?: profile.kdfParallelism,
8695
)
8796
val updatedAccount = account.copy(profile = updatedProfile)
8897
return this

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,18 @@ class VaultViewModel @Inject constructor(
170170
get() = state.vaultFilterData?.selectedVaultFilterType ?: VaultFilterType.AllVaults
171171

172172
init {
173-
// Attempt a sync each time we are on a fresh Vault Screen.
174-
vaultRepository.syncIfNecessary()
173+
// Force a sync if a KDF update is detected as necessary to ensure we have the latest KDF
174+
// settings from the server. This handles the case where the user updated their KDF
175+
// settings on another device. Otherwise, attempt a sync based on the normal criteria.
176+
if (authRepository.needsKdfUpdateToMinimums()) {
177+
mutableStateFlow.update { it.copy(isAwaitingKdfSync = true) }
178+
viewModelScope.launch {
179+
vaultRepository.syncForResult(forced = true)
180+
sendAction(VaultAction.Internal.KdfSyncCompletedReceive)
181+
}
182+
} else {
183+
vaultRepository.syncIfNecessary()
184+
}
175185

176186
// Reset the current vault filter type for the current user
177187
vaultRepository.vaultFilterType = vaultFilterTypeOrDefault
@@ -965,6 +975,10 @@ class VaultViewModel @Inject constructor(
965975
handleUpdatedKdfToMinimumsReceived(action)
966976
}
967977

978+
is VaultAction.Internal.KdfSyncCompletedReceive -> {
979+
handleKdfSyncCompletedReceive()
980+
}
981+
968982
is VaultAction.Internal.CredentialExchangeProtocolExportFlagUpdateReceive -> {
969983
handleCredentialExchangeProtocolExportFlagUpdateReceive(action)
970984
}
@@ -985,6 +999,23 @@ class VaultViewModel @Inject constructor(
985999
}
9861000
}
9871001

1002+
private fun handleKdfSyncCompletedReceive() {
1003+
mutableStateFlow.update { it.copy(isAwaitingKdfSync = false) }
1004+
if (authRepository.needsKdfUpdateToMinimums()) {
1005+
mutableStateFlow.update {
1006+
@Suppress("MaxLineLength")
1007+
it.copy(
1008+
dialog = VaultState.DialogState.VaultLoadKdfUpdateRequired(
1009+
title = BitwardenString.update_your_encryption_settings.asText(),
1010+
message = BitwardenString
1011+
.the_new_recommended_encryption_settings_will_improve_your_account_desc_long
1012+
.asText(),
1013+
),
1014+
)
1015+
}
1016+
}
1017+
}
1018+
9881019
private fun handleUpdatedKdfToMinimumsReceived(
9891020
action: VaultAction.Internal.UpdatedKdfToMinimumsReceived,
9901021
) {
@@ -1274,25 +1305,26 @@ class VaultViewModel @Inject constructor(
12741305
private fun getDialogVaultLoaded(
12751306
shouldShowDecryptionAlert: Boolean,
12761307
vaultData: DataState.Loaded<VaultData>,
1277-
): VaultState.DialogState? = if (authRepository.needsKdfUpdateToMinimums()) {
1278-
VaultState.DialogState.VaultLoadKdfUpdateRequired(
1279-
title = BitwardenString.update_your_encryption_settings.asText(),
1280-
message = BitwardenString
1281-
.the_new_recommended_encryption_settings_will_improve_your_account_desc_long
1282-
.asText(),
1283-
)
1284-
} else if (shouldShowDecryptionAlert ||
1285-
state.dialog is VaultState.DialogState.VaultLoadCipherDecryptionError
1286-
) {
1287-
VaultState.DialogState.VaultLoadCipherDecryptionError(
1288-
title = BitwardenString.decryption_error.asText(),
1289-
cipherCount = vaultData.data.decryptCipherListResult.failures.size,
1290-
)
1291-
} else if (state.dialog is VaultState.DialogState.ThirdPartyBrowserAutofill) {
1292-
state.dialog
1293-
} else {
1294-
null
1295-
}
1308+
): VaultState.DialogState? =
1309+
if (!state.isAwaitingKdfSync && authRepository.needsKdfUpdateToMinimums()) {
1310+
VaultState.DialogState.VaultLoadKdfUpdateRequired(
1311+
title = BitwardenString.update_your_encryption_settings.asText(),
1312+
message = BitwardenString
1313+
.the_new_recommended_encryption_settings_will_improve_your_account_desc_long
1314+
.asText(),
1315+
)
1316+
} else if (shouldShowDecryptionAlert ||
1317+
state.dialog is VaultState.DialogState.VaultLoadCipherDecryptionError
1318+
) {
1319+
VaultState.DialogState.VaultLoadCipherDecryptionError(
1320+
title = BitwardenString.decryption_error.asText(),
1321+
cipherCount = vaultData.data.decryptCipherListResult.failures.size,
1322+
)
1323+
} else if (state.dialog is VaultState.DialogState.ThirdPartyBrowserAutofill) {
1324+
state.dialog
1325+
} else {
1326+
null
1327+
}
12961328

12971329
private fun updateVaultState(
12981330
vaultData: VaultData,
@@ -1516,6 +1548,7 @@ data class VaultState(
15161548
val restrictItemTypesPolicyOrgIds: List<String>,
15171549
val isIntroducingArchiveActionCardDismissed: Boolean,
15181550
val isPremiumUpgradeBannerEligible: Boolean = false,
1551+
val isAwaitingKdfSync: Boolean = false,
15191552
) : Parcelable {
15201553

15211554
/**
@@ -2385,6 +2418,11 @@ sealed class VaultAction {
23852418
val result: UpdateKdfMinimumsResult,
23862419
) : Internal()
23872420

2421+
/**
2422+
* Indicates the forced sync triggered for a KDF update check has completed.
2423+
*/
2424+
data object KdfSyncCompletedReceive : Internal()
2425+
23882426
/**
23892427
* Indicates that the Credential Exchange Protocol export flag has been updated.
23902428
*/

app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
22

33
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
44
import com.bitwarden.data.repository.model.Environment
5+
import com.bitwarden.network.model.KdfJson
56
import com.bitwarden.network.model.KdfTypeJson
67
import com.bitwarden.network.model.KeyConnectorUserDecryptionOptionsJson
78
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
@@ -1750,6 +1751,10 @@ class UserStateJsonExtensionsTest {
17501751
hasPremium = false,
17511752
isTwoFactorEnabled = true,
17521753
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
1754+
kdfType = KdfTypeJson.PBKDF2_SHA256,
1755+
kdfIterations = 600000,
1756+
kdfMemory = 16,
1757+
kdfParallelism = 4,
17531758
userDecryptionOptions = UserDecryptionOptionsJson(
17541759
hasMasterPassword = true,
17551760
trustedDeviceUserDecryptionOptions = null,
@@ -1833,6 +1838,10 @@ class UserStateJsonExtensionsTest {
18331838
hasPremium = true,
18341839
isTwoFactorEnabled = true,
18351840
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
1841+
kdfType = KdfTypeJson.PBKDF2_SHA256,
1842+
kdfIterations = 600000,
1843+
kdfMemory = 16,
1844+
kdfParallelism = 4,
18361845
userDecryptionOptions = UserDecryptionOptionsJson(
18371846
hasMasterPassword = true,
18381847
trustedDeviceUserDecryptionOptions = trustedDeviceOptions,
@@ -1922,6 +1931,93 @@ class UserStateJsonExtensionsTest {
19221931
)
19231932
}
19241933

1934+
@Test
1935+
@Suppress("MaxLineLength")
1936+
fun `toUpdatedUserStateJson should update KDF settings when sync response provides updated values`() {
1937+
val originalProfile = AccountJson.Profile(
1938+
userId = "activeUserId",
1939+
email = "email",
1940+
isEmailVerified = true,
1941+
name = "name",
1942+
stamp = null,
1943+
organizationId = null,
1944+
avatarColorHex = null,
1945+
hasPremium = false,
1946+
forcePasswordResetReason = null,
1947+
kdfType = KdfTypeJson.PBKDF2_SHA256,
1948+
kdfIterations = 100_000,
1949+
kdfMemory = null,
1950+
kdfParallelism = null,
1951+
userDecryptionOptions = null,
1952+
isTwoFactorEnabled = false,
1953+
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
1954+
)
1955+
val originalAccount = AccountJson(
1956+
profile = originalProfile,
1957+
tokens = null,
1958+
settings = AccountJson.Settings(environmentUrlData = null),
1959+
)
1960+
val originalUserState = UserStateJson(
1961+
activeUserId = "activeUserId",
1962+
accounts = mapOf("activeUserId" to originalAccount),
1963+
)
1964+
1965+
val syncResponse = mockk<SyncResponseJson> {
1966+
every { profile } returns mockk {
1967+
every { id } returns "activeUserId"
1968+
every { avatarColor } returns null
1969+
every { securityStamp } returns null
1970+
every { isPremium } returns false
1971+
every { isPremiumFromOrganization } returns false
1972+
every { isTwoFactorEnabled } returns false
1973+
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
1974+
}
1975+
val updatedKdf = KdfJson(
1976+
kdfType = KdfTypeJson.PBKDF2_SHA256,
1977+
iterations = DEFAULT_PBKDF2_ITERATIONS,
1978+
memory = null,
1979+
parallelism = null,
1980+
)
1981+
val updatedMasterPasswordUnlock = MasterPasswordUnlockDataJson(
1982+
salt = "mockSalt",
1983+
kdf = updatedKdf,
1984+
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
1985+
)
1986+
every { userDecryption } returns UserDecryptionJson(
1987+
masterPasswordUnlock = updatedMasterPasswordUnlock,
1988+
)
1989+
}
1990+
1991+
assertEquals(
1992+
UserStateJson(
1993+
activeUserId = "activeUserId",
1994+
accounts = mapOf(
1995+
"activeUserId" to originalAccount.copy(
1996+
profile = originalProfile.copy(
1997+
kdfIterations = DEFAULT_PBKDF2_ITERATIONS,
1998+
userDecryptionOptions = UserDecryptionOptionsJson(
1999+
hasMasterPassword = true,
2000+
masterPasswordUnlock = MasterPasswordUnlockDataJson(
2001+
salt = "mockSalt",
2002+
kdf = KdfJson(
2003+
kdfType = KdfTypeJson.PBKDF2_SHA256,
2004+
iterations = DEFAULT_PBKDF2_ITERATIONS,
2005+
memory = null,
2006+
parallelism = null,
2007+
),
2008+
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
2009+
),
2010+
trustedDeviceUserDecryptionOptions = null,
2011+
keyConnectorUserDecryptionOptions = null,
2012+
),
2013+
),
2014+
),
2015+
),
2016+
),
2017+
originalUserState.toUpdatedUserStateJson(syncResponse),
2018+
)
2019+
}
2020+
19252021
@Test
19262022
fun `toUserStateJsonKdfUpdatedMinimums should update KDF settings to minimum values`() {
19272023
val originalProfile = AccountJson.Profile(
@@ -2111,6 +2207,11 @@ class UserStateJsonExtensionsTest {
21112207

21122208
private val MOCK_MASTER_PASSWORD_UNLOCK_DATA = MasterPasswordUnlockDataJson(
21132209
salt = "mockSalt",
2114-
kdf = mockk(),
2210+
kdf = KdfJson(
2211+
kdfType = KdfTypeJson.PBKDF2_SHA256,
2212+
iterations = 600_000,
2213+
memory = null,
2214+
parallelism = null,
2215+
),
21152216
masterKeyWrappedUserKey = "masterKeyWrappedUserKeyMock",
21162217
)

app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultSyncManagerTest.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import com.bitwarden.core.data.repository.model.DataState
1010
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
1111
import com.bitwarden.core.data.util.asFailure
1212
import com.bitwarden.core.data.util.asSuccess
13+
import com.bitwarden.network.model.KdfTypeJson
1314
import com.bitwarden.network.model.SyncResponseJson
15+
import com.bitwarden.network.model.UserDecryptionOptionsJson
1416
import com.bitwarden.network.model.createMockCipher
1517
import com.bitwarden.network.model.createMockCollection
1618
import com.bitwarden.network.model.createMockDomains
1719
import com.bitwarden.network.model.createMockFolder
20+
import com.bitwarden.network.model.createMockMasterPasswordUnlock
1821
import com.bitwarden.network.model.createMockOrganizationKeys
1922
import com.bitwarden.network.model.createMockOrganizationNetwork
2023
import com.bitwarden.network.model.createMockPolicy
@@ -728,6 +731,16 @@ class VaultSyncManagerTest {
728731
profile = MOCK_PROFILE.copy(
729732
avatarColorHex = "mockAvatarColor-1",
730733
stamp = "mockSecurityStamp-1",
734+
kdfType = KdfTypeJson.PBKDF2_SHA256,
735+
kdfIterations = 600000,
736+
kdfMemory = null,
737+
kdfParallelism = null,
738+
userDecryptionOptions = UserDecryptionOptionsJson(
739+
hasMasterPassword = true,
740+
masterPasswordUnlock = createMockMasterPasswordUnlock(number = 1),
741+
trustedDeviceUserDecryptionOptions = null,
742+
keyConnectorUserDecryptionOptions = null,
743+
),
731744
),
732745
),
733746
),

0 commit comments

Comments
 (0)