diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensions.kt index ab220ed0e8..9cab49a67f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensions.kt @@ -26,6 +26,7 @@ fun TrustDeviceResponse.toUserStateJson( hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) val deviceOptions = decryptionOptions .trustedDeviceUserDecryptionOptions diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt index f036bcc0ae..bf75128c9a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt @@ -32,6 +32,7 @@ fun UserStateJson.toRemovedPasswordUserStateJson( hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) val updatedProfile = profile.copy(userDecryptionOptions = updatedUserDecryptionOptions) val updatedAccount = account.copy(profile = updatedProfile) @@ -54,6 +55,23 @@ fun UserStateJson.toUpdatedUserStateJson( val userId = syncProfile.id val account = this.accounts[userId] ?: return this val profile = account.profile + val userDecryptionOptions = syncResponse + .userDecryption + ?.let { syncUserDecryption -> + profile + .userDecryptionOptions + ?.copy(masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock) + ?: UserDecryptionOptionsJson( + hasMasterPassword = syncUserDecryption.masterPasswordUnlock != null, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = syncUserDecryption.masterPasswordUnlock, + ) + } + ?: profile + .userDecryptionOptions + ?.copy(masterPasswordUnlock = null) + val updatedProfile = profile .copy( avatarColorHex = syncProfile.avatarColor, @@ -61,6 +79,7 @@ fun UserStateJson.toUpdatedUserStateJson( hasPremium = syncProfile.isPremium || syncProfile.isPremiumFromOrganization, isTwoFactorEnabled = syncProfile.isTwoFactorEnabled, creationDate = syncProfile.creationDate, + userDecryptionOptions = userDecryptionOptions, ) val updatedAccount = account.copy(profile = updatedProfile) return this @@ -90,6 +109,7 @@ fun UserStateJson.toUserStateJsonWithPassword(): UserStateJson { hasMasterPassword = true, keyConnectorUserDecryptionOptions = null, trustedDeviceUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ) val updatedAccount = account.copy(profile = updatedProfile) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 8d460330e8..372d1afac8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -1463,6 +1463,7 @@ private val USER_STATE = UserStateJson( keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson( keyConnectorUrl = "keyConnectorUrl", ), + masterPasswordUnlock = null, ), ), tokens = AccountTokensJson( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/TrustedDeviceManagerTests.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/TrustedDeviceManagerTests.kt index aabdfd36ae..3bede3f95b 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/TrustedDeviceManagerTests.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/TrustedDeviceManagerTests.kt @@ -273,12 +273,14 @@ private val DEFAULT_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDec hasMasterPassword = false, trustedDeviceUserDecryptionOptions = DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val UPDATED_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson( hasMasterPassword = false, trustedDeviceUserDecryptionOptions = UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val DEFAULT_ACCOUNT = AccountJson( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensionsTest.kt index c51eae6ef4..c0352d0c71 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/manager/util/TrustDeviceResponseExtensionsTest.kt @@ -55,12 +55,14 @@ private val DEFAULT_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDec hasMasterPassword = false, trustedDeviceUserDecryptionOptions = DEFAULT_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val UPDATED_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryptionOptionsJson( hasMasterPassword = false, trustedDeviceUserDecryptionOptions = UPDATED_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val DEFAULT_ACCOUNT = AccountJson( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index e753f3c667..d9e04a1324 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -2008,6 +2008,7 @@ class AuthRepositoryTest { hasMasterPassword = false, keyConnectorUserDecryptionOptions = null, trustedDeviceUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ) coEvery { @@ -7030,6 +7031,7 @@ class AuthRepositoryTest { hasMasterPassword = false, trustedDeviceUserDecryptionOptions = TRUSTED_DEVICE_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) @Deprecated( @@ -7138,6 +7140,7 @@ class AuthRepositoryTest { hasMasterPassword = true, keyConnectorUserDecryptionOptions = null, trustedDeviceUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ), ), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/GetTokenResponseExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/GetTokenResponseExtensionsTest.kt index 2dadfa8fdf..9d121816e1 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/GetTokenResponseExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/GetTokenResponseExtensionsTest.kt @@ -128,6 +128,7 @@ private val USER_DECRYPTION_OPTIONS = UserDecryptionOptionsJson( hasManageResetPasswordPermission = true, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val PROFILE_1 = AccountJson.Profile( userId = USER_ID_1, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt index 04b5dac9ee..6f6dc96a86 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt @@ -4,8 +4,11 @@ import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson import com.bitwarden.data.repository.model.Environment 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.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson @@ -83,6 +86,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ), ), @@ -112,6 +116,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = true, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ), isTwoFactorEnabled = false, creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), @@ -136,6 +141,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ), ), @@ -222,6 +228,7 @@ class UserStateJsonExtensionsTest { every { isTwoFactorEnabled } returns false every { creationDate } returns ZonedDateTime .parse("2024-09-13T01:00:00.00Z") + every { userDecryption } returns null } }, ), @@ -266,6 +273,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = true, keyConnectorUserDecryptionOptions = null, trustedDeviceUserDecryptionOptions = null, + masterPasswordUnlock = null, ), ), ), @@ -309,6 +317,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = true, keyConnectorUserDecryptionOptions = keyConnectorOptionsJson, trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson, + masterPasswordUnlock = null, ), isTwoFactorEnabled = false, creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), @@ -328,6 +337,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = true, keyConnectorUserDecryptionOptions = keyConnectorOptionsJson, trustedDeviceUserDecryptionOptions = trustedDeviceOptionsJson, + masterPasswordUnlock = null, ), ), ), @@ -396,6 +406,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = true, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = AccountTokensJson( @@ -509,6 +520,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = AccountTokensJson( @@ -629,6 +641,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -746,6 +759,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -863,6 +877,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -984,6 +999,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -1082,6 +1098,7 @@ class UserStateJsonExtensionsTest { keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson( keyConnectorUrl = "keyConnectorUrl", ), + masterPasswordUnlock = null, ) }, tokens = null, @@ -1157,6 +1174,7 @@ class UserStateJsonExtensionsTest { hasMasterPassword = false, trustedDeviceUserDecryptionOptions = null, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -1262,6 +1280,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -1381,6 +1400,7 @@ class UserStateJsonExtensionsTest { hasManageResetPasswordPermission = false, ), keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) }, tokens = null, @@ -1432,4 +1452,239 @@ class UserStateJsonExtensionsTest { ), ) } + + @Test + @Suppress("MaxLineLength") + fun `toUpdatedUserStateJson should create UserDecryptionOptionsJson when null and syncResponse has masterPasswordUnlock`() { + val originalProfile = AccountJson.Profile( + userId = "activeUserId", + email = "email", + isEmailVerified = true, + name = "name", + 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 originalAccount = AccountJson( + profile = originalProfile, + tokens = null, + settings = AccountJson.Settings(environmentUrlData = null), + ) + val originalUserState = UserStateJson( + activeUserId = "activeUserId", + accounts = mapOf("activeUserId" to originalAccount), + ) + + val syncResponse = mockk(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 ZonedDateTime.parse("2024-09-13T01:00:00.00Z") + } + every { userDecryption } returns UserDecryptionJson( + masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA, + ) + } + + assertEquals( + UserStateJson( + activeUserId = "activeUserId", + accounts = mapOf( + "activeUserId" to originalAccount.copy( + profile = originalProfile.copy( + avatarColorHex = "avatarColor", + stamp = "securityStamp", + hasPremium = false, + isTwoFactorEnabled = true, + creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA, + ), + ), + ), + ), + ), + originalUserState.toUpdatedUserStateJson(syncResponse), + ) + } + + @Test + @Suppress("MaxLineLength") + fun `toUpdatedUserStateJson should update existing UserDecryptionOptionsJson with masterPasswordUnlock`() { + val trustedDeviceOptions = TrustedDeviceUserDecryptionOptionsJson( + encryptedPrivateKey = "encryptedPrivateKey", + encryptedUserKey = "encryptedUserKey", + hasAdminApproval = true, + hasLoginApprovingDevice = false, + hasManageResetPasswordPermission = true, + ) + val originalProfile = AccountJson.Profile( + userId = "activeUserId", + email = "email", + isEmailVerified = true, + name = "name", + stamp = null, + organizationId = null, + avatarColorHex = null, + hasPremium = true, + forcePasswordResetReason = null, + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = trustedDeviceOptions, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = 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 syncResponse = mockk { + 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 ZonedDateTime.parse("2024-09-13T01:00:00.00Z") + } + every { userDecryption } returns UserDecryptionJson( + masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA, + ) + } + + assertEquals( + UserStateJson( + activeUserId = "activeUserId", + accounts = mapOf( + "activeUserId" to originalAccount.copy( + profile = originalProfile.copy( + avatarColorHex = "newAvatarColor", + stamp = "newSecurityStamp", + hasPremium = true, + isTwoFactorEnabled = true, + creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = trustedDeviceOptions, + keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA, + ), + ), + ), + ), + ), + originalUserState.toUpdatedUserStateJson(syncResponse), + ) + } + + @Test + @Suppress("MaxLineLength") + fun `toUpdatedUserStateJson should update existing UserDecryptionOptionsJson when syncResponse has no userDecryption`() { + val keyConnectorOptions = KeyConnectorUserDecryptionOptionsJson("keyConnectorUrl") + val originalProfile = AccountJson.Profile( + userId = "activeUserId", + email = "email", + isEmailVerified = true, + name = "name", + stamp = null, + organizationId = null, + avatarColorHex = null, + hasPremium = true, + forcePasswordResetReason = null, + kdfType = KdfTypeJson.ARGON2_ID, + kdfIterations = 600000, + kdfMemory = 16, + kdfParallelism = 4, + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = keyConnectorOptions, + masterPasswordUnlock = MOCK_MASTER_PASSWORD_UNLOCK_DATA, + ), + 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 syncResponse = mockk { + 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 ZonedDateTime.parse("2024-09-13T01:00:00.00Z") + } + every { userDecryption } returns null + } + + assertEquals( + UserStateJson( + activeUserId = "activeUserId", + accounts = mapOf( + "activeUserId" to originalAccount.copy( + profile = originalProfile.copy( + avatarColorHex = "updatedAvatarColor", + stamp = "updatedSecurityStamp", + hasPremium = true, + isTwoFactorEnabled = false, + creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), + userDecryptionOptions = UserDecryptionOptionsJson( + hasMasterPassword = true, + trustedDeviceUserDecryptionOptions = null, + keyConnectorUserDecryptionOptions = keyConnectorOptions, + masterPasswordUnlock = null, + ), + ), + ), + ), + ), + originalUserState.toUpdatedUserStateJson(syncResponse), + ) + } } + +private val MOCK_MASTER_PASSWORD_UNLOCK_DATA = MasterPasswordUnlockDataJson( + salt = "mockSalt", + kdf = mockk(), + masterKeyWrappedUserKey = "masterKeyWrappedUserKeyMock", +) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt index 4e2b2d6aeb..65b51f6794 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/manager/FirstTimeActionManagerTest.kt @@ -421,6 +421,7 @@ private val MOCK_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryp hasMasterPassword = false, trustedDeviceUserDecryptionOptions = MOCK_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val MOCK_PROFILE = AccountJson.Profile( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 261fd5c524..b2654eb4f5 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -1292,6 +1292,7 @@ private val MOCK_USER_DECRYPTION_OPTIONS: UserDecryptionOptionsJson = UserDecryp hasMasterPassword = false, trustedDeviceUserDecryptionOptions = MOCK_TRUSTED_DEVICE_USER_DECRYPTION_OPTIONS, keyConnectorUserDecryptionOptions = null, + masterPasswordUnlock = null, ) private val MOCK_PROFILE = AccountJson.Profile( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt index 91365846f7..132006c7cc 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/tools/generator/repository/GeneratorRepositoryTest.kt @@ -804,6 +804,7 @@ private val USER_STATE = UserStateJson( keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson( keyConnectorUrl = "keyConnectorUrl", ), + masterPasswordUnlock = null, ), isTwoFactorEnabled = false, creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"), diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt index 63d42b1064..d6e1293455 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/disk/VaultDiskSourceTest.kt @@ -356,6 +356,7 @@ private val VAULT_DATA: SyncResponseJson = SyncResponseJson( policies = null, domains = DOMAINS_1, sends = listOf(SEND_1), + userDecryption = null, ) private const val CIPHER_JSON = """ diff --git a/network/src/main/kotlin/com/bitwarden/network/model/KdfJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/KdfJson.kt new file mode 100644 index 0000000000..9e24730eaf --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/KdfJson.kt @@ -0,0 +1,22 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents the data used to create the kdf settings. + */ +@Serializable +data class KdfJson( + @SerialName("KdfType") + val kdfType: KdfTypeJson, + + @SerialName("Iterations") + val iterations: Int, + + @SerialName("Memory") + val memory: Int?, + + @SerialName("Parallelism") + val parallelism: Int?, +) diff --git a/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt new file mode 100644 index 0000000000..a3aa35a0ae --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/MasterPasswordUnlockDataJson.kt @@ -0,0 +1,25 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames + +/** + * Represents the data used to unlock with the master password. + */ +@Serializable +@OptIn(ExperimentalSerializationApi::class) +data class MasterPasswordUnlockDataJson( + @SerialName("Salt") + val salt: String, + + @SerialName("Kdf") + val kdf: KdfJson, + + // TODO: PM-26397 this was done due to naming inconsistency server side, + // should be cleaned up when server side is updated + @SerialName("MasterKeyWrappedUserKey") + @JsonNames("MasterKeyEncryptedUserKey") + val masterKeyWrappedUserKey: String, +) diff --git a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt index e1a1db6883..f38f56e558 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -48,6 +48,9 @@ data class SyncResponseJson( @SerialName("sends") val sends: List?, + + @SerialName("UserDecryption") + val userDecryption: UserDecryptionJson?, ) { /** * Represents domains in the vault response. diff --git a/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionJson.kt new file mode 100644 index 0000000000..892c9f7875 --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionJson.kt @@ -0,0 +1,13 @@ +package com.bitwarden.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents the user decryption options received on sync. + */ +@Serializable +data class UserDecryptionJson( + @SerialName("MasterPasswordUnlock") + val masterPasswordUnlock: MasterPasswordUnlockDataJson?, +) diff --git a/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionOptionsJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionOptionsJson.kt index e365b33f8c..b667940660 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionOptionsJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/UserDecryptionOptionsJson.kt @@ -21,6 +21,10 @@ data class UserDecryptionOptionsJson( @JsonNames("HasMasterPassword") val hasMasterPassword: Boolean, + @SerialName("masterPasswordUnlock") + @JsonNames("MasterPasswordUnlock") + val masterPasswordUnlock: MasterPasswordUnlockDataJson?, + @SerialName("trustedDeviceOption") @JsonNames("TrustedDeviceOption") val trustedDeviceUserDecryptionOptions: TrustedDeviceUserDecryptionOptionsJson?, diff --git a/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt index e21988cc74..5385658e79 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/IdentityServiceTest.kt @@ -626,6 +626,7 @@ private val LOGIN_SUCCESS = GetTokenResponseJson.Success( keyConnectorUserDecryptionOptions = KeyConnectorUserDecryptionOptionsJson( keyConnectorUrl = "keyConnectorUrl", ), + masterPasswordUnlock = null, ), keyConnectorUrl = "keyConnectorUrl", ) diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseUtil.kt index 220e2fd610..498c18cd85 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseUtil.kt @@ -13,6 +13,7 @@ fun createMockSyncResponse( policies: List = listOf(createMockPolicy(number = number)), domains: SyncResponseJson.Domains = createMockDomains(number = number), sends: List = listOf(createMockSend(number = number)), + userDecryption: UserDecryptionJson? = null, ): SyncResponseJson = SyncResponseJson( folders = folders, @@ -22,4 +23,5 @@ fun createMockSyncResponse( policies = policies, domains = domains, sends = sends, + userDecryption = userDecryption, )