mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Refactor logic for auth requests & decrypt all fingerprints (#800)
This commit is contained in:
parent
a4e99745bc
commit
a7e393e325
16 changed files with 367 additions and 194 deletions
|
@ -16,7 +16,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||
|
@ -184,11 +183,6 @@ interface AuthRepository : AuthenticatorProvider {
|
|||
*/
|
||||
suspend fun getAuthRequests(): AuthRequestsResult
|
||||
|
||||
/**
|
||||
* Gets a unique fingerprint phrase for this user.
|
||||
*/
|
||||
suspend fun getFingerprintPhrase(email: String): UserFingerprintResult
|
||||
|
||||
/**
|
||||
* Get a [Boolean] indicating whether this is a known device.
|
||||
*/
|
||||
|
|
|
@ -582,17 +582,15 @@ class AuthRepositoryImpl(
|
|||
authSdkSource
|
||||
.getNewAuthRequest(email)
|
||||
.flatMap { authRequest ->
|
||||
newAuthRequestService.createAuthRequest(
|
||||
newAuthRequestService
|
||||
.createAuthRequest(
|
||||
email = email,
|
||||
publicKey = authRequest.publicKey,
|
||||
deviceId = authDiskSource.uniqueAppId,
|
||||
accessCode = authRequest.accessCode,
|
||||
fingerprint = authRequest.fingerprint,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onSuccess = { request ->
|
||||
.map { request ->
|
||||
AuthRequestResult.Success(
|
||||
authRequest = AuthRequest(
|
||||
id = request.id,
|
||||
|
@ -605,19 +603,27 @@ class AuthRepositoryImpl(
|
|||
responseDate = request.responseDate,
|
||||
requestApproved = request.requestApproved ?: false,
|
||||
originUrl = request.originUrl,
|
||||
fingerprint = authRequest.fingerprint,
|
||||
),
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onSuccess = { it },
|
||||
)
|
||||
|
||||
override suspend fun getAuthRequests(): AuthRequestsResult =
|
||||
authRequestsService.getAuthRequests()
|
||||
authRequestsService
|
||||
.getAuthRequests()
|
||||
.fold(
|
||||
onFailure = { AuthRequestsResult.Error },
|
||||
onSuccess = { response ->
|
||||
AuthRequestsResult.Success(
|
||||
authRequests = response.authRequests.map { request ->
|
||||
AuthRequest(
|
||||
authRequests = response.authRequests.mapNotNull { request ->
|
||||
when (val result = getFingerprintPhrase(request.publicKey)) {
|
||||
is UserFingerprintResult.Error -> null
|
||||
is UserFingerprintResult.Success -> AuthRequest(
|
||||
id = request.id,
|
||||
publicKey = request.publicKey,
|
||||
platform = request.platform,
|
||||
|
@ -628,27 +634,12 @@ class AuthRepositoryImpl(
|
|||
responseDate = request.responseDate,
|
||||
requestApproved = request.requestApproved ?: false,
|
||||
originUrl = request.originUrl,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun getFingerprintPhrase(
|
||||
email: String,
|
||||
): UserFingerprintResult =
|
||||
authSdkSource
|
||||
.getNewAuthRequest(email)
|
||||
.flatMap { requestResponse ->
|
||||
authSdkSource
|
||||
.getUserFingerprint(
|
||||
email = email,
|
||||
publicKey = requestResponse.publicKey,
|
||||
fingerprint = result.fingerprint,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
|
@ -690,6 +681,23 @@ class AuthRepositoryImpl(
|
|||
},
|
||||
)
|
||||
|
||||
private suspend fun getFingerprintPhrase(
|
||||
publicKey: String,
|
||||
): UserFingerprintResult {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return UserFingerprintResult.Error
|
||||
|
||||
return authSdkSource
|
||||
.getUserFingerprint(
|
||||
email = profile.email,
|
||||
publicKey = publicKey,
|
||||
)
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the remembered two-factor token associated with the user's email, if applicable.
|
||||
*/
|
||||
|
|
|
@ -15,6 +15,7 @@ import java.time.ZonedDateTime
|
|||
* @param responseDate The date & time on which this request was responded to.
|
||||
* @param requestApproved Whether this request was approved.
|
||||
* @param originUrl The origin URL of this auth request.
|
||||
* @param fingerprint The fingerprint of this auth request.
|
||||
*/
|
||||
data class AuthRequest(
|
||||
val id: String,
|
||||
|
@ -27,4 +28,5 @@ data class AuthRequest(
|
|||
val responseDate: ZonedDateTime?,
|
||||
val requestApproved: Boolean,
|
||||
val originUrl: String,
|
||||
val fingerprint: String,
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.data.platform.repository
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||
|
@ -113,6 +114,11 @@ interface SettingsRepository {
|
|||
*/
|
||||
fun disableAutofill()
|
||||
|
||||
/**
|
||||
* Gets the unique fingerprint phrase for the current user.
|
||||
*/
|
||||
suspend fun getUserFingerprint(): UserFingerprintResult
|
||||
|
||||
/**
|
||||
* Sets default values for various settings for the given [userId] if necessary. This is
|
||||
* typically used when logging into a new account.
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
|||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
|
@ -252,6 +253,19 @@ class SettingsRepositoryImpl(
|
|||
mutableIsAutofillEnabledStateFlow.value = false
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override suspend fun getUserFingerprint(): UserFingerprintResult {
|
||||
val userId = activeUserId
|
||||
?: return UserFingerprintResult.Error
|
||||
|
||||
return vaultSdkSource
|
||||
.getUserFingerprint(userId)
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun setDefaultsIfNecessary(userId: String) {
|
||||
// Set Vault Settings defaults
|
||||
if (!isVaultTimeoutActionSet(userId = userId)) {
|
||||
|
|
|
@ -68,6 +68,11 @@ interface VaultSdkSource {
|
|||
*/
|
||||
suspend fun getUserEncryptionKey(userId: String): Result<String>
|
||||
|
||||
/**
|
||||
* Gets the user's fingerprint.
|
||||
*/
|
||||
suspend fun getUserFingerprint(userId: String): Result<String>
|
||||
|
||||
/**
|
||||
* Attempts to initialize cryptography functionality for an individual user with the given
|
||||
* [userId] for the Bitwarden SDK with a given [InitUserCryptoRequest].
|
||||
|
|
|
@ -65,6 +65,15 @@ class VaultSdkSourceImpl(
|
|||
.getUserEncryptionKey()
|
||||
}
|
||||
|
||||
override suspend fun getUserFingerprint(
|
||||
userId: String,
|
||||
): Result<String> =
|
||||
runCatching {
|
||||
getClient(userId = userId)
|
||||
.platform()
|
||||
.userFingerprint(userId)
|
||||
}
|
||||
|
||||
override suspend fun initializeCrypto(
|
||||
userId: String,
|
||||
request: InitUserCryptoRequest,
|
||||
|
|
|
@ -6,7 +6,6 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
|
@ -34,14 +33,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
) {
|
||||
init {
|
||||
sendNewAuthRequest()
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = authRepository.getFingerprintPhrase(state.emailAddress),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: LoginWithDeviceAction) {
|
||||
|
@ -53,10 +44,6 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> {
|
||||
handleNewAuthRequestResultReceived(action)
|
||||
}
|
||||
|
||||
is LoginWithDeviceAction.Internal.FingerprintPhraseReceived -> {
|
||||
handleFingerprintPhraseReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,27 +62,20 @@ class LoginWithDeviceViewModel @Inject constructor(
|
|||
|
||||
private fun handleNewAuthRequestResultReceived(
|
||||
action: LoginWithDeviceAction.Internal.NewAuthRequestResultReceive,
|
||||
) {
|
||||
if (action.result is AuthRequestResult.Error) {
|
||||
// TODO BIT-1563 handle error
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFingerprintPhraseReceived(
|
||||
action: LoginWithDeviceAction.Internal.FingerprintPhraseReceived,
|
||||
) {
|
||||
when (action.result) {
|
||||
is UserFingerprintResult.Success -> {
|
||||
is AuthRequestResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = action.result.fingerprint,
|
||||
fingerprintPhrase = action.result.authRequest.fingerprint,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is UserFingerprintResult.Error -> {
|
||||
is AuthRequestResult.Error -> {
|
||||
// TODO BIT-1563 display error dialog
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Error(
|
||||
|
@ -209,12 +189,5 @@ sealed class LoginWithDeviceAction {
|
|||
data class NewAuthRequestResultReceive(
|
||||
val result: AuthRequestResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* A fingerprint phrase for this user has been received.
|
||||
*/
|
||||
data class FingerprintPhraseReceived(
|
||||
val result: UserFingerprintResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
|
||||
|
@ -18,6 +19,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -38,7 +40,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: AccountSecurityState(
|
||||
dialog = null,
|
||||
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
||||
fingerprintPhrase = "".asText(), // This will be filled in dynamically
|
||||
isApproveLoginRequestsEnabled = settingsRepository.isApprovePasswordlessLoginsEnabled,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||
|
@ -59,6 +61,14 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
fingerprintResult = settingsRepository.getUserFingerprint(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
||||
|
@ -92,6 +102,10 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
is AccountSecurityAction.PushNotificationConfirm -> {
|
||||
handlePushNotificationConfirm()
|
||||
}
|
||||
|
||||
is AccountSecurityAction.Internal.FingerprintResultReceive -> {
|
||||
handleFingerprintResultReceived(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAccountFingerprintPhraseClick() {
|
||||
|
@ -239,6 +253,20 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFingerprintResultReceived(
|
||||
action: AccountSecurityAction.Internal.FingerprintResultReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
fingerprintPhrase = when (val result = action.fingerprintResult) {
|
||||
is UserFingerprintResult.Success -> result.fingerprint.asText()
|
||||
// This should never fail for an unlocked account.
|
||||
is UserFingerprintResult.Error -> "".asText()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -474,4 +502,16 @@ sealed class AccountSecurityAction {
|
|||
override val isUnlockWithPinEnabled: Boolean get() = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions that can be sent by the view model itself.
|
||||
*/
|
||||
sealed class Internal : AccountSecurityAction() {
|
||||
/**
|
||||
* A fingerprint has been received.
|
||||
*/
|
||||
data class FingerprintResultReceive(
|
||||
val fingerprintResult: UserFingerprintResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ class PendingRequestsViewModel @Inject constructor(
|
|||
PendingRequestsState.ViewState.Content(
|
||||
requests = result.authRequests.map { authRequest ->
|
||||
PendingRequestsState.ViewState.Content.PendingLoginRequest(
|
||||
fingerprintPhrase = authRequest.publicKey,
|
||||
fingerprintPhrase = authRequest.fingerprint,
|
||||
platform = authRequest.platform,
|
||||
timestamp = dateTimeFormatter.format(
|
||||
authRequest.creationDate,
|
||||
|
|
|
@ -50,7 +50,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
|||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
|
@ -2135,6 +2134,7 @@ class AuthRepositoryTest {
|
|||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = fingerprint,
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
|
@ -2179,11 +2179,12 @@ class AuthRepositoryTest {
|
|||
|
||||
@Test
|
||||
fun `getAuthRequests should return success when service returns success`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
val responseJson = AuthRequestsResponseJson(
|
||||
authRequests = listOf(
|
||||
AuthRequestsResponseJson.AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
|
@ -2199,7 +2200,46 @@ class AuthRepositoryTest {
|
|||
authRequests = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = fingerprint,
|
||||
),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
} returns Result.success(fingerprint)
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequests()
|
||||
} returns responseJson.asSuccess()
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.getAuthRequests()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRequestsService.getAuthRequests()
|
||||
authSdkSource.getUserFingerprint(EMAIL, PUBLIC_KEY)
|
||||
}
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAuthRequests should return empty list when user profile is null`() = runTest {
|
||||
val responseJson = AuthRequestsResponseJson(
|
||||
authRequests = listOf(
|
||||
AuthRequestsResponseJson.AuthRequest(
|
||||
id = "1",
|
||||
publicKey = PUBLIC_KEY,
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
|
@ -2211,6 +2251,7 @@ class AuthRepositoryTest {
|
|||
),
|
||||
),
|
||||
)
|
||||
val expected = AuthRequestsResult.Success(emptyList())
|
||||
coEvery {
|
||||
authRequestsService.getAuthRequests()
|
||||
} returns responseJson.asSuccess()
|
||||
|
@ -2223,68 +2264,6 @@ class AuthRepositoryTest {
|
|||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return failure when source returns failure`() = runTest {
|
||||
coEvery {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
} returns Result.success(
|
||||
mockk<AuthRequestResponse> {
|
||||
every { publicKey } returns PUBLIC_KEY
|
||||
},
|
||||
)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
} returns Result.failure(Throwable())
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.getFingerprintPhrase(EMAIL)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return success when source returns success`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
coEvery {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
} returns Result.success(
|
||||
AuthRequestResponse(
|
||||
fingerprint = fingerprint,
|
||||
publicKey = PUBLIC_KEY,
|
||||
privateKey = "key",
|
||||
accessCode = "accessCode",
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
} returns Result.success(fingerprint)
|
||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||
|
||||
val result = repository.getFingerprintPhrase(EMAIL)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authSdkSource.getNewAuthRequest(EMAIL)
|
||||
authSdkSource.getUserFingerprint(
|
||||
email = EMAIL,
|
||||
publicKey = PUBLIC_KEY,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Success(fingerprint), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getIsKnownDevice should return failure when service returns failure`() = runTest {
|
||||
coEvery {
|
||||
|
@ -2462,7 +2441,7 @@ class AuthRepositoryTest {
|
|||
private val ACCOUNT_1 = AccountJson(
|
||||
profile = AccountJson.Profile(
|
||||
userId = USER_ID_1,
|
||||
email = "test@bitwarden.com",
|
||||
email = EMAIL,
|
||||
isEmailVerified = true,
|
||||
name = "Bitwarden Tester",
|
||||
hasPremium = false,
|
||||
|
|
|
@ -5,6 +5,7 @@ import app.cash.turbine.test
|
|||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppForegroundManager
|
||||
|
@ -555,6 +556,56 @@ class SettingsRepositoryTest {
|
|||
verify { autofillManager.disableAutofillServices() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return failure with no active user`() = runTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = settingsRepository.getUserFingerprint()
|
||||
|
||||
assertEquals(UserFingerprintResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return failure with active user when source returns failure`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
coEvery {
|
||||
vaultSdkSource.getUserFingerprint(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
)
|
||||
} returns Result.failure(Throwable())
|
||||
|
||||
val result = settingsRepository.getUserFingerprint()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserFingerprint(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Error, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should return success with active user when source returns success`() =
|
||||
runTest {
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
val fingerprint = "fingerprint"
|
||||
coEvery {
|
||||
vaultSdkSource.getUserFingerprint(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
)
|
||||
} returns Result.success(fingerprint)
|
||||
|
||||
val result = settingsRepository.getUserFingerprint()
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultSdkSource.getUserFingerprint(
|
||||
userId = MOCK_USER_STATE.activeUserId,
|
||||
)
|
||||
}
|
||||
assertEquals(UserFingerprintResult.Success(fingerprint), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPullToRefreshEnabledFlow should react to changes in SettingsDiskSource`() = runTest {
|
||||
val userId = "userId"
|
||||
|
@ -728,7 +779,8 @@ class SettingsRepositoryTest {
|
|||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() = runTest {
|
||||
fun `isScreenCaptureAllowed property should update SettingsDiskSource and emit changes`() =
|
||||
runTest {
|
||||
val userId = "userId"
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import com.bitwarden.sdk.BitwardenException
|
|||
import com.bitwarden.sdk.Client
|
||||
import com.bitwarden.sdk.ClientCrypto
|
||||
import com.bitwarden.sdk.ClientPasswordHistory
|
||||
import com.bitwarden.sdk.ClientPlatform
|
||||
import com.bitwarden.sdk.ClientVault
|
||||
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
|
@ -42,12 +43,14 @@ import org.junit.jupiter.api.Test
|
|||
@Suppress("LargeClass")
|
||||
class VaultSdkSourceTest {
|
||||
private val clientCrypto = mockk<ClientCrypto>()
|
||||
private val clientPlatform = mockk<ClientPlatform>()
|
||||
private val clientPasswordHistory = mockk<ClientPasswordHistory>()
|
||||
private val clientVault = mockk<ClientVault>() {
|
||||
every { passwordHistory() } returns clientPasswordHistory
|
||||
}
|
||||
private val client = mockk<Client>() {
|
||||
every { vault() } returns clientVault
|
||||
every { platform() } returns clientPlatform
|
||||
every { crypto() } returns clientCrypto
|
||||
}
|
||||
private val sdkClientManager = mockk<SdkClientManager> {
|
||||
|
@ -132,6 +135,28 @@ class VaultSdkSourceTest {
|
|||
verify { sdkClientManager.getOrCreateClient(userId = userId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUserFingerprint should call SDK and return a Result with correct data`() = runBlocking {
|
||||
val userId = "userId"
|
||||
val expectedResult = "fingerprint"
|
||||
coEvery {
|
||||
clientPlatform.userFingerprint(
|
||||
fingerprintMaterial = userId,
|
||||
)
|
||||
} returns expectedResult
|
||||
|
||||
val result = vaultSdkSource.getUserFingerprint(userId)
|
||||
assertEquals(
|
||||
expectedResult.asSuccess(),
|
||||
result,
|
||||
)
|
||||
coVerify {
|
||||
clientPlatform.userFingerprint(
|
||||
fingerprintMaterial = userId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initializeUserCrypto with sdk success should return InitializeCryptoResult Success`() =
|
||||
runBlocking {
|
||||
|
|
|
@ -4,26 +4,25 @@ import androidx.lifecycle.SavedStateHandle
|
|||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
coEvery {
|
||||
getFingerprintPhrase(EMAIL)
|
||||
} returns UserFingerprintResult.Success("initialFingerprint")
|
||||
coEvery {
|
||||
createAuthRequest(EMAIL)
|
||||
} returns mockk<AuthRequestResult.Success>()
|
||||
} returns AuthRequestResult.Success(AUTH_REQUEST)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -33,7 +32,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
coVerify { authRepository.createAuthRequest(EMAIL) }
|
||||
coVerify { authRepository.getFingerprintPhrase(EMAIL) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -42,14 +40,11 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
|
||||
coEvery {
|
||||
authRepository.createAuthRequest(newEmail)
|
||||
} returns mockk<AuthRequestResult.Success>()
|
||||
coEvery {
|
||||
authRepository.getFingerprintPhrase(newEmail)
|
||||
} returns UserFingerprintResult.Success("initialFingerprint")
|
||||
} returns AuthRequestResult.Success(AUTH_REQUEST)
|
||||
val state = LoginWithDeviceState(
|
||||
emailAddress = newEmail,
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "initialFingerprint",
|
||||
fingerprintPhrase = FINGERPRINT,
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(state)
|
||||
|
@ -58,7 +53,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
coVerify {
|
||||
authRepository.createAuthRequest(newEmail)
|
||||
authRepository.getFingerprintPhrase(newEmail)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,13 +95,17 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on fingerprint result success received should show content`() = runTest {
|
||||
fun `on auth request result success received should show content`() = runTest {
|
||||
val newFingerprint = "newFingerprint"
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
viewModel.actionChannel.trySend(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = UserFingerprintResult.Success(newFingerprint),
|
||||
LoginWithDeviceAction.Internal.NewAuthRequestResultReceive(
|
||||
result = AuthRequestResult.Success(
|
||||
authRequest = mockk<AuthRequest> {
|
||||
every { fingerprint } returns newFingerprint
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -125,8 +123,8 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
viewModel.actionChannel.trySend(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = UserFingerprintResult.Error,
|
||||
LoginWithDeviceAction.Internal.NewAuthRequestResultReceive(
|
||||
result = AuthRequestResult.Error,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -149,11 +147,25 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
|
|||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private const val FINGERPRINT = "fingerprint"
|
||||
private val DEFAULT_STATE = LoginWithDeviceState(
|
||||
emailAddress = EMAIL,
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "initialFingerprint",
|
||||
fingerprintPhrase = FINGERPRINT,
|
||||
),
|
||||
)
|
||||
private val AUTH_REQUEST = AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "2",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "public",
|
||||
masterPasswordHash = "verySecureHash",
|
||||
creationDate = ZonedDateTime.parse("2024-09-13T00:00Z"),
|
||||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = FINGERPRINT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
|
@ -12,6 +13,8 @@ import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentReposito
|
|||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -29,8 +32,13 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Test
|
||||
fun `initial state should be correct when saved state is set`() {
|
||||
val viewModel = createViewModel(initialState = DEFAULT_STATE)
|
||||
val settingsRepository = getMockSettingsRepository()
|
||||
val viewModel = createViewModel(
|
||||
initialState = DEFAULT_STATE,
|
||||
settingsRepository = settingsRepository,
|
||||
)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
coVerify { settingsRepository.getUserFingerprint() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -40,6 +48,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
||||
every { isApprovePasswordlessLoginsEnabled } returns false
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = null,
|
||||
|
@ -49,6 +58,33 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
DEFAULT_STATE.copy(isUnlockWithPinEnabled = true),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
coVerify { settingsRepository.getUserFingerprint() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on FingerprintResultReceive should update the fingerprint phrase`() = runTest {
|
||||
val fingerprint = "fingerprint"
|
||||
val viewModel = createViewModel()
|
||||
// Set fingerprint phrase to value received
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
UserFingerprintResult.Success(fingerprint),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(fingerprintPhrase = fingerprint.asText()),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
// Clear fingerprint phrase
|
||||
viewModel.trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
UserFingerprintResult.Error,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(fingerprintPhrase = "".asText()),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -151,6 +187,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
fun `on VaultTimeoutTypeSelect should update the selection()`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeout = any() } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.trySendAction(
|
||||
|
@ -169,6 +206,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
fun `on CustomVaultTimeoutSelect should update the selection()`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeout = any() } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.trySendAction(
|
||||
|
@ -191,6 +229,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
fun `on VaultTimeoutActionSelect should update vault timeout action`() = runTest {
|
||||
val settingsRepository = mockk<SettingsRepository>() {
|
||||
every { vaultTimeoutAction = any() } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(settingsRepository = settingsRepository)
|
||||
viewModel.trySendAction(
|
||||
|
@ -257,6 +296,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { clearUnlockPin() } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
|
@ -296,6 +336,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
val settingsRepository: SettingsRepository = mockk() {
|
||||
every { storeUnlockPin(any(), any()) } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
initialState = initialState,
|
||||
|
@ -353,6 +394,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val settingsRepository = mockk<SettingsRepository> {
|
||||
every { isApprovePasswordlessLoginsEnabled = true } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
|
@ -387,6 +429,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
runTest {
|
||||
val settingsRepository = mockk<SettingsRepository> {
|
||||
every { isApprovePasswordlessLoginsEnabled = false } just runs
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
val viewModel = createViewModel(
|
||||
settingsRepository = settingsRepository,
|
||||
|
@ -416,13 +459,21 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [mockk] of the [SettingsRepository] with the call made on init already mocked.
|
||||
*/
|
||||
private fun getMockSettingsRepository(): SettingsRepository =
|
||||
mockk<SettingsRepository> {
|
||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createViewModel(
|
||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||
authRepository: AuthRepository = mockk(relaxed = true),
|
||||
vaultRepository: VaultRepository = mockk(relaxed = true),
|
||||
settingsRepository: SettingsRepository = mockk(relaxed = true),
|
||||
environmentRepository: EnvironmentRepository = fakeEnvironmentRepository,
|
||||
settingsRepository: SettingsRepository = getMockSettingsRepository(),
|
||||
savedStateHandle: SavedStateHandle = SavedStateHandle().apply {
|
||||
set("state", initialState)
|
||||
},
|
||||
|
@ -435,9 +486,10 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
|
||||
companion object {
|
||||
private const val FINGERPRINT = "fingerprint"
|
||||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
dialog = null,
|
||||
fingerprintPhrase = "fingerprint-placeholder".asText(),
|
||||
fingerprintPhrase = FINGERPRINT.asText(),
|
||||
isApproveLoginRequestsEnabled = false,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
isUnlockWithPinEnabled = false,
|
||||
|
|
|
@ -56,7 +56,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
authRequests = listOf(
|
||||
AuthRequest(
|
||||
id = "1",
|
||||
publicKey = "pantry-overdue-survive-sleep-jab",
|
||||
publicKey = "publicKey-1",
|
||||
platform = "Android",
|
||||
ipAddress = "192.168.0.1",
|
||||
key = "publicKey",
|
||||
|
@ -65,10 +65,11 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
responseDate = null,
|
||||
requestApproved = true,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "pantry-overdue-survive-sleep-jab",
|
||||
),
|
||||
AuthRequest(
|
||||
id = "2",
|
||||
publicKey = "erupt-anew-matchbook-disk-student",
|
||||
publicKey = "publicKey-2",
|
||||
platform = "iOS",
|
||||
ipAddress = "192.168.0.2",
|
||||
key = "publicKey",
|
||||
|
@ -77,6 +78,7 @@ class PendingRequestsViewModelTest : BaseViewModelTest() {
|
|||
responseDate = null,
|
||||
requestApproved = false,
|
||||
originUrl = "www.bitwarden.com",
|
||||
fingerprint = "erupt-anew-matchbook-disk-student",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue