mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BITAU-104 Implement BridgeService (#3954)
This commit is contained in:
parent
190f92ec67
commit
87e223bc59
10 changed files with 861 additions and 32 deletions
Binary file not shown.
|
@ -51,6 +51,7 @@ import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManage
|
|||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.BridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
||||
|
@ -80,8 +81,12 @@ object PlatformManagerModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideBridgeServiceProcessor(
|
||||
bridgeRepository: BridgeRepository,
|
||||
dispatcherManager: DispatcherManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
): BridgeServiceProcessor = BridgeServiceProcessorImpl(
|
||||
bridgeRepository = bridgeRepository,
|
||||
dispatcherManager = dispatcherManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
|
|
|
@ -2,22 +2,37 @@ package com.x8bit.bitwarden.data.platform.processor
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IInterface
|
||||
import android.os.RemoteCallbackList
|
||||
import com.bitwarden.bridge.IBridgeService
|
||||
import com.bitwarden.bridge.IBridgeServiceCallback
|
||||
import com.bitwarden.bridge.model.EncryptedAddTotpLoginItemData
|
||||
import com.bitwarden.bridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.bridge.model.SymmetricEncryptionKeyFingerprintData
|
||||
import com.bitwarden.bridge.util.NATIVE_BRIDGE_SDK_VERSION
|
||||
import com.bitwarden.bridge.util.encrypt
|
||||
import com.bitwarden.bridge.util.toFingerprint
|
||||
import com.bitwarden.bridge.util.toSymmetricEncryptionKeyData
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.BridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Default implementation of [BridgeServiceProcessor].
|
||||
*/
|
||||
class BridgeServiceProcessorImpl(
|
||||
private val bridgeRepository: BridgeRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : BridgeServiceProcessor {
|
||||
|
||||
private val callbacks by lazy { RemoteCallbackList<IBridgeServiceCallback>() }
|
||||
private val scope by lazy { CoroutineScope(dispatcherManager.default) }
|
||||
|
||||
override val binder: IBridgeService.Stub?
|
||||
get() {
|
||||
return if (
|
||||
|
@ -37,42 +52,75 @@ class BridgeServiceProcessorImpl(
|
|||
* Default implementation of the bridge service binder.
|
||||
*/
|
||||
private val defaultBinder = object : IBridgeService.Stub() {
|
||||
override fun getVersionNumber(): String {
|
||||
// TODO: BITAU-104
|
||||
return ""
|
||||
}
|
||||
|
||||
override fun getVersionNumber(): String = NATIVE_BRIDGE_SDK_VERSION
|
||||
|
||||
override fun checkSymmetricEncryptionKeyFingerprint(
|
||||
data: SymmetricEncryptionKeyFingerprintData?,
|
||||
symmetricKeyFingerprint: SymmetricEncryptionKeyFingerprintData?,
|
||||
): Boolean {
|
||||
// TODO: BITAU-104
|
||||
return false
|
||||
if (symmetricKeyFingerprint == null) return false
|
||||
val localSymmetricKeyFingerprint =
|
||||
bridgeRepository.authenticatorSyncSymmetricKey
|
||||
?.toSymmetricEncryptionKeyData()
|
||||
?.toFingerprint()
|
||||
?.getOrNull()
|
||||
return symmetricKeyFingerprint == localSymmetricKeyFingerprint
|
||||
}
|
||||
|
||||
override fun getSymmetricEncryptionKeyData(): SymmetricEncryptionKeyData? {
|
||||
// TODO: BITAU-104
|
||||
return null
|
||||
}
|
||||
override fun getSymmetricEncryptionKeyData(): SymmetricEncryptionKeyData? =
|
||||
bridgeRepository.authenticatorSyncSymmetricKey?.toSymmetricEncryptionKeyData()
|
||||
|
||||
override fun registerBridgeServiceCallback(callback: IBridgeServiceCallback?) {
|
||||
// TODO: BITAU-104
|
||||
if (callback == null) return
|
||||
callbacks.register(callback)
|
||||
}
|
||||
|
||||
override fun unregisterBridgeServiceCallback(callback: IBridgeServiceCallback?) {
|
||||
// TODO: BITAU-104
|
||||
if (callback == null) return
|
||||
callbacks.unregister(callback)
|
||||
}
|
||||
|
||||
override fun syncAccounts() {
|
||||
// TODO: BITAU-104
|
||||
val symmetricEncryptionKey = symmetricEncryptionKeyData ?: return
|
||||
scope.launch {
|
||||
// Encrypt the shared account data with the symmetric key:
|
||||
val encryptedSharedAccountData = bridgeRepository
|
||||
.getSharedAccounts()
|
||||
.encrypt(symmetricEncryptionKey)
|
||||
.getOrNull()
|
||||
?: return@launch
|
||||
|
||||
// Report results to callback:
|
||||
callbacks.forEach { callback ->
|
||||
callback.onAccountsSync(encryptedSharedAccountData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAddTotpLoginItemIntent(): Intent {
|
||||
// TODO: BITAU-104
|
||||
// TODO: BITAU-112
|
||||
return Intent()
|
||||
}
|
||||
|
||||
override fun setPendingAddTotpLoginItemData(data: EncryptedAddTotpLoginItemData?) {
|
||||
// TODO: BITAU-104
|
||||
// TODO: BITAU-112
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function mirrors the hidden "RemoteCallbackList.broadcast" function.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private fun <T : IInterface> RemoteCallbackList<T>.forEach(action: (T) -> Unit) {
|
||||
val count = this.beginBroadcast()
|
||||
try {
|
||||
for (index in 0..count) {
|
||||
action(this.getBroadcastItem(index))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Broadcast failed
|
||||
} finally {
|
||||
this.finishBroadcast()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.bridge.model.SharedAccountData
|
||||
|
||||
/**
|
||||
* Provides an API for querying disk sources required by Bridge service implementation.
|
||||
*/
|
||||
interface BridgeRepository {
|
||||
|
||||
/**
|
||||
* The currently persisted authenticator sync symmetric key. This key is used for
|
||||
* encrypting IPC traffic.
|
||||
*/
|
||||
val authenticatorSyncSymmetricKey: ByteArray?
|
||||
|
||||
/**
|
||||
* Get a list of shared account data. This function will go through all accounts and for each
|
||||
* one, check to see if the user has Authenticator account syncing enabled and if they
|
||||
* do, it will query and decrypt the user's shared account data.
|
||||
*
|
||||
* Users who do not have authenticator sync enabled or otherwise cannot have their ciphers
|
||||
* accessed will be omitted from the list.
|
||||
*/
|
||||
suspend fun getSharedAccounts(): SharedAccountData
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.bridge.model.SharedAccountData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
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.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedSdkCipher
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/**
|
||||
* Default implementation of [BridgeRepository].
|
||||
*/
|
||||
class BridgeRepositoryImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val authDiskSource: AuthDiskSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val vaultDiskSource: VaultDiskSource,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val settingsDiskSource: SettingsDiskSource,
|
||||
) : BridgeRepository {
|
||||
|
||||
override val authenticatorSyncSymmetricKey: ByteArray?
|
||||
get() = authDiskSource.authenticatorSyncSymmetricKey
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
val allAccounts = authRepository.userStateFlow.value?.accounts ?: emptyList()
|
||||
|
||||
return allAccounts
|
||||
.mapNotNull { account ->
|
||||
val userId = account.userId
|
||||
|
||||
// Grab the user's authenticator sync unlock key. If it is null,
|
||||
// the user has not enabled authenticator sync.
|
||||
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(
|
||||
userId = userId,
|
||||
decryptedUserKey = decryptedUserKey,
|
||||
)
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.GenericError,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Vault is unlocked, query vault disk source for totp logins:
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item:
|
||||
.filter { it.login?.totp != null }
|
||||
.mapNotNull {
|
||||
// Decrypt each cipher and take just totp codes:
|
||||
vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
)
|
||||
.getOrNull()
|
||||
?.login
|
||||
?.totp
|
||||
}
|
||||
|
||||
val lastSyncTime =
|
||||
settingsDiskSource.getLastSyncTime(userId) ?: return@mapNotNull null
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
if (!isVaultAlreadyUnlocked) {
|
||||
vaultRepository.lockVault(userId)
|
||||
}
|
||||
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
email = account.email,
|
||||
environmentLabel = account.environment.label,
|
||||
lastSyncTime = lastSyncTime,
|
||||
totpUris = totpUris,
|
||||
)
|
||||
}
|
||||
.let {
|
||||
SharedAccountData(it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.repository.di
|
|||
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.ConfigDiskSource
|
||||
|
@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.platform.datasource.network.service.ConfigServic
|
|||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.BridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.BridgeRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepositoryImpl
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
|
@ -20,7 +23,9 @@ import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepository
|
|||
import com.x8bit.bitwarden.data.platform.repository.ServerConfigRepositoryImpl
|
||||
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.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -35,6 +40,24 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object PlatformRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesBridgeRepository(
|
||||
authRepository: AuthRepository,
|
||||
authDiskSource: AuthDiskSource,
|
||||
vaultRepository: VaultRepository,
|
||||
vaultDiskSource: VaultDiskSource,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
settingsDiskSource: SettingsDiskSource,
|
||||
): BridgeRepository = BridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = authDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsDiskSource = settingsDiskSource,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideServerConfigRepository(
|
||||
|
|
|
@ -1,52 +1,221 @@
|
|||
package com.x8bit.bitwarden.data.platform.processor
|
||||
|
||||
import android.os.Build
|
||||
import android.os.RemoteCallbackList
|
||||
import com.bitwarden.bridge.IBridgeService
|
||||
import com.bitwarden.bridge.IBridgeServiceCallback
|
||||
import com.bitwarden.bridge.model.EncryptedSharedAccountData
|
||||
import com.bitwarden.bridge.model.SharedAccountData
|
||||
import com.bitwarden.bridge.util.NATIVE_BRIDGE_SDK_VERSION
|
||||
import com.bitwarden.bridge.util.encrypt
|
||||
import com.bitwarden.bridge.util.generateSecretKey
|
||||
import com.bitwarden.bridge.util.toFingerprint
|
||||
import com.bitwarden.bridge.util.toSymmetricEncryptionKeyData
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.BridgeRepository
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BridgeServiceProcessorTest {
|
||||
|
||||
private val featureFlagManager: FeatureFlagManager = mockk()
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>()
|
||||
private val bridgeRepository = mockk<BridgeRepository>()
|
||||
|
||||
private val bridgeServiceManager = BridgeServiceProcessorImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
private lateinit var bridgeServiceProcessor: BridgeServiceProcessorImpl
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
bridgeServiceProcessor = BridgeServiceProcessorImpl(
|
||||
bridgeRepository = bridgeRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
dispatcherManager = FakeDispatcherManager(),
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(::isBuildVersionBelow)
|
||||
unmockkStatic(SharedAccountData::encrypt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when AuthenticatorSync feature flag is off, should return null binder`() {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
every { isBuildVersionBelow(Build.VERSION_CODES.S) } returns false
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) } returns false
|
||||
assertNull(bridgeServiceManager.binder)
|
||||
assertNull(bridgeServiceProcessor.binder)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `when AuthenticatorSync feature flag is on and running Android level greater than S, should return non-null binder`() {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
every { isBuildVersionBelow(Build.VERSION_CODES.S) } returns false
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) } returns true
|
||||
assertNotNull(bridgeServiceManager.binder)
|
||||
assertNotNull(bridgeServiceProcessor.binder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when below Android level S, should never return a binder regardless of feature flag`() {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
every { isBuildVersionBelow(Build.VERSION_CODES.S) } returns true
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) } returns false
|
||||
assertNull(bridgeServiceManager.binder)
|
||||
assertNull(bridgeServiceProcessor.binder)
|
||||
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) } returns true
|
||||
assertNull(bridgeServiceManager.binder)
|
||||
assertNull(bridgeServiceProcessor.binder)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `versionNumber should match version of compiled bridge sdk`() {
|
||||
val binder = getDefaultBinder()
|
||||
assertEquals(
|
||||
NATIVE_BRIDGE_SDK_VERSION,
|
||||
binder.versionNumber,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `checkSymmetricEncryptionKeyFingerprint should return false when given fingerprint is null`() {
|
||||
val binder = getDefaultBinder()
|
||||
// Set disk symmetric key to null so that it is technically equal to given null fingerprint:
|
||||
every { bridgeRepository.authenticatorSyncSymmetricKey } returns null
|
||||
// Binder should still return false in this case:
|
||||
assertFalse(binder.checkSymmetricEncryptionKeyFingerprint(null))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `checkSymmetricEncryptionKeyFingerprint should return false if fingerprint doesn't match`() {
|
||||
val binder = getDefaultBinder()
|
||||
every { bridgeRepository.authenticatorSyncSymmetricKey } returns ByteArray(1)
|
||||
assertFalse(binder.checkSymmetricEncryptionKeyFingerprint(SYMMETRIC_KEY_FINGERPRINT))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `checkSymmetricEncryptionKeyFingerprint should return true if fingerprint does match`() {
|
||||
val binder = getDefaultBinder()
|
||||
every {
|
||||
bridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
assertTrue(binder.checkSymmetricEncryptionKeyFingerprint(SYMMETRIC_KEY_FINGERPRINT))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `getSymmetricEncryptionKeyData should return null when there is no symmetric key stored on disk`() {
|
||||
val binder = getDefaultBinder()
|
||||
every { bridgeRepository.authenticatorSyncSymmetricKey } returns null
|
||||
assertNull(binder.symmetricEncryptionKeyData)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `getSymmetricEncryptionKeyData should return the symmetric key stored on disk`() {
|
||||
val binder = getDefaultBinder()
|
||||
every {
|
||||
bridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
assertEquals(SYMMETRIC_KEY, binder.symmetricEncryptionKeyData)
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class SyncAccountsTest {
|
||||
|
||||
private var lastAccountsSync: EncryptedSharedAccountData? = null
|
||||
|
||||
private val serviceCallback = object : IBridgeServiceCallback.Stub() {
|
||||
override fun onAccountsSync(data: EncryptedSharedAccountData?) {
|
||||
lastAccountsSync = data
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
// Setup RemoteCallbackList to call back to serviceCallback:
|
||||
mockkConstructor(RemoteCallbackList::class)
|
||||
every {
|
||||
anyConstructed<RemoteCallbackList<IBridgeServiceCallback>>()
|
||||
.register(serviceCallback)
|
||||
} returns true
|
||||
every {
|
||||
anyConstructed<RemoteCallbackList<IBridgeServiceCallback>>()
|
||||
.beginBroadcast()
|
||||
} returns 1
|
||||
every {
|
||||
anyConstructed<RemoteCallbackList<IBridgeServiceCallback>>()
|
||||
.getBroadcastItem(0)
|
||||
} returns serviceCallback
|
||||
lastAccountsSync = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncAccounts when symmetricEncryptionKeyData is null should do nothing`() {
|
||||
every { bridgeRepository.authenticatorSyncSymmetricKey } returns null
|
||||
getDefaultBinder().syncAccounts()
|
||||
assertNull(lastAccountsSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncAccounts should encrypt result from BridgeRepository`() {
|
||||
val sharedAccountData = mockk<SharedAccountData>()
|
||||
val expected = mockk<EncryptedSharedAccountData>()
|
||||
every {
|
||||
bridgeRepository.authenticatorSyncSymmetricKey
|
||||
} returns SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
coEvery { bridgeRepository.getSharedAccounts() } returns sharedAccountData
|
||||
mockkStatic(SharedAccountData::encrypt)
|
||||
every { sharedAccountData.encrypt(SYMMETRIC_KEY) } returns expected.asSuccess()
|
||||
|
||||
getDefaultBinder().syncAccounts()
|
||||
|
||||
assertEquals(expected, lastAccountsSync)
|
||||
coVerify { bridgeRepository.getSharedAccounts() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for accessing the default implementation of [IBridgeService.Stub]. This
|
||||
* is particularly useful because the binder is nullable on [BridgeServiceProcessor] behind
|
||||
* a feature flag.
|
||||
*/
|
||||
private fun getDefaultBinder(): IBridgeService.Stub {
|
||||
mockkStatic(::isBuildVersionBelow)
|
||||
every { isBuildVersionBelow(Build.VERSION_CODES.S) } returns false
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) } returns true
|
||||
return bridgeServiceProcessor.binder!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Symmetric encryption key that can be used for test.
|
||||
*/
|
||||
private val SYMMETRIC_KEY = generateSecretKey()
|
||||
.getOrThrow()
|
||||
.encoded
|
||||
.toSymmetricEncryptionKeyData()
|
||||
|
||||
/**
|
||||
* Fingerprint of [SYMMETRIC_KEY].
|
||||
*/
|
||||
private val SYMMETRIC_KEY_FINGERPRINT = SYMMETRIC_KEY.toFingerprint().getOrThrow()
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.bitwarden.bridge.model.SharedAccountData
|
||||
import com.bitwarden.bridge.util.generateSecretKey
|
||||
import com.bitwarden.bridge.util.toSymmetricEncryptionKeyData
|
||||
import com.bitwarden.vault.Cipher
|
||||
import com.bitwarden.vault.CipherView
|
||||
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.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson
|
||||
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.repository.util.toEncryptedSdkCipher
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class BridgeRepositoryTest {
|
||||
|
||||
private val authRepository = mockk<AuthRepository>()
|
||||
private val vaultSdkSource = mockk<VaultSdkSource>()
|
||||
private val vaultDiskSource = mockk<VaultDiskSource>()
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
private val fakeAuthDiskSource = FakeAuthDiskSource()
|
||||
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||
|
||||
private val bridgeRepository = BridgeRepositoryImpl(
|
||||
authRepository = authRepository,
|
||||
authDiskSource = fakeAuthDiskSource,
|
||||
vaultRepository = vaultRepository,
|
||||
vaultDiskSource = vaultDiskSource,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
settingsDiskSource = fakeSettingsDiskSource,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
// Because there is so much setup for the happy path, we set that all up here and
|
||||
// then adjust accordingly in each test. The "base" setup here is that
|
||||
// there are two users, USER_1, which we will often manipulate in tests,
|
||||
// and USER_2, which will remain usually not manipulated to demonstrate that a single
|
||||
// account failing shouldn't impact other accounts.
|
||||
|
||||
// Store symmetric encryption key on disk:
|
||||
fakeAuthDiskSource.authenticatorSyncSymmetricKey =
|
||||
SYMMETRIC_KEY.symmetricEncryptionKey.byteArray
|
||||
|
||||
// Setup authRepository to return default USER_STATE:
|
||||
every { authRepository.userStateFlow } returns MutableStateFlow(USER_STATE)
|
||||
|
||||
// Setup authDiskSource to have each user's authenticator sync unlock key:
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = USER_1_ID,
|
||||
authenticatorSyncUnlockKey = USER_1_UNLOCK_KEY,
|
||||
)
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
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) } returns Unit
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
} returns VaultUnlockResult.Success
|
||||
|
||||
// Setup settingDiskSource to have lastSyncTime set:
|
||||
fakeSettingsDiskSource.storeLastSyncTime(USER_1_ID, LAST_SYNC_TIME)
|
||||
fakeSettingsDiskSource.storeLastSyncTime(USER_2_ID, LAST_SYNC_TIME)
|
||||
|
||||
// Add some ciphers to vaultDiskSource for each user,
|
||||
// and setup mock decryption for them:
|
||||
every { vaultDiskSource.getCiphers(USER_1_ID) } returns flowOf(USER_1_CIPHERS)
|
||||
every { vaultDiskSource.getCiphers(USER_2_ID) } returns flowOf(USER_2_CIPHERS)
|
||||
mockkStatic(SyncResponseJson.Cipher::toEncryptedSdkCipher)
|
||||
every {
|
||||
USER_1_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_1_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
every {
|
||||
USER_2_TOTP_CIPHER.toEncryptedSdkCipher()
|
||||
} returns USER_2_ENCRYPTED_SDK_TOTP_CIPHER
|
||||
coEvery {
|
||||
vaultSdkSource.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)
|
||||
} returns USER_2_DECRYPTED_TOTP_CIPHER.asSuccess()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
confirmVerified(authRepository, vaultSdkSource, vaultRepository, vaultDiskSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts with user 1 vault unlocked and all data present should send expected shared accounts data`() =
|
||||
runTest {
|
||||
val sharedAccounts = bridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
)
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultDiskSource.getCiphers(USER_1_ID) }
|
||||
verify { vaultDiskSource.getCiphers(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) }
|
||||
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)
|
||||
|
||||
val sharedData = bridgeRepository.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`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.storeAuthenticatorSyncUnlockKey(
|
||||
userId = USER_1_ID,
|
||||
authenticatorSyncUnlockKey = null,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)),
|
||||
bridgeRepository.getSharedAccounts(),
|
||||
)
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
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) }
|
||||
verify { vaultDiskSource.getCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `syncAccounts when vault is locked for both users should unlock and re-lock vault for both users`() =
|
||||
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) } returns Unit
|
||||
|
||||
val sharedAccounts = bridgeRepository.getSharedAccounts()
|
||||
assertEquals(
|
||||
BOTH_ACCOUNT_SUCCESS,
|
||||
sharedAccounts,
|
||||
)
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultDiskSource.getCiphers(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(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_1_ID) }
|
||||
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) }
|
||||
verify { vaultDiskSource.getCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncAccounts when getLastSyncTime is null should omit account from list`() = runTest {
|
||||
fakeSettingsDiskSource.storeLastSyncTime(USER_1_ID, null)
|
||||
val sharedAccounts = bridgeRepository.getSharedAccounts()
|
||||
assertEquals(SharedAccountData(listOf(USER_2_SHARED_ACCOUNT)), sharedAccounts)
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
verify { vaultDiskSource.getCiphers(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_1_ID, USER_1_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
verify { authRepository.userStateFlow }
|
||||
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) }
|
||||
verify { vaultDiskSource.getCiphers(USER_2_ID) }
|
||||
coVerify { vaultSdkSource.decryptCipher(USER_2_ID, USER_2_ENCRYPTED_SDK_TOTP_CIPHER) }
|
||||
}
|
||||
|
||||
@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`() =
|
||||
runTest {
|
||||
every { vaultRepository.isVaultUnlocked(USER_1_ID) } returns false
|
||||
coEvery {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(USER_1_ID, USER_1_UNLOCK_KEY)
|
||||
} returns VaultUnlockResult.InvalidStateError
|
||||
|
||||
val sharedAccounts = bridgeRepository.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(
|
||||
userId = USER_1_ID,
|
||||
decryptedUserKey = USER_1_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
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) }
|
||||
verify { vaultDiskSource.getCiphers(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 = bridgeRepository.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)
|
||||
vaultDiskSource.getCiphers(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),
|
||||
)
|
||||
|
||||
deferred.await()
|
||||
|
||||
verify { authRepository.userStateFlow }
|
||||
verify { vaultDiskSource.getCiphers(USER_1_ID) }
|
||||
verify { vaultDiskSource.getCiphers(USER_2_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_1_ID) }
|
||||
verify { vaultRepository.isVaultUnlocked(USER_2_ID) }
|
||||
verify { vaultRepository.vaultUnlockDataStateFlow }
|
||||
coVerify {
|
||||
vaultRepository.unlockVaultWithDecryptedUserKey(
|
||||
userId = USER_2_ID,
|
||||
decryptedUserKey = USER_2_UNLOCK_KEY,
|
||||
)
|
||||
}
|
||||
verify { vaultRepository.lockVault(USER_2_ID) }
|
||||
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 `authenticatorSyncSymmetricKey should read from authDiskSource`() {
|
||||
fakeAuthDiskSource.authenticatorSyncSymmetricKey = null
|
||||
assertNull(bridgeRepository.authenticatorSyncSymmetricKey)
|
||||
|
||||
val syncKey = generateSecretKey().getOrThrow().encoded
|
||||
fakeAuthDiskSource.authenticatorSyncSymmetricKey = syncKey
|
||||
|
||||
assertEquals(syncKey, bridgeRepository.authenticatorSyncSymmetricKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Symmetric encryption key that can be used for test.
|
||||
*/
|
||||
private val SYMMETRIC_KEY = generateSecretKey()
|
||||
.getOrThrow()
|
||||
.encoded
|
||||
.toSymmetricEncryptionKeyData()
|
||||
|
||||
private const val USER_1_ID = "user1Id"
|
||||
private const val USER_2_ID = "user2Id"
|
||||
|
||||
private const val USER_1_UNLOCK_KEY = "user1UnlockKey"
|
||||
private const val USER_2_UNLOCK_KEY = "user2UnlockKey"
|
||||
|
||||
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 { 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 { environment.label } returns "bitwarden.com"
|
||||
}
|
||||
|
||||
private val USER_STATE = UserState(
|
||||
activeUserId = USER_1_ID,
|
||||
accounts = listOf(
|
||||
ACCOUNT_1,
|
||||
ACCOUNT_2,
|
||||
),
|
||||
)
|
||||
|
||||
private val USER_1_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
|
||||
every { login?.totp } returns "encryptedTotp1"
|
||||
}
|
||||
|
||||
private val USER_2_TOTP_CIPHER = mockk<SyncResponseJson.Cipher> {
|
||||
every { login?.totp } returns "encryptedTotp2"
|
||||
}
|
||||
|
||||
private val USER_1_ENCRYPTED_SDK_TOTP_CIPHER = mockk<Cipher>()
|
||||
private val USER_2_ENCRYPTED_SDK_TOTP_CIPHER = mockk<Cipher>()
|
||||
|
||||
private val USER_1_DECRYPTED_TOTP_CIPHER = mockk<CipherView> {
|
||||
every { login?.totp } returns "totp1"
|
||||
}
|
||||
private val USER_2_DECRYPTED_TOTP_CIPHER = mockk<CipherView> {
|
||||
every { login?.totp } returns "totp2"
|
||||
}
|
||||
|
||||
private val USER_1_EXPECTED_TOTP_LIST = listOf("totp1")
|
||||
private val USER_2_EXPECTED_TOTP_LIST = listOf("totp2")
|
||||
|
||||
private val LAST_SYNC_TIME = Instant.now()
|
||||
|
||||
private val USER_1_SHARED_ACCOUNT = SharedAccountData.Account(
|
||||
userId = ACCOUNT_1.userId,
|
||||
name = ACCOUNT_1.name,
|
||||
email = ACCOUNT_1.email,
|
||||
environmentLabel = ACCOUNT_1.environment.label,
|
||||
totpUris = USER_1_EXPECTED_TOTP_LIST,
|
||||
lastSyncTime = LAST_SYNC_TIME,
|
||||
)
|
||||
|
||||
private val USER_2_SHARED_ACCOUNT = SharedAccountData.Account(
|
||||
userId = ACCOUNT_2.userId,
|
||||
name = ACCOUNT_2.name,
|
||||
email = ACCOUNT_2.email,
|
||||
environmentLabel = ACCOUNT_2.environment.label,
|
||||
totpUris = USER_2_EXPECTED_TOTP_LIST,
|
||||
lastSyncTime = LAST_SYNC_TIME,
|
||||
)
|
||||
|
||||
private val USER_1_CIPHERS = listOf(
|
||||
USER_1_TOTP_CIPHER,
|
||||
)
|
||||
|
||||
private val USER_2_CIPHERS = listOf(
|
||||
USER_2_TOTP_CIPHER,
|
||||
)
|
||||
|
||||
private val BOTH_ACCOUNT_SUCCESS = SharedAccountData(
|
||||
listOf(
|
||||
USER_1_SHARED_ACCOUNT,
|
||||
USER_2_SHARED_ACCOUNT,
|
||||
),
|
||||
)
|
|
@ -18,7 +18,7 @@ interface IBridgeService {
|
|||
String getVersionNumber();
|
||||
|
||||
// Returns true when the given symmetric fingerprint data matches that contained by the SDK.
|
||||
boolean checkSymmetricEncryptionKeyFingerprint(in SymmetricEncryptionKeyFingerprintData data);
|
||||
boolean checkSymmetricEncryptionKeyFingerprint(in SymmetricEncryptionKeyFingerprintData symmetricKeyFingerprint);
|
||||
|
||||
// Returns a symmetric key that will be used for encypting all IPC traffic.
|
||||
//
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.bitwarden.bridge.model.EncryptedSharedAccountData
|
|||
import com.bitwarden.bridge.model.SharedAccountData
|
||||
import com.bitwarden.bridge.model.SharedAccountDataJson
|
||||
import com.bitwarden.bridge.model.SymmetricEncryptionKeyData
|
||||
import com.bitwarden.bridge.model.SymmetricEncryptionKeyFingerprintData
|
||||
import com.bitwarden.bridge.model.toByteArrayContainer
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.security.MessageDigest
|
||||
|
@ -36,12 +37,13 @@ fun generateSecretKey(): Result<SecretKey> = runCatching {
|
|||
* [IBridgeService.checkSymmetricEncryptionKeyFingerprint], which allows callers of the service
|
||||
* to verify that they have the correct symmetric key without actually having to send the key.
|
||||
*/
|
||||
fun SymmetricEncryptionKeyData.toFingerprint(): Result<ByteArray> = runCatching {
|
||||
val messageDigest = MessageDigest.getInstance(KeyProperties.DIGEST_SHA256)
|
||||
messageDigest.reset()
|
||||
messageDigest.update(this.symmetricEncryptionKey.byteArray)
|
||||
messageDigest.digest()
|
||||
}
|
||||
fun SymmetricEncryptionKeyData.toFingerprint(): Result<SymmetricEncryptionKeyFingerprintData> =
|
||||
runCatching {
|
||||
val messageDigest = MessageDigest.getInstance(KeyProperties.DIGEST_SHA256)
|
||||
messageDigest.reset()
|
||||
messageDigest.update(this.symmetricEncryptionKey.byteArray)
|
||||
SymmetricEncryptionKeyFingerprintData(messageDigest.digest().toByteArrayContainer())
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt [SharedAccountData].
|
||||
|
|
Loading…
Reference in a new issue