mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-15056] Add exportVaultDataToCxf function to VaultRepository (#5847)
This commit is contained in:
parent
808d57edc5
commit
ba7ee04281
@ -24,6 +24,14 @@ interface VaultDiskSource {
|
|||||||
*/
|
*/
|
||||||
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
suspend fun getCiphers(userId: String): List<SyncResponseJson.Cipher>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all ciphers with the given [cipherIds] from the data source for a given [userId].
|
||||||
|
*/
|
||||||
|
suspend fun getSelectedCiphers(
|
||||||
|
userId: String,
|
||||||
|
cipherIds: List<String>,
|
||||||
|
): List<SyncResponseJson.Cipher>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
* Retrieves all ciphers from the data source for a given [userId] that contain TOTP codes.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -97,6 +97,24 @@ class VaultDiskSourceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getSelectedCiphers(
|
||||||
|
userId: String,
|
||||||
|
cipherIds: List<String>,
|
||||||
|
): List<SyncResponseJson.Cipher> {
|
||||||
|
val entities = ciphersDao.getSelectedCiphers(userId = userId, cipherIds = cipherIds)
|
||||||
|
return withContext(context = dispatcherManager.default) {
|
||||||
|
entities
|
||||||
|
.map { entity ->
|
||||||
|
async {
|
||||||
|
json.decodeFromStringWithErrorCallback<SyncResponseJson.Cipher>(
|
||||||
|
string = entity.cipherJson,
|
||||||
|
) { Timber.e(it, "Failed to deserialize Cipher in Vault") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
override suspend fun getTotpCiphers(userId: String): List<SyncResponseJson.Cipher> {
|
||||||
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
val entities = ciphersDao.getAllTotpCiphers(userId = userId)
|
||||||
return withContext(context = dispatcherManager.default) {
|
return withContext(context = dispatcherManager.default) {
|
||||||
|
|||||||
@ -37,6 +37,15 @@ interface CiphersDao {
|
|||||||
userId: String,
|
userId: String,
|
||||||
): List<CipherEntity>
|
): List<CipherEntity>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all ciphers from the database with the given [cipherIds] for a given [userId].
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM ciphers WHERE user_id = :userId AND id IN (:cipherIds)")
|
||||||
|
suspend fun getSelectedCiphers(
|
||||||
|
userId: String,
|
||||||
|
cipherIds: List<String>,
|
||||||
|
): List<CipherEntity>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all ciphers from the database for a given [userId].
|
* Retrieves all ciphers from the database for a given [userId].
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -266,6 +266,13 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
|||||||
*/
|
*/
|
||||||
suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult
|
suspend fun importCxfPayload(payload: String): ImportCxfPayloadResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to export the vault data to a CXF file.
|
||||||
|
*
|
||||||
|
* @param ciphers Ciphers selected for export.
|
||||||
|
*/
|
||||||
|
suspend fun exportVaultDataToCxf(ciphers: List<CipherListView>): Result<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flow that represents the data for a specific vault list item as found by ID. This may emit
|
* Flow that represents the data for a specific vault list item as found by ID. This may emit
|
||||||
* `null` if the item cannot be found.
|
* `null` if the item cannot be found.
|
||||||
|
|||||||
@ -94,6 +94,7 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolder
|
|||||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
|
||||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
|
||||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.util.toSdkAccount
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@ -986,6 +987,28 @@ class VaultRepositoryImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun exportVaultDataToCxf(
|
||||||
|
ciphers: List<CipherListView>,
|
||||||
|
): Result<String> {
|
||||||
|
val userId = activeUserId
|
||||||
|
?: return NoActiveUserException().asFailure()
|
||||||
|
val account = authDiskSource.userState
|
||||||
|
?.activeAccount
|
||||||
|
?.toSdkAccount()
|
||||||
|
?: return NoActiveUserException().asFailure()
|
||||||
|
|
||||||
|
val ciphers = vaultDiskSource
|
||||||
|
.getSelectedCiphers(userId = userId, cipherIds = ciphers.mapNotNull { it.id })
|
||||||
|
.map { it.toEncryptedSdkCipher() }
|
||||||
|
|
||||||
|
return vaultSdkSource
|
||||||
|
.exportVaultDataToCxf(
|
||||||
|
userId = userId,
|
||||||
|
account = account,
|
||||||
|
ciphers = ciphers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
|
* Checks if the given [userId] has an associated encrypted PIN key but not a pin-protected user
|
||||||
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
|
* key. This indicates a scenario in which a user has requested PIN unlocking but requires
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.repository.util
|
||||||
|
|
||||||
|
import com.bitwarden.exporters.Account
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [AccountJson] to a [Account] for use in the SDK.
|
||||||
|
*/
|
||||||
|
fun AccountJson.toSdkAccount(): Account = Account(
|
||||||
|
id = profile.userId,
|
||||||
|
email = profile.email,
|
||||||
|
name = profile.name,
|
||||||
|
)
|
||||||
@ -105,6 +105,22 @@ class VaultDiskSourceTest {
|
|||||||
assertEquals(ciphers, result2)
|
assertEquals(ciphers, result2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getSelectedCiphers should return selected CiphersDao ciphers`() = runTest {
|
||||||
|
val cipherEntities = listOf(
|
||||||
|
CIPHER_ENTITY,
|
||||||
|
CIPHER_ENTITY.copy(id = "otherCipherId"),
|
||||||
|
)
|
||||||
|
val ciphers = listOf(CIPHER_1)
|
||||||
|
val cipherIds = listOf("mockId-1")
|
||||||
|
|
||||||
|
val result1 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||||
|
assertEquals(emptyList<SyncResponseJson.Cipher>(), result1)
|
||||||
|
ciphersDao.insertCiphers(cipherEntities)
|
||||||
|
val result2 = vaultDiskSource.getSelectedCiphers(USER_ID, cipherIds)
|
||||||
|
assertEquals(ciphers, result2)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getTotpCiphers should return all CiphersDao totp ciphers`() = runTest {
|
fun `getTotpCiphers should return all CiphersDao totp ciphers`() = runTest {
|
||||||
val cipherEntities = listOf(
|
val cipherEntities = listOf(
|
||||||
|
|||||||
@ -41,6 +41,12 @@ class FakeCiphersDao : CiphersDao {
|
|||||||
override suspend fun getAllCiphers(userId: String): List<CipherEntity> =
|
override suspend fun getAllCiphers(userId: String): List<CipherEntity> =
|
||||||
storedCiphers.filter { it.userId == userId }
|
storedCiphers.filter { it.userId == userId }
|
||||||
|
|
||||||
|
override suspend fun getSelectedCiphers(
|
||||||
|
userId: String,
|
||||||
|
cipherIds: List<String>,
|
||||||
|
): List<CipherEntity> =
|
||||||
|
storedCiphers.filter { it.userId == userId && it.id in cipherIds }
|
||||||
|
|
||||||
override suspend fun getAllTotpCiphers(userId: String): List<CipherEntity> =
|
override suspend fun getAllTotpCiphers(userId: String): List<CipherEntity> =
|
||||||
storedCiphers.filter { it.userId == userId && it.hasTotp }
|
storedCiphers.filter { it.userId == userId && it.hasTotp }
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
|
||||||
|
|
||||||
|
import com.bitwarden.exporters.Account
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [com.bitwarden.exporters.Account] for testing purposes
|
||||||
|
*/
|
||||||
|
fun createMockAccount(
|
||||||
|
number: Int,
|
||||||
|
email: String = "mockEmail-$number",
|
||||||
|
name: String? = "mockName-$number",
|
||||||
|
): Account = Account(
|
||||||
|
id = "mockId-$number",
|
||||||
|
email = email,
|
||||||
|
name = name,
|
||||||
|
)
|
||||||
@ -71,6 +71,8 @@ import com.x8bit.bitwarden.data.platform.manager.model.SyncSendUpsertData
|
|||||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockAccount
|
||||||
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
|
||||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult
|
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDecryptCipherListResult
|
||||||
@ -4298,6 +4300,62 @@ class VaultRepositoryTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `exportVaultDataToCxf should return success result`() = runTest {
|
||||||
|
val userId = "mockId-1"
|
||||||
|
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||||
|
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||||
|
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.exportVaultDataToCxf(
|
||||||
|
userId = userId,
|
||||||
|
account = account,
|
||||||
|
ciphers = any(),
|
||||||
|
)
|
||||||
|
} returns "TestResult".asSuccess()
|
||||||
|
coEvery {
|
||||||
|
vaultDiskSource.getSelectedCiphers(
|
||||||
|
userId = userId,
|
||||||
|
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||||
|
)
|
||||||
|
} returns listOf(createMockCipher(number = 1))
|
||||||
|
|
||||||
|
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||||
|
|
||||||
|
assertEquals("TestResult".asSuccess(), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `exportVaultDataToCxf should return error result when exportVaultDataToCxf fails`() =
|
||||||
|
runTest {
|
||||||
|
val userId = "mockId-1"
|
||||||
|
val account = createMockAccount(number = 1, email = "email", name = null)
|
||||||
|
val cipherListViews = listOf(createMockCipherListView(number = 1))
|
||||||
|
val throwable = Throwable()
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
vaultSdkSource.exportVaultDataToCxf(
|
||||||
|
userId = userId,
|
||||||
|
account = account,
|
||||||
|
ciphers = any(),
|
||||||
|
)
|
||||||
|
} returns throwable.asFailure()
|
||||||
|
|
||||||
|
coEvery {
|
||||||
|
vaultDiskSource.getSelectedCiphers(
|
||||||
|
userId = userId,
|
||||||
|
cipherIds = cipherListViews.mapNotNull { it.id },
|
||||||
|
)
|
||||||
|
} returns listOf(createMockCipher(number = 1))
|
||||||
|
|
||||||
|
val result = vaultRepository.exportVaultDataToCxf(ciphers = cipherListViews)
|
||||||
|
|
||||||
|
assertEquals(throwable.asFailure(), result)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `silentlyDiscoverCredentials should return result`() = runTest {
|
fun `silentlyDiscoverCredentials should return result`() = runTest {
|
||||||
val userId = "userId"
|
val userId = "userId"
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.repository.model
|
||||||
|
|
||||||
|
import com.bitwarden.network.model.KdfTypeJson
|
||||||
|
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [AccountJson.Profile] for testing purposes.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
fun createMockAccountJsonProfile(
|
||||||
|
number: Int,
|
||||||
|
userId: String = "mockId-$number",
|
||||||
|
email: String = "mockEmail-$number",
|
||||||
|
isEmailVerified: Boolean = true,
|
||||||
|
name: String? = "mockName-$number",
|
||||||
|
stamp: String = "mockSecurityStamp-$number",
|
||||||
|
organizationId: String? = "mockOrganizationId-$number",
|
||||||
|
avatarColorHex: String? = "mockAvatarColorHex-$number",
|
||||||
|
hasPremium: Boolean = false,
|
||||||
|
forcePasswordResetReason: ForcePasswordResetReason? = null,
|
||||||
|
kdfType: KdfTypeJson? = null,
|
||||||
|
kdfIterations: Int? = null,
|
||||||
|
kdfMemory: Int? = null,
|
||||||
|
kdfParallelism: Int? = null,
|
||||||
|
userDecryptionOptions: UserDecryptionOptionsJson? = null,
|
||||||
|
isTwoFactorEnabled: Boolean = false,
|
||||||
|
creationDate: ZonedDateTime = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
|
||||||
|
): AccountJson.Profile = AccountJson.Profile(
|
||||||
|
userId = userId,
|
||||||
|
email = email,
|
||||||
|
isEmailVerified = isEmailVerified,
|
||||||
|
name = name,
|
||||||
|
stamp = stamp,
|
||||||
|
organizationId = organizationId,
|
||||||
|
avatarColorHex = avatarColorHex,
|
||||||
|
hasPremium = hasPremium,
|
||||||
|
forcePasswordResetReason = forcePasswordResetReason,
|
||||||
|
kdfType = kdfType,
|
||||||
|
kdfIterations = kdfIterations,
|
||||||
|
kdfMemory = kdfMemory,
|
||||||
|
kdfParallelism = kdfParallelism,
|
||||||
|
userDecryptionOptions = userDecryptionOptions,
|
||||||
|
isTwoFactorEnabled = isTwoFactorEnabled,
|
||||||
|
creationDate = creationDate,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mock [AccountJson] for testing purposes.
|
||||||
|
*/
|
||||||
|
fun createMockAccountJson(
|
||||||
|
number: Int,
|
||||||
|
profile: AccountJson.Profile = createMockAccountJsonProfile(number),
|
||||||
|
tokens: AccountTokensJson = AccountTokensJson(
|
||||||
|
accessToken = "accessToken-$number",
|
||||||
|
refreshToken = "refreshToken-$number",
|
||||||
|
),
|
||||||
|
settings: AccountJson.Settings = AccountJson.Settings(
|
||||||
|
environmentUrlData = null,
|
||||||
|
),
|
||||||
|
): AccountJson = AccountJson(
|
||||||
|
profile = profile,
|
||||||
|
tokens = tokens,
|
||||||
|
settings = settings,
|
||||||
|
)
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.x8bit.bitwarden.data.vault.repository.util
|
||||||
|
|
||||||
|
import com.bitwarden.exporters.Account
|
||||||
|
import com.x8bit.bitwarden.data.vault.repository.model.createMockAccountJson
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AccountJsonExtensionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toAccountData returns populated AccountData when account is non-null`() {
|
||||||
|
val account = createMockAccountJson(number = 1)
|
||||||
|
assertEquals(
|
||||||
|
Account(
|
||||||
|
id = "mockId-1",
|
||||||
|
email = "mockEmail-1",
|
||||||
|
name = "mockName-1",
|
||||||
|
),
|
||||||
|
account.toSdkAccount(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user