diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 68521b200e..fbd6988e47 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -18,10 +18,12 @@ import com.x8bit.bitwarden.data.auth.repository.model.LoginResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.toUserState +import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.flatMap +import com.x8bit.bitwarden.data.vault.repository.VaultRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -30,22 +32,20 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject import javax.inject.Singleton -private const val DEFAULT_KDF_ITERATIONS = 600000 - /** * Default implementation of [AuthRepository]. */ @Suppress("LongParameterList") @Singleton -class AuthRepositoryImpl @Inject constructor( +class AuthRepositoryImpl constructor( private val accountsService: AccountsService, private val haveIBeenPwnedService: HaveIBeenPwnedService, private val identityService: IdentityService, private val authSdkSource: AuthSdkSource, private val authDiskSource: AuthDiskSource, + private val vaultRepository: VaultRepository, dispatcherManager: DispatcherManager, ) : AuthRepository { private val scope = CoroutineScope(dispatcherManager.io) @@ -122,6 +122,7 @@ class AuthRepositoryImpl @Inject constructor( privateKey = it.privateKey, ) } + vaultRepository.unlockVaultAndSync(masterPassword = password) LoginResult.Success } @@ -176,7 +177,7 @@ class AuthRepositoryImpl @Inject constructor( } } } - val kdf = Kdf.Pbkdf2(DEFAULT_KDF_ITERATIONS.toUInt()) + val kdf = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()) return authSdkSource .makeRegisterKeys( email = email, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt index ca735adf3f..31a3baf1b3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/di/AuthRepositoryModule.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepositoryImpl import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager +import com.x8bit.bitwarden.data.vault.repository.VaultRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -31,6 +32,7 @@ object AuthRepositoryModule { authSdkSource: AuthSdkSource, authDiskSource: AuthDiskSource, dispatchers: DispatcherManager, + vaultRepository: VaultRepository, ): AuthRepository = AuthRepositoryImpl( accountsService = accountsService, identityService = identityService, @@ -38,5 +40,6 @@ object AuthRepositoryModule { authDiskSource = authDiskSource, haveIBeenPwnedService = haveIBeenPwnedService, dispatcherManager = dispatchers, + vaultRepository = vaultRepository, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AccountJsonExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AccountJsonExtensions.kt new file mode 100644 index 0000000000..651fc4ebf3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AccountJsonExtensions.kt @@ -0,0 +1,26 @@ +package com.x8bit.bitwarden.data.auth.repository.util + +import com.bitwarden.core.Kdf +import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson +import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson +import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants + +/** + * Convert [AccountJson.Profile] to [Kdf] params for use with Bitwarden SDK. + */ +fun AccountJson.Profile.toSdkParams(): Kdf { + return when (this.kdfType) { + KdfTypeJson.ARGON2_ID -> Kdf.Argon2id( + iterations = (kdfIterations ?: KdfParamsConstants.DEFAULT_ARGON2_ITERATIONS).toUInt(), + memory = (kdfMemory ?: KdfParamsConstants.DEFAULT_ARGON2_MEMORY).toUInt(), + parallelism = + (kdfParallelism ?: KdfParamsConstants.DEFAULT_ARGON2_PARALLELISM).toUInt(), + ) + + KdfTypeJson.PBKDF2_SHA256 -> Kdf.Pbkdf2( + iterations = (kdfIterations ?: KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS).toUInt(), + ) + + else -> Kdf.Pbkdf2(iterations = KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS.toUInt()) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsConstants.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsConstants.kt new file mode 100644 index 0000000000..6fa9f2224c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/KdfParamsConstants.kt @@ -0,0 +1,29 @@ +package com.x8bit.bitwarden.data.auth.util + +import com.bitwarden.core.Kdf + +/** + * Constants relating to [Kdf] initialization defaults. + */ +object KdfParamsConstants { + + /** + * The default number of iterations when calculating a user's password for [Kdf.Pbkdf2]. + */ + const val DEFAULT_PBKDF2_ITERATIONS: Int = 600000 + + /** + * The default number of iterations when calculating a user's password for [Kdf.Argon2id]. + */ + const val DEFAULT_ARGON2_ITERATIONS: Int = 3 + + /** + * The default amount of memory to use when calculating a password hash (MB) for [Kdf.Argon2id]. + */ + const val DEFAULT_ARGON2_MEMORY: Int = 64 + + /** + * The default number of threads to use when calculating a password hash for [Kdf.Argon2id]. + */ + const val DEFAULT_ARGON2_PARALLELISM: Int = 4 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt index 77e37a4ca4..b359acfe54 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSource.kt @@ -5,11 +5,20 @@ import com.bitwarden.core.CipherListView import com.bitwarden.core.CipherView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitCryptoRequest +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult /** * Source of vault information and functionality from the Bitwarden SDK. */ interface VaultSdkSource { + + /** + * Attempts to initialize cryptography functionality for the Bitwarden SDK + * with a given [InitCryptoRequest]. + */ + suspend fun initializeCrypto(request: InitCryptoRequest): Result + /** * Decrypts a [Cipher] returning a [CipherView] wrapped in a [Result]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt index 8026d06eb7..9d990b2d06 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceImpl.kt @@ -5,7 +5,11 @@ import com.bitwarden.core.CipherListView import com.bitwarden.core.CipherView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitCryptoRequest +import com.bitwarden.sdk.BitwardenException +import com.bitwarden.sdk.ClientCrypto import com.bitwarden.sdk.ClientVault +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult /** * Primary implementation of [VaultSdkSource] that serves as a convenience wrapper around a @@ -13,7 +17,20 @@ import com.bitwarden.sdk.ClientVault */ class VaultSdkSourceImpl( private val clientVault: ClientVault, + private val clientCrypto: ClientCrypto, ) : VaultSdkSource { + override suspend fun initializeCrypto( + request: InitCryptoRequest, + ): Result = + runCatching { + try { + clientCrypto.initializeCrypto(req = request) + InitializeCryptoResult.Success + } catch (exception: BitwardenException) { + // The only truly expected error from the SDK is an incorrect password. + InitializeCryptoResult.AuthenticationError + } + } override suspend fun decryptCipher(cipher: Cipher): Result = runCatching { clientVault.ciphers().decrypt(cipher) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt index a69896cdcd..bbcb67b4c7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/di/VaultSdkModule.kt @@ -14,11 +14,15 @@ import javax.inject.Singleton */ @Module @InstallIn(SingletonComponent::class) -class VaultSdkModule { +object VaultSdkModule { @Provides @Singleton fun providesVaultSdkSource( client: Client, - ): VaultSdkSource = VaultSdkSourceImpl(clientVault = client.vault()) + ): VaultSdkSource = + VaultSdkSourceImpl( + clientVault = client.vault(), + clientCrypto = client.crypto(), + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt new file mode 100644 index 0000000000..46b310e0ca --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/InitializeCryptoResult.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.data.vault.datasource.sdk.model + +/** + * Models result of initializing cryptography functionality for the Bitwarden SDK. + */ +sealed class InitializeCryptoResult { + + /** + * Successfully initialized cryptography functionality. + */ + data object Success : InitializeCryptoResult() + + /** + * Incorrect password provided. + */ + data object AuthenticationError : InitializeCryptoResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt index 9929709dcd..7f5f05b6f6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepository.kt @@ -1,5 +1,7 @@ package com.x8bit.bitwarden.data.vault.repository +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult + /** * Responsible for managing vault data inside the network layer. */ @@ -8,5 +10,10 @@ interface VaultRepository { /** * Attempt to sync the vault data. */ - suspend fun sync() + fun sync() + + /** + * Attempt to initialize crypto and sync the vault data. + */ + suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt index 91a9abd534..28c0ac26a2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryImpl.kt @@ -1,11 +1,15 @@ 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.manager.dispatcher.DispatcherManager 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.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.launch @@ -24,7 +28,7 @@ class VaultRepositoryImpl constructor( private var syncJob: Job = Job().apply { complete() } - override suspend fun sync() { + override fun sync() { if (!syncJob.isCompleted) return syncJob = scope.launch { syncService @@ -36,7 +40,6 @@ class VaultRepositoryImpl constructor( privateKey = syncResponse.profile?.privateKey, ) // TODO transform into domain object consumable by VaultViewModel BIT-205. - // TODO initialize crypto in BIT-990 syncResponse.ciphers?.let { networkCiphers -> vaultSdkSource.decryptCipherList( cipherList = networkCiphers.toEncryptedSdkCipherList(), @@ -55,6 +58,13 @@ class VaultRepositoryImpl constructor( } } + override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult { + return initializeCrypto(masterPassword = masterPassword) + .also { vaultUnlockedResult -> + if (vaultUnlockedResult is VaultUnlockResult.Success) sync() + } + } + private fun storeUserKeyAndPrivateKey( userKey: String?, privateKey: String?, @@ -72,4 +82,30 @@ class VaultRepositoryImpl constructor( ) } } + + @Suppress("ReturnCount") + private suspend fun initializeCrypto(masterPassword: String): VaultUnlockResult { + val userState = authDiskSource.userState + ?: return VaultUnlockResult.InvalidStateError + val userKey = authDiskSource.getUserKey(userId = userState.activeUserId) + ?: return VaultUnlockResult.InvalidStateError + val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId) + ?: return VaultUnlockResult.InvalidStateError + return vaultSdkSource + .initializeCrypto( + request = InitCryptoRequest( + kdfParams = userState.activeAccount.profile.toSdkParams(), + email = userState.activeAccount.profile.email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + // TODO use actual organization keys BIT-1091 + organizationKeys = mapOf(), + ), + ) + .fold( + onFailure = { VaultUnlockResult.GenericError }, + onSuccess = { it.toVaultUnlockResult() }, + ) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt index 642c91bae0..b11bcde595 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/di/VaultRepositoryModule.kt @@ -17,7 +17,7 @@ import javax.inject.Singleton */ @Module @InstallIn(SingletonComponent::class) -class VaultRepositoryModule { +object VaultRepositoryModule { @Provides @Singleton diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt new file mode 100644 index 0000000000..43be52a832 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/model/VaultUnlockResult.kt @@ -0,0 +1,27 @@ +package com.x8bit.bitwarden.data.vault.repository.model + +/** + * Models result of unlocking the vault. + */ +sealed class VaultUnlockResult { + + /** + * Vault successfully unlocked. + */ + data object Success : VaultUnlockResult() + + /** + * Incorrect password provided. + */ + data object AuthenticationError : VaultUnlockResult() + + /** + * Unable to access user state information. + */ + data object InvalidStateError : VaultUnlockResult() + + /** + * Generic error thrown by Bitwarden SDK. + */ + data object GenericError : VaultUnlockResult() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt new file mode 100644 index 0000000000..9e55ace45f --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/repository/util/VaultUnlockResultExtensions.kt @@ -0,0 +1,13 @@ +package com.x8bit.bitwarden.data.vault.repository.util + +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult + +/** + * Transform a [InitializeCryptoResult] to [VaultUnlockResult]. + */ +fun InitializeCryptoResult.toVaultUnlockResult(): VaultUnlockResult = + when (this) { + InitializeCryptoResult.AuthenticationError -> VaultUnlockResult.AuthenticationError + InitializeCryptoResult.Success -> VaultUnlockResult.Success + } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 47c814a00b..1037d5dfc6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -31,6 +31,8 @@ import com.x8bit.bitwarden.data.auth.util.toSdkParams import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.vault.repository.VaultRepository +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify @@ -45,12 +47,14 @@ import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +@Suppress("LargeClass") class AuthRepositoryTest { private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val accountsService: AccountsService = mockk() private val identityService: IdentityService = mockk() private val haveIBeenPwnedService: HaveIBeenPwnedService = mockk() + private val vaultRepository: VaultRepository = mockk() private val fakeAuthDiskSource = FakeAuthDiskSource() private val authSdkSource = mockk { coEvery { @@ -85,6 +89,7 @@ class AuthRepositoryTest { authSdkSource = authSdkSource, authDiskSource = fakeAuthDiskSource, dispatcherManager = dispatcherManager, + vaultRepository = vaultRepository, ) @BeforeEach @@ -183,7 +188,8 @@ class AuthRepositoryTest { } @Test - fun `login get token succeeds should return Success and update AuthState and stored keys`() = + @Suppress("MaxLineLength") + fun `login get token succeeds should return Success, update AuthState, update stored keys, and unlockVaultAndSync`() = runTest { val successResponse = GET_TOKEN_RESPONSE_SUCCESS coEvery { @@ -197,6 +203,9 @@ class AuthRepositoryTest { ) } .returns(Result.success(successResponse)) + coEvery { + vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + } returns VaultUnlockResult.Success every { GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null) } returns SINGLE_USER_STATE_1 @@ -219,6 +228,9 @@ class AuthRepositoryTest { captchaToken = null, ) } + coVerify { + vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + } } @Test @@ -597,6 +609,9 @@ class AuthRepositoryTest { captchaToken = null, ) } returns Result.success(successResponse) + coEvery { + vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + } returns VaultUnlockResult.Success every { GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = null) } returns SINGLE_USER_STATE_1 @@ -643,6 +658,9 @@ class AuthRepositoryTest { captchaToken = null, ) } returns Result.success(successResponse) + coEvery { + vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + } returns VaultUnlockResult.Success every { GET_TOKEN_RESPONSE_SUCCESS.toUserState(previousUserState = SINGLE_USER_STATE_2) } returns MULTI_USER_STATE diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt index 4f7745768b..bb1cdfa1a4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/VaultSdkSourceTest.kt @@ -5,22 +5,99 @@ import com.bitwarden.core.CipherListView import com.bitwarden.core.CipherView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitCryptoRequest +import com.bitwarden.sdk.BitwardenException +import com.bitwarden.sdk.ClientCrypto import com.bitwarden.sdk.ClientVault +import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import kotlin.IllegalStateException class VaultSdkSourceTest { private val clientVault = mockk() - + private val clientCrypto = mockk() private val vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl( clientVault = clientVault, + clientCrypto = clientCrypto, ) + @Test + fun `initializeCrypto with sdk success should return InitializeCryptoResult Success`() = + runBlocking { + val mockInitCryptoRequest = mockk() + coEvery { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } returns Unit + val result = vaultSdkSource.initializeCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + InitializeCryptoResult.Success.asSuccess(), + result, + ) + coVerify { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } + } + + @Test + fun `initializeCrypto with sdk failure should return failure`() = runBlocking { + val mockInitCryptoRequest = mockk() + val expectedException = IllegalStateException("mock") + coEvery { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } throws expectedException + val result = vaultSdkSource.initializeCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + expectedException.asFailure(), + result, + ) + coVerify { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } + } + + @Test + fun `initializeCrypto with BitwardenException failure should return AuthenticationError`() = + runBlocking { + val mockInitCryptoRequest = mockk() + val expectedException = BitwardenException.E(message = "") + coEvery { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } throws expectedException + val result = vaultSdkSource.initializeCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + InitializeCryptoResult.AuthenticationError.asSuccess(), + result, + ) + coVerify { + clientCrypto.initializeCrypto( + req = mockInitCryptoRequest, + ) + } + } + @Test fun `Cipher decrypt should call SDK and return a Result with correct data`() = runBlocking { val mockCipher = mockk() diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt index 6fe19f98ba..cc6269250c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt @@ -1,18 +1,25 @@ package com.x8bit.bitwarden.data.vault.repository +import com.bitwarden.core.InitCryptoRequest +import com.bitwarden.core.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource +import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService +import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkCipher import com.x8bit.bitwarden.data.vault.datasource.sdk.createMockSdkFolder +import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class VaultRepositoryTest { @@ -50,6 +57,182 @@ class VaultRepositoryTest { privateKey = "mockPrivateKey-1", ) } + + @Test + fun `unlockVaultAndSync with initializeCrypto Success should sync and return Success`() = + runTest { + coEvery { + syncService.sync() + } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns mockk() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns mockk() + fakeAuthDiskSource.storePrivateKey( + userId = "mockUserId", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockUserId", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), + email = "email", + password = "mockPassword-1", + userKey = "mockKey-1", + privateKey = "mockPrivateKey-1", + organizationKeys = mapOf(), + ), + ) + } returns Result.success(InitializeCryptoResult.Success) + + val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + + assertEquals( + VaultUnlockResult.Success, + result, + ) + coVerify { syncService.sync() } + } + + @Test + fun `unlockVaultAndSync with initializeCrypto failure should return GenericError`() = + runTest { + coEvery { + syncService.sync() + } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns mockk() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns mockk() + fakeAuthDiskSource.storePrivateKey( + userId = "mockUserId", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockUserId", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), + email = "email", + password = "mockPassword-1", + userKey = "mockKey-1", + privateKey = "mockPrivateKey-1", + organizationKeys = mapOf(), + ), + ) + } returns Result.failure(IllegalStateException()) + + val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + + assertEquals( + VaultUnlockResult.GenericError, + result, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultAndSync with initializeCrypto AuthenticationError should return AuthenticationError`() = + runTest { + coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns mockk() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns mockk() + fakeAuthDiskSource.storePrivateKey( + userId = "mockUserId", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockUserId", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), + email = "email", + password = "", + userKey = "mockKey-1", + privateKey = "mockPrivateKey-1", + organizationKeys = mapOf(), + ), + ) + } returns Result.success(InitializeCryptoResult.AuthenticationError) + + val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + assertEquals( + VaultUnlockResult.AuthenticationError, + result, + ) + } + + @Test + fun `unlockVaultAndSync with missing user state should return InvalidStateError `() = + runTest { + fakeAuthDiskSource.userState = null + + val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + + assertEquals( + VaultUnlockResult.InvalidStateError, + result, + ) + } + + @Test + fun `unlockVaultAndSync with missing user key should return InvalidStateError `() = + runTest { + val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + fakeAuthDiskSource.storeUserKey( + userId = "mockUserId", + userKey = null, + ) + fakeAuthDiskSource.storePrivateKey( + userId = "mockUserId", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + assertEquals( + VaultUnlockResult.InvalidStateError, + result, + ) + } + + @Test + fun `unlockVaultAndSync with missing private key should return InvalidStateError `() = + runTest { + val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + fakeAuthDiskSource.storeUserKey( + userId = "mockUserId", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.storePrivateKey( + userId = "mockUserId", + privateKey = null, + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + assertEquals( + VaultUnlockResult.InvalidStateError, + result, + ) + } } private val MOCK_USER_STATE = UserStateJson(