Add flow for creating attachments (#777)

This commit is contained in:
David Perez
2024-01-25 11:46:37 -06:00
committed by Álison Fernandes
parent 3e9852e9e7
commit 465cce42f0
14 changed files with 822 additions and 8 deletions

View File

@@ -1,8 +1,11 @@
package com.x8bit.bitwarden.data.vault.datasource.network.api
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import okhttp3.MultipartBody
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.POST
@@ -20,6 +23,25 @@ interface CiphersApi {
@POST("ciphers")
suspend fun createCipher(@Body body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Associates an attachment with a cipher.
*/
@POST("ciphers/{cipherId}/attachment/v2")
suspend fun createAttachment(
@Path("cipherId") cipherId: String,
@Body body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse>
/**
* Uploads the attachment associated with a cipher.
*/
@POST("ciphers/{cipherId}/attachment/{attachmentId}")
suspend fun uploadAttachment(
@Path("cipherId") cipherId: String,
@Path("attachmentId") attachmentId: String,
@Body body: MultipartBody,
): Result<Unit>
/**
* Updates a cipher.
*/

View File

@@ -28,9 +28,17 @@ object VaultNetworkModule {
fun provideCiphersService(
retrofits: Retrofits,
json: Json,
clock: Clock,
): CiphersService = CiphersServiceImpl(
azureApi = retrofits
.staticRetrofitBuilder
// This URL will be overridden dynamically
.baseUrl("https://www.bitwarden.com")
.build()
.create(),
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
json = json,
clock = clock,
)
@Provides
@@ -43,7 +51,7 @@ object VaultNetworkModule {
azureApi = retrofits
.staticRetrofitBuilder
// This URL will be overridden dynamically
.baseUrl("https://www.bitwaredn.com")
.baseUrl("https://www.bitwarden.com")
.build()
.create(),
sendsApi = retrofits.authenticatedApiRetrofit.create(),

View File

@@ -0,0 +1,19 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents a request to create an attachment.
*/
@Serializable
data class AttachmentJsonRequest(
@SerialName("fileName")
val fileName: String,
@SerialName("key")
val key: String,
@SerialName("fileSize")
val fileSize: String,
)

View File

@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.vault.datasource.network.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Represents the JSON response from creating a new attachment.
*/
@Serializable
data class AttachmentJsonResponse(
@SerialName("attachmentId")
val attachmentId: String,
@SerialName("url")
val url: String,
@SerialName("fileUploadType")
val fileUploadType: FileUploadType,
@SerialName("cipherResponse")
val cipherResponse: SyncResponseJson.Cipher,
)

View File

@@ -1,5 +1,7 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@@ -14,6 +16,22 @@ interface CiphersService {
*/
suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher>
/**
* Attempt to upload an attachment file.
*/
suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray,
): Result<SyncResponseJson.Cipher>
/**
* Attempt to create an attachment.
*/
suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse>
/**
* Attempt to update a cipher.
*/

View File

@@ -1,21 +1,88 @@
package com.x8bit.bitwarden.data.vault.datasource.network.service
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
import com.x8bit.bitwarden.data.vault.datasource.network.api.AzureApi
import com.x8bit.bitwarden.data.vault.datasource.network.api.CiphersApi
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonResponse
import com.x8bit.bitwarden.data.vault.datasource.network.model.CipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.FileUploadType
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.UpdateCipherResponseJson
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.time.Clock
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class CiphersServiceImpl constructor(
class CiphersServiceImpl(
private val azureApi: AzureApi,
private val ciphersApi: CiphersApi,
private val json: Json,
private val clock: Clock,
) : CiphersService {
override suspend fun createCipher(body: CipherJsonRequest): Result<SyncResponseJson.Cipher> =
ciphersApi.createCipher(body = body)
override suspend fun createAttachment(
cipherId: String,
body: AttachmentJsonRequest,
): Result<AttachmentJsonResponse> =
ciphersApi.createAttachment(
cipherId = cipherId,
body = body,
)
override suspend fun uploadAttachment(
attachmentJsonResponse: AttachmentJsonResponse,
encryptedFile: ByteArray,
): Result<SyncResponseJson.Cipher> {
val cipher = attachmentJsonResponse.cipherResponse
return when (attachmentJsonResponse.fileUploadType) {
FileUploadType.DIRECT -> {
ciphersApi.uploadAttachment(
cipherId = requireNotNull(cipher.id),
attachmentId = attachmentJsonResponse.attachmentId,
body = MultipartBody
.Builder(
boundary = "--BWMobileFormBoundary${clock.instant().toEpochMilli()}",
)
.addPart(
part = MultipartBody.Part.createFormData(
body = encryptedFile.toRequestBody(
contentType = "application/octet-stream".toMediaType(),
),
name = "data",
filename = cipher
.attachments
?.find { it.id == attachmentJsonResponse.attachmentId }
?.fileName,
),
)
.build(),
)
}
FileUploadType.AZURE -> {
azureApi.uploadAzureBlob(
url = attachmentJsonResponse.url,
date = DateTimeFormatter
.RFC_1123_DATE_TIME
.format(ZonedDateTime.ofInstant(clock.instant(), ZoneOffset.UTC)),
version = attachmentJsonResponse.url.toUri().getQueryParameter("sv"),
body = encryptedFile.toRequestBody(),
)
}
}
.map { cipher }
}
override suspend fun updateCipher(
cipherId: String,
body: CipherJsonRequest,

View File

@@ -11,6 +11,7 @@ import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.platform.repository.model.DataState
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.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@@ -182,6 +183,17 @@ interface VaultRepository : VaultLockManager {
*/
suspend fun createCipher(cipherView: CipherView): 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 delete a cipher.
*/

View File

@@ -1,6 +1,7 @@
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
@@ -26,6 +27,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoadin
import com.x8bit.bitwarden.data.platform.util.asFailure
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.ShareCipherJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.UpdateCipherResponseJson
@@ -38,6 +40,7 @@ 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.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.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteAttachmentResult
@@ -56,6 +59,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
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.toEncryptedNetworkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
@@ -643,6 +647,71 @@ class VaultRepositoryImpl(
)
}
override suspend fun createAttachment(
cipherId: String,
cipherView: CipherView,
fileSizeBytes: String,
fileName: String,
fileUri: Uri,
): CreateAttachmentResult {
val userId = requireNotNull(activeUserId)
val attachmentView = AttachmentView(
id = null,
url = null,
size = fileSizeBytes,
sizeName = null,
fileName = fileName,
key = null,
)
return vaultSdkSource
.encryptCipher(
userId = userId,
cipherView = cipherView,
)
.flatMap { cipher ->
vaultSdkSource.encryptAttachment(
userId = userId,
cipher = cipher,
attachmentView = attachmentView,
fileBuffer = fileManager.uriToByteArray(fileUri = fileUri),
)
}
.flatMap { attachmentEncryptResult ->
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(attachmentEncryptResult.attachment.fileName),
key = requireNotNull(attachmentEncryptResult.attachment.key),
fileSize = requireNotNull(attachmentEncryptResult.attachment.size),
),
)
.flatMap { attachmentJsonResponse ->
ciphersService.uploadAttachment(
attachmentJsonResponse = attachmentJsonResponse,
encryptedFile = attachmentEncryptResult.contents,
)
}
}
.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) },
)
}
override suspend fun createSend(
sendView: SendView,
fileUri: Uri?,

View File

@@ -0,0 +1,21 @@
package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.core.CipherView
/**
* Models result of creating an attachment.
*/
sealed class CreateAttachmentResult {
/**
* Attachment created successfully.
*/
data class Success(
val cipherView: CipherView,
) : CreateAttachmentResult()
/**
* Generic error while creating an attachment.
*/
data object Error : CreateAttachmentResult()
}