mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
PM-27817: Consolidate totp parsing with TotpUriUtils (#6122)
This commit is contained in:
parent
ed47ff4d18
commit
2bb06063c7
@ -1,21 +1,14 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.util.getTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
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].
|
||||
* Handles [QrCodeScanAction] and launches [QrCodeScanEvent] for the [QrCodeScanScreen].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class QrCodeScanViewModel @Inject constructor(
|
||||
@ -33,83 +26,25 @@ class QrCodeScanViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.NavigateBack,
|
||||
)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleManualEntryTextClick() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
}
|
||||
|
||||
// For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters
|
||||
private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) {
|
||||
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)
|
||||
val qrCode = action.qrCode
|
||||
qrCode
|
||||
.getTotpDataOrNull()
|
||||
?.let { vaultRepository.emitTotpCodeResult(TotpCodeResult.Success(code = qrCode)) }
|
||||
?: run { vaultRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError()) }
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleCameraErrorReceive() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth", "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
|
||||
sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,11 +89,3 @@ sealed class QrCodeScanAction {
|
||||
*/
|
||||
data object CameraSetupErrorReceive : QrCodeScanAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is using base32 digits.
|
||||
*/
|
||||
private fun String.isBase32(): Boolean {
|
||||
val regex = ("^[A-Za-z2-7]+=*$").toRegex()
|
||||
return regex.matches(this)
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.qrcodescan
|
||||
|
||||
import android.net.Uri
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.util.getTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import io.mockk.every
|
||||
@ -27,16 +27,15 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
every { totpCodeFlow } returns totpTestCodeFlow
|
||||
every { emitTotpCodeResult(any()) } just runs
|
||||
}
|
||||
private val uriMock = mockk<Uri>()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(Uri::class)
|
||||
mockkStatic(String::getTotpDataOrNull)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(Uri::class)
|
||||
unmockkStatic(String::getTotpDataOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -76,32 +75,11 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit new code and NavigateBack with a valid code with all values`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val validCode =
|
||||
"otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&algorithm=sha256&digits=8&period=60"
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.Success(validCode)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(validCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit new code and NavigateBack without optional values`() = runTest {
|
||||
setupMockUri(
|
||||
queryParameterNames = setOf(SECRET),
|
||||
)
|
||||
|
||||
val validCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP"
|
||||
fun `QrCodeScanReceive with valid code should emit new code and NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val validCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP"
|
||||
val result = TotpCodeResult.Success(validCode)
|
||||
every { validCode.getTotpDataOrNull() } returns mockk()
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(validCode))
|
||||
@ -112,60 +90,11 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid algorithm`() =
|
||||
runTest {
|
||||
setupMockUri(algorithm = "SHA-224")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&algorithm=sha224"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid digits`() = runTest {
|
||||
setupMockUri(digits = "11")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&digits=11"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack with invalid period`() = runTest {
|
||||
setupMockUri(period = "0")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP&period=0"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result without correct prefix`() = runTest {
|
||||
|
||||
fun `QrCodeScanReceive with invalid totp should emit failure result`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "nototpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP"
|
||||
every { invalidCode.getTotpDataOrNull() } returns null
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
@ -175,94 +104,8 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result with non base32 secret`() = runTest {
|
||||
setupMockUri(secret = "JBSWY3dpeHPK3PXP1")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me?secret=JBSWY3dpeHPK3PXP1"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack without Secret`() = runTest {
|
||||
setupMockUri(secret = null)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack if secret is empty`() = runTest {
|
||||
setupMockUri(secret = "")
|
||||
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = "otpauth://totp/Test:me?secret= "
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `QrCodeScan should emit failure result and NavigateBack if code is empty`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val result = TotpCodeResult.CodeScanningError()
|
||||
val invalidCode = ""
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidCode))
|
||||
|
||||
verify(exactly = 1) { vaultRepository.emitTotpCodeResult(result) }
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMockUri(
|
||||
secret: String? = "JBSWY3dpeHPK3PXP",
|
||||
algorithm: String = "SHA256",
|
||||
digits: String = "8",
|
||||
period: String = "60",
|
||||
queryParameterNames: Set<String> = setOf(
|
||||
ALGORITHM, PERIOD, DIGITS, SECRET,
|
||||
),
|
||||
) {
|
||||
every { Uri.parse(any()) } returns uriMock
|
||||
every { uriMock.getQueryParameter(SECRET) } returns secret
|
||||
every { uriMock.getQueryParameter(ALGORITHM) } returns algorithm
|
||||
every { uriMock.getQueryParameter(DIGITS) } returns digits
|
||||
every { uriMock.getQueryParameter(PERIOD) } returns period
|
||||
every { uriMock.queryParameterNames } returns queryParameterNames
|
||||
}
|
||||
|
||||
private fun createViewModel(): QrCodeScanViewModel =
|
||||
QrCodeScanViewModel(
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val ALGORITHM = "algorithm"
|
||||
private const val DIGITS = "digits"
|
||||
private const val PERIOD = "period"
|
||||
private const val SECRET = "secret"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
package com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
|
||||
@ -12,15 +10,14 @@ import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.base.util.isBase32
|
||||
import com.bitwarden.ui.platform.util.getTotpDataOrNull
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Handles [QrCodeScanAction],
|
||||
* and launches [QrCodeScanEvent] for the [QrCodeScanScreen].
|
||||
* Handles [QrCodeScanAction] and launches [QrCodeScanEvent] for the [QrCodeScanScreen].
|
||||
*/
|
||||
@HiltViewModel
|
||||
@Suppress("TooManyFunctions")
|
||||
@ -77,15 +74,11 @@ class QrCodeScanViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.NavigateBack,
|
||||
)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleManualEntryTextClick() {
|
||||
sendEvent(
|
||||
QrCodeScanEvent.NavigateToManualCodeEntry,
|
||||
)
|
||||
sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
}
|
||||
|
||||
private fun handleQrCodeScanReceive(action: QrCodeScanAction.QrCodeScanReceive) {
|
||||
@ -103,96 +96,48 @@ class QrCodeScanViewModel @Inject constructor(
|
||||
|
||||
// For more information: https://bitwarden.com/help/authenticator-keys/#support-for-more-parameters
|
||||
private fun handleTotpUriReceive(scannedCode: String) {
|
||||
val result = TotpCodeResult.TotpCodeScan(scannedCode)
|
||||
val scannedCodeUri = Uri.parse(scannedCode)
|
||||
val secretValue = scannedCodeUri
|
||||
.getQueryParameter(TotpCodeManager.SECRET_PARAM)
|
||||
.orEmpty()
|
||||
.toUpperCase(Locale.current)
|
||||
scannedCode
|
||||
.getTotpDataOrNull()
|
||||
?.let {
|
||||
val result = TotpCodeResult.TotpCodeScan(code = scannedCode)
|
||||
if (authenticatorRepository.sharedCodesStateFlow.value.isSyncWithBitwardenEnabled) {
|
||||
when (settingsRepository.defaultSaveOption) {
|
||||
DefaultSaveOption.BITWARDEN_APP -> {
|
||||
saveCodeToBitwardenAndNavigateBack(result = result)
|
||||
}
|
||||
|
||||
if (secretValue.isEmpty() || !secretValue.isBase32()) {
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
return
|
||||
}
|
||||
|
||||
val values = scannedCodeUri.queryParameterNames
|
||||
// If the parameters are not valid,
|
||||
if (!areParametersValid(scannedCode, values)) {
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
return
|
||||
}
|
||||
if (authenticatorRepository.sharedCodesStateFlow.value.isSyncWithBitwardenEnabled) {
|
||||
when (settingsRepository.defaultSaveOption) {
|
||||
DefaultSaveOption.BITWARDEN_APP -> saveCodeToBitwardenAndNavigateBack(result)
|
||||
DefaultSaveOption.LOCAL -> saveCodeLocallyAndNavigateBack(result)
|
||||
|
||||
DefaultSaveOption.NONE -> {
|
||||
pendingSuccessfulScan = result
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = QrCodeScanState.DialogState.ChooseSaveLocation,
|
||||
)
|
||||
DefaultSaveOption.LOCAL -> saveCodeLocallyAndNavigateBack(result = result)
|
||||
DefaultSaveOption.NONE -> {
|
||||
pendingSuccessfulScan = result
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = QrCodeScanState.DialogState.ChooseSaveLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Syncing with Bitwarden not enabled, save code locally:
|
||||
saveCodeLocallyAndNavigateBack(result = result)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Syncing with Bitwarden not enabled, save code locally:
|
||||
saveCodeLocallyAndNavigateBack(result)
|
||||
}
|
||||
?: run {
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGoogleExportUriReceive(scannedCode: String) {
|
||||
val uri = Uri.parse(scannedCode)
|
||||
val uri = scannedCode.toUri()
|
||||
val encodedData = uri.getQueryParameter(TotpCodeManager.DATA_PARAM)
|
||||
val result: TotpCodeResult = if (encodedData.isNullOrEmpty()) {
|
||||
TotpCodeResult.CodeScanningError
|
||||
if (encodedData.isNullOrEmpty()) {
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
} else {
|
||||
TotpCodeResult.GoogleExportScan(encodedData)
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.GoogleExportScan(encodedData))
|
||||
}
|
||||
authenticatorRepository.emitTotpCodeResult(result)
|
||||
sendEvent(QrCodeScanEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleCameraErrorReceive() {
|
||||
sendEvent(
|
||||
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) {
|
||||
TotpCodeManager.DIGITS_PARAM -> {
|
||||
val digit = value.toInt()
|
||||
if (digit > 10 || digit < 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
TotpCodeManager.PERIOD_PARAM -> {
|
||||
val period = value.toInt()
|
||||
if (period < 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
TotpCodeManager.ALGORITHM_PARAM -> {
|
||||
val lowercaseAlgo = value.lowercase()
|
||||
if (lowercaseAlgo != "sha1" &&
|
||||
lowercaseAlgo != "sha256" &&
|
||||
lowercaseAlgo != "sha512"
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
sendEvent(QrCodeScanEvent.NavigateToManualCodeEntry)
|
||||
}
|
||||
|
||||
private fun saveCodeToBitwardenAndNavigateBack(result: TotpCodeResult.TotpCodeScan) {
|
||||
|
||||
@ -9,6 +9,7 @@ import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
|
||||
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.util.getTotpDataOrNull
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
@ -37,13 +38,20 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(Uri::parse)
|
||||
mockkStatic(
|
||||
String::getTotpDataOrNull,
|
||||
Uri::parse,
|
||||
)
|
||||
every { VALID_TOTP_CODE.getTotpDataOrNull() } returns mockk()
|
||||
every { Uri.parse(VALID_TOTP_CODE) } returns VALID_TOTP_URI
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun teardown() {
|
||||
unmockkStatic(Uri::parse)
|
||||
unmockkStatic(
|
||||
String::getTotpDataOrNull,
|
||||
Uri::parse,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -242,13 +250,8 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
|
||||
every {
|
||||
authenticatorRepository.emitTotpCodeResult(TotpCodeResult.CodeScanningError)
|
||||
} just runs
|
||||
val invalidUri: Uri = mockk {
|
||||
every { getQueryParameter("secret") } returns "SECRET"
|
||||
every { queryParameterNames } returns setOf("digits")
|
||||
every { getQueryParameter("digits") } returns "100"
|
||||
}
|
||||
val invalidQrCode = "otpauth://totp/secret=SECRET"
|
||||
every { Uri.parse(invalidQrCode) } returns invalidUri
|
||||
every { invalidQrCode.getTotpDataOrNull() } returns null
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(QrCodeScanAction.QrCodeScanReceive(invalidQrCode))
|
||||
assertEquals(QrCodeScanEvent.NavigateBack, awaitItem())
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.bitwarden.ui.platform.util
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.bitwarden.ui.platform.base.util.isBase32
|
||||
import com.bitwarden.ui.platform.model.TotpData
|
||||
|
||||
@ -12,6 +13,12 @@ private const val PARAM_NAME_ISSUER: String = "issuer"
|
||||
private const val PARAM_NAME_PERIOD: String = "period"
|
||||
private const val PARAM_NAME_SECRET: String = "secret"
|
||||
|
||||
/**
|
||||
* Checks if the given [String] contains valid data for a TOTP. The [TotpData] will be returned
|
||||
* when the correct data is present or `null` if data is invalid or missing.
|
||||
*/
|
||||
fun String.getTotpDataOrNull(): TotpData? = this.toUri().getTotpDataOrNull()
|
||||
|
||||
/**
|
||||
* Checks if the given [Uri] contains valid data for a TOTP. The [TotpData] will be returned when
|
||||
* the correct data is present or `null` if data is invalid or missing.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user