mirror of
https://github.com/bitwarden/android.git
synced 2026-06-09 08:09:16 -05:00
Add flow for creating attachments (#777)
This commit is contained in:
committed by
Álison Fernandes
parent
3e9852e9e7
commit
465cce42f0
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user