Add VaultLockManager (#550)

This commit is contained in:
Brian Yencho 2024-01-09 12:05:06 -06:00 committed by Álison Fernandes
parent 8c2e2f8af6
commit 8ff3207f7a
8 changed files with 1143 additions and 1000 deletions

View file

@ -0,0 +1,58 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.StateFlow
/**
* Manages the locking and unlocking of user vaults.
*/
interface VaultLockManager {
/**
* Flow that represents the current vault state.
*/
val vaultStateFlow: StateFlow<VaultState>
/**
* Whether or not the vault is currently locked for the given [userId].
*/
fun isVaultUnlocked(userId: String): Boolean
/**
* Whether or not the vault is currently unlocking for the given [userId].
*/
fun isVaultUnlocking(userId: String): Boolean
/**
* Locks the vault for the user with the given [userId].
*/
fun lockVault(userId: String)
/**
* Locks the vault for the user with the given [userId] only if necessary.
*/
fun lockVaultIfNecessary(userId: String)
/**
* Locks the vault for the current user if currently unlocked.
*/
fun lockVaultForCurrentUser()
/**
* 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(
userId: String,
email: String,
kdf: Kdf,
privateKey: String,
initUserCryptoMethod: InitUserCryptoMethod,
organizationKeys: Map<String, String>?,
): VaultUnlockResult
}

View file

@ -0,0 +1,149 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
import com.bitwarden.core.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
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.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.update
/**
* Primary implementation [VaultLockManager].
*/
class VaultLockManagerImpl(
private val authDiskSource: AuthDiskSource,
private val vaultSdkSource: VaultSdkSource,
) : VaultLockManager {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
private val mutableVaultStateStateFlow =
MutableStateFlow(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
)
override val vaultStateFlow: StateFlow<VaultState>
get() = mutableVaultStateStateFlow.asStateFlow()
override fun isVaultUnlocked(userId: String): Boolean =
userId in mutableVaultStateStateFlow.value.unlockedVaultUserIds
override fun isVaultUnlocking(userId: String): Boolean =
userId in mutableVaultStateStateFlow.value.unlockingVaultUserIds
override fun lockVault(userId: String) {
setVaultToLocked(userId = userId)
}
override fun lockVaultForCurrentUser() {
activeUserId?.let {
lockVaultIfNecessary(it)
}
}
override fun lockVaultIfNecessary(userId: String) {
// TODO: Check for VaultTimeout.Never (BIT-1019)
lockVault(userId = userId)
}
override suspend fun unlockVault(
userId: String,
email: String,
kdf: Kdf,
privateKey: String,
initUserCryptoMethod: InitUserCryptoMethod,
organizationKeys: Map<String, String>?,
): VaultUnlockResult =
flow {
setVaultToUnlocking(userId = userId)
emit(
vaultSdkSource
.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = initUserCryptoMethod,
),
)
.flatMap { result ->
// Initialize the SDK for organizations if necessary
if (organizationKeys != null &&
result is InitializeCryptoResult.Success
) {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = organizationKeys,
),
)
} else {
result.asSuccess()
}
}
.fold(
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { initializeCryptoResult ->
initializeCryptoResult
.toVaultUnlockResult()
.also {
if (it is VaultUnlockResult.Success) {
setVaultToUnlocked(userId = userId)
}
}
},
),
)
}
.onCompletion { setVaultToNotUnlocking(userId = userId) }
.first()
private fun setVaultToUnlocked(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockedVaultUserIds = it.unlockedVaultUserIds + userId,
)
}
}
private fun setVaultToLocked(userId: String) {
vaultSdkSource.clearCrypto(userId = userId)
mutableVaultStateStateFlow.update {
it.copy(
unlockedVaultUserIds = it.unlockedVaultUserIds - userId,
)
}
}
private fun setVaultToUnlocking(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockingVaultUserIds = it.unlockingVaultUserIds + userId,
)
}
}
private fun setVaultToNotUnlocking(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockingVaultUserIds = it.unlockingVaultUserIds - userId,
)
}
}
}

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.data.vault.manager.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManagerImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides managers in the vault package.
*/
@Module
@InstallIn(SingletonComponent::class)
object VaultManagerModule {
@Provides
@Singleton
fun provideVaultLockManager(
authDiskSource: AuthDiskSource,
vaultSdkSource: VaultSdkSource,
): VaultLockManager =
VaultLockManagerImpl(
authDiskSource = authDiskSource,
vaultSdkSource = vaultSdkSource,
)
}

View file

@ -6,14 +6,14 @@ import com.bitwarden.core.FolderView
import com.bitwarden.core.Kdf
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.StateFlow
* Responsible for managing vault data inside the network layer.
*/
@Suppress("TooManyFunctions")
interface VaultRepository {
interface VaultRepository : VaultLockManager {
/**
* Flow that represents the current vault data.
@ -56,11 +56,6 @@ interface VaultRepository {
*/
val foldersStateFlow: StateFlow<DataState<List<FolderView>>>
/**
* Flow that represents the current vault state.
*/
val vaultStateFlow: StateFlow<VaultState>
/**
* Flow that represents the current send data.
*/
@ -98,16 +93,6 @@ interface VaultRepository {
*/
fun getVaultFolderStateFlow(folderId: String): StateFlow<DataState<FolderView?>>
/**
* Locks the vault for the current user if currently unlocked.
*/
fun lockVaultForCurrentUser()
/**
* Locks the vault for the user with the given [userId] if necessary.
*/
fun lockVaultIfNecessary(userId: String)
/**
* Emits the totp code result flow to listeners.
*/

View file

@ -5,7 +5,6 @@ import com.bitwarden.core.CollectionView
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
import com.bitwarden.core.SendView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
@ -19,7 +18,6 @@ import com.x8bit.bitwarden.data.platform.repository.util.combineDataStates
import com.x8bit.bitwarden.data.platform.repository.util.map
import com.x8bit.bitwarden.data.platform.repository.util.observeWhenSubscribedAndLoggedIn
import com.x8bit.bitwarden.data.platform.repository.util.updateToPendingOrLoading
import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.platform.util.flatMap
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
@ -29,15 +27,14 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
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.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
@ -46,7 +43,6 @@ import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCollectionLi
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkFolderList
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSend
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkSendList
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@ -56,11 +52,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
@ -84,8 +77,10 @@ class VaultRepositoryImpl(
private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource,
private val vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager,
) : VaultRepository {
) : VaultRepository,
VaultLockManager by vaultLockManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
private val ioScope = CoroutineScope(dispatcherManager.io)
@ -96,14 +91,6 @@ class VaultRepositoryImpl(
private val mutableTotpCodeResultFlow = bufferedMutableSharedFlow<TotpCodeResult>()
private val mutableVaultStateStateFlow =
MutableStateFlow(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
)
private val mutableSendDataStateFlow = MutableStateFlow<DataState<SendData>>(DataState.Loading)
private val mutableCiphersStateFlow =
@ -151,9 +138,6 @@ class VaultRepositoryImpl(
override val collectionsStateFlow: StateFlow<DataState<List<CollectionView>>>
get() = mutableCollectionsStateFlow.asStateFlow()
override val vaultStateFlow: StateFlow<VaultState>
get() = mutableVaultStateStateFlow.asStateFlow()
override val sendDataStateFlow: StateFlow<DataState<SendData>>
get() = mutableSendDataStateFlow.asStateFlow()
@ -276,16 +260,6 @@ class VaultRepositoryImpl(
initialValue = DataState.Loading,
)
override fun lockVaultForCurrentUser() {
authDiskSource.userState?.activeUserId?.let {
lockVaultIfNecessary(it)
}
}
override fun lockVaultIfNecessary(userId: String) {
setVaultToLocked(userId = userId)
}
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
}
@ -320,7 +294,7 @@ class VaultRepositoryImpl(
privateKey: String,
organizationKeys: Map<String, String>?,
): VaultUnlockResult =
unlockVaultInternal(
unlockVault(
userId = userId,
email = email,
kdf = kdf,
@ -446,46 +420,6 @@ class VaultRepositoryImpl(
)
}
// TODO: This is temporary. Eventually this needs to be based on the presence of various
// user keys but this will likely require SDK updates to support this (BIT-1190).
private fun setVaultToUnlocked(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockedVaultUserIds = it.unlockedVaultUserIds + userId,
)
}
}
// TODO: This is temporary. Eventually this needs to be based on the presence of various
// user keys but this will likely require SDK updates to support this (BIT-1190).
private fun setVaultToLocked(userId: String) {
vaultSdkSource.clearCrypto(userId = userId)
mutableVaultStateStateFlow.update {
it.copy(
unlockedVaultUserIds = it.unlockedVaultUserIds - userId,
)
}
}
private fun setVaultToUnlocking(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockingVaultUserIds = it.unlockingVaultUserIds + userId,
)
}
}
private fun setVaultToNotUnlocking(userId: String) {
mutableVaultStateStateFlow.update {
it.copy(
unlockingVaultUserIds = it.unlockingVaultUserIds - userId,
)
}
}
private fun isVaultUnlocking(userId: String) =
userId in mutableVaultStateStateFlow.value.unlockingVaultUserIds
private fun storeProfileData(
syncResponse: SyncResponseJson,
) {
@ -527,7 +461,7 @@ class VaultRepositoryImpl(
?: return VaultUnlockResult.InvalidStateError
val organizationKeys = authDiskSource
.getOrganizationKeys(userId = userId)
return unlockVaultInternal(
return unlockVault(
userId = userId,
email = account.profile.email,
kdf = account.profile.toSdkParams(),
@ -537,59 +471,6 @@ class VaultRepositoryImpl(
)
}
private suspend fun unlockVaultInternal(
userId: String,
email: String,
kdf: Kdf,
privateKey: String,
initUserCryptoMethod: InitUserCryptoMethod,
organizationKeys: Map<String, String>?,
): VaultUnlockResult =
flow {
setVaultToUnlocking(userId = userId)
emit(
vaultSdkSource
.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = initUserCryptoMethod,
),
)
.flatMap { result ->
// Initialize the SDK for organizations if necessary
if (organizationKeys != null &&
result is InitializeCryptoResult.Success
) {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = organizationKeys,
),
)
} else {
result.asSuccess()
}
}
.fold(
onFailure = { VaultUnlockResult.GenericError },
onSuccess = { initializeCryptoResult ->
initializeCryptoResult
.toVaultUnlockResult()
.also {
if (it is VaultUnlockResult.Success) {
setVaultToUnlocked(userId = userId)
}
}
},
),
)
}
.onCompletion { setVaultToNotUnlocking(userId = userId) }
.first()
private suspend fun unlockVaultForOrganizationsIfNecessary(
syncResponse: SyncResponseJson,
) {

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
import com.x8bit.bitwarden.data.vault.datasource.network.service.SendsService
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.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepositoryImpl
import dagger.Module
@ -31,6 +32,7 @@ object VaultRepositoryModule {
vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource,
vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager,
): VaultRepository = VaultRepositoryImpl(
syncService = syncService,
@ -39,6 +41,7 @@ object VaultRepositoryModule {
vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource,
vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager,
)
}

View file

@ -0,0 +1,631 @@
package com.x8bit.bitwarden.data.vault.manager
import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod
import com.bitwarden.core.InitUserCryptoRequest
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.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
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.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import io.mockk.awaits
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.async
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class VaultLockManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val vaultSdkSource: VaultSdkSource = mockk {
every { clearCrypto(userId = any()) } just runs
}
private val vaultLockManager: VaultLockManager = VaultLockManagerImpl(
authDiskSource = fakeAuthDiskSource,
vaultSdkSource = vaultSdkSource,
)
@Test
fun `isVaultUnlocked should return the correct value based on the vault lock state`() =
runTest {
val userId = "userId"
assertFalse(vaultLockManager.isVaultUnlocked(userId = userId))
verifyUnlockedVault(userId = userId)
assertTrue(vaultLockManager.isVaultUnlocked(userId = userId))
}
@Test
fun `isVaultLocking should return the correct value based on the vault unlocking state`() =
runTest {
val userId = "userId"
assertFalse(vaultLockManager.isVaultUnlocking(userId = userId))
val unlockingJob = async {
verifyUnlockingVault(userId = userId)
}
this.testScheduler.advanceUntilIdle()
assertTrue(vaultLockManager.isVaultUnlocking(userId = userId))
unlockingJob.cancel()
this.testScheduler.advanceUntilIdle()
assertFalse(vaultLockManager.isVaultUnlocking(userId = userId))
}
@Test
fun `lockVaultIfNecessary should lock the given account if it is currently unlocked`() =
runTest {
val userId = "userId"
verifyUnlockedVault(userId = userId)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf(userId),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
vaultLockManager.lockVaultIfNecessary(userId = userId)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
verify { vaultSdkSource.clearCrypto(userId = userId) }
}
@Suppress("MaxLineLength")
@Test
fun `lockVaultForCurrentUser should lock the vault for the current user if it is currently unlocked`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
verifyUnlockedVault(userId = userId)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf(userId),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
vaultLockManager.lockVaultForCurrentUser()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
verify { vaultSdkSource.clearCrypto(userId = userId) }
}
@Test
fun `unlockVault with initializeCrypto success should return Success`() = 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(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
} returns InitializeCryptoResult.Success.asSuccess()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
privateKey = privateKey,
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.Success, result)
assertEquals(
VaultState(
unlockedVaultUserIds = setOf(userId),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
}
coVerify(exactly = 1) {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `unlockVault with initializeCrypto authentication failure for users 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(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
privateKey = privateKey,
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.AuthenticationError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
}
}
@Suppress("MaxLineLength")
@Test
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(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
} returns InitializeCryptoResult.AuthenticationError.asSuccess()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
privateKey = privateKey,
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.AuthenticationError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
}
coVerify(exactly = 1) {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
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(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns Throwable("Fail").asFailure()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
privateKey = privateKey,
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.GenericError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
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(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
} returns Throwable("Fail").asFailure()
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
privateKey = privateKey,
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.GenericError, result)
assertEquals(
VaultState(
unlockedVaultUserIds = emptySet(),
unlockingVaultUserIds = emptySet(),
),
vaultLockManager.vaultStateFlow.value,
)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
}
coVerify(exactly = 1) {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
)
}
}
/**
* Helper to ensures that the vault for the user with the given [userId] is actively unlocking.
* Note that this call will actively hang.
*/
private suspend fun verifyUnlockingVault(userId: String) {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationKeys = null
coEvery {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} just awaits
vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
organizationKeys = organizationKeys,
)
}
/**
* Helper to ensures that the vault for the user with the given [userId] is unlocked.
*/
private suspend fun verifyUnlockedVault(userId: String) {
val kdf = MOCK_PROFILE.toSdkParams()
val email = MOCK_PROFILE.email
val masterPassword = "drowssap"
val userKey = "12345"
val privateKey = "54321"
val organizationKeys = null
coEvery {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
val result = vaultLockManager.unlockVault(
userId = userId,
kdf = kdf,
email = email,
privateKey = privateKey,
initUserCryptoMethod = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
organizationKeys = organizationKeys,
)
assertEquals(VaultUnlockResult.Success, result)
coVerify(exactly = 1) {
vaultSdkSource.initializeCrypto(
userId = userId,
request = InitUserCryptoRequest(
kdfParams = kdf,
email = email,
privateKey = privateKey,
method = InitUserCryptoMethod.Password(
password = masterPassword,
userKey = userKey,
),
),
)
}
}
}
private val MOCK_PROFILE = AccountJson.Profile(
userId = "mockId-1",
email = "email",
isEmailVerified = true,
name = null,
stamp = null,
organizationId = null,
avatarColorHex = null,
hasPremium = false,
forcePasswordResetReason = null,
kdfType = null,
kdfIterations = null,
kdfMemory = null,
kdfParallelism = null,
userDecryptionOptions = null,
)
private val MOCK_ACCOUNT = AccountJson(
profile = MOCK_PROFILE,
tokens = AccountJson.Tokens(
accessToken = "accessToken",
refreshToken = "refreshToken",
),
settings = AccountJson.Settings(
environmentUrlData = null,
),
)
private val MOCK_USER_STATE = UserStateJson(
activeUserId = "mockId-1",
accounts = mapOf(
"mockId-1" to MOCK_ACCOUNT,
),
)