mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
🍒 PM-23666: Construct unique SDK client for Authentocator Sync feature (#5528)
This commit is contained in:
parent
37af6a1773
commit
a721744a6b
@ -1,17 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.core.data.util.flatMap
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
|
||||
|
||||
/**
|
||||
* Default implementation of [AuthenticatorBridgeRepository].
|
||||
@ -19,9 +26,8 @@ import kotlinx.coroutines.flow.first
|
||||
class AuthenticatorBridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
) : AuthenticatorBridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
@ -45,52 +51,41 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
val allAccounts = authDiskSource.userState?.accounts.orEmpty()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
.mapNotNull { (userId, account) ->
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
// the user has not enabled authenticator sync and we skip the account.
|
||||
val decryptedUserKey = authDiskSource.getAuthenticatorSyncUnlockKey(userId)
|
||||
?: return@mapNotNull null
|
||||
|
||||
// Wait for any unlocking actions to finish:
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
// Unlock vault if necessary:
|
||||
val isVaultAlreadyUnlocked = vaultRepository.isVaultUnlocked(userId = userId)
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
val unlockResult = vaultRepository
|
||||
.unlockVaultWithDecryptedUserKey(
|
||||
val vaultUnlockResult = unlockClient(
|
||||
userId = userId,
|
||||
account = account,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
when (vaultUnlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
// happen we omit the account from list of shared accounts
|
||||
// and remove that user's authenticator sync unlock key.
|
||||
// This gives the user a way to potentially re-enable syncing
|
||||
// (going to Account Security and re-enabling the toggle)
|
||||
authDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = userId,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
// Destroy our stand-alone instance of the vault.
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
return@mapNotNull null
|
||||
}
|
||||
// Proceed
|
||||
VaultUnlockResult.Success -> Unit
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
@ -99,7 +94,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
// Filter out any deleted ciphers.
|
||||
.filter { it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
val decryptedCipher = vaultSdkSource
|
||||
val decryptedCipher = scopedVaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
@ -113,19 +108,18 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(
|
||||
userId = userId,
|
||||
isUserInitiated = false,
|
||||
)
|
||||
}
|
||||
// Lock and destroy our stand-alone instance of the vault:
|
||||
scopedVaultSdkSource.clearCrypto(userId = userId)
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
userId = userId,
|
||||
name = account.profile.name,
|
||||
email = account.profile.email,
|
||||
environmentLabel = account
|
||||
.settings
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
@ -133,4 +127,44 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
SharedAccountData(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockClient(
|
||||
userId: String,
|
||||
account: AccountJson,
|
||||
decryptedUserKey: String,
|
||||
): VaultUnlockResult {
|
||||
val privateKey = authDiskSource
|
||||
.getPrivateKey(userId = userId)
|
||||
?: return VaultUnlockResult.InvalidStateError(MissingPropertyException("Private key"))
|
||||
return scopedVaultSdkSource
|
||||
.initializeCrypto(
|
||||
userId = userId,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = userId,
|
||||
kdfParams = account.profile.toSdkParams(),
|
||||
email = account.profile.email,
|
||||
privateKey = privateKey,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
.flatMap { result ->
|
||||
// Initialize the SDK for organizations if necessary
|
||||
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
|
||||
if (organizationKeys != null && result is InitializeCryptoResult.Success) {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = userId,
|
||||
request = InitOrgCryptoRequest(organizationKeys = organizationKeys),
|
||||
)
|
||||
} else {
|
||||
result.asSuccess()
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { VaultUnlockResult.GenericError(error = it) },
|
||||
onSuccess = { it.toVaultUnlockResult() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,8 +21,8 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -41,15 +41,13 @@ object PlatformRepositoryModule {
|
||||
fun providesAuthenticatorBridgeRepository(
|
||||
authRepository: AuthRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
scopedVaultSdkSource: ScopedVaultSdkSource,
|
||||
): AuthenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
/**
|
||||
* This is a non-singleton instance of the [VaultSdkSource] that is intentionally separate; this
|
||||
* allows you to temporarily unlock vaults for a given user within its own scope without affecting
|
||||
* the foreground behavior of the app.
|
||||
*
|
||||
* Users of this class must always call [ScopedVaultSdkSource.clearCrypto] when they are done using
|
||||
* the unlocked vault in order to ensure that this instance of the vault is re-locked.
|
||||
*/
|
||||
interface ScopedVaultSdkSource : VaultSdkSource
|
||||
@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
|
||||
/**
|
||||
* The default instance of the [ScopedVaultSdkSource]. This uses its own instance of the
|
||||
* [SdkClientManagerImpl] to keep it separate from the rest of the app.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
vaultSdkSource: VaultSdkSource = VaultSdkSourceImpl(
|
||||
sdkClientManager = SdkClientManagerImpl(
|
||||
// We do not want to have the real NativeLibraryManager used here to avoid
|
||||
// initializing the library twice.
|
||||
nativeLibraryManager = object : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> = Unit.asSuccess()
|
||||
},
|
||||
sdkRepoFactory = sdkRepositoryFactory,
|
||||
featureFlagManager = featureFlagManager,
|
||||
),
|
||||
dispatcherManager = dispatcherManager,
|
||||
),
|
||||
) : ScopedVaultSdkSource, VaultSdkSource by vaultSdkSource
|
||||
@ -3,7 +3,11 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSourceImpl
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.Fido2CredentialStoreImpl
|
||||
@ -32,6 +36,18 @@ object VaultSdkModule {
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
fun providesScopedVaultSdkSource(
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
sdkRepositoryFactory: SdkRepositoryFactory,
|
||||
): ScopedVaultSdkSource =
|
||||
ScopedVaultSdkSourceImpl(
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
sdkRepositoryFactory = sdkRepositoryFactory,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFido2CredentialStore(
|
||||
|
||||
@ -3,29 +3,38 @@ package com.x8bit.bitwarden.data.platform.repository
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.bitwarden.authenticatorbridge.util.generateSecretKey
|
||||
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.InitUserCryptoRequest
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.crypto.Kdf
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.data.repository.util.toEnvironmentUrlsOrDefault
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
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.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.ScopedVaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.InitializeCryptoResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
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 io.mockk.verify
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@ -39,18 +48,17 @@ import java.time.ZonedDateTime
|
||||
class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
private val authRepository = mockk<AuthRepository>()
|
||||
private val vaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val scopedVaultSdkSource = mockk<ScopedVaultSdkSource>()
|
||||
private val vaultDiskSource = mockk<VaultDiskSource>()
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
|
||||
private val authenticatorBridgeRepository = AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
)
|
||||
private val authenticatorBridgeRepository: AuthenticatorBridgeRepository =
|
||||
AuthenticatorBridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
scopedVaultSdkSource = scopedVaultSdkSource,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
@ -66,6 +74,7 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
// Setup authRepository to return default USER_STATE:
|
||||
every { authRepository.userStateFlow } returns MutableStateFlow(USER_STATE)
|
||||
fakeAuthDiskSource.userState = USER_STATE_JSON
|
||||
|
||||
// Setup authDiskSource to have each user's authenticator sync unlock key:
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
@ -76,30 +85,69 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
userId = USER_2_ID,
|
||||
authenticatorSyncUnlockKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
// Setup vaultRepository to not be stuck unlocking:
|
||||
every { vaultRepository.vaultUnlockDataStateFlow } returns MutableStateFlow(
|
||||
listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
),
|
||||
)
|
||||
// Setup vaultRepository to be unlocked for user 1:
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns true
|
||||
// But locked for user 2:
|
||||
every { vaultRepository.isVaultUnlocked(USER_2_ID) } returns false
|
||||
every { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) } returns Unit
|
||||
fakeAuthDiskSource.storePrivateKey(userId = USER_1_ID, privateKey = USER_1_PRIVATE_KEY)
|
||||
fakeAuthDiskSource.storePrivateKey(userId = USER_2_ID, privateKey = USER_2_PRIVATE_KEY)
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
fakeAuthDiskSource.storeOrganizationKeys(
|
||||
userId = USER_1_ID,
|
||||
organizationKeys = USER_1_ORG_KEYS,
|
||||
)
|
||||
fakeAuthDiskSource.storeOrganizationKeys(
|
||||
userId = USER_2_ID,
|
||||
organizationKeys = USER_2_ORG_KEYS,
|
||||
)
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_1_ORG_KEYS),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
coEvery {
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
} returns InitializeCryptoResult.Success.asSuccess()
|
||||
every { scopedVaultSdkSource.clearCrypto(userId = USER_1_ID) } just runs
|
||||
every { scopedVaultSdkSource.clearCrypto(userId = USER_2_ID) } just runs
|
||||
|
||||
// Add some ciphers to vaultDiskSource for each user,
|
||||
// and setup mock decryption for them:
|
||||
coEvery { vaultDiskSource.getTotpCiphers(USER_1_ID) } returns USER_1_CIPHERS
|
||||
coEvery { vaultDiskSource.getTotpCiphers(USER_2_ID) } returns USER_2_CIPHERS
|
||||
mockkStatic(SyncResponseJson.Cipher::toEncryptedSdkCipher)
|
||||
mockkStatic(
|
||||
SyncResponseJson.Cipher::toEncryptedSdkCipher,
|
||||
EnvironmentUrlDataJson::toEnvironmentUrlsOrDefault,
|
||||
)
|
||||
every {
|
||||
USER_1_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_1_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
@ -107,10 +155,10 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
USER_2_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_2_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
scopedVaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
} returns USER_1_DECRYPTED_TOTP_CIPHER.asSuccess()
|
||||
coEvery {
|
||||
vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
scopedVaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER)
|
||||
} returns USER_2_DECRYPTED_TOTP_CIPHER.asSuccess()
|
||||
mockkStatic(String::sanitizeTotpUri)
|
||||
every {
|
||||
@ -120,50 +168,26 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
confirmVerified(authRepository, vaultSdkSource, vaultRepository, vaultDiskSource)
|
||||
unmockkStatic(SyncResponseJson.Cipher::toEncryptedSdkCipher)
|
||||
unmockkStatic(String::sanitizeTotpUri)
|
||||
confirmVerified(authRepository, scopedVaultSdkSource, vaultDiskSource)
|
||||
unmockkStatic(
|
||||
SyncResponseJson.Cipher::toEncryptedSdkCipher,
|
||||
EnvironmentUrlDataJson::toEnvironmentUrlsOrDefault,
|
||||
String::sanitizeTotpUri,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts with user 1 vault unlocked and all data present should send expected shared accounts data`() =
|
||||
runTest {
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
)
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncAccounts when userStateFlow is null should return an empty list`() = runTest {
|
||||
every { authRepository.userStateFlow } returns MutableStateFlow(null)
|
||||
fun `getSharedAccounts when userStateFlow is null should return an empty list`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val sharedData = authenticatorBridgeRepository.getSharedAccounts()
|
||||
|
||||
assertTrue(sharedData.accounts.isEmpty())
|
||||
verify { authRepository.userStateFlow }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when there is no authenticator sync unlock key for user 1 should omit user 1 from list`() =
|
||||
fun `getSharedAccounts when there is no authenticator sync unlock key for user 1 should omit user 1 from list`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = USER_1_ID,
|
||||
@ -175,138 +199,162 @@ class AuthenticatorBridgeRepositoryTest {
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when vault is locked for both users should unlock and re-lock vault for both users and filter out deleted ciphers`() =
|
||||
fun `getSharedAccounts should unlock and re-lock vault for both users and filter out deleted ciphers`() =
|
||||
runTest {
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns false
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(USER_1_ID, USER_1_UNLOCK_KEY)
|
||||
} returns VaultUnlockResult.Success
|
||||
every { vaultRepository.lockVault(USER_1_ID, isUserInitiated = false) } returns Unit
|
||||
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_1_ID, isUserInitiated = false) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_1_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_1_ID,
|
||||
cipher = USER_1_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when for user 1 vault is locked and unlock fails should reset authenticator sync unlock key and omit user from the list`() =
|
||||
fun `getSharedAccounts when for user 1 vault fails to unlock should reset authenticator sync unlock key and omit user from the list`() =
|
||||
runTest {
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns false
|
||||
val error = Throwable("Fail")
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(USER_1_ID, USER_1_UNLOCK_KEY)
|
||||
} returns VaultUnlockResult.InvalidStateError(error = error)
|
||||
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)), sharedAccounts)
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_1_ID))
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when when the vault repository never leaves unlocking state should never callback`() =
|
||||
runTest {
|
||||
val vaultUnlockStateFlow = MutableStateFlow(
|
||||
listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKING),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
),
|
||||
)
|
||||
every { vaultRepository.vaultUnlockDataStateFlow } returns vaultUnlockStateFlow
|
||||
val deferred = async {
|
||||
val sharedAccounts = authenticatorBridgeRepository.getSharedAccounts()
|
||||
assertEquals(BOTH_ACCOUNT_SUCCESS, sharedAccounts)
|
||||
}
|
||||
|
||||
// None of these calls should happen until after user 1's vault state is not UNLOCKING:
|
||||
verify(exactly = 0) { vaultRepository.isVaultUnlocked(userId = USER_1_ID) }
|
||||
coVerify(exactly = 0) { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
|
||||
// Then move out of UNLOCKING state, and things should proceed as normal:
|
||||
vaultUnlockStateFlow.value = listOf(
|
||||
VaultUnlockData(USER_1_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
VaultUnlockData(USER_2_ID, VaultUnlockData.Status.UNLOCKED),
|
||||
} returns InitializeCryptoResult.AuthenticationError(error = Throwable()).asSuccess()
|
||||
assertEquals(
|
||||
SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)),
|
||||
authenticatorBridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
|
||||
deferred.await()
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_1_ID) }
|
||||
coVerify { vaultDiskSource.getTotpCiphers(USER_2_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
assertNull(fakeAuthDiskSource.getAuthenticatorSyncUnlockKey(USER_1_ID))
|
||||
coVerify(exactly = 1) {
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_1_ID,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_1_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_1_EMAIL,
|
||||
privateKey = USER_1_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeCrypto(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
request = InitUserCryptoRequest(
|
||||
userId = USER_2_ID,
|
||||
kdfParams = Kdf.Argon2id(iterations = 0U, memory = 0U, parallelism = 0U),
|
||||
email = USER_2_EMAIL,
|
||||
privateKey = USER_2_PRIVATE_KEY,
|
||||
method = InitUserCryptoMethod.DecryptedKey(
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
),
|
||||
signingKey = null,
|
||||
),
|
||||
)
|
||||
scopedVaultSdkSource.initializeOrganizationCrypto(
|
||||
userId = USER_2_ID,
|
||||
request = InitOrgCryptoRequest(organizationKeys = USER_2_ORG_KEYS),
|
||||
)
|
||||
vaultDiskSource.getTotpCiphers(userId = USER_2_ID)
|
||||
scopedVaultSdkSource.decryptCipher(
|
||||
userId = USER_2_ID,
|
||||
cipher = USER_2_ENCRYPTED_SDK_TOTP_CIPHER,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_2_ID, isUserInitiated = false) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify(exactly = 1) {
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_1_ID)
|
||||
scopedVaultSdkSource.clearCrypto(userId = USER_2_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -362,20 +410,79 @@ private val SYMMETRIC_KEY = generateSecretKey()
|
||||
private const val USER_1_ID = "user1Id"
|
||||
private const val USER_2_ID = "user2Id"
|
||||
|
||||
private const val USER_1_EMAIL = "john@doe.com"
|
||||
private const val USER_2_EMAIL = "jane@doe.com"
|
||||
|
||||
private const val USER_1_PRIVATE_KEY = "user1PrivateKey"
|
||||
private const val USER_2_PRIVATE_KEY = "user2PrivateKey"
|
||||
|
||||
private const val USER_1_UNLOCK_KEY = "user1UnlockKey"
|
||||
private const val USER_2_UNLOCK_KEY = "user2UnlockKey"
|
||||
|
||||
private val USER_1_ORG_KEYS = mapOf("test_1" to "test_1_data")
|
||||
private val USER_2_ORG_KEYS = mapOf("test_2" to "test_2_data")
|
||||
|
||||
private val ACCOUNT_JSON_1 = AccountJson(
|
||||
profile = mockk {
|
||||
every { userId } returns USER_1_ID
|
||||
every { name } returns "John Doe"
|
||||
every { email } returns USER_1_EMAIL
|
||||
every { kdfType } returns KdfTypeJson.ARGON2_ID
|
||||
every { kdfIterations } returns 0
|
||||
every { kdfMemory } returns 0
|
||||
every { kdfParallelism } returns 0
|
||||
},
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken1",
|
||||
refreshToken = "refreshToken1",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val ACCOUNT_JSON_2 = AccountJson(
|
||||
profile = mockk {
|
||||
every { userId } returns USER_2_ID
|
||||
every { name } returns "Jane Doe"
|
||||
every { email } returns USER_2_EMAIL
|
||||
every { kdfType } returns KdfTypeJson.ARGON2_ID
|
||||
every { kdfIterations } returns 0
|
||||
every { kdfMemory } returns 0
|
||||
every { kdfParallelism } returns 0
|
||||
},
|
||||
tokens = AccountTokensJson(
|
||||
accessToken = "accessToken2",
|
||||
refreshToken = "refreshToken2",
|
||||
),
|
||||
settings = AccountJson.Settings(
|
||||
environmentUrlData = EnvironmentUrlDataJson(
|
||||
base = "https://vault.bitwarden.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val USER_STATE_JSON = UserStateJson(
|
||||
activeUserId = USER_1_ID,
|
||||
accounts = mapOf(
|
||||
USER_1_ID to ACCOUNT_JSON_1,
|
||||
USER_2_ID to ACCOUNT_JSON_2,
|
||||
),
|
||||
)
|
||||
|
||||
private val ACCOUNT_1 = mockk<UserState.Account> {
|
||||
every { userId } returns USER_1_ID
|
||||
every { name } returns "John Doe"
|
||||
every { email } returns "john@doe.com"
|
||||
every { email } returns USER_1_EMAIL
|
||||
every { environment.label } returns "bitwarden.com"
|
||||
}
|
||||
|
||||
private val ACCOUNT_2 = mockk<UserState.Account> {
|
||||
every { userId } returns USER_2_ID
|
||||
every { name } returns "Jane Doe"
|
||||
every { email } returns "Jane@doe.com"
|
||||
every { email } returns USER_2_EMAIL
|
||||
every { environment.label } returns "bitwarden.com"
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user