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 ffa8d045f2..63ddbad922 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 @@ -121,33 +121,40 @@ class AuthRepositoryImpl constructor( } .fold( onFailure = { LoginResult.Error(errorMessage = null) }, - onSuccess = { - when (it) { - is CaptchaRequired -> LoginResult.CaptchaRequired(it.captchaKey) + onSuccess = { loginResponse -> + when (loginResponse) { + is CaptchaRequired -> LoginResult.CaptchaRequired(loginResponse.captchaKey) is Success -> { - authDiskSource.userState = it - .toUserState( - previousUserState = authDiskSource.userState, - environmentUrlData = environmentRepository - .environment - .environmentUrlData, - ) - .also { userState -> - authDiskSource.storeUserKey( - userId = userState.activeUserId, - userKey = it.key, - ) - authDiskSource.storePrivateKey( - userId = userState.activeUserId, - privateKey = it.privateKey, - ) - } - vaultRepository.unlockVaultAndSync(masterPassword = password) + val userStateJson = loginResponse.toUserState( + previousUserState = authDiskSource.userState, + environmentUrlData = environmentRepository + .environment + .environmentUrlData, + ) + vaultRepository.unlockVault( + email = userStateJson.activeAccount.profile.email, + kdf = userStateJson.activeAccount.profile.toSdkParams(), + userKey = loginResponse.key, + privateKey = loginResponse.privateKey, + // TODO use actual organization keys BIT-1091 + organizationalKeys = emptyMap(), + masterPassword = password, + ) + authDiskSource.userState = userStateJson + authDiskSource.storeUserKey( + userId = userStateJson.activeUserId, + userKey = loginResponse.key, + ) + authDiskSource.storePrivateKey( + userId = userStateJson.activeUserId, + privateKey = loginResponse.privateKey, + ) + vaultRepository.sync() LoginResult.Success } is GetTokenResponseJson.Invalid -> { - LoginResult.Error(errorMessage = it.errorModel.errorMessage) + LoginResult.Error(errorMessage = loginResponse.errorModel.errorMessage) } } }, 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 830dd0edfa..e8d961afa8 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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.repository import com.bitwarden.core.CipherView import com.bitwarden.core.FolderView +import com.bitwarden.core.Kdf import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.VaultData @@ -46,7 +47,20 @@ interface VaultRepository { fun getVaultFolderStateFlow(folderId: String): StateFlow> /** - * Attempt to initialize crypto and sync the vault data. + * Attempt to unlock the vault and sync the vault data for the currently active user. */ - suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult + suspend fun unlockVaultAndSyncForCurrentUser(masterPassword: String): VaultUnlockResult + + /** + * Attempt to unlock the vault with the specified user information. + */ + @Suppress("LongParameterList") + suspend fun unlockVault( + masterPassword: String, + email: String, + kdf: Kdf, + userKey: String, + privateKey: String, + organizationalKeys: Map, + ): 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 b5003c2f5a..91d6ecb259 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository import com.bitwarden.core.CipherView import com.bitwarden.core.FolderView import com.bitwarden.core.InitCryptoRequest +import com.bitwarden.core.Kdf import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson @@ -31,7 +32,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -146,18 +146,62 @@ class VaultRepositoryImpl constructor( initialValue = DataState.Loading, ) - override suspend fun unlockVaultAndSync(masterPassword: String): VaultUnlockResult { - return flow { - willSyncAfterUnlock = true - emit(initializeCrypto(masterPassword = masterPassword)) - } - .onEach { - willSyncAfterUnlock = false - if (it is VaultUnlockResult.Success) sync() + @Suppress("ReturnCount") + override suspend fun unlockVaultAndSyncForCurrentUser( + 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 unlockVault( + masterPassword = masterPassword, + email = userState.activeAccount.profile.email, + kdf = userState.activeAccount.profile.toSdkParams(), + userKey = userKey, + privateKey = privateKey, + // TODO use actual organization keys BIT-1091 + organizationalKeys = emptyMap(), + ) + .also { + if (it is VaultUnlockResult.Success) { + sync() + } } + } + + override suspend fun unlockVault( + masterPassword: String, + email: String, + kdf: Kdf, + userKey: String, + privateKey: String, + organizationalKeys: Map, + ): VaultUnlockResult = + flow { + willSyncAfterUnlock = true + emit( + vaultSdkSource + .initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + .fold( + onFailure = { VaultUnlockResult.GenericError }, + onSuccess = { it.toVaultUnlockResult() }, + ), + ) + } .onCompletion { willSyncAfterUnlock = false } .first() - } private fun storeUserKeyAndPrivateKey( userKey: String?, @@ -177,32 +221,6 @@ 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() }, - ) - } - private suspend fun decryptSendsAndUpdateSendDataState(sendList: List?) { val newState = vaultSdkSource .decryptSendList( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 378d45d9b2..12a9629ae2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -92,7 +92,7 @@ class VaultUnlockViewModel @Inject constructor( private fun handleUnlockClick() { mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) } viewModelScope.launch { - val vaultUnlockResult = vaultRepo.unlockVaultAndSync( + val vaultUnlockResult = vaultRepo.unlockVaultAndSyncForCurrentUser( mutableStateFlow.value.passwordInput, ) sendAction(VaultUnlockAction.Internal.ReceiveVaultUnlockResult(vaultUnlockResult)) 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 4a0b6074ef..7ad761dc9e 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 @@ -42,8 +42,10 @@ import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -269,7 +271,7 @@ class AuthRepositoryTest { @Test @Suppress("MaxLineLength") - fun `login get token succeeds should return Success, update AuthState, update stored keys, and unlockVaultAndSync`() = + fun `login get token succeeds should return Success, unlockVault, update AuthState, update stored keys, and sync`() = runTest { val successResponse = GET_TOKEN_RESPONSE_SUCCESS coEvery { @@ -284,8 +286,16 @@ class AuthRepositoryTest { } .returns(Result.success(successResponse)) coEvery { - vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + vaultRepository.unlockVault( + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key, + privateKey = successResponse.privateKey, + organizationalKeys = emptyMap(), + masterPassword = PASSWORD, + ) } returns VaultUnlockResult.Success + coEvery { vaultRepository.sync() } just runs every { GET_TOKEN_RESPONSE_SUCCESS.toUserState( previousUserState = null, @@ -310,9 +320,15 @@ class AuthRepositoryTest { passwordHash = PASSWORD_HASH, captchaToken = null, ) - } - coVerify { - vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + vaultRepository.unlockVault( + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key, + privateKey = successResponse.privateKey, + organizationalKeys = emptyMap(), + masterPassword = PASSWORD, + ) + vaultRepository.sync() } } @@ -679,7 +695,7 @@ class AuthRepositoryTest { } @Test - fun `logout for single account should clear the access toke and stored keys`() = runTest { + fun `logout for single account should clear the access token and stored keys`() = runTest { // First login: val successResponse = GET_TOKEN_RESPONSE_SUCCESS coEvery { @@ -693,8 +709,16 @@ class AuthRepositoryTest { ) } returns Result.success(successResponse) coEvery { - vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + vaultRepository.unlockVault( + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key, + privateKey = successResponse.privateKey, + organizationalKeys = emptyMap(), + masterPassword = PASSWORD, + ) } returns VaultUnlockResult.Success + coEvery { vaultRepository.sync() } just runs every { GET_TOKEN_RESPONSE_SUCCESS.toUserState( previousUserState = null, @@ -745,8 +769,16 @@ class AuthRepositoryTest { ) } returns Result.success(successResponse) coEvery { - vaultRepository.unlockVaultAndSync(masterPassword = PASSWORD) + vaultRepository.unlockVault( + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + userKey = successResponse.key, + privateKey = successResponse.privateKey, + organizationalKeys = emptyMap(), + masterPassword = PASSWORD, + ) } returns VaultUnlockResult.Success + coEvery { vaultRepository.sync() } just runs every { GET_TOKEN_RESPONSE_SUCCESS.toUserState( previousUserState = SINGLE_USER_STATE_2, 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 4d86cf3ec9..85bff92290 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 @@ -8,6 +8,7 @@ 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.repository.util.toSdkParams 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 @@ -460,8 +461,9 @@ class VaultRepositoryTest { } } + @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSync with initializeCrypto Success should sync and return Success`() = + fun `unlockVaultAndSyncForCurrentUser with unlockVault Success should sync and return Success`() = runTest { coEvery { syncService.sync() @@ -497,7 +499,9 @@ class VaultRepositoryTest { ) } returns Result.success(InitializeCryptoResult.Success) - val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser( + masterPassword = "mockPassword-1", + ) assertEquals( VaultUnlockResult.Success, @@ -507,100 +511,102 @@ class VaultRepositoryTest { } @Test - fun `sync should be able to be called after unlockVaultAndSync is canceled`() = runTest { - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) - } returns listOf(createMockSendView(number = 1)).asSuccess() - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - 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(), - ), + fun `sync should be able to be called after unlockVaultAndSyncForCurrentUser is canceled`() = + runTest { + coEvery { + syncService.sync() + } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns listOf(createMockCipherView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns listOf(createMockFolderView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptSendList(listOf(createMockSdkSend(number = 1))) + } returns listOf(createMockSendView(number = 1)).asSuccess() + fakeAuthDiskSource.storePrivateKey( + userId = "mockId-1", + privateKey = "mockPrivateKey-1", ) - } just awaits + fakeAuthDiskSource.storeUserKey( + userId = "mockId-1", + 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(), + ), + ) + } just awaits - val scope = CoroutineScope(Dispatchers.Unconfined) - scope.launch { - vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + val scope = CoroutineScope(Dispatchers.Unconfined) + scope.launch { + vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "mockPassword-1") + } + coVerify(exactly = 0) { syncService.sync() } + scope.cancel() + vaultRepository.sync() + + coVerify(exactly = 1) { syncService.sync() } } - coVerify(exactly = 0) { syncService.sync() } - scope.cancel() - vaultRepository.sync() - - coVerify(exactly = 1) { syncService.sync() } - } @Test - fun `sync should not be able to be called while unlockVaultAndSync is called`() = runTest { - coEvery { - syncService.sync() - } returns Result.success(createMockSyncResponse(number = 1)) - coEvery { - vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) - } returns listOf(createMockCipherView(number = 1)).asSuccess() - coEvery { - vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) - } returns listOf(createMockFolderView(number = 1)).asSuccess() - fakeAuthDiskSource.storePrivateKey( - userId = "mockId-1", - privateKey = "mockPrivateKey-1", - ) - fakeAuthDiskSource.storeUserKey( - userId = "mockId-1", - 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(), - ), + fun `sync should not be able to be called while unlockVaultAndSyncForCurrentUser is called`() = + runTest { + coEvery { + syncService.sync() + } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) + } returns listOf(createMockCipherView(number = 1)).asSuccess() + coEvery { + vaultSdkSource.decryptFolderList(listOf(createMockSdkFolder(1))) + } returns listOf(createMockFolderView(number = 1)).asSuccess() + fakeAuthDiskSource.storePrivateKey( + userId = "mockId-1", + privateKey = "mockPrivateKey-1", ) - } just awaits + fakeAuthDiskSource.storeUserKey( + userId = "mockId-1", + 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(), + ), + ) + } just awaits - val scope = CoroutineScope(Dispatchers.Unconfined) - scope.launch { - vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + val scope = CoroutineScope(Dispatchers.Unconfined) + scope.launch { + vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "mockPassword-1") + } + // We call sync here but the call to the SyncService should be blocked + // by the active call to unlockVaultAndSync + vaultRepository.sync() + + scope.cancel() + + coVerify(exactly = 0) { syncService.sync() } } - // We call sync here but the call to the SyncService should be blocked - // by the active call to unlockVaultAndSync - vaultRepository.sync() - - scope.cancel() - - coVerify(exactly = 0) { syncService.sync() } - } @Test - fun `unlockVaultAndSync with initializeCrypto failure should return GenericError`() = + fun `unlockVaultAndSyncForCurrentUser with unlockVault failure should return GenericError`() = runTest { coEvery { syncService.sync() @@ -633,7 +639,9 @@ class VaultRepositoryTest { ) } returns Result.failure(IllegalStateException()) - val result = vaultRepository.unlockVaultAndSync(masterPassword = "mockPassword-1") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser( + masterPassword = "mockPassword-1", + ) assertEquals( VaultUnlockResult.GenericError, @@ -643,7 +651,7 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSync with initializeCrypto AuthenticationError should return AuthenticationError`() = + fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError should return AuthenticationError`() = runTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) coEvery { @@ -674,19 +682,20 @@ class VaultRepositoryTest { ) } returns Result.success(InitializeCryptoResult.AuthenticationError) - val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") assertEquals( VaultUnlockResult.AuthenticationError, result, ) } + @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSync with missing user state should return InvalidStateError `() = + fun `unlockVaultAndSyncForCurrentUser with missing user state should return InvalidStateError `() = runTest { fakeAuthDiskSource.userState = null - val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") assertEquals( VaultUnlockResult.InvalidStateError, @@ -694,10 +703,11 @@ class VaultRepositoryTest { ) } + @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSync with missing user key should return InvalidStateError `() = + fun `unlockVaultAndSyncForCurrentUser with missing user key should return InvalidStateError `() = runTest { - val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") fakeAuthDiskSource.storeUserKey( userId = "mockId-1", userKey = null, @@ -713,10 +723,11 @@ class VaultRepositoryTest { ) } + @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSync with missing private key should return InvalidStateError `() = + fun `unlockVaultAndSyncForCurrentUser with missing private key should return InvalidStateError `() = runTest { - val result = vaultRepository.unlockVaultAndSync(masterPassword = "") + val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") fakeAuthDiskSource.storeUserKey( userId = "mockId-1", userKey = "mockKey-1", @@ -732,6 +743,188 @@ class VaultRepositoryTest { ) } + @Test + fun `unlockVault with initializeCrypto success should return Success`() = runTest { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationalKeys = emptyMap() + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + val result = vaultRepository.unlockVault( + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationalKeys = organizationalKeys, + ) + assertEquals(VaultUnlockResult.Success, result) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVault with initializeCrypto authentication failure should return AuthenticationError`() = + runTest { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationalKeys = emptyMap() + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } returns InitializeCryptoResult.AuthenticationError.asSuccess() + val result = vaultRepository.unlockVault( + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationalKeys = organizationalKeys, + ) + assertEquals(VaultUnlockResult.AuthenticationError, result) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto failure should return GenericError`() = runTest { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationalKeys = emptyMap() + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } returns Throwable("Fail").asFailure() + val result = vaultRepository.unlockVault( + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationalKeys = organizationalKeys, + ) + assertEquals(VaultUnlockResult.GenericError, result) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto awaiting should block calls to sync`() = runTest { + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationalKeys = emptyMap() + coEvery { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } just awaits + + val scope = CoroutineScope(Dispatchers.Unconfined) + scope.launch { + vaultRepository.unlockVault( + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationalKeys = organizationalKeys, + ) + } + // Does nothing because we are blocking + vaultRepository.sync() + scope.cancel() + + coVerify(exactly = 0) { syncService.sync() } + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitCryptoRequest( + kdfParams = kdf, + email = email, + password = masterPassword, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationalKeys, + ), + ) + } + } + @Test fun `clearUnlockedData should update the vaultDataStateFlow to Loading`() = runTest { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 5ff285ac85..e39bf7afc7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -103,7 +103,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val initialState = DEFAULT_STATE.copy(passwordInput = password) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } returns VaultUnlockResult.AuthenticationError viewModel.trySendAction(VaultUnlockAction.UnlockClick) @@ -116,7 +116,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) coVerify { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } } @@ -126,7 +126,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val initialState = DEFAULT_STATE.copy(passwordInput = password) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } returns VaultUnlockResult.GenericError viewModel.trySendAction(VaultUnlockAction.UnlockClick) @@ -139,7 +139,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) coVerify { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } } @@ -149,7 +149,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val initialState = DEFAULT_STATE.copy(passwordInput = password) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } returns VaultUnlockResult.InvalidStateError viewModel.trySendAction(VaultUnlockAction.UnlockClick) @@ -162,7 +162,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) coVerify { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } } @@ -172,7 +172,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { val initialState = DEFAULT_STATE.copy(passwordInput = password) val viewModel = createViewModel(state = initialState) coEvery { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } returns VaultUnlockResult.Success viewModel.trySendAction(VaultUnlockAction.UnlockClick) @@ -181,7 +181,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { viewModel.stateFlow.value, ) coVerify { - vaultRepository.unlockVaultAndSync(password) + vaultRepository.unlockVaultAndSyncForCurrentUser(password) } }