From 0892e0ff1fa2e32546c1a9d7e844e10460de04ee Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:34:46 -0400 Subject: [PATCH] [PM-21782] Pass `encryptedFor` to cipher functions (#5297) --- .../vault/datasource/sdk/VaultSdkSource.kt | 7 +- .../datasource/sdk/VaultSdkSourceImpl.kt | 7 +- .../sdk/model/Fido2CredentialStoreImpl.kt | 11 +- .../data/vault/manager/CipherManagerImpl.kt | 66 +++--- .../vault/manager/VaultLockManagerImpl.kt | 1 + .../util/VaultSdkCipherExtensions.kt | 29 ++- .../datasource/disk/VaultDiskSourceTest.kt | 3 +- .../datasource/sdk/VaultSdkSourceTest.kt | 55 ++--- .../datasource/sdk/model/CipherViewUtil.kt | 3 +- .../model/VaultSdkEncryptionContextUtil.kt | 29 +++ .../data/vault/manager/CipherManagerTest.kt | 220 +++++++++++++----- .../vault/manager/VaultLockManagerTest.kt | 19 ++ .../util/VaultSdkCipherExtensionsTest.kt | 35 ++- gradle/libs.versions.toml | 2 +- .../network/model/CipherJsonRequest.kt | 4 + .../network/model/SyncResponseJson.kt | 5 + .../network/service/CiphersServiceTest.kt | 6 +- .../network/service/SyncServiceTest.kt | 3 +- .../network/model/CipherJsonRequestUtil.kt | 2 + .../network/model/SyncResponseCipherUtil.kt | 2 + 20 files changed, 372 insertions(+), 137 deletions(-) create mode 100644 app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkEncryptionContextUtil.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt index c78822c0dd..677caaa156 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt @@ -22,6 +22,7 @@ import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView import com.bitwarden.vault.Collection import com.bitwarden.vault.CollectionView +import com.bitwarden.vault.EncryptionContext import com.bitwarden.vault.Folder import com.bitwarden.vault.FolderView import com.bitwarden.vault.PasswordHistory @@ -187,7 +188,7 @@ interface VaultSdkSource { suspend fun encryptCipher( userId: String, cipherView: CipherView, - ): Result + ): Result /** * Decrypts a [Cipher] for the user with the given [userId], returning a [CipherView] wrapped @@ -349,13 +350,13 @@ interface VaultSdkSource { ): Result> /** - * Decrypts a [cipher] [attachment] file found at [encryptedFilePath] saving it at + * Decrypts a [cipher] [attachmentView] file found at [encryptedFilePath] saving it at * [decryptedFilePath] for the user with the given [userId] */ suspend fun decryptFile( userId: String, cipher: Cipher, - attachment: Attachment, + attachmentView: AttachmentView, encryptedFilePath: String, decryptedFilePath: String, ): Result diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt index d7bca41874..acc4923fb6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt @@ -25,6 +25,7 @@ import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView import com.bitwarden.vault.Collection import com.bitwarden.vault.CollectionView +import com.bitwarden.vault.EncryptionContext import com.bitwarden.vault.Folder import com.bitwarden.vault.FolderView import com.bitwarden.vault.PasswordHistory @@ -269,7 +270,7 @@ class VaultSdkSourceImpl( override suspend fun encryptCipher( userId: String, cipherView: CipherView, - ): Result = + ): Result = runCatchingWithLogs { getClient(userId = userId) .vault() @@ -389,7 +390,7 @@ class VaultSdkSourceImpl( override suspend fun decryptFile( userId: String, cipher: Cipher, - attachment: Attachment, + attachmentView: AttachmentView, encryptedFilePath: String, decryptedFilePath: String, ): Result = @@ -399,7 +400,7 @@ class VaultSdkSourceImpl( .attachments() .decryptFile( cipher = cipher, - attachment = attachment, + attachment = attachmentView, encryptedFilePath = encryptedFilePath, decryptedFilePath = decryptedFilePath, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt index 3278f0d006..57fd67d296 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/Fido2CredentialStoreImpl.kt @@ -3,9 +3,9 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.model import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.fido.Fido2CredentialAutofillView import com.bitwarden.sdk.Fido2CredentialStore -import com.bitwarden.vault.Cipher import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView +import com.bitwarden.vault.EncryptionContext import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource @@ -91,11 +91,12 @@ class Fido2CredentialStoreImpl( /** * Save the provided [cred] to the users vault. */ - override suspend fun saveCredential(cred: Cipher) { - val userId = getActiveUserIdOrThrow() - + override suspend fun saveCredential(cred: EncryptionContext) { vaultSdkSource - .decryptCipher(userId, cred) + .decryptCipher( + userId = cred.encryptedFor, + cipher = cred.cipher, + ) .map { decryptedCipherView -> decryptedCipherView.id ?.let { vaultRepository.updateCipher(it, decryptedCipherView) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt index cd2707bfd2..8730c02e43 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt @@ -12,8 +12,8 @@ import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest import com.bitwarden.network.model.UpdateCipherResponseJson import com.bitwarden.network.service.CiphersService import com.bitwarden.vault.AttachmentView -import com.bitwarden.vault.Cipher import com.bitwarden.vault.CipherView +import com.bitwarden.vault.EncryptionContext import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.platform.error.NoActiveUserException import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager @@ -80,10 +80,10 @@ class CipherManagerImpl( userId = userId, cipherView = cipherView, ) - .flatMap { cipher -> + .flatMap { ciphersService.createCipherInOrganization( body = CreateCipherInOrganizationJsonRequest( - cipher = cipher.toEncryptedNetworkCipher(), + cipher = it.toEncryptedNetworkCipher(), collectionIds = collectionIds, ), ) @@ -123,10 +123,15 @@ class CipherManagerImpl( ?: return DeleteCipherResult.Error(error = NoActiveUserException()) return cipherView .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId) - .flatMap { cipher -> + .flatMap { encryptionContext -> ciphersService .softDeleteCipher(cipherId = cipherId) - .flatMap { vaultSdkSource.decryptCipher(userId = userId, cipher = cipher) } + .flatMap { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = encryptionContext.cipher, + ) + } } .flatMap { vaultSdkSource.encryptCipher( @@ -165,7 +170,7 @@ class CipherManagerImpl( cipherId: String, attachmentId: String, cipherView: CipherView, - ): Result { + ): Result { val userId = activeUserId ?: return NoActiveUserException().asFailure() return ciphersService .deleteCipherAttachment( @@ -181,10 +186,10 @@ class CipherManagerImpl( ) .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId) } - .onSuccess { cipher -> + .onSuccess { encryptionContext -> vaultDiskSource.saveCipher( userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), + cipher = encryptionContext.toEncryptedNetworkCipherResponse(), ) } } @@ -220,10 +225,10 @@ class CipherManagerImpl( userId = userId, cipherView = cipherView, ) - .flatMap { cipher -> + .flatMap { ciphersService.updateCipher( cipherId = cipherId, - body = cipher.toEncryptedNetworkCipher(), + body = it.toEncryptedNetworkCipher(), ) } .map { response -> @@ -263,11 +268,11 @@ class CipherManagerImpl( ) } .flatMap { vaultSdkSource.encryptCipher(userId = userId, cipherView = it) } - .flatMap { cipher -> + .flatMap { ciphersService.shareCipher( cipherId = cipherId, body = ShareCipherJsonRequest( - cipher = cipher.toEncryptedNetworkCipher(), + cipher = it.toEncryptedNetworkCipher(), collectionIds = collectionIds, ), ) @@ -301,10 +306,10 @@ class CipherManagerImpl( cipherView = cipherView.copy(collectionIds = collectionIds), ) } - .onSuccess { cipher -> + .onSuccess { encryptionContext -> vaultDiskSource.saveCipher( userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), + cipher = encryptionContext.toEncryptedNetworkCipherResponse(), ) } .fold( @@ -362,14 +367,14 @@ class CipherManagerImpl( userId = userId, cipherId = requireNotNull(cipherView.id), ) - .flatMap { cipher -> + .flatMap { encryptionContext -> fileManager .writeUriToCache(fileUri = fileUri) .flatMap { cacheFile -> vaultSdkSource .encryptAttachment( userId = userId, - cipher = cipher, + cipher = encryptionContext.cipher, attachmentView = attachmentView, decryptedFilePath = cacheFile.absolutePath, encryptedFilePath = "${cacheFile.absolutePath}.enc", @@ -447,10 +452,10 @@ class CipherManagerImpl( cipherId = requireNotNull(cipherView.id), ) .fold( - onSuccess = { it }, + onSuccess = { it.cipher }, onFailure = { return it.asFailure() }, ) - val attachment = cipher.attachments?.find { it.id == attachmentId } + val attachmentView = cipherView.attachments?.find { it.id == attachmentId } ?: return IllegalStateException("No attachment to download").asFailure() val attachmentData = ciphersService @@ -479,7 +484,7 @@ class CipherManagerImpl( .decryptFile( userId = userId, cipher = cipher, - attachment = attachment, + attachmentView = attachmentView, encryptedFilePath = encryptedFile.path, decryptedFilePath = decryptedFile.path, ) @@ -494,17 +499,17 @@ class CipherManagerImpl( private suspend fun CipherView.encryptCipherAndCheckForMigration( userId: String, cipherId: String, - ): Result = + ): Result = vaultSdkSource .encryptCipher(userId = userId, cipherView = this) - .flatMap { + .flatMap { encryptionContext -> // We only migrate the cipher if the original cipher did not have a key and the // new cipher does. This means the SDK created the key and migration is required. - if (it.key != null && this.key == null) { + if (encryptionContext.cipher.key != null && this.key == null) { ciphersService .updateCipher( cipherId = cipherId, - body = it.toEncryptedNetworkCipher(), + body = encryptionContext.toEncryptedNetworkCipher(), ) .flatMap { response -> when (response) { @@ -520,12 +525,14 @@ class CipherManagerImpl( userId = userId, cipher = response.cipher, ) - response.cipher.toEncryptedSdkCipher().asSuccess() + encryptionContext + .copy(cipher = response.cipher.toEncryptedSdkCipher()) + .asSuccess() } } } } else { - it.asSuccess() + encryptionContext.asSuccess() } } @@ -541,7 +548,7 @@ class CipherManagerImpl( ?: return IllegalStateException("CipherView must have an ID").asFailure() var migratedCipherView = cipherView .encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherViewId) - .flatMap { vaultSdkSource.decryptCipher(userId = userId, cipher = it) } + .flatMap { vaultSdkSource.decryptCipher(userId = userId, cipher = it.cipher) } .getOrElse { return it.asFailure() } attachmentViewsToMigrate @@ -574,7 +581,12 @@ class CipherManagerImpl( cipherId = cipherViewId, ) } - .flatMap { vaultSdkSource.decryptCipher(userId = userId, cipher = it) } + .flatMap { + vaultSdkSource.decryptCipher( + userId = userId, + cipher = it.cipher, + ) + } .onSuccess { migratedCipherView = it } } ?: IllegalStateException("AttachmentView must have an ID").asFailure() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt index 46e22f1d76..666dc6dd3c 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerImpl.kt @@ -181,6 +181,7 @@ class VaultLockManagerImpl( email = email, privateKey = privateKey, method = initUserCryptoMethod, + userId = userId, ), ) .flatMap { result -> diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt index 765456f53e..98f3a54993 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensions.kt @@ -20,6 +20,7 @@ import com.bitwarden.vault.CipherPermissions import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherType import com.bitwarden.vault.CipherView +import com.bitwarden.vault.EncryptionContext import com.bitwarden.vault.Fido2Credential import com.bitwarden.vault.Field import com.bitwarden.vault.FieldType @@ -37,8 +38,12 @@ import java.time.ZonedDateTime /** * Converts a Bitwarden SDK [Cipher] object to a corresponding * [SyncResponseJson.Cipher] object. + * + * @param encryptedFor The ID of the user who this cipher is encrypted by. */ -fun Cipher.toEncryptedNetworkCipher(): CipherJsonRequest = +fun Cipher.toEncryptedNetworkCipher( + encryptedFor: String, +): CipherJsonRequest = CipherJsonRequest( notes = notes, attachments = attachments @@ -59,13 +64,18 @@ fun Cipher.toEncryptedNetworkCipher(): CipherJsonRequest = card = card?.toEncryptedNetworkCard(), key = key, sshKey = sshKey?.toEncryptedNetworkSshKey(), + encryptedFor = encryptedFor, ) /** * Converts a Bitwarden SDK [Cipher] object to a corresponding * [SyncResponseJson.Cipher] object. + * + * @param encryptedFor The ID of the user who this cipher is encrypted by. */ -fun Cipher.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher = +fun Cipher.toEncryptedNetworkCipherResponse( + encryptedFor: String, +): SyncResponseJson.Cipher = SyncResponseJson.Cipher( notes = notes, reprompt = reprompt.toNetworkRepromptType(), @@ -92,6 +102,7 @@ fun Cipher.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher = id = id.orEmpty(), shouldViewPassword = viewPassword, key = key, + encryptedFor = encryptedFor, ) /** @@ -626,3 +637,17 @@ fun List.sortAlphabetically(): List { }, ) } + +/** + * Converts a Bitwarden SDK [EncryptionContext] object to a corresponding [CipherJsonRequest] + * object. + */ +fun EncryptionContext.toEncryptedNetworkCipher(): CipherJsonRequest = + cipher.toEncryptedNetworkCipher(encryptedFor = encryptedFor) + +/** + * Converts a Bitwarden SDK [EncryptionContext] object to a corresponding [SyncResponseJson.Cipher] + * object. + */ +fun EncryptionContext.toEncryptedNetworkCipherResponse(): SyncResponseJson.Cipher = + cipher.toEncryptedNetworkCipherResponse(encryptedFor = encryptedFor) 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 c2539aae1f..009dcdef02 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 @@ -419,7 +419,8 @@ private const val CIPHER_JSON = """ "publicKey": "mockPublicKey-1", "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" - } + }, + "encryptedFor": "mockEncryptedFor-1" } """ diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt index 1beac7ed41..de87e1b4d9 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt @@ -20,15 +20,15 @@ import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAttestationResponse import com.bitwarden.fido.UnverifiedAssetLink import com.bitwarden.sdk.AuthClient import com.bitwarden.sdk.BitwardenException +import com.bitwarden.sdk.CiphersClient import com.bitwarden.sdk.Client -import com.bitwarden.sdk.ClientCiphers import com.bitwarden.sdk.ClientFido2 import com.bitwarden.sdk.ClientFido2Authenticator import com.bitwarden.sdk.ClientFido2Client -import com.bitwarden.sdk.ClientPasswordHistory import com.bitwarden.sdk.CryptoClient import com.bitwarden.sdk.ExporterClient import com.bitwarden.sdk.Fido2CredentialStore +import com.bitwarden.sdk.PasswordHistoryClient import com.bitwarden.sdk.PlatformClient import com.bitwarden.sdk.SendClient import com.bitwarden.sdk.VaultClient @@ -41,6 +41,7 @@ import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherView import com.bitwarden.vault.Collection import com.bitwarden.vault.CollectionView +import com.bitwarden.vault.EncryptionContext import com.bitwarden.vault.Folder import com.bitwarden.vault.FolderView import com.bitwarden.vault.PasswordHistory @@ -83,12 +84,12 @@ class VaultSdkSourceTest { private val clientPlatform = mockk { every { fido2() } returns clientFido2 } - private val clientPasswordHistory = mockk() + private val passwordHistoryClient = mockk() private val clientSends = mockk() - private val clientCiphers = mockk() + private val ciphersClient = mockk() private val clientVault = mockk { - every { ciphers() } returns clientCiphers - every { passwordHistory() } returns clientPasswordHistory + every { ciphers() } returns ciphersClient + every { passwordHistory() } returns passwordHistoryClient } private val clientExporters = mockk { coEvery { exportVault(any(), any(), any()) } @@ -615,8 +616,10 @@ class VaultSdkSourceTest { fun `decryptCipher should call SDK and return a Result with correct data`() = runBlocking { val userId = "userId" val mockCipher = mockk() - val expectedResult = mockk() - coEvery { clientCiphers.encrypt(cipherView = mockCipher) } returns expectedResult + val expectedResult = mockk { + every { cipher } returns createMockSdkCipher(number = 1) + } + coEvery { ciphersClient.encrypt(cipherView = mockCipher) } returns expectedResult val result = vaultSdkSource.encryptCipher( userId = userId, cipherView = mockCipher, @@ -626,7 +629,7 @@ class VaultSdkSourceTest { result, ) coVerify { - clientCiphers.encrypt(cipherView = mockCipher) + ciphersClient.encrypt(cipherView = mockCipher) sdkClientManager.getOrCreateClient(userId = userId) } } @@ -636,7 +639,7 @@ class VaultSdkSourceTest { val userId = "userId" val mockCipher = mockk() val expectedResult = mockk() - coEvery { clientCiphers.decrypt(cipher = mockCipher) } returns expectedResult + coEvery { ciphersClient.decrypt(cipher = mockCipher) } returns expectedResult val result = vaultSdkSource.decryptCipher( userId = userId, cipher = mockCipher, @@ -646,7 +649,7 @@ class VaultSdkSourceTest { result, ) coVerify { - clientCiphers.decrypt(cipher = mockCipher) + ciphersClient.decrypt(cipher = mockCipher) sdkClientManager.getOrCreateClient(userId = userId) } } @@ -657,7 +660,7 @@ class VaultSdkSourceTest { val userId = "userId" val mockCiphers = mockk>() val expectedResult = mockk>() - coEvery { clientCiphers.decryptList(ciphers = mockCiphers) } returns expectedResult + coEvery { ciphersClient.decryptList(ciphers = mockCiphers) } returns expectedResult val result = vaultSdkSource.decryptCipherListCollection( userId = userId, cipherList = mockCiphers, @@ -667,7 +670,7 @@ class VaultSdkSourceTest { result, ) coVerify { - clientCiphers.decryptList(ciphers = mockCiphers) + ciphersClient.decryptList(ciphers = mockCiphers) sdkClientManager.getOrCreateClient(userId = userId) } } @@ -679,8 +682,8 @@ class VaultSdkSourceTest { val mockCipher2 = mockk() val cipherView1 = mockk() val cipherView2 = mockk() - coEvery { clientCiphers.decrypt(cipher = mockCipher1) } returns cipherView1 - coEvery { clientCiphers.decrypt(cipher = mockCipher2) } returns cipherView2 + coEvery { ciphersClient.decrypt(cipher = mockCipher1) } returns cipherView1 + coEvery { ciphersClient.decrypt(cipher = mockCipher2) } returns cipherView2 val result = vaultSdkSource.decryptCipherList( userId = userId, cipherList = listOf(mockCipher1, mockCipher2), @@ -690,8 +693,8 @@ class VaultSdkSourceTest { result, ) coVerify(exactly = 1) { - clientCiphers.decrypt(cipher = mockCipher1) - clientCiphers.decrypt(cipher = mockCipher2) + ciphersClient.decrypt(cipher = mockCipher1) + ciphersClient.decrypt(cipher = mockCipher2) // It's important that we only fetch the client once sdkClientManager.getOrCreateClient(userId = userId) client.vault() @@ -936,12 +939,12 @@ class VaultSdkSourceTest { fun `File decrypt should call SDK and return a Result with correct data`() = runBlocking { val userId = "userId" val mockCipher = mockk() - val mockAttachment = mockk() + val mockAttachmentView = mockk() val expectedResult = Unit coEvery { clientVault.attachments().decryptFile( cipher = mockCipher, - attachment = mockAttachment, + attachment = mockAttachmentView, encryptedFilePath = "encrypted_path", decryptedFilePath = "decrypted_path", ) @@ -949,7 +952,7 @@ class VaultSdkSourceTest { val result = vaultSdkSource.decryptFile( userId = userId, cipher = mockCipher, - attachment = mockAttachment, + attachmentView = mockAttachmentView, encryptedFilePath = "encrypted_path", decryptedFilePath = "decrypted_path", ) @@ -960,7 +963,7 @@ class VaultSdkSourceTest { coVerify { clientVault.attachments().decryptFile( cipher = mockCipher, - attachment = mockAttachment, + attachment = mockAttachmentView, encryptedFilePath = "encrypted_path", decryptedFilePath = "decrypted_path", ) @@ -975,7 +978,7 @@ class VaultSdkSourceTest { val mockPasswordHistoryView = mockk() val expectedResult = mockk() coEvery { - clientPasswordHistory.encrypt( + passwordHistoryClient.encrypt( passwordHistory = mockPasswordHistoryView, ) } returns expectedResult @@ -987,7 +990,7 @@ class VaultSdkSourceTest { assertEquals(expectedResult.asSuccess(), result) coVerify { - clientPasswordHistory.encrypt( + passwordHistoryClient.encrypt( passwordHistory = mockPasswordHistoryView, ) } @@ -1001,7 +1004,7 @@ class VaultSdkSourceTest { val mockPasswordHistoryList = mockk>() val expectedResult = mockk>() coEvery { - clientPasswordHistory.decryptList( + passwordHistoryClient.decryptList( list = mockPasswordHistoryList, ) } returns expectedResult @@ -1013,7 +1016,7 @@ class VaultSdkSourceTest { assertEquals(expectedResult.asSuccess(), result) coVerify { - clientPasswordHistory.decryptList( + passwordHistoryClient.decryptList( list = mockPasswordHistoryList, ) } @@ -1051,7 +1054,7 @@ class VaultSdkSourceTest { val mockCipher = mockk() val expectedResult = mockk() coEvery { - clientCiphers.moveToOrganization(cipher = mockCipher, organizationId = organizationId) + ciphersClient.moveToOrganization(cipher = mockCipher, organizationId = organizationId) } returns expectedResult val result = vaultSdkSource.moveToOrganization( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index 8fc012d2dd..3b49fe1c5d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -56,6 +56,7 @@ fun createMockCipherView( clock = clock, fido2Credentials = fido2Credentials, ), + attachments: List = listOf(createMockAttachmentView(number = number)), ): CipherView = CipherView( id = "mockId-$number", @@ -74,7 +75,7 @@ fun createMockCipherView( null }, revisionDate = clock.instant(), - attachments = listOf(createMockAttachmentView(number = number)), + attachments = attachments, card = createMockCardView(number = number).takeIf { cipherType == CipherType.CARD }, fields = listOf(createMockFieldView(number = number)), identity = createMockIdentityView(number = number).takeIf { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkEncryptionContextUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkEncryptionContextUtil.kt new file mode 100644 index 0000000000..3b746a3bae --- /dev/null +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/datasource/sdk/model/VaultSdkEncryptionContextUtil.kt @@ -0,0 +1,29 @@ +package com.x8bit.bitwarden.data.vault.datasource.sdk.model + +import com.bitwarden.vault.Cipher +import com.bitwarden.vault.EncryptionContext +import java.time.Clock +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime + +/** + * Default date time used for [ZonedDateTime] properties of mock objects. + */ +private const val DEFAULT_TIMESTAMP = "2023-10-27T12:00:00Z" +private val FIXED_CLOCK: Clock = Clock.fixed( + Instant.parse(DEFAULT_TIMESTAMP), + ZoneOffset.UTC, +) + +/** + * Create a mock [EncryptionContext] with a given [number]. + */ +fun createMockEncryptionContext( + number: Int, + cipher: Cipher = createMockSdkCipher(number, FIXED_CLOCK), +): EncryptionContext = + EncryptionContext( + encryptedFor = "mockEncryptedFor-$number", + cipher = cipher, + ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt index 495aa3e293..0e59933d90 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt @@ -19,6 +19,7 @@ import com.bitwarden.network.service.CiphersService import com.bitwarden.vault.Attachment import com.bitwarden.vault.AttachmentView import com.bitwarden.vault.Cipher +import com.bitwarden.vault.EncryptionContext import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson @@ -29,6 +30,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAttachmentView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockEncryptionContext import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult @@ -156,7 +158,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.createCipher( body = createMockCipherJsonRequest( @@ -183,7 +188,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.createCipher( @@ -250,7 +258,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.createCipherInOrganization( body = CreateCipherInOrganizationJsonRequest( @@ -283,7 +294,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.createCipherInOrganization( @@ -364,7 +378,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.updateCipher( cipherId = cipherId, @@ -396,7 +413,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.updateCipher( cipherId = cipherId, @@ -439,7 +459,10 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView, ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() val mockCipher = createMockCipher(number = 1) coEvery { ciphersService.updateCipher( @@ -529,11 +552,11 @@ class CipherManagerTest { val userId = MOCK_USER_STATE.activeUserId val cipherId = "mockId-1" val cipherView = createMockCipherView(number = 1) - val cipher = createMockSdkCipher(number = 1) + val encryptionContext = createMockEncryptionContext(number = 1) val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns cipher.asSuccess() + } returns encryptionContext.asSuccess() coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns error.asFailure() @@ -553,26 +576,32 @@ class CipherManagerTest { val fixedInstant = Instant.parse("2023-10-27T12:00:00Z") val userId = "mockId-1" val cipherId = "mockId-1" - val cipher = createMockSdkCipher(number = 1, clock = clock) + val encryptionContext = createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ) val cipherView = createMockCipherView(number = 1) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns cipher.asSuccess() + } returns encryptionContext.asSuccess() coEvery { vaultSdkSource.encryptCipher( userId = userId, cipherView = cipherView.copy(deletedDate = fixedInstant), ) - } returns cipher.asSuccess() + } returns encryptionContext.asSuccess() coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = cipher) + vaultSdkSource.decryptCipher( + userId = userId, + cipher = encryptionContext.cipher, + ) } returns cipherView.asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() coEvery { vaultDiskSource.saveCipher( userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), + cipher = encryptionContext.toEncryptedNetworkCipherResponse(), ) } just runs @@ -591,16 +620,19 @@ class CipherManagerTest { val fixedInstant = Instant.parse("2023-10-27T12:00:00Z") val userId = "mockId-1" val cipherId = "mockId-1" - val cipher = createMockSdkCipher(number = 1, clock = clock) + val encryptionContext = createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ) val cipherView = createMockCipherView(number = 1).copy(key = null) val networkCipher = createMockCipher(number = 1).copy(key = null) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = cipherView) - } returns cipher.asSuccess() + } returns encryptionContext.asSuccess() coEvery { ciphersService.updateCipher( cipherId = cipherId, - body = cipher.toEncryptedNetworkCipher(), + body = encryptionContext.toEncryptedNetworkCipher(), ) } returns UpdateCipherResponseJson.Success(networkCipher).asSuccess() coEvery { @@ -617,16 +649,16 @@ class CipherManagerTest { userId = userId, cipherView = cipherView.copy(deletedDate = fixedInstant), ) - } returns cipher.asSuccess() + } returns encryptionContext.asSuccess() coEvery { - vaultSdkSource.decryptCipher(userId = userId, cipher = cipher) + vaultSdkSource.decryptCipher(userId = userId, cipher = encryptionContext.cipher) } returns cipherView.asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.softDeleteCipher(cipherId = cipherId) } returns Unit.asSuccess() coEvery { vaultDiskSource.saveCipher( userId = userId, - cipher = cipher.toEncryptedNetworkCipherResponse(), + cipher = encryptionContext.toEncryptedNetworkCipherResponse(), ) } just runs @@ -639,7 +671,7 @@ class CipherManagerTest { coVerify(exactly = 1) { ciphersService.updateCipher( cipherId = cipherId, - body = cipher.toEncryptedNetworkCipher(), + body = encryptionContext.toEncryptedNetworkCipher(), ) vaultDiskSource.saveCipher(userId = userId, cipher = networkCipher) } @@ -692,7 +724,11 @@ class CipherManagerTest { runTest { mockkStatic(Cipher::toEncryptedNetworkCipherResponse) every { - createMockSdkCipher(number = 1, clock = clock).toEncryptedNetworkCipherResponse() + createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ) + .toEncryptedNetworkCipherResponse() } returns createMockCipher(number = 1) val fixedInstant = Instant.parse("2021-01-01T00:00:00Z") val userId = "mockId-1" @@ -703,7 +739,10 @@ class CipherManagerTest { userId = userId, cipherView = createMockCipherView(number = 1).copy(attachments = emptyList()), ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { ciphersService.deleteCipherAttachment( @@ -820,7 +859,10 @@ class CipherManagerTest { } returns mockCipherView.asSuccess() coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.shareCipher( cipherId = "mockId-1", @@ -864,6 +906,10 @@ class CipherManagerTest { val mockCipher = createMockSdkCipher(number = 1).copy( attachments = listOf(mockAttachment), ) + val mockEncryptionContext = createMockEncryptionContext( + number = 1, + cipher = mockCipher, + ) val attachment = createMockAttachment(number = 1) val encryptedFile = File("path/to/encrypted/file") val decryptedFile = File("path/to/encrypted/file_decrypted") @@ -874,7 +920,7 @@ class CipherManagerTest { // Handle mocks for migration coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = initialCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { vaultSdkSource.decryptCipher(userId = userId, cipher = mockCipher) } returns initialCipherView.asSuccess() @@ -888,7 +934,7 @@ class CipherManagerTest { vaultSdkSource.decryptFile( userId = userId, cipher = mockCipher, - attachment = mockAttachment, + attachmentView = mockAttachmentView, encryptedFilePath = encryptedFile.path, decryptedFilePath = decryptedFile.path, ) @@ -950,11 +996,11 @@ class CipherManagerTest { userId = userId, cipherView = mockCipherView.copy(attachments = emptyList()), ) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { vaultDiskSource.saveCipher( userId = userId, - cipher = mockCipher.toEncryptedNetworkCipherResponse(), + cipher = mockCipher.toEncryptedNetworkCipherResponse("mockEncryptedFor-1"), ) } just runs // Done with mocks for migration @@ -968,7 +1014,10 @@ class CipherManagerTest { } returns mockCipherView.asSuccess() coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.shareCipher( cipherId = "mockId-1", @@ -1015,7 +1064,10 @@ class CipherManagerTest { } returns mockCipherView.asSuccess() coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() val error = Throwable("Fail") coEvery { ciphersService.shareCipher( @@ -1102,7 +1154,10 @@ class CipherManagerTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() coEvery { ciphersService.updateCipherCollections( cipherId = "mockId-1", @@ -1135,7 +1190,10 @@ class CipherManagerTest { userId = userId, cipherView = createMockCipherView(number = 1), ) - } returns createMockSdkCipher(number = 1, clock = clock).asSuccess() + } returns createMockEncryptionContext( + number = 1, + cipher = createMockSdkCipher(number = 1, clock = clock), + ).asSuccess() val error = Throwable("Fail") coEvery { ciphersService.updateCipherCollections( @@ -1251,6 +1309,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFile = File.createTempFile("mockFile", "temp") val mockFileName = "mockFileName-1" val mockFileSize = "1" @@ -1263,7 +1322,7 @@ class CipherManagerTest { val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1298,12 +1357,13 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns error.asFailure() @@ -1329,6 +1389,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1342,7 +1403,7 @@ class CipherManagerTest { val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1387,6 +1448,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1401,7 +1463,7 @@ class CipherManagerTest { val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1452,6 +1514,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1470,7 +1533,7 @@ class CipherManagerTest { val error = Throwable("Fail") coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1530,6 +1593,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1547,7 +1611,7 @@ class CipherManagerTest { ) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1605,6 +1669,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1622,7 +1687,7 @@ class CipherManagerTest { ) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1682,6 +1747,7 @@ class CipherManagerTest { val mockUri = setupMockUri(url = "www.test.com") val mockCipherView = createMockCipherView(number = 1) val mockCipher = createMockSdkCipher(number = 1, clock = clock) + val mockEncryptionContext = createMockEncryptionContext(number = 1, cipher = mockCipher) val mockFileName = "mockFileName-1" val mockFileSize = "1" val mockAttachmentView = createMockAttachmentView(number = 1).copy( @@ -1695,7 +1761,7 @@ class CipherManagerTest { val mockAttachmentJsonResponse = createMockAttachmentResponse(number = 1) coEvery { vaultSdkSource.encryptCipher(userId = userId, cipherView = mockCipherView) - } returns mockCipher.asSuccess() + } returns mockEncryptionContext.asSuccess() coEvery { fileManager.writeUriToCache(fileUri = mockUri) } returns mockFile.asSuccess() @@ -1743,18 +1809,22 @@ class CipherManagerTest { fakeAuthDiskSource.userState = MOCK_USER_STATE val attachmentId = "mockId-1" - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns emptyList() every { id } returns "mockId-1" } - val cipherView = createMockCipherView(number = 1) + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } + val cipherView = createMockCipherView(number = 1, attachments = emptyList()) coEvery { vaultSdkSource.encryptCipher( userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() assertEquals( DownloadAttachmentResult.Failure(IllegalStateException()), @@ -1784,18 +1854,22 @@ class CipherManagerTest { val attachment = mockk { every { id } returns attachmentId } - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns listOf(attachment) every { id } returns "mockId-1" } + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } val cipherView = createMockCipherView(number = 1) coEvery { vaultSdkSource.encryptCipher( userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() val error = Throwable() coEvery { @@ -1830,18 +1904,22 @@ class CipherManagerTest { val attachment = mockk { every { id } returns attachmentId } - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns listOf(attachment) every { id } returns "mockId-1" } + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } val cipherView = createMockCipherView(number = 1) coEvery { vaultSdkSource.encryptCipher( userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() val response = mockk { every { url } returns null @@ -1878,11 +1956,15 @@ class CipherManagerTest { val attachment = mockk { every { id } returns attachmentId } - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns listOf(attachment) every { id } returns "mockId-1" } + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } val cipherView = createMockCipherView(number = 1) coEvery { @@ -1890,7 +1972,7 @@ class CipherManagerTest { userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() val response = mockk { every { url } returns "https://bitwarden.com" @@ -1932,18 +2014,23 @@ class CipherManagerTest { val attachment = mockk { every { id } returns attachmentId } - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns listOf(attachment) every { id } returns "mockId-1" } - val cipherView = createMockCipherView(number = 1) + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } + val attachmentView = createMockAttachmentView(number = 1) + val cipherView = createMockCipherView(number = 1, attachments = listOf(attachmentView)) coEvery { vaultSdkSource.encryptCipher( userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() val response = mockk { every { url } returns "https://bitwarden.com" @@ -1963,8 +2050,8 @@ class CipherManagerTest { coEvery { vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, + cipher = mockCipher, + attachmentView = attachmentView, encryptedFilePath = "path/to/encrypted/file", decryptedFilePath = "path/to/encrypted/file_decrypted", ) @@ -1990,8 +2077,8 @@ class CipherManagerTest { fileManager.downloadFileToCache("https://bitwarden.com") vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, + cipher = mockCipher, + attachmentView = attachmentView, encryptedFilePath = "path/to/encrypted/file", decryptedFilePath = "path/to/encrypted/file_decrypted", ) @@ -2008,18 +2095,25 @@ class CipherManagerTest { val attachment = mockk { every { id } returns attachmentId } - val cipher = mockk { + val mockCipher = mockk { every { key } returns "key" every { attachments } returns listOf(attachment) every { id } returns "mockId-1" } - val cipherView = createMockCipherView(number = 1) + val mockEncryptionContext = mockk { + every { encryptedFor } returns "mockEncryptedFor-1" + every { cipher } returns mockCipher + } + val attachmentView = mockk { + every { id } returns attachmentId + } + val cipherView = createMockCipherView(number = 1, attachments = listOf(attachmentView)) coEvery { vaultSdkSource.encryptCipher( userId = MOCK_USER_STATE.activeUserId, cipherView = cipherView, ) - } returns cipher.asSuccess() + } returns mockEncryptionContext.asSuccess() val response = mockk { every { url } returns "https://bitwarden.com" @@ -2039,8 +2133,8 @@ class CipherManagerTest { coEvery { vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, + cipher = mockCipher, + attachmentView = attachmentView, encryptedFilePath = "path/to/encrypted/file", decryptedFilePath = "path/to/encrypted/file_decrypted", ) @@ -2068,8 +2162,8 @@ class CipherManagerTest { fileManager.downloadFileToCache(url = "https://bitwarden.com") vaultSdkSource.decryptFile( userId = MOCK_USER_STATE.activeUserId, - cipher = cipher, - attachment = attachment, + cipher = mockCipher, + attachmentView = attachmentView, encryptedFilePath = "path/to/encrypted/file", decryptedFilePath = "path/to/encrypted/file_decrypted", ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt index 8cd5cf1ab6..62d09c91a6 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultLockManagerTest.kt @@ -741,6 +741,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = MOCK_PROFILE.toSdkParams(), email = MOCK_PROFILE.email, privateKey = privateKey, @@ -764,6 +765,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = MOCK_PROFILE.toSdkParams(), email = MOCK_PROFILE.email, privateKey = privateKey, @@ -894,6 +896,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -958,6 +961,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -990,6 +994,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1065,6 +1070,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1098,6 +1104,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1143,6 +1150,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1169,6 +1177,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1221,6 +1230,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1254,6 +1264,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1298,6 +1309,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1324,6 +1336,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1375,6 +1388,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1407,6 +1421,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1465,6 +1480,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = USER_ID, request = InitUserCryptoRequest( + userId = USER_ID, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1589,6 +1605,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = userId, request = InitUserCryptoRequest( + userId = userId, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1634,6 +1651,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = userId, request = InitUserCryptoRequest( + userId = userId, kdfParams = kdf, email = email, privateKey = privateKey, @@ -1668,6 +1686,7 @@ class VaultLockManagerTest { vaultSdkSource.initializeCrypto( userId = userId, request = InitUserCryptoRequest( + userId = userId, kdfParams = kdf, email = email, privateKey = privateKey, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt index b5d0802f2a..b5d7badd36 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/util/VaultSdkCipherExtensionsTest.kt @@ -21,6 +21,7 @@ import com.bitwarden.vault.CipherType import com.bitwarden.vault.FieldType import com.bitwarden.vault.UriMatchType import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockEncryptionContext import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkAttachment import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCard import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkCipher @@ -53,7 +54,9 @@ class VaultSdkCipherExtensionsTest { fun `toEncryptedNetworkCipherResponse should convert an Sdk Cipher to a cipher`() { val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) - val result = sdkCipher.toEncryptedNetworkCipherResponse() + val result = sdkCipher.toEncryptedNetworkCipherResponse( + encryptedFor = "mockEncryptedFor-1", + ) assertEquals( createMockCipher( @@ -67,7 +70,9 @@ class VaultSdkCipherExtensionsTest { @Test fun `toEncryptedNetworkCipher should convert an Sdk Cipher to a Network Cipher`() { val sdkCipher = createMockSdkCipher(number = 1, clock = FIXED_CLOCK) - val syncCipher = sdkCipher.toEncryptedNetworkCipher() + val syncCipher = sdkCipher.toEncryptedNetworkCipher( + encryptedFor = "mockEncryptedFor-1", + ) assertEquals( createMockCipherJsonRequest( number = 1, @@ -343,4 +348,30 @@ class VaultSdkCipherExtensionsTest { list.sortAlphabetically(), ) } + + @Suppress("MaxLineLength") + @Test + fun `EncryptionContext toEncryptedNetworkCipher should convert an EncryptionContext to a Network Cipher`() { + val encryptionContext = createMockEncryptionContext(number = 1) + assertEquals( + createMockCipherJsonRequest( + number = 1, + login = createMockLogin(number = 1, uri = null), + ), + encryptionContext.toEncryptedNetworkCipher(), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `EncryptionContext toEncryptedNetworkCipherResponse should convert an EncryptionContext to a cipher`() { + val encryptionContext = createMockEncryptionContext(number = 1) + assertEquals( + createMockCipher( + number = 1, + login = createMockLogin(number = 1, uri = null), + ), + encryptionContext.toEncryptedNetworkCipherResponse(), + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ebcef63257..403c062ced 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ androidxSplash = "1.1.0-rc01" androidXAppCompat = "1.7.0" androdixAutofill = "1.1.0" androidxWork = "2.10.1" -bitwardenSdk = "1.0.0-20250418.073303-151" +bitwardenSdk = "1.0.0-20250602.132903-199" crashlytics = "3.0.3" detekt = "1.23.8" firebaseBom = "33.14.0" diff --git a/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt b/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt index 281ffa8565..3f97ca83ea 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/CipherJsonRequest.kt @@ -23,6 +23,7 @@ import java.time.ZonedDateTime * @property isFavorite If the cipher is a favorite. * @property card The card of the cipher. * @property key The key of the cipher (nullable). + * @property encryptedFor ID of the user who the cipher is encrypted by. */ @Serializable data class CipherJsonRequest( @@ -77,4 +78,7 @@ data class CipherJsonRequest( @SerialName("key") val key: String?, + + @SerialName("encryptedFor") + val encryptedFor: 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 150556589c..d830ad2131 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -438,6 +438,8 @@ data class SyncResponseJson( * @property shouldViewPassword If the password can be viewed for the cipher. * @property isFavorite If the cipher is a favorite. * @property card The card of the cipher. + * @property key The key of the cipher (nullable). + * @property encryptedFor ID of the user who the cipher is encrypted by. */ @Serializable data class Cipher( @@ -518,6 +520,9 @@ data class SyncResponseJson( @SerialName("key") val key: String?, + + @SerialName("encryptedFor") + val encryptedFor: String?, ) { /** * Represents an attachment in the vault response. diff --git a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt index e2e1b787c2..bba7f1d8cd 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/CiphersServiceTest.kt @@ -467,7 +467,8 @@ private const val CREATE_ATTACHMENT_SUCCESS_JSON = """ "publicKey": "mockPublicKey-1", "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" - } + }, + "encryptedFor": "mockEncryptedFor-1" } } """ @@ -594,7 +595,8 @@ private const val CREATE_RESTORE_UPDATE_CIPHER_SUCCESS_JSON = """ "publicKey": "mockPublicKey-1", "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" - } + }, + "encryptedFor": "mockEncryptedFor-1" } """ diff --git a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt index c676fee3a5..af4eda61cd 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt @@ -315,7 +315,8 @@ private const val SYNC_SUCCESS_JSON = """ "publicKey": "mockPublicKey-1", "privateKey": "mockPrivateKey-1", "keyFingerprint": "mockKeyFingerprint-1" - } + }, + "encryptedFor": "mockEncryptedFor-1" } ], "domains": { diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt index 9d19632599..8e049a3b06 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/CipherJsonRequestUtil.kt @@ -29,6 +29,7 @@ fun createMockCipherJsonRequest( reprompt: CipherRepromptTypeJson = CipherRepromptTypeJson.NONE, lastKnownRevisionDate: ZonedDateTime? = ZonedDateTime.parse("2023-10-27T12:00:00Z"), key: String? = "mockKey-$number", + encryptedFor: String? = "mockEncryptedFor-$number", ): CipherJsonRequest = CipherJsonRequest( attachments = attachments, @@ -48,4 +49,5 @@ fun createMockCipherJsonRequest( reprompt = reprompt, lastKnownRevisionDate = lastKnownRevisionDate, key = key, + encryptedFor = encryptedFor, ) diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt index e17298fe14..a4b22addb2 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseCipherUtil.kt @@ -43,6 +43,7 @@ fun createMockCipher( shouldOrganizationUseTotp: Boolean = false, shouldViewPassword: Boolean = false, key: String? = "mockKey-$number", + encryptedFor: String? = "mockEncryptedFor-$number", ): SyncResponseJson.Cipher = SyncResponseJson.Cipher( id = id, @@ -70,6 +71,7 @@ fun createMockCipher( shouldOrganizationUseTotp = shouldOrganizationUseTotp, shouldViewPassword = shouldViewPassword, key = key, + encryptedFor = encryptedFor, ) /**