Make SdkClientManager the single source of the Bitwarden SDK Client (#1242)

This commit is contained in:
David Perez 2024-04-08 16:47:25 -05:00 committed by Álison Fernandes
parent bf26db1d4f
commit de39f76627
9 changed files with 94 additions and 114 deletions

View file

@ -6,53 +6,43 @@ import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.crypto.HashPurpose import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientAuth import com.bitwarden.sdk.ClientAuth
import com.bitwarden.sdk.ClientPlatform
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toPasswordStrengthOrNull
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/** /**
* Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a * Primary implementation of [AuthSdkSource] that serves as a convenience wrapper around a
* [ClientAuth]. * [ClientAuth].
*/ */
class AuthSdkSourceImpl( class AuthSdkSourceImpl(
private val clientAuth: ClientAuth, private val sdkClientManager: SdkClientManager,
private val clientPlatform: ClientPlatform,
dispatcherManager: DispatcherManager,
featureFlagManager: BitwardenFeatureFlagManager,
) : AuthSdkSource { ) : AuthSdkSource {
private val ioScope = CoroutineScope(dispatcherManager.io)
init {
ioScope.launch {
clientPlatform.loadFlags(featureFlagManager.featureFlags)
}
}
override suspend fun getNewAuthRequest( override suspend fun getNewAuthRequest(
email: String, email: String,
): Result<AuthRequestResponse> = runCatching { ): Result<AuthRequestResponse> = runCatching {
clientAuth.newAuthRequest( getClient()
email = email, .auth()
) .newAuthRequest(
email = email,
)
} }
override suspend fun getUserFingerprint( override suspend fun getUserFingerprint(
email: String, email: String,
publicKey: String, publicKey: String,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientPlatform.fingerprint( getClient()
req = FingerprintRequest( .platform()
fingerprintMaterial = email, .fingerprint(
publicKey = publicKey, req = FingerprintRequest(
), fingerprintMaterial = email,
) publicKey = publicKey,
),
)
} }
override suspend fun hashPassword( override suspend fun hashPassword(
@ -61,12 +51,14 @@ class AuthSdkSourceImpl(
kdf: Kdf, kdf: Kdf,
purpose: HashPurpose, purpose: HashPurpose,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientAuth.hashPassword( getClient()
email = email, .auth()
password = password, .hashPassword(
kdfParams = kdf, email = email,
purpose = purpose, password = password,
) kdfParams = kdf,
purpose = purpose,
)
} }
override suspend fun makeRegisterKeys( override suspend fun makeRegisterKeys(
@ -74,11 +66,13 @@ class AuthSdkSourceImpl(
password: String, password: String,
kdf: Kdf, kdf: Kdf,
): Result<RegisterKeyResponse> = runCatching { ): Result<RegisterKeyResponse> = runCatching {
clientAuth.makeRegisterKeys( getClient()
email = email, .auth()
password = password, .makeRegisterKeys(
kdf = kdf, email = email,
) password = password,
kdf = kdf,
)
} }
override suspend fun passwordStrength( override suspend fun passwordStrength(
@ -87,7 +81,8 @@ class AuthSdkSourceImpl(
additionalInputs: List<String>, additionalInputs: List<String>,
): Result<PasswordStrength> = runCatching { ): Result<PasswordStrength> = runCatching {
@Suppress("UnsafeCallOnNullableType") @Suppress("UnsafeCallOnNullableType")
clientAuth getClient()
.auth()
.passwordStrength( .passwordStrength(
password = password, password = password,
email = email, email = email,
@ -101,10 +96,16 @@ class AuthSdkSourceImpl(
passwordStrength: PasswordStrength, passwordStrength: PasswordStrength,
policy: MasterPasswordPolicyOptions, policy: MasterPasswordPolicyOptions,
): Result<Boolean> = runCatching { ): Result<Boolean> = runCatching {
clientAuth.satisfiesPolicy( getClient()
password = password, .auth()
strength = passwordStrength.toUByte(), .satisfiesPolicy(
policy = policy, password = password,
) strength = passwordStrength.toUByte(),
policy = policy,
)
} }
private suspend fun getClient(
userId: String? = null,
): Client = sdkClientManager.getOrCreateClient(userId = userId)
} }

View file

@ -1,10 +1,8 @@
package com.x8bit.bitwarden.data.auth.datasource.sdk.di package com.x8bit.bitwarden.data.auth.datasource.sdk.di
import com.bitwarden.sdk.Client
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSource
import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSourceImpl import com.x8bit.bitwarden.data.auth.datasource.sdk.AuthSdkSourceImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -21,13 +19,8 @@ object AuthSdkModule {
@Provides @Provides
@Singleton @Singleton
fun provideAuthSdkSource( fun provideAuthSdkSource(
client: Client, sdkClientManager: SdkClientManager,
featureFlagManager: BitwardenFeatureFlagManager,
dispatcherManager: DispatcherManager,
): AuthSdkSource = AuthSdkSourceImpl( ): AuthSdkSource = AuthSdkSourceImpl(
clientAuth = client.auth(), sdkClientManager = sdkClientManager,
clientPlatform = client.platform(),
featureFlagManager = featureFlagManager,
dispatcherManager = dispatcherManager,
) )
} }

View file

@ -1,22 +0,0 @@
package com.x8bit.bitwarden.data.platform.datasource.di
import com.bitwarden.sdk.Client
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* Provides dependencies related to encryption / decryption / secure generation.
*/
@Module
@InstallIn(SingletonComponent::class)
object EncryptionModule {
@Provides
@Singleton
fun provideBitwardenClient(): Client {
return Client(null)
}
}

View file

@ -11,11 +11,11 @@ interface SdkClientManager {
* Returns the cached [Client] instance for the given [userId], otherwise creates and caches * Returns the cached [Client] instance for the given [userId], otherwise creates and caches
* a new one and returns it. * a new one and returns it.
*/ */
suspend fun getOrCreateClient(userId: String): Client suspend fun getOrCreateClient(userId: String?): Client
/** /**
* Clears any resources from the [Client] associated with the given [userId] and removes it * Clears any resources from the [Client] associated with the given [userId] and removes it
* from the internal cache. * from the internal cache.
*/ */
fun destroyClient(userId: String) fun destroyClient(userId: String?)
} }

View file

@ -9,19 +9,19 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
class SdkClientManagerImpl( class SdkClientManagerImpl(
private val featureFlagManager: BitwardenFeatureFlagManager, private val featureFlagManager: BitwardenFeatureFlagManager,
private val clientProvider: suspend () -> Client = { private val clientProvider: suspend () -> Client = {
Client(null) Client(settings = null).apply {
.apply { platform().loadFlags(featureFlagManager.featureFlags) } platform().loadFlags(featureFlagManager.featureFlags)
}
}, },
) : SdkClientManager { ) : SdkClientManager {
private val userIdToClientMap = mutableMapOf<String, Client>() private val userIdToClientMap = mutableMapOf<String?, Client>()
override suspend fun getOrCreateClient( override suspend fun getOrCreateClient(
userId: String, userId: String?,
): Client = ): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
userIdToClientMap.getOrPut(key = userId) { clientProvider() }
override fun destroyClient( override fun destroyClient(
userId: String, userId: String?,
) { ) {
userIdToClientMap userIdToClientMap
.remove(key = userId) .remove(key = userId)

View file

@ -3,50 +3,57 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.sdk
import com.bitwarden.generators.PassphraseGeneratorRequest import com.bitwarden.generators.PassphraseGeneratorRequest
import com.bitwarden.generators.PasswordGeneratorRequest import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.generators.UsernameGeneratorRequest import com.bitwarden.generators.UsernameGeneratorRequest
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientGenerators import com.bitwarden.sdk.ClientGenerators
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
/** /**
* Implementation of [GeneratorSdkSource] that delegates password generation. * Implementation of [GeneratorSdkSource] that delegates password generation.
* *
* @property clientGenerator An instance of [ClientGenerators] provided by the Bitwarden SDK. * @property sdkClientManager The [SdkClientManager] used to retrieve an instance of the
* [ClientGenerators] provided by the Bitwarden SDK.
*/ */
class GeneratorSdkSourceImpl( class GeneratorSdkSourceImpl(
private val clientGenerator: ClientGenerators, private val sdkClientManager: SdkClientManager,
) : GeneratorSdkSource { ) : GeneratorSdkSource {
override suspend fun generatePassword( override suspend fun generatePassword(
request: PasswordGeneratorRequest, request: PasswordGeneratorRequest,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.password(request) getClient().generators().password(request)
} }
override suspend fun generatePassphrase( override suspend fun generatePassphrase(
request: PassphraseGeneratorRequest, request: PassphraseGeneratorRequest,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.passphrase(request) getClient().generators().passphrase(request)
} }
override suspend fun generatePlusAddressedEmail( override suspend fun generatePlusAddressedEmail(
request: UsernameGeneratorRequest.Subaddress, request: UsernameGeneratorRequest.Subaddress,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.username(request) getClient().generators().username(request)
} }
override suspend fun generateCatchAllEmail( override suspend fun generateCatchAllEmail(
request: UsernameGeneratorRequest.Catchall, request: UsernameGeneratorRequest.Catchall,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.username(request) getClient().generators().username(request)
} }
override suspend fun generateRandomWord( override suspend fun generateRandomWord(
request: UsernameGeneratorRequest.Word, request: UsernameGeneratorRequest.Word,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.username(request) getClient().generators().username(request)
} }
override suspend fun generateForwardedServiceEmail( override suspend fun generateForwardedServiceEmail(
request: UsernameGeneratorRequest.Forwarded, request: UsernameGeneratorRequest.Forwarded,
): Result<String> = runCatching { ): Result<String> = runCatching {
clientGenerator.username(request) getClient().generators().username(request)
} }
private suspend fun getClient(
userId: String? = null,
): Client = sdkClientManager.getOrCreateClient(userId = userId)
} }

View file

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.tools.generator.datasource.sdk.di package com.x8bit.bitwarden.data.tools.generator.datasource.sdk.di
import com.bitwarden.sdk.Client import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSource
import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourceImpl import com.x8bit.bitwarden.data.tools.generator.datasource.sdk.GeneratorSdkSourceImpl
import dagger.Module import dagger.Module
@ -19,6 +19,6 @@ object GeneratorSdkModule {
@Provides @Provides
@Singleton @Singleton
fun provideGeneratorSdkSource( fun provideGeneratorSdkSource(
client: Client, sdkClientManager: SdkClientManager,
): GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerator = client.generators()) ): GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager = sdkClientManager)
} }

View file

@ -6,20 +6,20 @@ import com.bitwarden.core.MasterPasswordPolicyOptions
import com.bitwarden.core.RegisterKeyResponse import com.bitwarden.core.RegisterKeyResponse
import com.bitwarden.crypto.HashPurpose import com.bitwarden.crypto.HashPurpose
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientAuth import com.bitwarden.sdk.ClientAuth
import com.bitwarden.sdk.ClientPlatform import com.bitwarden.sdk.ClientPlatform
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import com.x8bit.bitwarden.data.vault.datasource.sdk.BitwardenFeatureFlagManager
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.runs
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -28,26 +28,18 @@ class AuthSdkSourceTest {
private val clientPlatform = mockk<ClientPlatform> { private val clientPlatform = mockk<ClientPlatform> {
coEvery { loadFlags(any()) } just runs coEvery { loadFlags(any()) } just runs
} }
private val featureFlagManager = mockk<BitwardenFeatureFlagManager> { private val client = mockk<Client> {
coEvery { featureFlags } returns emptyMap() every { auth() } returns clientAuth
every { platform() } returns clientPlatform
}
private val sdkClientManager = mockk<SdkClientManager> {
coEvery { getOrCreateClient(userId = null) } returns client
} }
private val dispatcherManager = FakeDispatcherManager()
private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl( private val authSkdSource: AuthSdkSource = AuthSdkSourceImpl(
clientAuth = clientAuth, sdkClientManager = sdkClientManager,
clientPlatform = clientPlatform,
featureFlagManager = featureFlagManager,
dispatcherManager = dispatcherManager,
) )
@BeforeEach
fun setup() {
coVerify(exactly = 1) {
featureFlagManager.featureFlags
clientPlatform.loadFlags(any())
}
}
@Test @Test
fun `getNewAuthRequest should call SDK and return a Result with correct data`() = runBlocking { fun `getNewAuthRequest should call SDK and return a Result with correct data`() = runBlocking {
val email = "test@gmail.com" val email = "test@gmail.com"

View file

@ -5,10 +5,13 @@ import com.bitwarden.generators.ForwarderServiceType
import com.bitwarden.generators.PassphraseGeneratorRequest import com.bitwarden.generators.PassphraseGeneratorRequest
import com.bitwarden.generators.PasswordGeneratorRequest import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.generators.UsernameGeneratorRequest import com.bitwarden.generators.UsernameGeneratorRequest
import com.bitwarden.sdk.Client
import com.bitwarden.sdk.ClientGenerators import com.bitwarden.sdk.ClientGenerators
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.util.asSuccess import com.x8bit.bitwarden.data.platform.util.asSuccess
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -16,7 +19,13 @@ import org.junit.Test
class GeneratorSdkSourceTest { class GeneratorSdkSourceTest {
private val clientGenerators = mockk<ClientGenerators>() private val clientGenerators = mockk<ClientGenerators>()
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(clientGenerators) private val client = mockk<Client> {
every { generators() } returns clientGenerators
}
private val sdkClientManager = mockk<SdkClientManager> {
coEvery { getOrCreateClient(userId = null) } returns client
}
private val generatorSdkSource: GeneratorSdkSource = GeneratorSdkSourceImpl(sdkClientManager)
@Test @Test
fun `generatePassword should call SDK and return a Result with the generated password`() = fun `generatePassword should call SDK and return a Result with the generated password`() =