mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 10:25:57 +03:00
BITAU-97 Add AuthenticatorBridgeManager
(#3987)
This commit is contained in:
parent
757baf0290
commit
9e4119fe32
20 changed files with 965 additions and 0 deletions
Binary file not shown.
BIN
app/libs/bridge-0.1.0-SNAPSHOT-release.aar
Normal file
BIN
app/libs/bridge-0.1.0-SNAPSHOT-release.aar
Normal file
Binary file not shown.
|
@ -54,6 +54,21 @@ The following is a list of all third-party dependencies required by the SDK.
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> The SDK does not come packaged with these dependencies, so consumers of the SDK must provide them.
|
> The SDK does not come packaged with these dependencies, so consumers of the SDK must provide them.
|
||||||
|
|
||||||
|
- **AndroidX Appcompat**
|
||||||
|
- https://developer.android.com/jetpack/androidx/releases/appcompat
|
||||||
|
- Purpose: Allows access to new APIs on older API versions.
|
||||||
|
- License: Apache 2.0
|
||||||
|
|
||||||
|
- **AndroidX Lifecycle**
|
||||||
|
- https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||||
|
- Purpose: Lifecycle aware components and tooling.
|
||||||
|
- License: Apache 2.0
|
||||||
|
|
||||||
|
- **kotlinx.coroutines**
|
||||||
|
- https://github.com/Kotlin/kotlinx.coroutines
|
||||||
|
- Purpose: Kotlin coroutines library for asynchronous and reactive code.
|
||||||
|
- License: Apache 2.0
|
||||||
|
|
||||||
- **kotlinx.serialization**
|
- **kotlinx.serialization**
|
||||||
- https://github.com/Kotlin/kotlinx.serialization/
|
- https://github.com/Kotlin/kotlinx.serialization/
|
||||||
- Purpose: JSON serialization library for Kotlin.
|
- Purpose: JSON serialization library for Kotlin.
|
||||||
|
|
|
@ -60,7 +60,10 @@ kotlin {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// SDK dependencies:
|
// SDK dependencies:
|
||||||
|
implementation(libs.androidx.appcompat)
|
||||||
|
implementation(libs.androidx.lifecycle.process)
|
||||||
implementation(libs.kotlinx.serialization)
|
implementation(libs.kotlinx.serialization)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
// Test environment dependencies:
|
// Test environment dependencies:
|
||||||
testImplementation(libs.junit.junit5)
|
testImplementation(libs.junit.junit5)
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.factory
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManagerImpl
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for supplying implementation instances of Authenticator Bridge SDK interfaces.
|
||||||
|
*/
|
||||||
|
class AuthenticatorBridgeFactory(
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new instance of [AuthenticatorBridgeManager].
|
||||||
|
*
|
||||||
|
* @param connectionType Specifies which build variant to connect to.
|
||||||
|
* @param symmetricKeyStorageProvider Provides access to local storage of the symmetric
|
||||||
|
* encryption key.
|
||||||
|
*/
|
||||||
|
fun getAuthenticatorBridgeManager(
|
||||||
|
connectionType: AuthenticatorBridgeConnectionType,
|
||||||
|
symmetricKeyStorageProvider: SymmetricKeyStorageProvider,
|
||||||
|
): AuthenticatorBridgeManager = AuthenticatorBridgeManagerImpl(
|
||||||
|
context = applicationContext,
|
||||||
|
connectionType = connectionType,
|
||||||
|
symmetricKeyStorageProvider = symmetricKeyStorageProvider,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an API to make it simpler for consuming applications to
|
||||||
|
* query [IAuthenticatorBridgeService].
|
||||||
|
*/
|
||||||
|
interface AuthenticatorBridgeManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State flow representing the current [AccountSyncState].
|
||||||
|
*/
|
||||||
|
val accountSyncStateFlow: StateFlow<AccountSyncState>
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.util.toPackageName
|
||||||
|
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.AuthenticatorBridgeCallbackProvider
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.StubAuthenticatorBridgeCallbackProvider
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider
|
||||||
|
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||||
|
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
private const val AUTHENTICATOR_BRIDGE_SERVICE_CLASS =
|
||||||
|
"com.x8bit.bitwarden.data.platform.service.AuthenticatorBridgeService"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [AuthenticatorBridgeManager].
|
||||||
|
*
|
||||||
|
* @param context The Context that will be used to bind to AuthenticatorBridgeService.
|
||||||
|
* @param connectionType Specifies which build variant to connect to.
|
||||||
|
* @param symmetricKeyStorageProvider Provides access to local storage of the symmetric encryption
|
||||||
|
* key.
|
||||||
|
* @param callbackProvider Provides a way to construct a service callback that can be mocked in
|
||||||
|
* tests.
|
||||||
|
* @param processLifecycleOwner Lifecycle owner that is used to listen for start/stop
|
||||||
|
* lifecycle events.
|
||||||
|
*/
|
||||||
|
internal class AuthenticatorBridgeManagerImpl(
|
||||||
|
private val connectionType: AuthenticatorBridgeConnectionType,
|
||||||
|
private val symmetricKeyStorageProvider: SymmetricKeyStorageProvider,
|
||||||
|
callbackProvider: AuthenticatorBridgeCallbackProvider = StubAuthenticatorBridgeCallbackProvider(),
|
||||||
|
context: Context,
|
||||||
|
processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get(),
|
||||||
|
) : AuthenticatorBridgeManager {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main AuthenticatorBridgeService access point.
|
||||||
|
*/
|
||||||
|
private var bridgeService: IAuthenticatorBridgeService? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal state of [accountSyncStateFlow].
|
||||||
|
*/
|
||||||
|
private val mutableSharedAccountsStateFlow: MutableStateFlow<AccountSyncState> =
|
||||||
|
MutableStateFlow(
|
||||||
|
if (isBitwardenAppInstalled()) {
|
||||||
|
AccountSyncState.Loading
|
||||||
|
} else {
|
||||||
|
AccountSyncState.AppNotInstalled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback registered with AuthenticatorBridgeService.
|
||||||
|
*/
|
||||||
|
private val authenticatorBridgeCallback = callbackProvider.getCallback(::onAccountsSync)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service connection that listens for connected and disconnected service events.
|
||||||
|
*/
|
||||||
|
private val bridgeServiceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
|
onServiceConnected(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
onServiceDisconnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val accountSyncStateFlow: StateFlow<AccountSyncState> =
|
||||||
|
mutableSharedAccountsStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Listen for lifecycle events
|
||||||
|
processLifecycleOwner.lifecycle.addObserver(
|
||||||
|
object : DefaultLifecycleObserver {
|
||||||
|
override fun onStart(owner: LifecycleOwner) {
|
||||||
|
bindService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
|
unbindService()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindService() {
|
||||||
|
if (!isBitwardenAppInstalled()) {
|
||||||
|
mutableSharedAccountsStateFlow.value = AccountSyncState.AppNotInstalled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val intent = Intent().apply {
|
||||||
|
component = ComponentName(
|
||||||
|
connectionType.toPackageName(),
|
||||||
|
AUTHENTICATOR_BRIDGE_SERVICE_CLASS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isBound = try {
|
||||||
|
applicationContext.bindService(
|
||||||
|
intent,
|
||||||
|
bridgeServiceConnection,
|
||||||
|
Context.BIND_AUTO_CREATE,
|
||||||
|
)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
unbindService()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBound) {
|
||||||
|
mutableSharedAccountsStateFlow.value = AccountSyncState.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAccountsSync(data: EncryptedSharedAccountData) {
|
||||||
|
// Received account sync update. Decrypt with local symmetric key and update StateFlow:
|
||||||
|
mutableSharedAccountsStateFlow.value = symmetricKeyStorageProvider.symmetricKey
|
||||||
|
?.let { data.decrypt(it) }
|
||||||
|
?.getOrNull()
|
||||||
|
?.let { AccountSyncState.Success(it.accounts) }
|
||||||
|
?: AccountSyncState.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onServiceConnected(binder: IBinder) {
|
||||||
|
val service = IAuthenticatorBridgeService.Stub
|
||||||
|
.asInterface(binder)
|
||||||
|
.also { bridgeService = it }
|
||||||
|
|
||||||
|
// TODO: Add check for version mismatch between client and server SDKs: BITAU-72
|
||||||
|
|
||||||
|
// Ensure we are using the correct symmetric key:
|
||||||
|
val localKeyFingerprint =
|
||||||
|
symmetricKeyStorageProvider.symmetricKey?.toFingerprint()?.getOrNull()
|
||||||
|
|
||||||
|
// Query bridge service to see if we have a matching symmetric key:
|
||||||
|
val haveCorrectKey = service
|
||||||
|
.safeCall { checkSymmetricEncryptionKeyFingerprint(localKeyFingerprint) }
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it },
|
||||||
|
onFailure = { false },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!haveCorrectKey) {
|
||||||
|
// If we don't have the correct key, query for key:
|
||||||
|
service
|
||||||
|
.safeCall { symmetricEncryptionKeyData }
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
symmetricKeyStorageProvider.symmetricKey = it
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
mutableSharedAccountsStateFlow.value = AccountSyncState.Error
|
||||||
|
unbindService()
|
||||||
|
return
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symmetricKeyStorageProvider.symmetricKey == null) {
|
||||||
|
// This means bridgeService returned a null key, which means we can make
|
||||||
|
// no valid operations. We should disconnect form the service and expose to the
|
||||||
|
// calling application that authenticator sync is not enabled.
|
||||||
|
mutableSharedAccountsStateFlow.value = AccountSyncState.SyncNotEnabled
|
||||||
|
unbindService()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register callback:
|
||||||
|
service.safeCall { registerBridgeServiceCallback(authenticatorBridgeCallback) }
|
||||||
|
|
||||||
|
// Sync data:
|
||||||
|
service.safeCall { syncAccounts() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onServiceDisconnected() {
|
||||||
|
bridgeService = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unbindService() {
|
||||||
|
bridgeService?.safeCall { unregisterBridgeServiceCallback(authenticatorBridgeCallback) }
|
||||||
|
bridgeService = null
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
try {
|
||||||
|
applicationContext.unbindService(bridgeServiceConnection)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// We want to be super safe when unbinding to assure no crashes.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBitwardenAppInstalled(): Boolean =
|
||||||
|
// Check to see if correct Bitwarden app is installed:
|
||||||
|
try {
|
||||||
|
applicationContext.packageManager.getPackageInfo(connectionType.toPackageName(), 0)
|
||||||
|
true
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for wrapping all calls to [IAuthenticatorBridgeService] around try catch.
|
||||||
|
*
|
||||||
|
* This is important because all calls to [IAuthenticatorBridgeService] can throw
|
||||||
|
* DeadObjectExceptions as well as RemoteExceptions.
|
||||||
|
*/
|
||||||
|
private fun <T> IAuthenticatorBridgeService.safeCall(
|
||||||
|
action: IAuthenticatorBridgeService.() -> T,
|
||||||
|
): Result<T> =
|
||||||
|
runCatching {
|
||||||
|
this.let { action.invoke(it) }
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager.model
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models various states of account syncing.
|
||||||
|
*/
|
||||||
|
sealed class AccountSyncState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Bitwarden app is not installed and therefore accounts cannot be synced.
|
||||||
|
*/
|
||||||
|
data object AppNotInstalled : AccountSyncState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Something went wrong syncing accounts.
|
||||||
|
*/
|
||||||
|
data object Error : AccountSyncState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user needs to enable authenticator syncing from the bitwarden app.
|
||||||
|
*/
|
||||||
|
data object SyncNotEnabled : AccountSyncState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounts are being synced.
|
||||||
|
*/
|
||||||
|
data object Loading : AccountSyncState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounts successfully synced.
|
||||||
|
*/
|
||||||
|
data class Success(val accounts: List<SharedAccountData.Account>) : AccountSyncState()
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager.model
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models different connection types for [AuthenticatorBridgeManager].
|
||||||
|
*/
|
||||||
|
enum class AuthenticatorBridgeConnectionType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to release build variant.
|
||||||
|
*/
|
||||||
|
RELEASE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to dev build variant.
|
||||||
|
*/
|
||||||
|
DEV,
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a [AuthenticatorBridgeConnectionType] to raw package name for connection.
|
||||||
|
*/
|
||||||
|
internal fun AuthenticatorBridgeConnectionType.toPackageName() =
|
||||||
|
when (this) {
|
||||||
|
AuthenticatorBridgeConnectionType.RELEASE -> "com.x8bit.bitwarden"
|
||||||
|
AuthenticatorBridgeConnectionType.DEV -> "com.x8bit.bitwarden.dev"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.provider
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||||
|
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an implementation of [IAuthenticatorBridgeServiceCallback]. This is useful
|
||||||
|
* for writing unit tests that don't touch Binder logic.
|
||||||
|
*/
|
||||||
|
interface AuthenticatorBridgeCallbackProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [IAuthenticatorBridgeServiceCallback] that will call delegate [onAccountsSync] call
|
||||||
|
* to the given lambda.
|
||||||
|
*
|
||||||
|
* @param onAccountsSync Lambda that will be invoked when [onAccountsSync] calls back.
|
||||||
|
*/
|
||||||
|
fun getCallback(
|
||||||
|
onAccountsSync: (EncryptedSharedAccountData) -> Unit,
|
||||||
|
): IAuthenticatorBridgeServiceCallback
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.provider
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||||
|
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [AuthenticatorBridgeCallbackProvider] that provides a live
|
||||||
|
* [IAuthenticatorBridgeServiceCallback.Stub] implementation.
|
||||||
|
*/
|
||||||
|
class StubAuthenticatorBridgeCallbackProvider : AuthenticatorBridgeCallbackProvider {
|
||||||
|
|
||||||
|
override fun getCallback(
|
||||||
|
onAccountsSync: (EncryptedSharedAccountData) -> Unit,
|
||||||
|
): IAuthenticatorBridgeServiceCallback = object : IAuthenticatorBridgeServiceCallback.Stub() {
|
||||||
|
|
||||||
|
override fun onAccountsSync(data: EncryptedSharedAccountData) = onAccountsSync.invoke(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.provider
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a way for calling Applications to implement symmetric key storage.
|
||||||
|
*/
|
||||||
|
interface SymmetricKeyStorageProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stored symmetric encryption key.
|
||||||
|
*/
|
||||||
|
var symmetricKey: SymmetricEncryptionKeyData?
|
||||||
|
}
|
|
@ -0,0 +1,440 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeService
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AccountSyncState
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||||
|
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||||
|
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||||
|
import com.bitwarden.authenticatorbridge.util.FakeLifecycleOwner
|
||||||
|
import com.bitwarden.authenticatorbridge.util.FakeSymmetricKeyStorageProvider
|
||||||
|
import com.bitwarden.authenticatorbridge.util.TestAuthenticatorBridgeCallbackProvider
|
||||||
|
import com.bitwarden.authenticatorbridge.util.decrypt
|
||||||
|
import com.bitwarden.authenticatorbridge.util.generateSecretKey
|
||||||
|
import com.bitwarden.authenticatorbridge.util.toFingerprint
|
||||||
|
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkConstructor
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.unmockkConstructor
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AuthenticatorBridgeManagerTest {
|
||||||
|
|
||||||
|
private val context = mockk<Context> {
|
||||||
|
every { applicationContext } returns this
|
||||||
|
every {
|
||||||
|
packageManager.getPackageInfo("com.x8bit.bitwarden.dev", 0)
|
||||||
|
} returns mockk()
|
||||||
|
}
|
||||||
|
private val mockBridgeService: IAuthenticatorBridgeService = mockk()
|
||||||
|
private val fakeLifecycleOwner = FakeLifecycleOwner()
|
||||||
|
private val fakeSymmetricKeyStorageProvider = FakeSymmetricKeyStorageProvider()
|
||||||
|
private val testAuthenticatorBridgeCallbackProvider = TestAuthenticatorBridgeCallbackProvider()
|
||||||
|
|
||||||
|
private val manager: AuthenticatorBridgeManagerImpl = AuthenticatorBridgeManagerImpl(
|
||||||
|
context = context,
|
||||||
|
connectionType = AuthenticatorBridgeConnectionType.DEV,
|
||||||
|
symmetricKeyStorageProvider = fakeSymmetricKeyStorageProvider,
|
||||||
|
callbackProvider = testAuthenticatorBridgeCallbackProvider,
|
||||||
|
processLifecycleOwner = fakeLifecycleOwner,
|
||||||
|
)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
mockkConstructor(Intent::class)
|
||||||
|
mockkStatic(IAuthenticatorBridgeService.Stub::class)
|
||||||
|
mockkStatic(EncryptedSharedAccountData::decrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun teardown() {
|
||||||
|
unmockkConstructor(Intent::class)
|
||||||
|
unmockkStatic(IAuthenticatorBridgeService.Stub::class)
|
||||||
|
unmockkStatic(EncryptedSharedAccountData::decrypt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial AccountSyncState should be Loading when Bitwarden app is present`() {
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial AccountSyncState should be AppNotInstalled when Bitwarden app is not present`() {
|
||||||
|
every {
|
||||||
|
context.packageManager.getPackageInfo("com.x8bit.bitwarden.dev", 0)
|
||||||
|
} throws NameNotFoundException()
|
||||||
|
val manager = AuthenticatorBridgeManagerImpl(
|
||||||
|
context = context,
|
||||||
|
connectionType = AuthenticatorBridgeConnectionType.DEV,
|
||||||
|
symmetricKeyStorageProvider = fakeSymmetricKeyStorageProvider,
|
||||||
|
callbackProvider = testAuthenticatorBridgeCallbackProvider,
|
||||||
|
processLifecycleOwner = fakeLifecycleOwner,
|
||||||
|
)
|
||||||
|
assertEquals(AccountSyncState.AppNotInstalled, manager.accountSyncStateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onStart when bindService fails should set state to error`() {
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
|
||||||
|
every { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) } returns false
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Error, manager.accountSyncStateFlow.value)
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onStart when Bitwarden app is not present should set state to AppNotInstalled`() {
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every {
|
||||||
|
context.packageManager.getPackageInfo("com.x8bit.bitwarden.dev", 0)
|
||||||
|
} throws NameNotFoundException()
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
|
||||||
|
every {
|
||||||
|
context.bindService(any(), any(), Context.BIND_AUTO_CREATE)
|
||||||
|
} throws SecurityException()
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.AppNotInstalled, manager.accountSyncStateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onStart when bindService throws security exception should set state to error`() {
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
|
||||||
|
every {
|
||||||
|
context.bindService(any(), any(), Context.BIND_AUTO_CREATE)
|
||||||
|
} throws SecurityException()
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Error, manager.accountSyncStateFlow.value)
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onStart when Bitwarden app is present and bindService succeeds should set state to Loading before service calls back`() {
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
|
||||||
|
every { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) } returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onServiceConnected when symmetric key is not present and service returns null symmetric key state should be SyncNotEnabled and should unbind service`() {
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = null
|
||||||
|
every { mockBridgeService.symmetricEncryptionKeyData } returns null
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.SyncNotEnabled, manager.accountSyncStateFlow.value)
|
||||||
|
verify { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { context.unbindService(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onServiceConnected when symmetric key does not match should set symmetric key from service`() {
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = null
|
||||||
|
every { mockBridgeService.symmetricEncryptionKeyData } returns SYMMETRIC_KEY
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
assertEquals(SYMMETRIC_KEY, fakeSymmetricKeyStorageProvider.symmetricKey)
|
||||||
|
verify { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onServiceConnected when symmetric key does match should not query for symmetric key`() {
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = SYMMETRIC_KEY
|
||||||
|
every {
|
||||||
|
mockBridgeService.checkSymmetricEncryptionKeyFingerprint(
|
||||||
|
SYMMETRIC_KEY.toFingerprint().getOrNull()
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
assertEquals(SYMMETRIC_KEY, fakeSymmetricKeyStorageProvider.symmetricKey)
|
||||||
|
verify(exactly = 0) { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { mockBridgeService.registerBridgeServiceCallback(any()) }
|
||||||
|
verify { mockBridgeService.syncAccounts() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onServiceConnected when symmetric key does not match and query for key fails state should be error`() {
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = SYMMETRIC_KEY
|
||||||
|
every {
|
||||||
|
mockBridgeService.checkSymmetricEncryptionKeyFingerprint(
|
||||||
|
SYMMETRIC_KEY.toFingerprint().getOrNull()
|
||||||
|
)
|
||||||
|
} returns false
|
||||||
|
every {
|
||||||
|
mockBridgeService.symmetricEncryptionKeyData
|
||||||
|
} throws RuntimeException()
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Error, manager.accountSyncStateFlow.value)
|
||||||
|
assertEquals(SYMMETRIC_KEY, fakeSymmetricKeyStorageProvider.symmetricKey)
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { context.unbindService(any()) }
|
||||||
|
verify { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onAccountsSync should set AccountSyncState to decrypted response`() {
|
||||||
|
val expectedAccounts = listOf<SharedAccountData.Account>(
|
||||||
|
mockk()
|
||||||
|
)
|
||||||
|
val encryptedAccounts: EncryptedSharedAccountData = mockk()
|
||||||
|
val decryptedAccounts: SharedAccountData = mockk {
|
||||||
|
every { accounts } returns expectedAccounts
|
||||||
|
}
|
||||||
|
mockkStatic(EncryptedSharedAccountData::decrypt)
|
||||||
|
every { encryptedAccounts.decrypt(SYMMETRIC_KEY) } returns Result.success(decryptedAccounts)
|
||||||
|
every {
|
||||||
|
mockBridgeService.checkSymmetricEncryptionKeyFingerprint(
|
||||||
|
SYMMETRIC_KEY.toFingerprint().getOrNull()
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = SYMMETRIC_KEY
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val callback = slot<IAuthenticatorBridgeServiceCallback>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every { mockBridgeService.registerBridgeServiceCallback(capture(callback)) } just runs
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
callback.captured.onAccountsSync(encryptedAccounts)
|
||||||
|
assertEquals(AccountSyncState.Success(expectedAccounts), manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
assertEquals(SYMMETRIC_KEY, fakeSymmetricKeyStorageProvider.symmetricKey)
|
||||||
|
verify(exactly = 0) { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { mockBridgeService.registerBridgeServiceCallback(any()) }
|
||||||
|
verify { mockBridgeService.syncAccounts() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onAccountsSync when symmetric key is missing should not set state to Success`() {
|
||||||
|
val encryptedAccounts: EncryptedSharedAccountData = mockk()
|
||||||
|
every {
|
||||||
|
mockBridgeService.checkSymmetricEncryptionKeyFingerprint(
|
||||||
|
SYMMETRIC_KEY.toFingerprint().getOrNull()
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = SYMMETRIC_KEY
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val callback = slot<IAuthenticatorBridgeServiceCallback>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every { mockBridgeService.registerBridgeServiceCallback(capture(callback)) } just runs
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = null
|
||||||
|
callback.captured.onAccountsSync(encryptedAccounts)
|
||||||
|
assertEquals(AccountSyncState.Error, manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
verify(exactly = 0) { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { mockBridgeService.registerBridgeServiceCallback(any()) }
|
||||||
|
verify { mockBridgeService.syncAccounts() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `onAccountsSync when decryption fails state should be error`() {
|
||||||
|
val encryptedAccounts: EncryptedSharedAccountData = mockk()
|
||||||
|
mockkStatic(EncryptedSharedAccountData::decrypt)
|
||||||
|
every { encryptedAccounts.decrypt(SYMMETRIC_KEY) } returns Result.failure(RuntimeException())
|
||||||
|
every {
|
||||||
|
mockBridgeService.checkSymmetricEncryptionKeyFingerprint(
|
||||||
|
SYMMETRIC_KEY.toFingerprint().getOrNull()
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
fakeSymmetricKeyStorageProvider.symmetricKey = SYMMETRIC_KEY
|
||||||
|
val serviceConnection = slot<ServiceConnection>()
|
||||||
|
val callback = slot<IAuthenticatorBridgeServiceCallback>()
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every { IAuthenticatorBridgeService.Stub.asInterface(any()) } returns mockBridgeService
|
||||||
|
every { mockBridgeService.registerBridgeServiceCallback(capture(callback)) } just runs
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
every {
|
||||||
|
context.bindService(
|
||||||
|
any(),
|
||||||
|
capture(serviceConnection),
|
||||||
|
Context.BIND_AUTO_CREATE
|
||||||
|
)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
serviceConnection.captured.onServiceConnected(mockk(), mockk())
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
callback.captured.onAccountsSync(encryptedAccounts)
|
||||||
|
assertEquals(AccountSyncState.Error, manager.accountSyncStateFlow.value)
|
||||||
|
|
||||||
|
assertEquals(SYMMETRIC_KEY, fakeSymmetricKeyStorageProvider.symmetricKey)
|
||||||
|
verify(exactly = 0) { mockBridgeService.symmetricEncryptionKeyData }
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { mockBridgeService.registerBridgeServiceCallback(any()) }
|
||||||
|
verify { mockBridgeService.syncAccounts() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onStop when service has been started should unbind service`() {
|
||||||
|
val mockIntent: Intent = mockk()
|
||||||
|
every {
|
||||||
|
anyConstructed<Intent>().setComponent(any())
|
||||||
|
} returns mockIntent
|
||||||
|
|
||||||
|
every { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) } returns true
|
||||||
|
every { context.unbindService(any()) } just runs
|
||||||
|
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStart()
|
||||||
|
fakeLifecycleOwner.lifecycle.dispatchOnStop()
|
||||||
|
|
||||||
|
assertEquals(AccountSyncState.Loading, manager.accountSyncStateFlow.value)
|
||||||
|
verify { context.bindService(any(), any(), Context.BIND_AUTO_CREATE) }
|
||||||
|
verify { context.unbindService(any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SYMMETRIC_KEY = generateSecretKey()
|
||||||
|
.getOrThrow()
|
||||||
|
.encoded
|
||||||
|
.toSymmetricEncryptionKeyData()
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.manager.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.manager.model.AuthenticatorBridgeConnectionType
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AuthenticatorBridgeConnectionTypeExtensionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toPackageName RELEASE should map to correct release package`() {
|
||||||
|
assertEquals(
|
||||||
|
"com.x8bit.bitwarden",
|
||||||
|
AuthenticatorBridgeConnectionType.RELEASE.toPackageName()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toPackageName DEV should map to correct dev package`() {
|
||||||
|
assertEquals(
|
||||||
|
"com.x8bit.bitwarden.dev",
|
||||||
|
AuthenticatorBridgeConnectionType.DEV.toPackageName()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.util
|
||||||
|
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake implementation of [LifecycleOwner] and [Lifecycle] for testing purposes.
|
||||||
|
*/
|
||||||
|
class FakeLifecycle(
|
||||||
|
private val lifecycleOwner: LifecycleOwner,
|
||||||
|
) : Lifecycle() {
|
||||||
|
private val observers = mutableSetOf<DefaultLifecycleObserver>()
|
||||||
|
|
||||||
|
override var currentState: State = State.INITIALIZED
|
||||||
|
|
||||||
|
override fun addObserver(observer: LifecycleObserver) {
|
||||||
|
observers += (observer as DefaultLifecycleObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeObserver(observer: LifecycleObserver) {
|
||||||
|
observers -= (observer as DefaultLifecycleObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers [DefaultLifecycleObserver.onStart] calls for each registered observer.
|
||||||
|
*/
|
||||||
|
fun dispatchOnStart() {
|
||||||
|
currentState = State.STARTED
|
||||||
|
observers.forEach { observer ->
|
||||||
|
observer.onStart(lifecycleOwner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers [DefaultLifecycleObserver.onStop] calls for each registered observer.
|
||||||
|
*/
|
||||||
|
fun dispatchOnStop() {
|
||||||
|
currentState = State.CREATED
|
||||||
|
observers.forEach { observer ->
|
||||||
|
observer.onStop(lifecycleOwner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.util
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake implementation of [LifecycleOwner] for testing purposes.
|
||||||
|
*/
|
||||||
|
class FakeLifecycleOwner : LifecycleOwner {
|
||||||
|
override val lifecycle: FakeLifecycle = FakeLifecycle(lifecycleOwner = this)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.model.SymmetricEncryptionKeyData
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.SymmetricKeyStorageProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake implementation of [SymmetricKeyStorageProvider] for testing purposes.
|
||||||
|
*/
|
||||||
|
class FakeSymmetricKeyStorageProvider : SymmetricKeyStorageProvider {
|
||||||
|
override var symmetricKey: SymmetricEncryptionKeyData? = null
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.bitwarden.authenticatorbridge.util
|
||||||
|
|
||||||
|
import com.bitwarden.authenticatorbridge.IAuthenticatorBridgeServiceCallback
|
||||||
|
import com.bitwarden.authenticatorbridge.model.EncryptedSharedAccountData
|
||||||
|
import com.bitwarden.authenticatorbridge.provider.AuthenticatorBridgeCallbackProvider
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of [AuthenticatorBridgeCallbackProvider] that provides a testable
|
||||||
|
* [IAuthenticatorBridgeServiceCallback.Default] implementation.
|
||||||
|
*/
|
||||||
|
class TestAuthenticatorBridgeCallbackProvider : AuthenticatorBridgeCallbackProvider {
|
||||||
|
|
||||||
|
override fun getCallback(
|
||||||
|
onAccountsSync: (EncryptedSharedAccountData) -> Unit,
|
||||||
|
): IAuthenticatorBridgeServiceCallback = object : IAuthenticatorBridgeServiceCallback.Default() {
|
||||||
|
|
||||||
|
override fun onAccountsSync(data: EncryptedSharedAccountData) = onAccountsSync.invoke(data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -97,6 +97,7 @@ junit-junit5 = { module = "org.junit.jupiter:junit-jupiter", version.ref = "juni
|
||||||
junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" }
|
junit-vintage = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" }
|
||||||
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
|
||||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
||||||
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||||
mockk-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
mockk-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||||
|
|
Loading…
Reference in a new issue