mirror of
https://github.com/bitwarden/android.git
synced 2026-11-09 10:34:18 -06:00
[PM-44192] fix: Keep server cipher data after vault migration (#7432)
This commit is contained in:
+2
-3
@@ -28,7 +28,6 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.MigratePersonalVaultResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.updateFromMiniResponse
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -298,8 +297,8 @@ class VaultMigrationManagerImpl(
|
||||
encryptedCiphersMap[miniResponse.id]?.let {
|
||||
vaultDiskSource.saveCipher(
|
||||
userId = userId,
|
||||
cipher = it.updateFromMiniResponse(
|
||||
miniResponse = miniResponse,
|
||||
cipher = miniResponse.toEncryptedNetworkCipher(
|
||||
originalCipher = it,
|
||||
collectionIds = collectionIds,
|
||||
),
|
||||
)
|
||||
|
||||
+41
-19
@@ -125,29 +125,51 @@ fun Cipher.toEncryptedNetworkCipherResponse(
|
||||
)
|
||||
|
||||
/**
|
||||
* Updates a [SyncResponseJson.Cipher] with metadata from a
|
||||
* [CipherMiniResponseJson.CipherMiniResponse].
|
||||
* Converts a [CipherMiniResponseJson.CipherMiniResponse] into a [SyncResponseJson.Cipher],
|
||||
* taking fields the mini response lacks (folder, favorite, edit, permissions, view password,
|
||||
* encrypted for) from [originalCipher].
|
||||
* This is useful for updating local cipher data after bulk operations that return mini responses.
|
||||
*
|
||||
* @param miniResponse The mini response containing updated cipher metadata.
|
||||
* @param collectionIds Optional list of collection IDs to update.
|
||||
* If null, keeps existing collection IDs.
|
||||
* @return A new [SyncResponseJson.Cipher] with updated fields from the mini response.
|
||||
* @param originalCipher The local cipher to take fields missing from the mini response from.
|
||||
* @param collectionIds Optional list of collection IDs to set.
|
||||
* If null, keeps the collection IDs of [originalCipher].
|
||||
* @return A new [SyncResponseJson.Cipher] built from the mini response.
|
||||
*/
|
||||
fun SyncResponseJson.Cipher.updateFromMiniResponse(
|
||||
miniResponse: CipherMiniResponseJson.CipherMiniResponse,
|
||||
fun CipherMiniResponseJson.CipherMiniResponse.toEncryptedNetworkCipher(
|
||||
originalCipher: SyncResponseJson.Cipher,
|
||||
collectionIds: List<String>? = null,
|
||||
): SyncResponseJson.Cipher = copy(
|
||||
organizationId = miniResponse.organizationId,
|
||||
collectionIds = collectionIds ?: this.collectionIds,
|
||||
revisionDate = miniResponse.revisionDate,
|
||||
key = miniResponse.key,
|
||||
attachments = miniResponse.attachments,
|
||||
archivedDate = miniResponse.archivedDate,
|
||||
deletedDate = miniResponse.deletedDate,
|
||||
reprompt = miniResponse.reprompt,
|
||||
shouldOrganizationUseTotp = miniResponse.shouldOrganizationUseTotp,
|
||||
type = miniResponse.type,
|
||||
): SyncResponseJson.Cipher = SyncResponseJson.Cipher(
|
||||
id = id,
|
||||
organizationId = organizationId,
|
||||
folderId = originalCipher.folderId,
|
||||
collectionIds = collectionIds ?: originalCipher.collectionIds,
|
||||
key = key,
|
||||
name = name,
|
||||
notes = notes,
|
||||
type = type,
|
||||
login = login,
|
||||
identity = identity,
|
||||
card = card,
|
||||
secureNote = secureNote,
|
||||
sshKey = sshKey,
|
||||
bankAccount = bankAccount,
|
||||
driversLicense = driversLicense,
|
||||
passport = passport,
|
||||
isFavorite = originalCipher.isFavorite,
|
||||
reprompt = reprompt,
|
||||
shouldOrganizationUseTotp = shouldOrganizationUseTotp,
|
||||
shouldEdit = originalCipher.shouldEdit,
|
||||
permissions = originalCipher.permissions,
|
||||
shouldViewPassword = originalCipher.shouldViewPassword,
|
||||
encryptedFor = originalCipher.encryptedFor,
|
||||
attachments = attachments,
|
||||
fields = fields,
|
||||
passwordHistory = passwordHistory,
|
||||
creationDate = creationDate,
|
||||
deletedDate = deletedDate,
|
||||
revisionDate = revisionDate,
|
||||
archivedDate = archivedDate,
|
||||
data = data,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+28
-10
@@ -554,7 +554,7 @@ class VaultSdkCipherExtensionsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateFromMiniResponse should update cipher with mini response data`() {
|
||||
fun `toEncryptedNetworkCipher from mini response should use mini response data`() {
|
||||
val originalCipher = createMockCipher(
|
||||
number = 1,
|
||||
organizationId = null,
|
||||
@@ -563,8 +563,8 @@ class VaultSdkCipherExtensionsTest {
|
||||
|
||||
val miniResponse = createMockCipherMiniResponse(number = 2)
|
||||
|
||||
val result = originalCipher.updateFromMiniResponse(
|
||||
miniResponse = miniResponse,
|
||||
val result = miniResponse.toEncryptedNetworkCipher(
|
||||
originalCipher = originalCipher,
|
||||
collectionIds = listOf("collection-1"),
|
||||
)
|
||||
|
||||
@@ -577,14 +577,32 @@ class VaultSdkCipherExtensionsTest {
|
||||
assertEquals(miniResponse.deletedDate, result.deletedDate)
|
||||
assertEquals(miniResponse.reprompt, result.reprompt)
|
||||
assertEquals(miniResponse.shouldOrganizationUseTotp, result.shouldOrganizationUseTotp)
|
||||
// Verify unchanged fields remain the same
|
||||
assertEquals(originalCipher.name, result.name)
|
||||
assertEquals(originalCipher.notes, result.notes)
|
||||
assertEquals(originalCipher.id, result.id)
|
||||
assertEquals(miniResponse.data, result.data)
|
||||
assertEquals(miniResponse.creationDate, result.creationDate)
|
||||
assertEquals(miniResponse.name, result.name)
|
||||
assertEquals(miniResponse.notes, result.notes)
|
||||
assertEquals(miniResponse.login, result.login)
|
||||
assertEquals(miniResponse.card, result.card)
|
||||
assertEquals(miniResponse.identity, result.identity)
|
||||
assertEquals(miniResponse.secureNote, result.secureNote)
|
||||
assertEquals(miniResponse.sshKey, result.sshKey)
|
||||
assertEquals(miniResponse.bankAccount, result.bankAccount)
|
||||
assertEquals(miniResponse.driversLicense, result.driversLicense)
|
||||
assertEquals(miniResponse.passport, result.passport)
|
||||
assertEquals(miniResponse.fields, result.fields)
|
||||
assertEquals(miniResponse.passwordHistory, result.passwordHistory)
|
||||
assertEquals(miniResponse.id, result.id)
|
||||
// Verify fields absent from the mini response remain the same
|
||||
assertEquals(originalCipher.folderId, result.folderId)
|
||||
assertEquals(originalCipher.isFavorite, result.isFavorite)
|
||||
assertEquals(originalCipher.shouldEdit, result.shouldEdit)
|
||||
assertEquals(originalCipher.permissions, result.permissions)
|
||||
assertEquals(originalCipher.shouldViewPassword, result.shouldViewPassword)
|
||||
assertEquals(originalCipher.encryptedFor, result.encryptedFor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateFromMiniResponse should preserve existing collectionIds when not provided`() {
|
||||
fun `toEncryptedNetworkCipher from mini response should keep collectionIds when null`() {
|
||||
val originalCipher = createMockCipher(
|
||||
number = 1,
|
||||
collectionIds = listOf("original-collection-1", "original-collection-2"),
|
||||
@@ -592,8 +610,8 @@ class VaultSdkCipherExtensionsTest {
|
||||
|
||||
val miniResponse = createMockCipherMiniResponse(number = 2)
|
||||
|
||||
val result = originalCipher.updateFromMiniResponse(
|
||||
miniResponse = miniResponse,
|
||||
val result = miniResponse.toEncryptedNetworkCipher(
|
||||
originalCipher = originalCipher,
|
||||
collectionIds = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import java.time.Instant
|
||||
|
||||
/**
|
||||
* Represents a minimal cipher response from the API, typically returned from bulk operations.
|
||||
* Contains core cipher metadata without detailed type-specific fields.
|
||||
* Contains cipher metadata and encrypted fields, but not user-specific fields (folder,
|
||||
* favorite, permissions).
|
||||
*
|
||||
* @property cipherMiniResponse The list of mini responses.
|
||||
*/
|
||||
@@ -29,6 +30,18 @@ data class CipherMiniResponseJson(
|
||||
* @property reprompt The reprompt type.
|
||||
* @property key The cipher key (nullable).
|
||||
* @property archivedDate The archived date (nullable).
|
||||
* @property name The encrypted name (nullable, `null` for blob-encrypted ciphers).
|
||||
* @property notes The encrypted notes (nullable).
|
||||
* @property login The login data (nullable).
|
||||
* @property card The card data (nullable).
|
||||
* @property identity The identity data (nullable).
|
||||
* @property secureNote The secure note data (nullable).
|
||||
* @property sshKey The SSH key data (nullable).
|
||||
* @property bankAccount The bank account data (nullable).
|
||||
* @property driversLicense The driver's license data (nullable).
|
||||
* @property passport The passport data (nullable).
|
||||
* @property fields List of custom fields (nullable).
|
||||
* @property passwordHistory List of password history entries (nullable).
|
||||
*/
|
||||
@Serializable
|
||||
data class CipherMiniResponse(
|
||||
@@ -71,5 +84,41 @@ data class CipherMiniResponseJson(
|
||||
@SerialName("archivedDate")
|
||||
@Contextual
|
||||
val archivedDate: Instant?,
|
||||
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
|
||||
@SerialName("notes")
|
||||
val notes: String?,
|
||||
|
||||
@SerialName("login")
|
||||
val login: SyncResponseJson.Cipher.Login?,
|
||||
|
||||
@SerialName("card")
|
||||
val card: SyncResponseJson.Cipher.Card?,
|
||||
|
||||
@SerialName("identity")
|
||||
val identity: SyncResponseJson.Cipher.Identity?,
|
||||
|
||||
@SerialName("secureNote")
|
||||
val secureNote: SyncResponseJson.Cipher.SecureNote?,
|
||||
|
||||
@SerialName("sshKey")
|
||||
val sshKey: SyncResponseJson.Cipher.SshKey?,
|
||||
|
||||
@SerialName("bankAccount")
|
||||
val bankAccount: SyncResponseJson.Cipher.BankAccount?,
|
||||
|
||||
@SerialName("driversLicense")
|
||||
val driversLicense: SyncResponseJson.Cipher.DriversLicense?,
|
||||
|
||||
@SerialName("passport")
|
||||
val passport: SyncResponseJson.Cipher.Passport?,
|
||||
|
||||
@SerialName("fields")
|
||||
val fields: List<SyncResponseJson.Cipher.Field>?,
|
||||
|
||||
@SerialName("passwordHistory")
|
||||
val passwordHistory: List<SyncResponseJson.Cipher.PasswordHistory>?,
|
||||
)
|
||||
}
|
||||
|
||||
+12
@@ -20,6 +20,18 @@ fun createMockCipherMiniResponse(
|
||||
reprompt = CipherRepromptTypeJson.NONE,
|
||||
key = "mockKey-$number",
|
||||
archivedDate = null,
|
||||
name = "mockName-$number",
|
||||
notes = "mockNotes-$number",
|
||||
login = createMockLogin(number = number),
|
||||
card = createMockCard(number = number),
|
||||
identity = createMockIdentity(number = number),
|
||||
secureNote = createMockSecureNote(),
|
||||
sshKey = createMockSshKey(number = number),
|
||||
bankAccount = createMockBankAccount(number = number),
|
||||
driversLicense = createMockDriversLicense(number = number),
|
||||
passport = createMockPassport(number = number),
|
||||
fields = listOf(createMockField(number = number)),
|
||||
passwordHistory = listOf(createMockPasswordHistory(number = number)),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user