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 427480667..2863273a3 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 @@ -179,9 +179,10 @@ class AuthRepositoryImpl constructor( kdf = userStateJson.activeAccount.profile.toSdkParams(), userKey = loginResponse.key, privateKey = loginResponse.privateKey, - // TODO use actual organization keys BIT-1091 - organizationalKeys = emptyMap(), masterPassword = password, + // We can separately unlock the vault for organization data after + // receiving the sync response. + organizationKeys = null, ) authDiskSource.userState = userStateJson authDiskSource.storeUserKey( 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 4f09b390b..d436892e9 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 @@ -7,6 +7,7 @@ import com.bitwarden.core.Collection import com.bitwarden.core.CollectionView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Send import com.bitwarden.core.SendView @@ -19,11 +20,21 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResul interface VaultSdkSource { /** - * Attempts to initialize cryptography functionality for the Bitwarden SDK - * with a given [InitCryptoRequest]. + * Attempts to initialize cryptography functionality for an individual user for the + * Bitwarden SDK with a given [InitUserCryptoRequest]. */ suspend fun initializeCrypto(request: InitUserCryptoRequest): Result + /** + * Attempts to initialize cryptography functionality for organization data associated with + * the current user for the Bitwarden SDK with a given [InitOrgCryptoRequest]. + * + * This should only be called after a successful call to [initializeCrypto]. + */ + suspend fun initializeOrganizationCrypto( + request: InitOrgCryptoRequest, + ): Result + /** * Encrypts a [CipherView] returning a [Cipher] 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 6f2eead36..bb3b29e46 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 @@ -7,6 +7,7 @@ import com.bitwarden.core.Collection import com.bitwarden.core.CollectionView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Send import com.bitwarden.core.SendView @@ -32,7 +33,20 @@ class VaultSdkSourceImpl( clientCrypto.initializeUserCrypto(req = request) InitializeCryptoResult.Success } catch (exception: BitwardenException) { - // The only truly expected error from the SDK is an incorrect password. + // The only truly expected error from the SDK is an incorrect key/password. + InitializeCryptoResult.AuthenticationError + } + } + + override suspend fun initializeOrganizationCrypto( + request: InitOrgCryptoRequest, + ): Result = + runCatching { + try { + clientCrypto.initializeOrgCrypto(req = request) + InitializeCryptoResult.Success + } catch (exception: BitwardenException) { + // The only truly expected error from the SDK is for incorrect keys. InitializeCryptoResult.AuthenticationError } } 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 index 46b310e0c..197fe3d32 100644 --- 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 @@ -11,7 +11,7 @@ sealed class InitializeCryptoResult { data object Success : InitializeCryptoResult() /** - * Incorrect password provided. + * Incorrect password or key(s) 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 832693666..145d7e03f 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 @@ -71,6 +71,9 @@ interface VaultRepository { /** * Attempt to unlock the vault with the specified user information. + * + * Note that when [organizationKeys] is absent, no attempt will be made to unlock the vault + * for organization data. */ @Suppress("LongParameterList") suspend fun unlockVault( @@ -80,7 +83,7 @@ interface VaultRepository { kdf: Kdf, userKey: String, privateKey: String, - organizationalKeys: Map, + organizationKeys: 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 1f94ed1df..b6154846d 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 @@ -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.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Kdf @@ -12,11 +13,13 @@ import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionE import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.map +import com.x8bit.bitwarden.data.platform.util.asSuccess 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.CiphersService 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.datasource.sdk.model.InitializeCryptoResult import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.SendData import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult @@ -107,6 +110,7 @@ class VaultRepositoryImpl constructor( syncResponse = syncResponse, ) + unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse) storeKeys(syncResponse = syncResponse) decryptSyncResponseAndUpdateVaultDataState(syncResponse = syncResponse) decryptSendsAndUpdateSendDataState(sendList = syncResponse.sends) @@ -177,6 +181,8 @@ class VaultRepositoryImpl constructor( ?: return VaultUnlockResult.InvalidStateError val privateKey = authDiskSource.getPrivateKey(userId = userState.activeUserId) ?: return VaultUnlockResult.InvalidStateError + val organizationKeys = authDiskSource + .getOrganizationKeys(userId = userState.activeUserId) return unlockVault( userId = userState.activeUserId, masterPassword = masterPassword, @@ -184,8 +190,7 @@ class VaultRepositoryImpl constructor( kdf = userState.activeAccount.profile.toSdkParams(), userKey = userKey, privateKey = privateKey, - // TODO use actual organization keys BIT-1091 - organizationalKeys = emptyMap(), + organizationKeys = organizationKeys, ) .also { if (it is VaultUnlockResult.Success) { @@ -201,7 +206,7 @@ class VaultRepositoryImpl constructor( kdf: Kdf, userKey: String, privateKey: String, - organizationalKeys: Map, + organizationKeys: Map?, ): VaultUnlockResult = flow { willSyncAfterUnlock = true @@ -218,6 +223,20 @@ class VaultRepositoryImpl constructor( ), ), ) + .flatMap { result -> + // Initialize the SDK for organizations if necessary + if (organizationKeys != null && + result is InitializeCryptoResult.Success + ) { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = organizationKeys, + ), + ) + } else { + result.asSuccess() + } + } .fold( onFailure = { VaultUnlockResult.GenericError }, onSuccess = { initializeCryptoResult -> @@ -320,6 +339,27 @@ class VaultRepositoryImpl constructor( } } + private suspend fun unlockVaultForOrganizationsIfNecessary( + syncResponse: SyncResponseJson, + ) { + val profile = syncResponse.profile ?: return + val organizationKeys = profile.organizations + .orEmpty() + .filter { it.key != null } + .associate { it.id to requireNotNull(it.key) } + .takeUnless { it.isEmpty() } + ?: return + + // There shouldn't be issues when unlocking directly from the syncResponse so we can ignore + // the return type here. + vaultSdkSource + .initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = organizationKeys, + ), + ) + } + private suspend fun decryptSendsAndUpdateSendDataState(sendList: List?) { val newState = vaultSdkSource .decryptSendList( 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 0edbeee79..e1aff681f 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 @@ -429,7 +429,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) } returns VaultUnlockResult.Success @@ -465,7 +465,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) vaultRepository.sync() @@ -508,7 +508,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) } returns VaultUnlockResult.Success @@ -546,7 +546,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) vaultRepository.sync() @@ -946,7 +946,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) } returns VaultUnlockResult.Success @@ -1028,7 +1028,7 @@ class AuthRepositoryTest { kdf = ACCOUNT_1.profile.toSdkParams(), userKey = successResponse.key, privateKey = successResponse.privateKey, - organizationalKeys = emptyMap(), + organizationKeys = null, masterPassword = PASSWORD, ) } returns VaultUnlockResult.Success diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseProfileUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseProfileUtil.kt index 3c42c8915..6e2bef5a7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseProfileUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/network/model/SyncResponseProfileUtil.kt @@ -77,6 +77,13 @@ fun createMockOrganization(number: Int): SyncResponseJson.Profile.Organization = status = 1, ) +/** + * Create a mock set of organization keys with the given [number]. + */ +fun createMockOrganizationKeys(number: Int): Map = + createMockOrganization(number = number) + .let { mapOf(it.id to requireNotNull(it.key)) } + /** * Create a mock [SyncResponseJson.Profile.Permissions]. */ 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 4264a84d9..6f8ca9d03 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 @@ -7,6 +7,7 @@ import com.bitwarden.core.Collection import com.bitwarden.core.CollectionView import com.bitwarden.core.Folder import com.bitwarden.core.FolderView +import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Send import com.bitwarden.core.SendView @@ -32,7 +33,7 @@ class VaultSdkSourceTest { ) @Test - fun `initializeCrypto with sdk success should return InitializeCryptoResult Success`() = + fun `initializeUserCrypto with sdk success should return InitializeCryptoResult Success`() = runBlocking { val mockInitCryptoRequest = mockk() coEvery { @@ -101,6 +102,76 @@ class VaultSdkSourceTest { } } + @Test + fun `initializeOrgCrypto with sdk success should return InitializeCryptoResult Success`() = + runBlocking { + val mockInitCryptoRequest = mockk() + coEvery { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } returns Unit + val result = vaultSdkSource.initializeOrganizationCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + InitializeCryptoResult.Success.asSuccess(), + result, + ) + coVerify { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } + } + + @Test + fun `initializeOrgCrypto with sdk failure should return failure`() = runBlocking { + val mockInitCryptoRequest = mockk() + val expectedException = IllegalStateException("mock") + coEvery { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } throws expectedException + val result = vaultSdkSource.initializeOrganizationCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + expectedException.asFailure(), + result, + ) + coVerify { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } + } + + @Test + fun `initializeOrgCrypto with BitwardenException failure should return AuthenticationError`() = + runBlocking { + val mockInitCryptoRequest = mockk() + val expectedException = BitwardenException.E(message = "") + coEvery { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } throws expectedException + val result = vaultSdkSource.initializeOrganizationCrypto( + request = mockInitCryptoRequest, + ) + assertEquals( + InitializeCryptoResult.AuthenticationError.asSuccess(), + result, + ) + coVerify { + clientCrypto.initializeOrgCrypto( + req = mockInitCryptoRequest, + ) + } + } + @Test fun `decryptCipher 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 7e6e1e76e..e06879c28 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository import app.cash.turbine.test import com.bitwarden.core.CipherView import com.bitwarden.core.FolderView +import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoRequest import com.bitwarden.core.Kdf @@ -18,6 +19,7 @@ import com.x8bit.bitwarden.data.platform.util.asFailure import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipher import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockCipherJsonRequest +import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.SyncService @@ -65,12 +67,20 @@ class VaultRepositoryTest { dispatcherManager = dispatcherManager, ) + @Suppress("MaxLineLength") @Test - fun `sync with syncService Success should update AuthDiskSource and DataStateFlows`() = + fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and DataStateFlows`() = runTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -127,6 +137,13 @@ class VaultRepositoryTest { ), vaultRepository.sendDataStateFlow.value, ) + coVerify { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } } @Test @@ -135,6 +152,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -189,6 +213,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -241,6 +272,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns mockException.asFailure() @@ -267,6 +305,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -293,6 +338,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -369,6 +421,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -429,6 +488,13 @@ class VaultRepositoryTest { Result.success(createMockSyncResponse(number = 1)), UnknownHostException().asFailure(), ) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -529,6 +595,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -546,6 +619,10 @@ class VaultRepositoryTest { userId = "mockId-1", userKey = "mockKey-1", ) + fakeAuthDiskSource.storeOrganizationKeys( + userId = "mockId-1", + organizationKeys = createMockOrganizationKeys(number = 1), + ) fakeAuthDiskSource.userState = MOCK_USER_STATE coEvery { vaultSdkSource.initializeCrypto( @@ -590,6 +667,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -681,8 +765,9 @@ class VaultRepositoryTest { coVerify(exactly = 0) { syncService.sync() } } + @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault failure should return GenericError`() = + fun `unlockVaultAndSyncForCurrentUser with unlockVault failure for users should return GenericError`() = runTest { coEvery { syncService.sync() @@ -740,7 +825,77 @@ class VaultRepositoryTest { @Suppress("MaxLineLength") @Test - fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError should return AuthenticationError`() = + fun `unlockVaultAndSyncForCurrentUser with unlockVault failure for orgs 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 = "mockId-1", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockId-1", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.storeOrganizationKeys( + userId = "mockId-1", + organizationKeys = createMockOrganizationKeys(number = 1), + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), + email = "email", + privateKey = "mockPrivateKey-1", + method = InitUserCryptoMethod.Password( + password = "mockPassword-1", + userKey = "mockKey-1", + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns IllegalStateException().asFailure() + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + + val result = vaultRepository.unlockVaultAndSyncForCurrentUser( + masterPassword = "mockPassword-1", + ) + + assertEquals( + VaultUnlockResult.GenericError, + result, + ) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for users should return AuthenticationError`() = runTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) coEvery { @@ -791,6 +946,70 @@ class VaultRepositoryTest { ) } + @Suppress("MaxLineLength") + @Test + fun `unlockVaultAndSyncForCurrentUser with unlockVault AuthenticationError for orgs 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 = "mockId-1", + privateKey = "mockPrivateKey-1", + ) + fakeAuthDiskSource.storeUserKey( + userId = "mockId-1", + userKey = "mockKey-1", + ) + fakeAuthDiskSource.storeOrganizationKeys( + userId = "mockId-1", + organizationKeys = createMockOrganizationKeys(number = 1), + ) + fakeAuthDiskSource.userState = MOCK_USER_STATE + coEvery { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = Kdf.Pbkdf2(iterations = DEFAULT_PBKDF2_ITERATIONS.toUInt()), + email = "email", + privateKey = "mockPrivateKey-1", + method = InitUserCryptoMethod.Password( + password = "", + userKey = "mockKey-1", + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.AuthenticationError.asSuccess() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + + val result = vaultRepository.unlockVaultAndSyncForCurrentUser(masterPassword = "") + assertEquals( + VaultUnlockResult.AuthenticationError, + result, + ) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + } + @Suppress("MaxLineLength") @Test fun `unlockVaultAndSyncForCurrentUser with missing user state should return InvalidStateError `() = @@ -890,7 +1109,7 @@ class VaultRepositoryTest { val masterPassword = "drowssap" val userKey = "12345" val privateKey = "54321" - val organizationalKeys = emptyMap() + val organizationKeys = mapOf("orgId1" to "orgKey1") coEvery { vaultSdkSource.initializeCrypto( request = InitUserCryptoRequest( @@ -904,6 +1123,11 @@ class VaultRepositoryTest { ), ) } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns InitializeCryptoResult.Success.asSuccess() assertEquals( VaultState( unlockedVaultUserIds = emptySet(), @@ -918,7 +1142,7 @@ class VaultRepositoryTest { email = email, userKey = userKey, privateKey = privateKey, - organizationalKeys = organizationalKeys, + organizationKeys = organizationKeys, ) assertEquals(VaultUnlockResult.Success, result) @@ -941,11 +1165,16 @@ class VaultRepositoryTest { ), ) } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } } @Suppress("MaxLineLength") @Test - fun `unlockVault with initializeCrypto authentication failure should return AuthenticationError`() = + fun `unlockVault with initializeCrypto authentication failure for users should return AuthenticationError`() = runTest { val userId = "userId" val kdf = MOCK_PROFILE.toSdkParams() @@ -953,7 +1182,7 @@ class VaultRepositoryTest { val masterPassword = "drowssap" val userKey = "12345" val privateKey = "54321" - val organizationalKeys = emptyMap() + val organizationKeys = mapOf("orgId1" to "orgKey1") coEvery { vaultSdkSource.initializeCrypto( request = InitUserCryptoRequest( @@ -967,6 +1196,7 @@ class VaultRepositoryTest { ), ) } returns InitializeCryptoResult.AuthenticationError.asSuccess() + assertEquals( VaultState( unlockedVaultUserIds = emptySet(), @@ -981,7 +1211,7 @@ class VaultRepositoryTest { email = email, userKey = userKey, privateKey = privateKey, - organizationalKeys = organizationalKeys, + organizationKeys = organizationKeys, ) assertEquals(VaultUnlockResult.AuthenticationError, result) @@ -1006,66 +1236,213 @@ class VaultRepositoryTest { } } + @Suppress("MaxLineLength") @Test - fun `unlockVault with initializeCrypto failure should return GenericError`() = runTest { - val userId = "userId" - 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 = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, + fun `unlockVault with initializeCrypto authentication failure for orgs should return AuthenticationError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns InitializeCryptoResult.AuthenticationError.asSuccess() + + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), ), + vaultRepository.vaultStateFlow.value, ) - } returns Throwable("Fail").asFailure() - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - val result = vaultRepository.unlockVault( - userId = userId, - masterPassword = masterPassword, - kdf = kdf, - email = email, - userKey = userKey, - privateKey = privateKey, - organizationalKeys = organizationalKeys, - ) + val result = vaultRepository.unlockVault( + userId = userId, + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationKeys, + ) - assertEquals(VaultUnlockResult.GenericError, result) - assertEquals( - VaultState( - unlockedVaultUserIds = emptySet(), - ), - vaultRepository.vaultStateFlow.value, - ) - coVerify(exactly = 1) { - vaultSdkSource.initializeCrypto( - request = InitUserCryptoRequest( - kdfParams = kdf, - email = email, - privateKey = privateKey, - method = InitUserCryptoMethod.Password( - password = masterPassword, - userKey = userKey, + assertEquals(VaultUnlockResult.AuthenticationError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), ), - ), - ) + ) + } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto failure for users should return GenericError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns Throwable("Fail").asFailure() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + + val result = vaultRepository.unlockVault( + userId = userId, + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.GenericError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + } + + @Test + fun `unlockVault with initializeCrypto failure for orgs should return GenericError`() = + runTest { + val userId = "userId" + val kdf = MOCK_PROFILE.toSdkParams() + val email = MOCK_PROFILE.email + val masterPassword = "drowssap" + val userKey = "12345" + val privateKey = "54321" + val organizationKeys = mapOf("orgId1" to "orgKey1") + coEvery { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } returns Throwable("Fail").asFailure() + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + + val result = vaultRepository.unlockVault( + userId = userId, + masterPassword = masterPassword, + kdf = kdf, + email = email, + userKey = userKey, + privateKey = privateKey, + organizationKeys = organizationKeys, + ) + + assertEquals(VaultUnlockResult.GenericError, result) + assertEquals( + VaultState( + unlockedVaultUserIds = emptySet(), + ), + vaultRepository.vaultStateFlow.value, + ) + coVerify(exactly = 1) { + vaultSdkSource.initializeCrypto( + request = InitUserCryptoRequest( + kdfParams = kdf, + email = email, + privateKey = privateKey, + method = InitUserCryptoMethod.Password( + password = masterPassword, + userKey = userKey, + ), + ), + ) + } + coVerify(exactly = 1) { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest(organizationKeys = organizationKeys), + ) + } } - } @Test fun `unlockVault with initializeCrypto awaiting should block calls to sync`() = runTest { @@ -1075,7 +1452,7 @@ class VaultRepositoryTest { val masterPassword = "drowssap" val userKey = "12345" val privateKey = "54321" - val organizationalKeys = emptyMap() + val organizationKeys = null coEvery { vaultSdkSource.initializeCrypto( request = InitUserCryptoRequest( @@ -1099,7 +1476,7 @@ class VaultRepositoryTest { email = email, userKey = userKey, privateKey = privateKey, - organizationalKeys = organizationalKeys, + organizationKeys = organizationKeys, ) } // Does nothing because we are blocking @@ -1128,6 +1505,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -1170,6 +1554,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(number = 1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(number = 1)).asSuccess() @@ -1213,6 +1604,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(itemId)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(itemId), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(itemId))) } returns listOf(item).asSuccess() @@ -1287,6 +1685,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(1)).asSuccess() @@ -1319,6 +1724,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(folderId)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(folderId), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(folderId))) } returns listOf(createMockCipherView(folderId)).asSuccess() @@ -1393,6 +1805,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(1)).asSuccess() @@ -1471,6 +1890,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(1)).asSuccess() @@ -1548,6 +1974,13 @@ class VaultRepositoryTest { coEvery { syncService.sync() } returns Result.success(createMockSyncResponse(1)) + coEvery { + vaultSdkSource.initializeOrganizationCrypto( + request = InitOrgCryptoRequest( + organizationKeys = createMockOrganizationKeys(1), + ), + ) + } returns InitializeCryptoResult.Success.asSuccess() coEvery { vaultSdkSource.decryptCipherList(listOf(createMockSdkCipher(1))) } returns listOf(createMockCipherView(1)).asSuccess() @@ -1577,7 +2010,7 @@ class VaultRepositoryTest { val masterPassword = "drowssap" val userKey = "12345" val privateKey = "54321" - val organizationalKeys = emptyMap() + val organizationKeys = null coEvery { vaultSdkSource.initializeCrypto( request = InitUserCryptoRequest( @@ -1599,7 +2032,7 @@ class VaultRepositoryTest { email = email, userKey = userKey, privateKey = privateKey, - organizationalKeys = organizationalKeys, + organizationKeys = organizationKeys, ) assertEquals(VaultUnlockResult.Success, result)