BIT-1069 Adding error handling for scanning (#549)

This commit is contained in:
Oleg Semenenko
2024-01-09 11:26:40 -06:00
committed by Álison Fernandes
parent d95e5df2a7
commit b8d397f71f
9 changed files with 343 additions and 35 deletions

View File

@@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -68,7 +69,7 @@ interface VaultRepository {
/**
* Flow that represents the totp code.
*/
val totpCodeFlow: Flow<String>
val totpCodeFlow: Flow<TotpCodeResult>
/**
* Clear any previously unlocked, in-memory data (vault, send, etc).
@@ -108,9 +109,9 @@ interface VaultRepository {
fun lockVaultIfNecessary(userId: String)
/**
* Emits the totp code flow to listeners.
* Emits the totp code result flow to listeners.
*/
fun emitTotpCode(totpCode: String)
fun emitTotpCodeResult(totpCodeResult: TotpCodeResult)
/**
* Attempt to unlock the vault and sync the vault data for the currently active user.

View File

@@ -37,6 +37,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultState
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
@@ -93,7 +94,7 @@ class VaultRepositoryImpl(
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
private val mutableTotpCodeFlow = bufferedMutableSharedFlow<String>()
private val mutableTotpCodeResultFlow = bufferedMutableSharedFlow<TotpCodeResult>()
private val mutableVaultStateStateFlow =
MutableStateFlow(
@@ -138,8 +139,8 @@ class VaultRepositoryImpl(
initialValue = DataState.Loading,
)
override val totpCodeFlow: Flow<String>
get() = mutableTotpCodeFlow.asSharedFlow()
override val totpCodeFlow: Flow<TotpCodeResult>
get() = mutableTotpCodeResultFlow.asSharedFlow()
override val ciphersStateFlow: StateFlow<DataState<List<CipherView>>>
get() = mutableCiphersStateFlow.asStateFlow()
@@ -285,8 +286,8 @@ class VaultRepositoryImpl(
setVaultToLocked(userId = userId)
}
override fun emitTotpCode(totpCode: String) {
mutableTotpCodeFlow.tryEmit(totpCode)
override fun emitTotpCodeResult(totpCodeResult: TotpCodeResult) {
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
}
@Suppress("ReturnCount")

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.vault.repository.model
/**
* Models result of the user adding a totp code.
*/
sealed class TotpCodeResult {
/**
* Code has been successfully added.
*/
data class Success(val code: String) : TotpCodeResult()
/**
* There was an error scanning the code.
*/
data object CodeScanningError : TotpCodeResult()
}

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
@@ -89,7 +90,7 @@ class VaultAddEditViewModel @Inject constructor(
vaultRepository
.totpCodeFlow
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpCode = it) }
.map { VaultAddEditAction.Internal.TotpCodeReceive(totpResult = it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
@@ -843,15 +844,29 @@ class VaultAddEditViewModel @Inject constructor(
}
private fun handleVaultTotpCodeReceive(action: VaultAddEditAction.Internal.TotpCodeReceive) {
updateLoginContent { loginType ->
loginType.copy(totp = action.totpCode)
}
when (action.totpResult) {
is TotpCodeResult.Success -> {
sendEvent(
event = VaultAddEditEvent.ShowToast(
message = R.string.authenticator_key_added.asText(),
),
)
sendEvent(
event = VaultAddEditEvent.ShowToast(
message = R.string.authenticator_key_added.asText(),
),
)
updateLoginContent { loginType ->
loginType.copy(totp = action.totpResult.code)
}
}
TotpCodeResult.CodeScanningError -> {
mutableStateFlow.update {
it.copy(
dialog = VaultAddEditState.DialogState.Error(
R.string.authenticator_key_read_error.asText(),
),
)
}
}
}
}
//endregion Internal Type Handlers
@@ -1612,9 +1627,9 @@ sealed class VaultAddEditAction {
sealed class Internal : VaultAddEditAction() {
/**
* Indicates that the vault totp code has been received.
* Indicates that the vault totp code result has been received.
*/
data class TotpCodeReceive(val totpCode: String) : Internal()
data class TotpCodeReceive(val totpResult: TotpCodeResult) : Internal()
/**
* Indicates that the vault item data has been received.

View File

@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.manualcodeentry
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -45,7 +46,7 @@ class ManualCodeEntryViewModel @Inject constructor(
}
private fun handleCodeSubmit() {
vaultRepository.emitTotpCode(state.code)
vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(state.code))
sendEvent(ManualCodeEntryEvent.NavigateBack)
}

View File

@@ -1,11 +1,19 @@
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
import android.net.Uri
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
private const val ALGORITHM = "algorithm"
private const val DIGITS = "digits"
private const val PERIOD = "period"
private const val SECRET = "secret"
private const val TOTP_CODE_PREFIX = "otpauth://totp"
/**
* Handles [QrCodeScanAction],
* and launches [QrCodeScanEvent] for the [QrCodeScanScreen].
@@ -37,8 +45,31 @@ class QrCodeScanViewModel @Inject constructor(
)
}
// For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters
private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) {
vaultRepository.emitTotpCode(action.qrCode)
var result: TotpCodeResult = TotpCodeResult.Success(action.qrCode)
val scannedCode = action.qrCode
if (scannedCode.isBlank() || !scannedCode.startsWith(TOTP_CODE_PREFIX)) {
vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
sendEvent(QrCodeScanEvent.NavigateBack)
return
}
val scannedCodeUri = Uri.parse(scannedCode)
val secretValue = scannedCodeUri.getQueryParameter(SECRET)
if (secretValue == null || !secretValue.isBase32()) {
vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
sendEvent(QrCodeScanEvent.NavigateBack)
return
}
val values = scannedCodeUri.queryParameterNames
if (!areParametersValid(scannedCode, values)) {
result = TotpCodeResult.CodeScanningError
}
vaultRepository.emitTotpCodeResult(result)
sendEvent(QrCodeScanEvent.NavigateBack)
}
@@ -47,6 +78,40 @@ class QrCodeScanViewModel @Inject constructor(
QrCodeScanEvent.NavigateToManualCodeEntry,
)
}
@Suppress("NestedBlockDepth", "ReturnCount", "MagicNumber")
private fun areParametersValid(scannedCode: String, parameters: Set<String>): Boolean {
parameters.forEach { parameter ->
Uri.parse(scannedCode).getQueryParameter(parameter)?.let { value ->
when (parameter) {
DIGITS -> {
val digit = value.toInt()
if (digit > 10 || digit < 1) {
return false
}
}
PERIOD -> {
val period = value.toInt()
if (period < 1) {
return false
}
}
ALGORITHM -> {
val lowercaseAlgo = value.lowercase()
if (lowercaseAlgo != "sha1" &&
lowercaseAlgo != "sha256" &&
lowercaseAlgo != "sha512"
) {
return false
}
}
}
}
}
return true
}
}
/**
@@ -95,3 +160,11 @@ sealed class QrCodeScanAction {
*/
data object CameraSetupErrorReceive : QrCodeScanAction()
}
/**
* Checks if a string is using base32 digits.
*/
private fun String.isBase32(): Boolean {
val regex = ("^[A-Z2-7]+=*$").toRegex()
return regex.matches(this)
}