Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.data.util.flatMap
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.data.manager.model.DownloadResult
import com.bitwarden.network.model.ArchiveCipherResponseJson
import com.bitwarden.network.model.AttachmentJsonResponse
import com.bitwarden.network.model.CreateCipherInOrganizationJsonRequest
Expand All @@ -18,6 +17,7 @@ import com.bitwarden.network.model.UnarchiveCipherResponseJson
import com.bitwarden.network.model.UpdateCipherCollectionsJsonRequest
import com.bitwarden.network.model.UpdateCipherResponseJson
import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.DownloadService
import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.CipherView
import com.bitwarden.vault.EncryptionContext
Expand Down Expand Up @@ -57,6 +57,7 @@ import java.time.Clock
*/
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
class CipherManagerImpl(
private val downloadService: DownloadService,
private val fileManager: FileManager,
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
Expand Down Expand Up @@ -622,14 +623,13 @@ class CipherManagerImpl(
val url = attachmentData.url
?: return IllegalStateException("Attachment does not have a url").asFailure()

val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) {
is DownloadResult.Failure -> {
return IllegalStateException("Download failed", result.error).asFailure()
val encryptedFile = downloadService
.getDataStream(url = url)
.flatMap { fileManager.streamFileToCache(stream = it.byteStream()) }
.getOrElse {
return IllegalStateException("Download failed", it).asFailure()
}

is DownloadResult.Success -> result.file
}

val decryptedFile = File(encryptedFile.path + "_decrypted")
return vaultSdkSource
.decryptFile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.bitwarden.cxf.parser.CredentialExchangePayloadParser
import com.bitwarden.data.manager.appstate.AppStateManager
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.DownloadService
import com.bitwarden.network.service.FolderService
import com.bitwarden.network.service.SendsService
import com.bitwarden.network.service.SyncService
Expand Down Expand Up @@ -98,6 +99,7 @@ object VaultManagerModule {
@Provides
@Singleton
fun provideCipherManager(
downloadService: DownloadService,
ciphersService: CiphersService,
settingsDiskSource: SettingsDiskSource,
vaultDiskSource: VaultDiskSource,
Expand All @@ -109,6 +111,7 @@ object VaultManagerModule {
dispatcherManager: DispatcherManager,
pushManager: PushManager,
): CipherManager = CipherManagerImpl(
downloadService = downloadService,
fileManager = fileManager,
settingsDiskSource = settingsDiskSource,
authDiskSource = authDiskSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.data.manager.model.DownloadResult
import com.bitwarden.network.exception.CookieRedirectException
import com.bitwarden.network.model.ArchiveCipherResponseJson
import com.bitwarden.network.model.AttachmentJsonRequest
Expand All @@ -27,6 +26,7 @@ import com.bitwarden.network.model.createMockCipherJsonRequest
import com.bitwarden.network.model.createMockCollection
import com.bitwarden.network.model.createMockLogin
import com.bitwarden.network.service.CiphersService
import com.bitwarden.network.service.DownloadService
import com.bitwarden.vault.Attachment
import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.Cipher
Expand Down Expand Up @@ -77,12 +77,14 @@ import io.mockk.unmockkConstructor
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.File
import java.io.InputStream
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
Expand Down Expand Up @@ -112,8 +114,10 @@ class CipherManagerTest {
every { syncCipherDeleteFlow } returns mutableSyncCipherDeleteFlow
every { syncCipherUpsertFlow } returns mutableSyncCipherUpsertFlow
}
private val downloadService: DownloadService = mockk()

private val cipherManager: CipherManager = CipherManagerImpl(
downloadService = downloadService,
ciphersService = ciphersService,
settingsDiskSource = fakeSettingsDiskSource,
vaultDiskSource = vaultDiskSource,
Expand Down Expand Up @@ -1303,9 +1307,16 @@ class CipherManagerTest {
coEvery {
ciphersService.getCipherAttachment(cipherId = "mockId-1", attachmentId = "mockId-1")
} returns attachment.asSuccess()
val mockInputStream = mockk<InputStream>()
val mockResponseBody = mockk<ResponseBody> {
every { byteStream() } returns mockInputStream
}
coEvery {
downloadService.getDataStream(url = "mockUrl-1")
} returns mockResponseBody.asSuccess()
coEvery {
fileManager.downloadFileToCache(url = "mockUrl-1")
} returns DownloadResult.Success(file = encryptedFile)
fileManager.streamFileToCache(stream = mockInputStream)
} returns encryptedFile.asSuccess()
coEvery {
vaultSdkSource.decryptFile(
userId = userId,
Expand Down Expand Up @@ -2311,7 +2322,7 @@ class CipherManagerTest {
}

@Test
fun `downloadAttachment with failed download should return Failure`() = runTest {
fun `downloadAttachment with failed data stream request should return Failure`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE

val attachmentId = "mockId-1"
Expand Down Expand Up @@ -2343,8 +2354,8 @@ class CipherManagerTest {
ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any())
} returns response.asSuccess()
coEvery {
fileManager.downloadFileToCache(url = any())
} returns DownloadResult.Failure(error = Throwable("Fail!"))
downloadService.getDataStream(url = any())
} returns Throwable("Fail!").asFailure()

assertEquals(
DownloadAttachmentResult.Failure(IllegalStateException()),
Expand All @@ -2363,7 +2374,75 @@ class CipherManagerTest {
cipherId = requireNotNull(cipherView.id),
attachmentId = attachmentId,
)
fileManager.downloadFileToCache("https://bitwarden.com")
downloadService.getDataStream(url = "https://bitwarden.com")
}
coVerify(exactly = 0) {
fileManager.streamFileToCache(stream = any())
}
}

@Test
fun `downloadAttachment with failed stream to cache should return Failure`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE

val attachmentId = "mockId-1"
val attachment = mockk<Attachment> {
every { id } returns attachmentId
}
val mockCipher = mockk<Cipher> {
every { key } returns "key"
every { attachments } returns listOf(attachment)
every { id } returns "mockId-1"
}
val mockEncryptionContext = mockk<EncryptionContext> {
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 mockEncryptionContext.asSuccess()

val response = mockk<SyncResponseJson.Cipher.Attachment> {
every { url } returns "https://bitwarden.com"
}
coEvery {
ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any())
} returns response.asSuccess()
val mockInputStream = mockk<InputStream>()
val mockResponseBody = mockk<ResponseBody> {
every { byteStream() } returns mockInputStream
}
coEvery {
downloadService.getDataStream(url = any())
} returns mockResponseBody.asSuccess()
coEvery {
fileManager.streamFileToCache(stream = mockInputStream)
} returns Throwable("Fail!").asFailure()

assertEquals(
DownloadAttachmentResult.Failure(IllegalStateException()),
cipherManager.downloadAttachment(
cipherView = cipherView,
attachmentId = attachmentId,
),
)

coVerify(exactly = 1) {
vaultSdkSource.encryptCipher(
userId = MOCK_USER_STATE.activeUserId,
cipherView = cipherView,
)
ciphersService.getCipherAttachment(
cipherId = requireNotNull(cipherView.id),
attachmentId = attachmentId,
)
downloadService.getDataStream(url = "https://bitwarden.com")
fileManager.streamFileToCache(stream = mockInputStream)
}
}

Expand Down Expand Up @@ -2405,9 +2484,16 @@ class CipherManagerTest {
every { path } returns "path/to/encrypted/file"
}
coEvery { fileManager.delete(file) } just runs
val mockInputStream = mockk<InputStream>()
val mockResponseBody = mockk<ResponseBody> {
every { byteStream() } returns mockInputStream
}
coEvery {
fileManager.downloadFileToCache(url = any())
} returns DownloadResult.Success(file)
downloadService.getDataStream(url = any())
} returns mockResponseBody.asSuccess()
coEvery {
fileManager.streamFileToCache(stream = mockInputStream)
} returns file.asSuccess()
val error = Throwable("Fail")
coEvery {
vaultSdkSource.decryptFile(
Expand Down Expand Up @@ -2436,7 +2522,8 @@ class CipherManagerTest {
cipherId = requireNotNull(cipherView.id),
attachmentId = attachmentId,
)
fileManager.downloadFileToCache("https://bitwarden.com")
downloadService.getDataStream(url = "https://bitwarden.com")
fileManager.streamFileToCache(stream = mockInputStream)
vaultSdkSource.decryptFile(
userId = MOCK_USER_STATE.activeUserId,
cipher = mockCipher,
Expand Down Expand Up @@ -2488,9 +2575,16 @@ class CipherManagerTest {
every { path } returns "path/to/encrypted/file"
}
coEvery { fileManager.delete(file) } just runs
val mockInputStream = mockk<InputStream>()
val mockResponseBody = mockk<ResponseBody> {
every { byteStream() } returns mockInputStream
}
coEvery {
downloadService.getDataStream(url = any())
} returns mockResponseBody.asSuccess()
coEvery {
fileManager.downloadFileToCache(any())
} returns DownloadResult.Success(file)
fileManager.streamFileToCache(stream = mockInputStream)
} returns file.asSuccess()

coEvery {
vaultSdkSource.decryptFile(
Expand Down Expand Up @@ -2521,7 +2615,8 @@ class CipherManagerTest {
cipherId = requireNotNull(cipherView.id),
attachmentId = attachmentId,
)
fileManager.downloadFileToCache(url = "https://bitwarden.com")
downloadService.getDataStream(url = "https://bitwarden.com")
fileManager.streamFileToCache(stream = mockInputStream)
vaultSdkSource.decryptFile(
userId = MOCK_USER_STATE.activeUserId,
cipher = mockCipher,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.bitwarden.authenticator.data.platform.manager.lock.AppLockManagerImpl
import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.core.data.manager.UuidManager
import com.bitwarden.core.data.manager.UuidManagerImpl
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManagerImpl
import com.bitwarden.core.data.manager.realtime.RealtimeManager
Expand Down Expand Up @@ -128,10 +127,6 @@ object PlatformManagerModule {
@Singleton
fun provideEncodingManager(): BitwardenEncodingManager = BitwardenEncodingManagerImpl()

@Provides
@Singleton
fun provideUuidManager(): UuidManager = UuidManagerImpl()

@Provides
@Singleton
fun providesFeatureFlagManager(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.bitwarden.core.data.manager.di

import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.UuidManager
import com.bitwarden.core.data.manager.UuidManagerImpl
import com.bitwarden.core.data.manager.encryption.EncryptionManager
import com.bitwarden.core.data.manager.encryption.EncryptionManagerImpl
import com.bitwarden.core.data.manager.encryption.KeystoreManager
Expand Down Expand Up @@ -33,4 +35,8 @@ object CoreManagerModule {
): KeystoreManager = KeystoreManagerImpl(
buildInfoManager = buildInfoManager,
)

@Provides
@Singleton
fun provideUuidManager(): UuidManager = UuidManagerImpl()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.bitwarden.data.manager.di
import android.app.Application
import android.content.Context
import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.UuidManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.manager.BitwardenPackageManager
import com.bitwarden.data.manager.BitwardenPackageManagerImpl
Expand All @@ -17,8 +19,6 @@ import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManagerImpl
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriterImpl
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.service.DownloadService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -50,11 +50,11 @@ object DataManagerModule {
@Singleton
fun provideFileManager(
@ApplicationContext context: Context,
downloadService: DownloadService,
uuidManager: UuidManager,
dispatcherManager: DispatcherManager,
): FileManager = FileManagerImpl(
context = context,
downloadService = downloadService,
uuidManager = uuidManager,
dispatcherManager = dispatcherManager,
)

Expand All @@ -81,13 +81,13 @@ object DataManagerModule {
fileManager: FileManager,
dispatcherManager: DispatcherManager,
buildInfoManager: BuildInfoManager,
serverConfigRepository: ServerConfigRepository,
configDiskSource: ConfigDiskSource,
): FlightRecorderWriter = FlightRecorderWriterImpl(
clock = clock,
fileManager = fileManager,
dispatcherManager = dispatcherManager,
buildInfoManager = buildInfoManager,
serverConfigRepository = serverConfigRepository,
configDiskSource = configDiskSource,
)

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package com.bitwarden.data.manager.file

import android.net.Uri
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.data.manager.model.DownloadResult
import com.bitwarden.data.manager.model.ZipFileResult
import java.io.File
import java.io.InputStream

/**
* Manages reading files.
Expand All @@ -28,10 +28,10 @@ interface FileManager {
suspend fun delete(vararg files: File)

/**
* Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain
* Opens the provided [stream] and writes it to cache. A successful [Result] will contain
* the final file path.
*/
suspend fun downloadFileToCache(url: String): DownloadResult
suspend fun streamFileToCache(stream: InputStream): Result<File>

/**
* Writes an existing [file] to a [fileUri]. `true` will be returned if the file was
Expand Down
Loading
Loading