mirror of
https://github.com/bitwarden/android.git
synced 2026-06-15 02:45:35 -05:00
Compare commits
1 Commits
crowdin-pu
...
PM-39006-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37fefc4f8 |
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.MasterPasswordUnlockData
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
import com.bitwarden.core.WrappedAccountCryptographicState
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
@@ -1145,10 +1146,10 @@ class AuthRepositoryImpl(
|
||||
organizationIdentifier: String,
|
||||
password: String,
|
||||
passwordHint: String?,
|
||||
): SetPasswordResult {
|
||||
): SetPasswordResult = userStateManager.userStateTransaction {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return SetPasswordResult.Error(error = NoActiveUserException())
|
||||
return when (profile.forcePasswordResetReason) {
|
||||
?: return@userStateTransaction SetPasswordResult.Error(error = NoActiveUserException())
|
||||
return@userStateTransaction when (profile.forcePasswordResetReason) {
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION -> {
|
||||
setUpdatedPassword(
|
||||
profile = profile,
|
||||
@@ -1196,30 +1197,25 @@ class AuthRepositoryImpl(
|
||||
keys = null,
|
||||
),
|
||||
)
|
||||
.map { response.passwordHash }
|
||||
.map { response }
|
||||
}
|
||||
.flatMap { masterPasswordHash ->
|
||||
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
is VaultUnlockResult.Success -> {
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = masterPasswordHash,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultUnlockError -> {
|
||||
(result.error ?: IllegalStateException("Failed to unlock vault"))
|
||||
.asFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
.onSuccess { response ->
|
||||
authDiskSource.userState = authDiskSource.userState?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = null,
|
||||
masterPasswordUnlock = MasterPasswordUnlockData(
|
||||
kdf = profile.toSdkParams(),
|
||||
masterKeyWrappedUserKey = response.newKey,
|
||||
salt = profile.email,
|
||||
),
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.flatMap { response ->
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = response.passwordHash,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { SetPasswordResult.Error(error = it) },
|
||||
onSuccess = { SetPasswordResult.Success },
|
||||
@@ -1326,16 +1322,26 @@ class AuthRepositoryImpl(
|
||||
privateKey = response.keys.private,
|
||||
),
|
||||
)
|
||||
authDiskSource.userState = authDiskSource
|
||||
.userState
|
||||
?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = MasterPasswordUnlockData(
|
||||
kdf = profile.toSdkParams(),
|
||||
masterKeyWrappedUserKey = response.encryptedUserKey,
|
||||
salt = profile.email,
|
||||
),
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.map { response.masterPasswordHash }
|
||||
.map { response }
|
||||
}
|
||||
.flatMap { masterPasswordHash ->
|
||||
.flatMap { response ->
|
||||
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
|
||||
is VaultUnlockResult.Success -> {
|
||||
enrollUserInPasswordReset(
|
||||
userId = userId,
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
passwordHash = masterPasswordHash,
|
||||
passwordHash = response.masterPasswordHash,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1345,12 +1351,6 @@ class AuthRepositoryImpl(
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSuccess {
|
||||
authDiskSource.userState = authDiskSource.userState?.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
this.organizationIdentifier = null
|
||||
}
|
||||
.fold(
|
||||
onFailure = { SetPasswordResult.Error(error = it) },
|
||||
onSuccess = { SetPasswordResult.Success },
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.bitwarden.policies.PolicyView
|
||||
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
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.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toKdfRequestModel
|
||||
@@ -84,10 +85,17 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
}
|
||||
?: profile
|
||||
.userDecryptionOptions
|
||||
?.copy(masterPasswordUnlock = null)
|
||||
|
||||
?.copy(
|
||||
hasMasterPassword = false,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
val forcePasswordResetReason = syncProfile.getForcePasswordResetReason(
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
previousForcePasswordResetReason = profile.forcePasswordResetReason,
|
||||
)
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
avatarColorHex = syncProfile.avatarColor,
|
||||
stamp = syncProfile.securityStamp,
|
||||
hasPremiumPersonally = syncProfile.isPremium,
|
||||
@@ -95,24 +103,31 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
isTwoFactorEnabled = syncProfile.isTwoFactorEnabled,
|
||||
creationDate = syncProfile.creationDate,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
kdfType = masterPasswordUnlockKdf?.kdfType
|
||||
?: profile.kdfType,
|
||||
kdfIterations = masterPasswordUnlockKdf?.iterations
|
||||
?: profile.kdfIterations,
|
||||
kdfMemory = masterPasswordUnlockKdf?.memory
|
||||
?: profile.kdfMemory,
|
||||
kdfParallelism = masterPasswordUnlockKdf?.parallelism
|
||||
?: profile.kdfParallelism,
|
||||
kdfType = masterPasswordUnlockKdf?.kdfType ?: profile.kdfType,
|
||||
kdfIterations = masterPasswordUnlockKdf?.iterations ?: profile.kdfIterations,
|
||||
kdfMemory = masterPasswordUnlockKdf?.memory ?: profile.kdfMemory,
|
||||
kdfParallelism = masterPasswordUnlockKdf?.parallelism ?: profile.kdfParallelism,
|
||||
)
|
||||
val updatedAccount = account.copy(profile = updatedProfile)
|
||||
return this
|
||||
.copy(
|
||||
accounts = accounts
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
replace(userId, updatedAccount)
|
||||
},
|
||||
)
|
||||
return this.copy(accounts = accounts.toMutableMap().apply { replace(userId, updatedAccount) })
|
||||
}
|
||||
|
||||
private fun SyncResponseJson.Profile.getForcePasswordResetReason(
|
||||
userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
previousForcePasswordResetReason: ForcePasswordResetReason?,
|
||||
): ForcePasswordResetReason? {
|
||||
val hasManageResetPasswordPermission = this.organizations.orEmpty().any {
|
||||
it.type == OrganizationType.OWNER ||
|
||||
it.type == OrganizationType.ADMIN ||
|
||||
it.permissions.shouldManageResetPassword
|
||||
}
|
||||
return ForcePasswordResetReason
|
||||
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION
|
||||
.takeIf {
|
||||
userDecryptionOptions?.hasMasterPassword == false &&
|
||||
hasManageResetPasswordPermission
|
||||
}
|
||||
?: previousForcePasswordResetReason
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,20 +135,16 @@ fun UserStateJson.toUpdatedUserStateJson(
|
||||
* their password.
|
||||
*/
|
||||
fun UserStateJson.toUserStateJsonWithPassword(
|
||||
masterPasswordUnlock: MasterPasswordUnlockData?,
|
||||
masterPasswordUnlock: MasterPasswordUnlockData,
|
||||
): UserStateJson {
|
||||
val account = this.activeAccount
|
||||
val profile = account.profile
|
||||
val userDecryptionOptions = profile.userDecryptionOptions
|
||||
val masterPasswordUnlockJson = masterPasswordUnlock
|
||||
?.let {
|
||||
MasterPasswordUnlockDataJson(
|
||||
salt = it.salt,
|
||||
kdf = it.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = it.masterKeyWrappedUserKey,
|
||||
)
|
||||
}
|
||||
?: userDecryptionOptions?.masterPasswordUnlock
|
||||
val masterPasswordUnlockJson = MasterPasswordUnlockDataJson(
|
||||
salt = masterPasswordUnlock.salt,
|
||||
kdf = masterPasswordUnlock.kdf.toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = masterPasswordUnlock.masterKeyWrappedUserKey,
|
||||
)
|
||||
val updatedProfile = profile
|
||||
.copy(
|
||||
forcePasswordResetReason = null,
|
||||
|
||||
@@ -5795,9 +5795,6 @@ class AuthRepositoryTest {
|
||||
userId = profile.userId,
|
||||
)
|
||||
} returns resetPasswordKey.asSuccess()
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithMasterPassword(password)
|
||||
} returns VaultUnlockResult.Success
|
||||
|
||||
val result = repository.setPassword(
|
||||
organizationIdentifier = organizationIdentifier,
|
||||
@@ -5822,7 +5819,6 @@ class AuthRepositoryTest {
|
||||
passwordHash = passwordHash,
|
||||
resetPasswordKey = resetPasswordKey,
|
||||
)
|
||||
vaultRepository.unlockVaultWithMasterPassword(password)
|
||||
vaultSdkSource.getResetPasswordKey(
|
||||
orgPublicKey = publicOrgKey,
|
||||
userId = profile.userId,
|
||||
@@ -7481,7 +7477,7 @@ class AuthRepositoryTest {
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = ENCRYPTED_USER_KEY,
|
||||
salt = "mockSalt",
|
||||
salt = EMAIL,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -7533,7 +7529,7 @@ class AuthRepositoryTest {
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
kdf = BASE_PROFILE_1.toSdkParams().toKdfRequestModel(),
|
||||
masterKeyWrappedUserKey = ENCRYPTED_USER_KEY,
|
||||
salt = "mockSalt",
|
||||
salt = EMAIL,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -9,10 +9,13 @@ import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.MasterPasswordUnlockDataJson
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.UserDecryptionJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.createMockOrganizationNetwork
|
||||
import com.bitwarden.network.model.createMockPermissions
|
||||
import com.bitwarden.network.model.createMockProfile
|
||||
import com.bitwarden.network.model.createMockSyncResponse
|
||||
import com.bitwarden.policies.PolicyType
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
@@ -234,36 +237,36 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
|
||||
val orgOnlyResult = originalState.toUpdatedUserStateJson(
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "color"
|
||||
every { securityStamp } returns "stamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "color",
|
||||
securityStamp = "stamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = true,
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
val orgOnlyProfile = orgOnlyResult.accounts.getValue("activeUserId").profile
|
||||
assertEquals(false, orgOnlyProfile.hasPremiumPersonally)
|
||||
assertEquals(true, orgOnlyProfile.hasPremiumFromOrganization)
|
||||
|
||||
val personalOnlyResult = originalState.toUpdatedUserStateJson(
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "color"
|
||||
every { securityStamp } returns "stamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "color",
|
||||
securityStamp = "stamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = false,
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
val personalOnlyProfile = personalOnlyResult.accounts.getValue("activeUserId").profile
|
||||
assertEquals(true, personalOnlyProfile.hasPremiumPersonally)
|
||||
@@ -398,74 +401,20 @@ class UserStateJsonExtensionsTest {
|
||||
),
|
||||
)
|
||||
.toUpdatedUserStateJson(
|
||||
syncResponse = mockk {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "avatarColor"
|
||||
every { securityStamp } returns "securityStamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toUserStateJsonWithPassword should update active account to set hasMasterPassword and clear forcePasswordResetReason`() {
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = ForcePasswordResetReason
|
||||
.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = null,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = mockk(),
|
||||
settings = mockk(),
|
||||
)
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
forcePasswordResetReason = null,
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "avatarColor",
|
||||
securityStamp = "securityStamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount,
|
||||
),
|
||||
)
|
||||
.toUserStateJsonWithPassword(masterPasswordUnlock = null),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -536,71 +485,6 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonWithPassword should preserve values of userDecryptionOptions`() {
|
||||
val keyConnectorOptionsJson = KeyConnectorUserDecryptionOptionsJson("key")
|
||||
val trustedDeviceOptionsJson = TrustedDeviceUserDecryptionOptionsJson(
|
||||
encryptedPrivateKey = "encryptedPrivateKey",
|
||||
encryptedUserKey = "encryptedUserKey",
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasManageResetPasswordPermission = true,
|
||||
)
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = null,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptionsJson,
|
||||
trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
)
|
||||
val originalAccount = AccountJson(
|
||||
profile = originalProfile,
|
||||
tokens = mockk(),
|
||||
settings = mockk(),
|
||||
)
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount.copy(
|
||||
profile = originalProfile.copy(
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptionsJson,
|
||||
trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to originalAccount,
|
||||
),
|
||||
)
|
||||
.toUserStateJsonWithPassword(masterPasswordUnlock = null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserState should return the correct UserState for an unlocked vault`() {
|
||||
val expectedCreationDate = Instant.parse("2024-06-15T10:30:00Z")
|
||||
@@ -1993,20 +1877,22 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = mockk<SyncResponseJson>(relaxed = true) {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "avatarColor"
|
||||
every { securityStamp } returns "securityStamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns true
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "avatarColor",
|
||||
securityStamp = "securityStamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = false,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2082,20 +1968,22 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "newAvatarColor"
|
||||
every { securityStamp } returns "newSecurityStamp"
|
||||
every { isPremium } returns true
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns true
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "newAvatarColor",
|
||||
securityStamp = "newSecurityStamp",
|
||||
isPremium = true,
|
||||
isPremiumFromOrganization = false,
|
||||
isTwoFactorEnabled = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2129,7 +2017,7 @@ class UserStateJsonExtensionsTest {
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should update existing UserDecryptionOptionsJson when syncResponse has no userDecryption`() {
|
||||
fun `toUpdatedUserStateJson should clear hasMasterPassword and masterPasswordUnlock when syncResponse has no userDecryption`() {
|
||||
val keyConnectorOptions = KeyConnectorUserDecryptionOptionsJson("keyConnectorUrl")
|
||||
val originalProfile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
@@ -2165,18 +2053,20 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns "updatedAvatarColor"
|
||||
every { securityStamp } returns "updatedSecurityStamp"
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns true
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
every { userDecryption } returns null
|
||||
}
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = "updatedAvatarColor",
|
||||
securityStamp = "updatedSecurityStamp",
|
||||
isPremium = false,
|
||||
isPremiumFromOrganization = true,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
organizations = emptyList(),
|
||||
),
|
||||
userDecryption = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2191,7 +2081,7 @@ class UserStateJsonExtensionsTest {
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = keyConnectorOptions,
|
||||
masterPasswordUnlock = null,
|
||||
@@ -2236,31 +2126,28 @@ class UserStateJsonExtensionsTest {
|
||||
accounts = mapOf("activeUserId" to originalAccount),
|
||||
)
|
||||
|
||||
val syncResponse = mockk<SyncResponseJson> {
|
||||
every { profile } returns mockk {
|
||||
every { id } returns "activeUserId"
|
||||
every { avatarColor } returns null
|
||||
every { securityStamp } returns null
|
||||
every { isPremium } returns false
|
||||
every { isPremiumFromOrganization } returns false
|
||||
every { isTwoFactorEnabled } returns false
|
||||
every { creationDate } returns Instant.parse("2024-09-13T01:00:00.00Z")
|
||||
}
|
||||
val updatedKdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
)
|
||||
val updatedMasterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
salt = "mockSalt",
|
||||
kdf = updatedKdf,
|
||||
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
|
||||
)
|
||||
every { userDecryption } returns UserDecryptionJson(
|
||||
masterPasswordUnlock = updatedMasterPasswordUnlock,
|
||||
)
|
||||
}
|
||||
val syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
avatarColor = null,
|
||||
securityStamp = null,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MasterPasswordUnlockDataJson(
|
||||
salt = "mockSalt",
|
||||
kdf = KdfJson(
|
||||
kdfType = KdfTypeJson.PBKDF2_SHA256,
|
||||
iterations = DEFAULT_PBKDF2_ITERATIONS,
|
||||
memory = null,
|
||||
parallelism = null,
|
||||
),
|
||||
masterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
UserStateJson(
|
||||
@@ -2293,6 +2180,137 @@ class UserStateJsonExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should set forcePasswordResetReason when user without master password is an organization admin`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.ADMIN,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should set forcePasswordResetReason when user without master password has reset password permission`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.USER,
|
||||
permissions = createMockPermissions(
|
||||
shouldManageResetPassword = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(masterPasswordUnlock = null),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.TDE_USER_WITHOUT_PASSWORD_HAS_PASSWORD_RESET_PERMISSION,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should not set forcePasswordResetReason when user without master password lacks reset password permission`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = TDE_USER_DECRYPTION_OPTIONS,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.USER,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = null,
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
null,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `toUpdatedUserStateJson should preserve previous forcePasswordResetReason when user has a master password`() {
|
||||
val originalUserState = createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = true,
|
||||
trustedDeviceUserDecryptionOptions = null,
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
),
|
||||
forcePasswordResetReason = ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
)
|
||||
|
||||
val result = originalUserState.toUpdatedUserStateJson(
|
||||
syncResponse = createMockSyncResponse(
|
||||
number = 1,
|
||||
profile = createMockProfile(
|
||||
number = 1,
|
||||
id = "activeUserId",
|
||||
organizations = listOf(
|
||||
createMockOrganizationNetwork(
|
||||
number = 1,
|
||||
type = OrganizationType.OWNER,
|
||||
),
|
||||
),
|
||||
),
|
||||
userDecryption = UserDecryptionJson(
|
||||
masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ForcePasswordResetReason.WEAK_MASTER_PASSWORD_ON_LOGIN,
|
||||
result.accounts.getValue("activeUserId").profile.forcePasswordResetReason,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toUserStateJsonKdfUpdatedMinimums should update KDF settings to minimum values`() {
|
||||
val originalProfile = AccountJson.Profile(
|
||||
@@ -2494,3 +2512,53 @@ private val MOCK_MASTER_PASSWORD_UNLOCK_DATA = MasterPasswordUnlockDataJson(
|
||||
),
|
||||
masterKeyWrappedUserKey = "masterKeyWrappedUserKeyMock",
|
||||
)
|
||||
|
||||
private val TDE_USER_DECRYPTION_OPTIONS = UserDecryptionOptionsJson(
|
||||
hasMasterPassword = false,
|
||||
trustedDeviceUserDecryptionOptions = TrustedDeviceUserDecryptionOptionsJson(
|
||||
encryptedPrivateKey = "encryptedPrivateKey",
|
||||
encryptedUserKey = "encryptedUserKey",
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = false,
|
||||
hasManageResetPasswordPermission = false,
|
||||
),
|
||||
keyConnectorUserDecryptionOptions = null,
|
||||
masterPasswordUnlock = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a [UserStateJson] with a single "activeUserId" account using the given
|
||||
* [userDecryptionOptions] and [forcePasswordResetReason].
|
||||
*/
|
||||
private fun createUserStateWithDecryptionOptions(
|
||||
userDecryptionOptions: UserDecryptionOptionsJson?,
|
||||
forcePasswordResetReason: ForcePasswordResetReason? = null,
|
||||
): UserStateJson =
|
||||
UserStateJson(
|
||||
activeUserId = "activeUserId",
|
||||
accounts = mapOf(
|
||||
"activeUserId" to AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = "activeUserId",
|
||||
email = "email",
|
||||
isEmailVerified = true,
|
||||
name = "name",
|
||||
stamp = null,
|
||||
organizationId = null,
|
||||
avatarColorHex = null,
|
||||
hasPremiumPersonally = true,
|
||||
hasPremiumFromOrganization = null,
|
||||
forcePasswordResetReason = forcePasswordResetReason,
|
||||
kdfType = KdfTypeJson.ARGON2_ID,
|
||||
kdfIterations = 600000,
|
||||
kdfMemory = 16,
|
||||
kdfParallelism = 4,
|
||||
userDecryptionOptions = userDecryptionOptions,
|
||||
isTwoFactorEnabled = false,
|
||||
creationDate = Instant.parse("2024-09-13T01:00:00.00Z"),
|
||||
),
|
||||
tokens = null,
|
||||
settings = AccountJson.Settings(environmentUrlData = null),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user