BIT-1128: Expose vault data (#227)

This commit is contained in:
Ramsey Smith
2023-11-09 16:16:06 -07:00
committed by Álison Fernandes
parent a4c34a8704
commit a43a541719
13 changed files with 507 additions and 48 deletions

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.util
import okio.ByteString.Companion.decodeBase64
import java.net.UnknownHostException
import java.nio.charset.Charset
import java.util.Base64
@@ -32,3 +33,11 @@ fun String.base64UrlDecodeOrNull(): String? =
.replace("_", "/")
.decodeBase64()
?.string(Charset.defaultCharset())
/**
* Returns true if the throwable represents a no network error.
*/
fun Throwable?.isNoConnectionError(): Boolean {
return this is UnknownHostException ||
this?.cause?.isNoConnectionError() ?: false
}

View File

@@ -0,0 +1,48 @@
package com.x8bit.bitwarden.data.platform.repository.model
/**
* A data state that can be used as a template for data in the repository layer.
*/
sealed class DataState<out T> {
/**
* Data that is being wrapped by [DataState].
*/
abstract val data: T?
/**
* Loading state that has no data is available.
*/
data object Loading : DataState<Nothing>() {
override val data: Nothing? get() = null
}
/**
* Loaded state that has data available.
*/
data class Loaded<T>(
override val data: T,
) : DataState<T>()
/**
* Pending state that has data available.
*/
data class Pending<T>(
override val data: T,
) : DataState<T>()
/**
* Error state that may have data available.
*/
data class Error<T>(
val error: Throwable,
override val data: T? = null,
) : DataState<T>()
/**
* No network state that may have data is available.
*/
data class NoNetwork<T>(
override val data: T? = null,
) : DataState<T>()
}

View File

@@ -1,12 +1,25 @@
package com.x8bit.bitwarden.data.vault.repository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.StateFlow
/**
* Responsible for managing vault data inside the network layer.
*/
interface VaultRepository {
/**
* Flow that represents the current vault data.
*/
val vaultDataStateFlow: StateFlow<DataState<VaultData>>
/**
* Clear the in memory vault data.
*/
fun clearVaultData()
/**
* Attempt to sync the vault data.
*/

View File

@@ -3,15 +3,24 @@ package com.x8bit.bitwarden.data.vault.repository
import com.bitwarden.core.InitCryptoRequest
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
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.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipherList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
@@ -28,8 +37,23 @@ class VaultRepositoryImpl constructor(
private var syncJob: Job = Job().apply { complete() }
private val vaultDataMutableStateFlow =
MutableStateFlow<DataState<VaultData>>(DataState.Loading)
override val vaultDataStateFlow: StateFlow<DataState<VaultData>>
get() = vaultDataMutableStateFlow.asStateFlow()
override fun clearVaultData() {
vaultDataMutableStateFlow.update { DataState.Loading }
}
override fun sync() {
if (!syncJob.isCompleted) return
vaultDataMutableStateFlow.value.data?.let { data ->
vaultDataMutableStateFlow.update {
DataState.Pending(data = data)
}
}
syncJob = scope.launch {
syncService
.sync()
@@ -39,20 +63,23 @@ class VaultRepositoryImpl constructor(
userKey = syncResponse.profile?.key,
privateKey = syncResponse.profile?.privateKey,
)
// TODO transform into domain object consumable by VaultViewModel BIT-205.
syncResponse.ciphers?.let { networkCiphers ->
vaultSdkSource.decryptCipherList(
cipherList = networkCiphers.toEncryptedSdkCipherList(),
)
}
syncResponse.folders?.let { networkFolders ->
vaultSdkSource.decryptFolderList(
folderList = networkFolders.toEncryptedSdkFolderList(),
)
}
decryptSyncResponseAndUpdateVaultDataState(
syncResponse = syncResponse,
)
},
onFailure = {
// TODO handle failure BIT-205.
onFailure = { throwable ->
vaultDataMutableStateFlow.update {
if (throwable.isNoConnectionError()) {
DataState.NoNetwork(
data = it.data,
)
} else {
DataState.Error(
error = throwable,
data = it.data,
)
}
}
},
)
}
@@ -108,4 +135,34 @@ class VaultRepositoryImpl constructor(
onSuccess = { it.toVaultUnlockResult() },
)
}
private suspend fun decryptSyncResponseAndUpdateVaultDataState(syncResponse: SyncResponseJson) {
val newState = vaultSdkSource
.decryptCipherList(
cipherList = (syncResponse.ciphers ?: emptyList())
.toEncryptedSdkCipherList(),
)
.flatMap { decryptedCipherList ->
vaultSdkSource
.decryptFolderList(
folderList = (syncResponse.folders ?: emptyList())
.toEncryptedSdkFolderList(),
)
.map { decryptedFolderList ->
decryptedCipherList to decryptedFolderList
}
}
.fold(
onSuccess = { (decryptedCipherList, decryptedFolderList) ->
DataState.Loaded(
data = VaultData(
cipherListViewList = decryptedCipherList,
folderViewList = decryptedFolderList,
),
)
},
onFailure = { DataState.Error(error = it) },
)
vaultDataMutableStateFlow.update { newState }
}
}

View File

@@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.vault.repository.model
import com.bitwarden.core.CipherListView
import com.bitwarden.core.FolderView
/**
* Represents decrypted vault data.
*
* @param cipherListViewList List of decrypted ciphers.
* @param folderViewList List of decrypted folders.
*/
data class VaultData(
val cipherListViewList: List<CipherListView>,
val folderViewList: List<FolderView>,
)