Create manager class to isolate logic for ciphers (#1432)

This commit is contained in:
David Perez 2024-06-07 10:35:48 -05:00 committed by Álison Fernandes
parent 913b53bcac
commit 6392918f52
8 changed files with 2392 additions and 2281 deletions

View file

@ -0,0 +1,112 @@
package com.x8bit.bitwarden.data.vault.manager
import android.net.Uri
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
/**
* Manages the creating, updating, and deleting ciphers and their attachments.
*/
@Suppress("TooManyFunctions")
interface CipherManager {
/**
* Attempt to create a cipher.
*/
suspend fun createCipher(
cipherView: CipherView,
): CreateCipherResult
/**
* Attempt to create a cipher that belongs to an organization.
*/
suspend fun createCipherInOrganization(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult
/**
* Attempt to create an attachment for the given [cipherView].
*/
suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult
/**
* Attempt to download an attachment file, specified by [attachmentId], for the given
* [cipherView].
*/
suspend fun downloadAttachment(
cipherView: CipherView,
attachmentId: String,
): DownloadAttachmentResult
/**
* Attempt to delete a cipher.
*/
suspend fun hardDeleteCipher(
cipherId: String,
): DeleteCipherResult
/**
* Attempt to soft delete a cipher.
*/
suspend fun softDeleteCipher(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult
/**
* Attempt to delete an attachment from a send.
*/
suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult
/**
* Attempt to restore a cipher.
*/
suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult
/**
* Attempt to share a cipher to the collections with the given collectionIds.
*/
suspend fun shareCipher(
cipherId: String,
organizationId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult
/**
* Attempt to update a cipher.
*/
suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult
/**
* Attempt to update a cipher with the given collectionIds.
*/
suspend fun updateCipherCollections(
cipherId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult
}

View file

@ -0,0 +1,419 @@
package com.x8bit.bitwarden.data.vault.manager
import android.net.Uri
import com.bitwarden.core.AttachmentView
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import java.io.File
import java.time.Clock
/**
* The default implementation of the [CipherManager].
*/
@Suppress("TooManyFunctions")
class CipherManagerImpl(
private val fileManager: FileManager,
private val authDiskSource: AuthDiskSource,
private val ciphersService: CiphersService,
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val clock: Clock,
) : CipherManager {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) }
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
.fold(
onFailure = { CreateCipherResult.Error },
onSuccess = { CreateCipherResult.Success },
)
}
override suspend fun createCipherInOrganization(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
ciphersService.createCipherInOrganization(
body = CreateCipherInOrganizationJsonRequest(
cipher = cipher.toEncryptedNetworkCipher(),
collectionIds = collectionIds,
),
)
}
.onSuccess {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.copy(collectionIds = collectionIds),
)
}
.fold(
onFailure = { CreateCipherResult.Error },
onSuccess = { CreateCipherResult.Success },
)
}
override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
return ciphersService
.hardDeleteCipher(cipherId = cipherId)
.onSuccess { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
)
}
override suspend fun softDeleteCipher(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
return ciphersService
.softDeleteCipher(cipherId = cipherId)
.onSuccess {
vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView.copy(deletedDate = clock.instant()),
)
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
}
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
)
}
override suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult {
val userId = activeUserId ?: return DeleteAttachmentResult.Error
return ciphersService
.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = cipherView.copy(
attachments = cipherView.attachments?.mapNotNull {
if (it.id == attachmentId) null else it
},
),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { DeleteAttachmentResult.Success },
onFailure = { DeleteAttachmentResult.Error },
)
}
override suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult {
val userId = activeUserId ?: return RestoreCipherResult.Error
return ciphersService
.restoreCipher(cipherId = cipherId)
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = cipherView.copy(deletedDate = null),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { RestoreCipherResult.Success },
onFailure = { RestoreCipherResult.Error },
)
}
override suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult {
val userId = activeUserId ?: return UpdateCipherResult.Error(errorMessage = null)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
ciphersService.updateCipher(
cipherId = cipherId,
body = cipher.toEncryptedNetworkCipher(),
)
}
.map { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
UpdateCipherResult.Error(errorMessage = response.message)
}
is UpdateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = response.cipher.copy(collectionIds = cipherView.collectionIds),
)
UpdateCipherResult.Success
}
}
}
.fold(
onFailure = { UpdateCipherResult.Error(errorMessage = null) },
onSuccess = { it },
)
}
override suspend fun shareCipher(
cipherId: String,
organizationId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
return vaultSdkSource
.moveToOrganization(
userId = userId,
organizationId = organizationId,
cipherView = cipherView,
)
.flatMap { vaultSdkSource.encryptCipher(userId = userId, cipherView = it) }
.flatMap { cipher ->
ciphersService.shareCipher(
cipherId = cipherId,
body = ShareCipherJsonRequest(
cipher = cipher.toEncryptedNetworkCipher(),
collectionIds = collectionIds,
),
)
}
.onSuccess {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.copy(collectionIds = collectionIds),
)
}
.fold(
onFailure = { ShareCipherResult.Error },
onSuccess = { ShareCipherResult.Success },
)
}
override suspend fun updateCipherCollections(
cipherId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
return ciphersService
.updateCipherCollections(
cipherId = cipherId,
body = UpdateCipherCollectionsJsonRequest(collectionIds = collectionIds),
)
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = cipherView.copy(collectionIds = collectionIds),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { ShareCipherResult.Success },
onFailure = { ShareCipherResult.Error },
)
}
@Suppress("LongMethod")
override suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult {
val userId = activeUserId ?: return CreateAttachmentResult.Error
val attachmentView = AttachmentView(
id = null,
url = null,
size = fileSizeBytes,
sizeName = null,
fileName = fileName,
key = null,
)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
fileManager
.writeUriToCache(fileUri = fileUri)
.flatMap { cacheFile ->
vaultSdkSource
.encryptAttachment(
userId = userId,
cipher = cipher,
attachmentView = attachmentView,
decryptedFilePath = cacheFile.absolutePath,
encryptedFilePath = "${cacheFile.absolutePath}.enc",
)
.flatMap { attachment ->
ciphersService
.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
// We know these values are present because
// - the filename/size are passed into the function
// - the SDK call fills in the key
fileName = requireNotNull(attachment.fileName),
key = requireNotNull(attachment.key),
fileSize = requireNotNull(attachment.size),
),
)
.flatMap { attachmentJsonResponse ->
val encryptedFile = File("${cacheFile.absolutePath}.enc")
ciphersService
.uploadAttachment(
attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = encryptedFile,
)
.onSuccess {
fileManager.delete(cacheFile, encryptedFile)
}
.onFailure {
fileManager.delete(cacheFile, encryptedFile)
}
}
}
}
}
.map { it.copy(collectionIds = cipherView.collectionIds) }
.onSuccess {
// Save the send immediately, regardless of whether the decrypt succeeds
vaultDiskSource.saveCipher(userId = userId, cipher = it)
}
.flatMap {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = it.toEncryptedSdkCipher(),
)
}
.fold(
onFailure = { CreateAttachmentResult.Error },
onSuccess = { CreateAttachmentResult.Success(cipherView = it) },
)
}
@Suppress("ReturnCount")
override suspend fun downloadAttachment(
cipherView: CipherView,
attachmentId: String,
): DownloadAttachmentResult {
val userId = activeUserId ?: return DownloadAttachmentResult.Failure
val cipher = vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.fold(
onSuccess = { it },
onFailure = { return DownloadAttachmentResult.Failure },
)
val attachment = cipher.attachments?.find { it.id == attachmentId }
?: return DownloadAttachmentResult.Failure
val attachmentData = ciphersService
.getCipherAttachment(
cipherId = requireNotNull(cipher.id),
attachmentId = attachmentId,
)
.fold(
onSuccess = { it },
onFailure = { return DownloadAttachmentResult.Failure },
)
val url = attachmentData.url ?: return DownloadAttachmentResult.Failure
val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) {
DownloadResult.Failure -> return DownloadAttachmentResult.Failure
is DownloadResult.Success -> result.file
}
val decryptedFile = File(encryptedFile.path + "_decrypted")
return vaultSdkSource
.decryptFile(
userId = userId,
cipher = cipher,
attachment = attachment,
encryptedFilePath = encryptedFile.path,
decryptedFilePath = decryptedFile.path,
)
.onSuccess { fileManager.delete(encryptedFile) }
.onFailure { fileManager.delete(encryptedFile) }
.fold(
onSuccess = { DownloadAttachmentResult.Success(file = decryptedFile) },
onFailure = { DownloadAttachmentResult.Failure },
)
}
}

View file

@ -8,8 +8,12 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.DownloadService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.CipherManagerImpl
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.FileManagerImpl
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
@ -31,6 +35,24 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
object VaultManagerModule {
@Provides
@Singleton
fun provideCipherManager(
ciphersService: CiphersService,
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
fileManager: FileManager,
clock: Clock,
): CipherManager = CipherManagerImpl(
fileManager = fileManager,
authDiskSource = authDiskSource,
ciphersService = ciphersService,
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
clock = clock,
)
@Provides
@Singleton
fun provideFileManager(

View file

@ -10,26 +10,19 @@ import com.bitwarden.core.SendType
import com.bitwarden.core.SendView
import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
@ -42,7 +35,7 @@ import kotlinx.coroutines.flow.StateFlow
* Responsible for managing vault data inside the network layer.
*/
@Suppress("TooManyFunctions")
interface VaultRepository : VaultLockManager {
interface VaultRepository : CipherManager, VaultLockManager {
/**
* The [VaultFilterType] for the current user.
@ -193,87 +186,6 @@ interface VaultRepository : VaultLockManager {
organizationKeys: Map<String, String>?,
): VaultUnlockResult
/**
* Attempt to create a cipher.
*/
suspend fun createCipher(cipherView: CipherView): CreateCipherResult
/**
* Attempt to create a cipher that belongs to an organization.
*/
suspend fun createCipherInOrganization(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult
/**
* Attempt to create an attachment for the given [cipherView].
*/
suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult
/**
* Attempt to download an attachment file, specified by [attachmentId], for the given
* [cipherView].
*/
suspend fun downloadAttachment(
cipherView: CipherView,
attachmentId: String,
): DownloadAttachmentResult
/**
* Attempt to delete a cipher.
*/
suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult
/**
* Attempt to soft delete a cipher.
*/
suspend fun softDeleteCipher(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult
/**
* Attempt to restore a cipher.
*/
suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult
/**
* Attempt to update a cipher.
*/
suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult
/**
* Attempt to share a cipher to the collections with the given collectionIds.
*/
suspend fun shareCipher(
cipherId: String,
organizationId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult
/**
* Attempt to update a cipher with the given collectionIds.
*/
suspend fun updateCipherCollections(
cipherId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult
/**
* Attempt to create a send. The [fileUri] _must_ be present when the given [SendView] has a
* [SendView.type] of [SendType.FILE].
@ -303,15 +215,6 @@ interface VaultRepository : VaultLockManager {
*/
suspend fun deleteSend(sendId: String): DeleteSendResult
/**
* Attempt to delete an attachment from a send.
*/
suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult
/**
* Attempt to create a folder.
*/

View file

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.data.vault.repository
import android.net.Uri
import com.bitwarden.core.AttachmentView
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
@ -40,14 +39,9 @@ import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateCipherInOrganizationJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateFileSendResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CreateSendJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherCollectionsJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateFolderResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateSendResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
@ -55,37 +49,27 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.FolderService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.model.DownloadResult
import com.x8bit.bitwarden.data.vault.manager.model.VerificationCodeItem
import com.x8bit.bitwarden.data.vault.repository.model.CreateAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.data.vault.repository.model.DownloadAttachmentResult
import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RestoreCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipherResponse
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
@ -122,9 +106,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.File
import java.time.Clock
import java.time.Instant
import java.time.temporal.ChronoUnit
/**
@ -146,6 +128,7 @@ class VaultRepositoryImpl(
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val cipherManager: CipherManager,
private val fileManager: FileManager,
private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager,
@ -154,6 +137,7 @@ class VaultRepositoryImpl(
private val clock: Clock,
dispatcherManager: DispatcherManager,
) : VaultRepository,
CipherManager by cipherManager,
VaultLockManager by vaultLockManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
@ -617,409 +601,6 @@ class VaultRepositoryImpl(
organizationKeys = organizationKeys,
)
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
ciphersService
.createCipher(
body = cipher.toEncryptedNetworkCipher(),
)
}
.fold(
onFailure = {
CreateCipherResult.Error
},
onSuccess = {
vaultDiskSource.saveCipher(userId = userId, cipher = it)
CreateCipherResult.Success
},
)
}
override suspend fun createCipherInOrganization(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
ciphersService
.createCipherInOrganization(
body = CreateCipherInOrganizationJsonRequest(
cipher = cipher.toEncryptedNetworkCipher(),
collectionIds = collectionIds,
),
)
}
.onSuccess {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.copy(collectionIds = collectionIds),
)
}
.fold(
onFailure = { CreateCipherResult.Error },
onSuccess = { CreateCipherResult.Success },
)
}
override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
return ciphersService
.hardDeleteCipher(cipherId)
.onSuccess { vaultDiskSource.deleteCipher(userId, cipherId) }
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
)
}
override suspend fun softDeleteCipher(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
return ciphersService
.softDeleteCipher(cipherId)
.fold(
onSuccess = {
vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView.copy(
deletedDate = Instant.now(),
),
)
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
DeleteCipherResult.Success
},
onFailure = { DeleteCipherResult.Error },
)
}
override suspend fun deleteCipherAttachment(
cipherId: String,
attachmentId: String,
cipherView: CipherView,
): DeleteAttachmentResult {
val userId = activeUserId ?: return DeleteAttachmentResult.Error
return ciphersService
.deleteCipherAttachment(
cipherId = cipherId,
attachmentId = attachmentId,
)
.flatMap {
vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView.copy(
attachments = cipherView.attachments?.mapNotNull {
if (it.id == attachmentId) null else it
},
),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { DeleteAttachmentResult.Success },
onFailure = { DeleteAttachmentResult.Error },
)
}
override suspend fun restoreCipher(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult {
val userId = activeUserId ?: return RestoreCipherResult.Error
return ciphersService
.restoreCipher(cipherId)
.flatMap {
vaultSdkSource.encryptCipher(
userId = userId,
cipherView = cipherView.copy(
deletedDate = null,
),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { RestoreCipherResult.Success },
onFailure = { RestoreCipherResult.Error },
)
}
override suspend fun updateCipher(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult {
val userId = activeUserId ?: return UpdateCipherResult.Error(null)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
ciphersService.updateCipher(
cipherId = cipherId,
body = cipher.toEncryptedNetworkCipher(),
)
}
.fold(
onFailure = { UpdateCipherResult.Error(errorMessage = null) },
onSuccess = { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
UpdateCipherResult.Error(errorMessage = response.message)
}
is UpdateCipherResponseJson.Success -> {
vaultDiskSource.saveCipher(
userId = userId,
cipher = response.cipher
.copy(collectionIds = cipherView.collectionIds),
)
UpdateCipherResult.Success
}
}
},
)
}
override suspend fun shareCipher(
cipherId: String,
organizationId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
return vaultSdkSource
.moveToOrganization(
userId = userId,
organizationId = organizationId,
cipherView = cipherView,
)
.flatMap { vaultSdkSource.encryptCipher(userId = userId, cipherView = it) }
.flatMap { cipher ->
ciphersService.shareCipher(
cipherId = cipherId,
body = ShareCipherJsonRequest(
cipher = cipher.toEncryptedNetworkCipher(),
collectionIds = collectionIds,
),
)
}
.onSuccess {
vaultDiskSource.saveCipher(
userId = userId,
cipher = it.copy(collectionIds = collectionIds),
)
}
.fold(
onFailure = { ShareCipherResult.Error },
onSuccess = { ShareCipherResult.Success },
)
}
override suspend fun updateCipherCollections(
cipherId: String,
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
return ciphersService
.updateCipherCollections(
cipherId = cipherId,
body = UpdateCipherCollectionsJsonRequest(collectionIds = collectionIds),
)
.flatMap {
vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView.copy(collectionIds = collectionIds),
)
}
.onSuccess { cipher ->
vaultDiskSource.saveCipher(
userId = userId,
cipher = cipher.toEncryptedNetworkCipherResponse(),
)
}
.fold(
onSuccess = { ShareCipherResult.Success },
onFailure = { ShareCipherResult.Error },
)
}
@Suppress("LongMethod")
override suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult {
val userId = activeUserId ?: return CreateAttachmentResult.Error
val attachmentView = AttachmentView(
id = null,
url = null,
size = fileSizeBytes,
sizeName = null,
fileName = fileName,
key = null,
)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
fileManager
.writeUriToCache(fileUri = fileUri)
.flatMap { cacheFile ->
vaultSdkSource
.encryptAttachment(
userId = userId,
cipher = cipher,
attachmentView = attachmentView,
decryptedFilePath = cacheFile.absolutePath,
encryptedFilePath = "${cacheFile.absolutePath}.enc",
)
.flatMap { attachment ->
ciphersService
.createAttachment(
cipherId = cipherId,
body = AttachmentJsonRequest(
// We know these values are present because
// - the filename/size are passed into the function
// - the SDK call fills in the key
fileName = requireNotNull(attachment.fileName),
key = requireNotNull(attachment.key),
fileSize = requireNotNull(attachment.size),
),
)
.flatMap { attachmentJsonResponse ->
val encryptedFile = File("${cacheFile.absolutePath}.enc")
ciphersService
.uploadAttachment(
attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = encryptedFile,
)
.onSuccess {
fileManager
.delete(
cacheFile,
encryptedFile,
)
}
.onFailure {
fileManager
.delete(
cacheFile,
encryptedFile,
)
}
}
}
}
}
.map { it.copy(collectionIds = cipherView.collectionIds) }
.onSuccess {
// Save the send immediately, regardless of whether the decrypt succeeds
vaultDiskSource.saveCipher(userId = userId, cipher = it)
}
.flatMap {
vaultSdkSource.decryptCipher(
userId = userId,
cipher = it.toEncryptedSdkCipher(),
)
}
.fold(
onFailure = { CreateAttachmentResult.Error },
onSuccess = { CreateAttachmentResult.Success(it) },
)
}
@Suppress("ReturnCount")
override suspend fun downloadAttachment(
cipherView: CipherView,
attachmentId: String,
): DownloadAttachmentResult {
val userId = requireNotNull(authDiskSource.userState?.activeUserId)
val cipher = vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.fold(
onSuccess = { it },
onFailure = { return DownloadAttachmentResult.Failure },
)
val attachment = cipher.attachments?.find { it.id == attachmentId }
?: return DownloadAttachmentResult.Failure
val attachmentData = ciphersService
.getCipherAttachment(
cipherId = requireNotNull(cipher.id),
attachmentId = attachmentId,
)
.fold(
onSuccess = { it },
onFailure = { return DownloadAttachmentResult.Failure },
)
val url = attachmentData.url ?: return DownloadAttachmentResult.Failure
val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) {
DownloadResult.Failure -> return DownloadAttachmentResult.Failure
is DownloadResult.Success -> result.file
}
val decryptedFile = File(encryptedFile.path + "_decrypted")
return vaultSdkSource
.decryptFile(
userId = userId,
cipher = cipher,
attachment = attachment,
encryptedFilePath = encryptedFile.path,
decryptedFilePath = decryptedFile.path,
)
.fold(
onSuccess = {
encryptedFile.delete()
DownloadAttachmentResult.Success(decryptedFile)
},
onFailure = {
encryptedFile.delete()
DownloadAttachmentResult.Failure
},
)
}
@Suppress("ReturnCount")
override suspend fun createSend(
sendView: SendView,

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.FolderService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.CipherManager
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.TotpCodeManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
@ -41,6 +42,7 @@ object VaultRepositoryModule {
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
cipherManager: CipherManager,
fileManager: FileManager,
vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager,
@ -57,6 +59,7 @@ object VaultRepositoryModule {
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
cipherManager = cipherManager,
fileManager = fileManager,
vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager,

File diff suppressed because it is too large Load diff