mirror of
https://github.com/bitwarden/android.git
synced 2026-06-09 08:09:16 -05:00
BIT-809: Generate fingerprint on Login with Device (#781)
This commit is contained in:
committed by
Álison Fernandes
parent
cd020f2af9
commit
3635d368f9
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
@@ -10,6 +11,21 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
|
||||
* Source of authentication information and functionality from the Bitwarden SDK.
|
||||
*/
|
||||
interface AuthSdkSource {
|
||||
/**
|
||||
* Gets the data needed to create a new auth request.
|
||||
*/
|
||||
suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse>
|
||||
|
||||
/**
|
||||
* Gets the fingerprint phrase for this [email] and [publicKey].
|
||||
*/
|
||||
suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a hashed password provided the given [email], [password], [kdf], and [purpose].
|
||||
*/
|
||||
@@ -21,7 +37,7 @@ interface AuthSdkSource {
|
||||
): Result<String>
|
||||
|
||||
/**
|
||||
* Creates a set of encryption key information for registraation pers
|
||||
* Creates a set of encryption key information for registration.
|
||||
*/
|
||||
suspend fun makeRegisterKeys(
|
||||
email: String,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.FingerprintRequest
|
||||
import com.bitwarden.core.MasterPasswordPolicyOptions
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.crypto.HashPurpose
|
||||
import com.bitwarden.crypto.Kdf
|
||||
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.util.toPasswordStrengthOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
@@ -15,8 +18,29 @@ import com.x8bit.bitwarden.data.auth.datasource.sdk.util.toUByte
|
||||
*/
|
||||
class AuthSdkSourceImpl(
|
||||
private val clientAuth: ClientAuth,
|
||||
private val clientPlatform: ClientPlatform,
|
||||
) : AuthSdkSource {
|
||||
|
||||
override suspend fun getNewAuthRequest(
|
||||
email: String,
|
||||
): Result<AuthRequestResponse> = runCatching {
|
||||
clientAuth.newAuthRequest(
|
||||
email = email,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getUserFingerprint(
|
||||
email: String,
|
||||
publicKey: String,
|
||||
): Result<String> = runCatching {
|
||||
clientPlatform.fingerprint(
|
||||
req = FingerprintRequest(
|
||||
fingerprintMaterial = email,
|
||||
publicKey = publicKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun hashPassword(
|
||||
email: String,
|
||||
password: String,
|
||||
|
||||
@@ -20,5 +20,8 @@ object AuthSdkModule {
|
||||
@Singleton
|
||||
fun provideAuthSdkSource(
|
||||
client: Client,
|
||||
): AuthSdkSource = AuthSdkSourceImpl(clientAuth = client.auth())
|
||||
): AuthSdkSource = AuthSdkSourceImpl(
|
||||
clientAuth = client.auth(),
|
||||
clientPlatform = client.platform(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
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.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
|
||||
@@ -136,6 +137,11 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||
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.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.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||
@@ -410,6 +411,7 @@ class AuthRepositoryImpl(
|
||||
is PasswordHintResponseJson.Error -> {
|
||||
PasswordHintResult.Error(it.errorMessage)
|
||||
}
|
||||
|
||||
PasswordHintResponseJson.Success -> PasswordHintResult.Success
|
||||
}
|
||||
},
|
||||
@@ -466,6 +468,22 @@ class AuthRepositoryImpl(
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun getFingerprintPhrase(
|
||||
email: String,
|
||||
): UserFingerprintResult =
|
||||
authSdkSource.getNewAuthRequest(email)
|
||||
.flatMap { requestResponse ->
|
||||
authSdkSource
|
||||
.getUserFingerprint(
|
||||
email = email,
|
||||
publicKey = requestResponse.publicKey,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
|
||||
override suspend fun getIsKnownDevice(emailAddress: String): KnownDeviceResult =
|
||||
devicesService
|
||||
.getIsKnownDevice(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
/**
|
||||
* Models result of getting the user fingerprint.
|
||||
*/
|
||||
sealed class UserFingerprintResult {
|
||||
/**
|
||||
* Contains the user fingerprint.
|
||||
*/
|
||||
data class Success(
|
||||
val fingerprint: String,
|
||||
) : UserFingerprintResult()
|
||||
|
||||
/**
|
||||
* There was an error getting the user fingerprint.
|
||||
*/
|
||||
data object Error : UserFingerprintResult()
|
||||
}
|
||||
@@ -69,7 +69,11 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
||||
)
|
||||
},
|
||||
onNavigateToEnterpriseSignOn = { navController.navigateToEnterpriseSignOn() },
|
||||
onNavigateToLoginWithDevice = { navController.navigateToLoginWithDevice() },
|
||||
onNavigateToLoginWithDevice = { emailAddress ->
|
||||
navController.navigateToLoginWithDevice(
|
||||
emailAddress = emailAddress,
|
||||
)
|
||||
},
|
||||
onNavigateToTwoFactorLogin = { navController.navigateToTwoFactorLogin() },
|
||||
)
|
||||
loginWithDeviceDestination(
|
||||
|
||||
@@ -45,7 +45,7 @@ fun NavGraphBuilder.loginDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (emailAddress: String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToLoginWithDevice: () -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
|
||||
@@ -61,12 +61,12 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "LongParameterList")
|
||||
fun LoginScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToMasterPasswordHint: (String) -> Unit,
|
||||
onNavigateToEnterpriseSignOn: () -> Unit,
|
||||
onNavigateToLoginWithDevice: () -> Unit,
|
||||
onNavigateToLoginWithDevice: (emailAddress: String) -> Unit,
|
||||
onNavigateToTwoFactorLogin: () -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
@@ -85,7 +85,10 @@ fun LoginScreen(
|
||||
}
|
||||
|
||||
LoginEvent.NavigateToEnterpriseSignOn -> onNavigateToEnterpriseSignOn()
|
||||
LoginEvent.NavigateToLoginWithDevice -> onNavigateToLoginWithDevice()
|
||||
is LoginEvent.NavigateToLoginWithDevice -> {
|
||||
onNavigateToLoginWithDevice(event.emailAddress)
|
||||
}
|
||||
|
||||
LoginEvent.NavigateToTwoFactorLogin -> onNavigateToTwoFactorLogin()
|
||||
is LoginEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -217,7 +217,7 @@ class LoginViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleLoginWithDeviceButtonClicked() {
|
||||
sendEvent(LoginEvent.NavigateToLoginWithDevice)
|
||||
sendEvent(LoginEvent.NavigateToLoginWithDevice(state.emailAddress))
|
||||
}
|
||||
|
||||
private fun attemptLogin() {
|
||||
@@ -310,7 +310,9 @@ sealed class LoginEvent {
|
||||
/**
|
||||
* Navigates to the login with device screen.
|
||||
*/
|
||||
data object NavigateToLoginWithDevice : LoginEvent()
|
||||
data class NavigateToLoginWithDevice(
|
||||
val emailAddress: String,
|
||||
) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the two-factor login screen.
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE = "login_with_device"
|
||||
private const val EMAIL_ADDRESS: String = "email_address"
|
||||
private const val LOGIN_WITH_DEVICE_PREFIX = "login_with_device"
|
||||
private const val LOGIN_WITH_DEVICE_ROUTE = "$LOGIN_WITH_DEVICE_PREFIX/{$EMAIL_ADDRESS}"
|
||||
|
||||
/**
|
||||
* Class to retrieve login with device arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class LoginWithDeviceArgs(val emailAddress: String) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Login with Device screen.
|
||||
*/
|
||||
fun NavController.navigateToLoginWithDevice(navOptions: NavOptions? = null) {
|
||||
this.navigate(LOGIN_WITH_DEVICE_ROUTE, navOptions)
|
||||
fun NavController.navigateToLoginWithDevice(
|
||||
emailAddress: String,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
this.navigate("$LOGIN_WITH_DEVICE_PREFIX/$emailAddress", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,16 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
|
||||
|
||||
import android.os.Parcelable
|
||||
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.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -16,19 +22,20 @@ private const val KEY_STATE = "state"
|
||||
*/
|
||||
@HiltViewModel
|
||||
class LoginWithDeviceViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LoginWithDeviceState, LoginWithDeviceEvent, LoginWithDeviceAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: LoginWithDeviceState(
|
||||
emailAddress = LoginWithDeviceArgs(savedStateHandle).emailAddress,
|
||||
viewState = LoginWithDeviceState.ViewState.Loading,
|
||||
),
|
||||
) {
|
||||
init {
|
||||
mutableStateFlow.update {
|
||||
// TODO BIT-809: Pull phrase from SDK
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
LoginWithDeviceAction.Internal.FingerprintPhraseReceived(
|
||||
result = authRepository.getFingerprintPhrase(state.emailAddress),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -39,6 +46,10 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
LoginWithDeviceAction.CloseButtonClick -> handleCloseButtonClicked()
|
||||
LoginWithDeviceAction.ResendNotificationClick -> handleResendNotificationClicked()
|
||||
LoginWithDeviceAction.ViewAllLogInOptionsClick -> handleViewAllLogInOptionsClicked()
|
||||
|
||||
is LoginWithDeviceAction.Internal.FingerprintPhraseReceived -> {
|
||||
handleFingerprintPhraseReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +65,32 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
private fun handleViewAllLogInOptionsClicked() {
|
||||
sendEvent(LoginWithDeviceEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleFingerprintPhraseReceived(
|
||||
action: LoginWithDeviceAction.Internal.FingerprintPhraseReceived,
|
||||
) {
|
||||
when (action.result) {
|
||||
is UserFingerprintResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Content(
|
||||
fingerprintPhrase = action.result.fingerprint,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is UserFingerprintResult.Error -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = LoginWithDeviceState.ViewState.Error(
|
||||
message = R.string.generic_error_message.asText(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +98,7 @@ class LoginWithDeviceViewModel @Inject constructor(
|
||||
*/
|
||||
@Parcelize
|
||||
data class LoginWithDeviceState(
|
||||
val emailAddress: String,
|
||||
val viewState: ViewState,
|
||||
) : Parcelable {
|
||||
/**
|
||||
@@ -133,4 +171,16 @@ sealed class LoginWithDeviceAction {
|
||||
* Indicates that the "View all log in options" text has been clicked.
|
||||
*/
|
||||
data object ViewAllLogInOptionsClick : LoginWithDeviceAction()
|
||||
|
||||
/**
|
||||
* Models actions for internal use by the view model.
|
||||
*/
|
||||
sealed class Internal : LoginWithDeviceAction() {
|
||||
/**
|
||||
* A fingerprint phrase for this user has been received.
|
||||
*/
|
||||
data class FingerprintPhraseReceived(
|
||||
val result: UserFingerprintResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user