BIT-990: Initialize Crypto for Vault (#213)

This commit is contained in:
Ramsey Smith
2023-11-07 09:17:32 -07:00
committed by GitHub
parent c92413b91c
commit d19ff58009
16 changed files with 481 additions and 14 deletions

View File

@@ -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<AuthSdkSource> {
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

View File

@@ -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<ClientVault>()
private val clientCrypto = mockk<ClientCrypto>()
private val vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
clientVault = clientVault,
clientCrypto = clientCrypto,
)
@Test
fun `initializeCrypto with sdk success should return InitializeCryptoResult Success`() =
runBlocking {
val mockInitCryptoRequest = mockk<InitCryptoRequest>()
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<InitCryptoRequest>()
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<InitCryptoRequest>()
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<Cipher>()

View File

@@ -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(