PM-27771: Improve TOTP parsing (#6119)

This commit is contained in:
David Perez 2025-11-04 14:17:03 -06:00 committed by GitHub
parent 448ba97ae2
commit ed47ff4d18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 192 additions and 83 deletions

View File

@ -11,6 +11,7 @@ import com.bitwarden.cxf.util.getProviderImportCredentialsRequest
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.share.ShareManager import com.bitwarden.ui.platform.manager.share.ShareManager
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
@ -46,7 +47,6 @@ import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview

View File

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.manager package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
/** /**
* Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP * Manager for keeping track of requests from the Bitwarden Authenticator app to add a TOTP

View File

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.manager package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
/** /**
* Default in memory implementation for [AddTotpItemFromAuthenticatorManager]. * Default in memory implementation for [AddTotpItemFromAuthenticatorManager].

View File

@ -4,13 +4,13 @@ import android.os.Parcelable
import androidx.credentials.CredentialManager import androidx.credentials.CredentialManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.ui.platform.manager.share.model.ShareData import com.bitwarden.ui.platform.manager.share.model.ShareData
import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionRequest
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.ui.vault.model.TotpData
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**

View File

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.util package com.x8bit.bitwarden.data.platform.manager.util
import com.bitwarden.cxf.model.ImportCredentialsRequestData import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@ -8,7 +9,6 @@ import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionReques
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest import com.x8bit.bitwarden.data.credentials.model.ProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.vault.model.TotpData
/** /**
* Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance]. * Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance].

View File

@ -17,10 +17,10 @@ import com.bitwarden.authenticatorbridge.util.toFingerprint
import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.data.manager.DispatcherManager import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.ui.platform.util.getTotpDataOrNull
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManager
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber

View File

@ -13,6 +13,7 @@ import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -59,7 +60,6 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.applyRestrictItemTypesPolicy import com.x8bit.bitwarden.ui.vault.feature.vault.util.applyRestrictItemTypesPolicy
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toVaultFilterData import com.x8bit.bitwarden.ui.vault.feature.vault.util.toVaultFilterData
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel

View File

@ -14,6 +14,7 @@ import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.ui.platform.base.BackgroundEvent import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenPlurals
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
@ -76,7 +77,6 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.messageResourceId
import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections import com.x8bit.bitwarden.ui.vault.feature.util.canAssignToCollections
import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection import com.x8bit.bitwarden.ui.vault.feature.util.hasDeletePermissionInAtLeastOneCollection
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
@ -2320,8 +2320,8 @@ data class VaultAddEditState(
val shouldShowMoveToOrganization: Boolean val shouldShowMoveToOrganization: Boolean
get() = !isAddItemMode && get() = !isAddItemMode &&
!isCipherInCollection && !isCipherInCollection &&
hasOrganizations hasOrganizations
/** /**
* Enum representing the main type options for the vault, such as LOGIN, CARD, etc. * Enum representing the main type options for the vault, such as LOGIN, CARD, etc.

View File

@ -5,6 +5,7 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.collections.CollectionType import com.bitwarden.collections.CollectionType
import com.bitwarden.collections.CollectionView import com.bitwarden.collections.CollectionView
import com.bitwarden.core.data.util.toFormattedDateTimeStyle import com.bitwarden.core.data.util.toFormattedDateTimeStyle
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherRepromptType
@ -19,7 +20,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth

View File

@ -1,7 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.TotpData
/** /**
* Returns pre-filled content that may be used for an "add" type * Returns pre-filled content that may be used for an "add" type

View File

@ -23,6 +23,7 @@ import com.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import com.bitwarden.ui.platform.components.account.model.AccountSummary import com.bitwarden.ui.platform.components.account.model.AccountSummary
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -100,7 +101,6 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel

View File

@ -9,6 +9,7 @@ import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.base.util.toHostOrPathOrNull import com.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -36,7 +37,6 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.applyRestrictItemTypesPolicy import com.x8bit.bitwarden.ui.vault.feature.vault.util.applyRestrictItemTypesPolicy
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.toSdkCipherType import com.x8bit.bitwarden.ui.vault.util.toSdkCipherType
import java.time.Clock import java.time.Clock
import java.time.format.FormatStyle import java.time.format.FormatStyle

View File

@ -1,7 +1,8 @@
package com.x8bit.bitwarden.ui.vault.util package com.x8bit.bitwarden.ui.vault.util
import android.content.Intent import android.content.Intent
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.util.getTotpDataOrNull
/** /**
* Checks if the given [Intent] contains data for a TOTP. The [TotpData] will be returned when the * Checks if the given [Intent] contains data for a TOTP. The [TotpData] will be returned when the

View File

@ -1,48 +0,0 @@
package com.x8bit.bitwarden.ui.vault.util
import android.net.Uri
import com.x8bit.bitwarden.ui.vault.model.TotpData
private const val TOTP_HOST_NAME: String = "totp"
private const val TOTP_SCHEME_NAME: String = "otpauth"
private const val PARAM_NAME_ALGORITHM: String = "algorithm"
private const val PARAM_NAME_DIGITS: String = "digits"
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 [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.
*/
fun Uri.getTotpDataOrNull(): TotpData? {
// Must be a "otpauth" scheme
if (!this.scheme.equals(other = TOTP_SCHEME_NAME, ignoreCase = true)) return null
// Must be a "totp" host
if (!this.host.equals(other = TOTP_HOST_NAME, ignoreCase = true)) return null
// Must contain a "secret"
val secret = this.getQueryParameter(PARAM_NAME_SECRET)?.trim() ?: return null
val segments = this.pathSegments?.firstOrNull()?.split(":")
val segmentCount = segments?.size ?: 0
return TotpData(
uri = this.toString(),
issuer = this.getQueryParameter(PARAM_NAME_ISSUER)
?: segments?.firstOrNull()?.trim()?.takeIf { segmentCount > 1 },
accountName = if (segmentCount > 1) {
segments?.getOrNull(index = 1)?.trim()
} else {
segments?.firstOrNull()?.trim()
},
secret = secret,
digits = this.getQueryParameter(PARAM_NAME_DIGITS)?.trim()?.toIntOrNull() ?: 6,
period = this
.getQueryParameter(PARAM_NAME_PERIOD)
?.trim()
?.toIntOrNull()
?.takeUnless { it <= 0 }
?: 30,
algorithm = TotpData.CryptoHashAlgorithm
.parse(value = this.getQueryParameter(PARAM_NAME_ALGORITHM)?.trim())
?: TotpData.CryptoHashAlgorithm.SHA_1,
)
}

View File

@ -21,6 +21,7 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.bitwarden.ui.platform.manager.share.ShareManager import com.bitwarden.ui.platform.manager.share.ShareManager
import com.bitwarden.ui.platform.manager.share.model.ShareData import com.bitwarden.ui.platform.manager.share.model.ShareData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
@ -76,7 +77,6 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLang
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every

View File

@ -1,6 +1,6 @@
package com.x8bit.bitwarden.data.auth.manager package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
import io.mockk.mockk import io.mockk.mockk
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.platform.manager.util
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bitwarden.cxf.model.ImportCredentialsRequestData import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
@ -9,7 +10,6 @@ import com.x8bit.bitwarden.data.credentials.model.createMockFido2CredentialAsser
import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsRequest import com.x8bit.bitwarden.data.credentials.model.createMockGetCredentialsRequest
import com.x8bit.bitwarden.data.credentials.model.createMockProviderGetPasswordCredentialRequest import com.x8bit.bitwarden.data.credentials.model.createMockProviderGetPasswordCredentialRequest
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.vault.model.TotpData
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull

View File

@ -20,11 +20,11 @@ import com.bitwarden.authenticatorbridge.util.toSymmetricEncryptionKeyData
import com.bitwarden.core.data.util.asSuccess import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.core.util.isBuildVersionAtLeast
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.util.getTotpDataOrNull
import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl import com.x8bit.bitwarden.data.auth.manager.AddTotpItemFromAuthenticatorManagerImpl
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent import com.x8bit.bitwarden.data.platform.util.createAddTotpItemFromAuthenticatorIntent
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every

View File

@ -20,6 +20,7 @@ import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenPlurals
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
@ -87,7 +88,6 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth

View File

@ -1,7 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.model.TotpData
import io.mockk.every import io.mockk.every
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic

View File

@ -31,6 +31,7 @@ import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.account.model.AccountSummary import com.bitwarden.ui.platform.components.account.model.AccountSummary
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
@ -110,7 +111,6 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayIt
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import io.mockk.Ordering import io.mockk.Ordering

View File

@ -10,6 +10,7 @@ import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -33,7 +34,6 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.model.TotpData
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject import io.mockk.mockkObject

View File

@ -2,7 +2,8 @@ package com.x8bit.bitwarden.ui.vault.util
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
import com.bitwarden.ui.platform.util.getTotpDataOrNull
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic

View File

@ -242,7 +242,4 @@ fun String.prefixHttpsIfNecessary(): String =
/** /**
* Checks if a string is using base32 digits. * Checks if a string is using base32 digits.
*/ */
fun String.isBase32(): Boolean { fun String.isBase32(): Boolean = "^[A-Za-z2-7]+=*$".toRegex().matches(this)
val regex = ("^[A-Z2-7]+=*$").toRegex()
return regex.matches(this)
}

View File

@ -1,4 +1,4 @@
package com.x8bit.bitwarden.ui.vault.model package com.bitwarden.ui.platform.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View File

@ -0,0 +1,109 @@
package com.bitwarden.ui.platform.util
import android.net.Uri
import com.bitwarden.ui.platform.base.util.isBase32
import com.bitwarden.ui.platform.model.TotpData
private const val TOTP_HOST_NAME: String = "totp"
private const val TOTP_SCHEME_NAME: String = "otpauth"
private const val PARAM_NAME_ALGORITHM: String = "algorithm"
private const val PARAM_NAME_DIGITS: String = "digits"
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 [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.
*/
fun Uri.getTotpDataOrNull(): TotpData? {
// Must be a "otpauth" scheme
if (!this.scheme.equals(other = TOTP_SCHEME_NAME, ignoreCase = true)) return null
// Must be a "totp" host
if (!this.host.equals(other = TOTP_HOST_NAME, ignoreCase = true)) return null
val secret = this.getSecret() ?: return null
val digits = this.getDigits() ?: return null
val period = this.getPeriod() ?: return null
val algorithm = this.getAlgorithm() ?: return null
val segments = this.pathSegments?.firstOrNull()?.split(":")
val segmentCount = segments?.size ?: 0
return TotpData(
uri = this.toString(),
issuer = this.getQueryParameter(PARAM_NAME_ISSUER)
?: segments?.firstOrNull()?.trim()?.takeIf { segmentCount > 1 },
accountName = if (segmentCount > 1) {
segments?.getOrNull(index = 1)?.trim()
} else {
segments?.firstOrNull()?.trim()
},
secret = secret,
digits = digits,
period = period,
algorithm = algorithm,
)
}
/**
* Attempts to extract the algorithm from the given totp [Uri].
*/
private fun Uri.getAlgorithm(): TotpData.CryptoHashAlgorithm? {
val algorithm = this
.getQueryParameter(PARAM_NAME_ALGORITHM)
?.trim()
?.lowercase()
return if (algorithm == null) {
// If no value was provided, then we'll default to SHA_1.
TotpData.CryptoHashAlgorithm.SHA_1
} else {
// If the value is unidentifiable, then it's invalid.
// If it's identifiable, then we return the valid value.
// We specifically do not use a `let` here, since we do not want to map an unidentified
// value to the default value.
TotpData.CryptoHashAlgorithm.parse(value = algorithm)
}
}
/**
* Attempts to extract the digits from the given totp [Uri].
*/
@Suppress("MagicNumber")
private fun Uri.getDigits(): Int? {
val digits = this.getQueryParameter(PARAM_NAME_DIGITS)?.trim()?.toIntOrNull()
return if (digits == null) {
// If no value was provided, then we'll default to 6.
6
} else if (digits < 1 || digits > 10) {
// If the value is less than 1 or greater than 10, then it's invalid.
null
} else {
// If the value is valid, then we'll return it.
digits
}
}
/**
* Attempts to extract the period from the given totp [Uri].
*/
@Suppress("MagicNumber")
private fun Uri.getPeriod(): Int? {
val period = this.getQueryParameter(PARAM_NAME_PERIOD)?.trim()?.toIntOrNull()
return if (period == null) {
// If no value was provided, then we'll default to 30.
30
} else if (period < 1) {
// If the value is less than 1, then it's invalid.
null
} else {
// If the value is valid, then we'll return it.
period
}
}
/**
* Attempts to extract the secret from the given totp [Uri].
*/
private fun Uri.getSecret(): String? =
this
.getQueryParameter(PARAM_NAME_SECRET)
?.trim()
?.takeIf { it.isNotEmpty() && it.isBase32() }

View File

@ -1,8 +1,7 @@
package com.x8bit.bitwarden.ui.vault.util package com.bitwarden.ui.platform.util
import android.net.Uri import android.net.Uri
import com.x8bit.bitwarden.ui.vault.model.TotpData import com.bitwarden.ui.platform.model.TotpData
import com.x8bit.bitwarden.ui.vault.model.TotpData.CryptoHashAlgorithm
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@ -41,6 +40,56 @@ class TotpUriUtilsTest {
assertNull(uri.getTotpDataOrNull()) assertNull(uri.getTotpDataOrNull())
} }
@Test
fun `getTotpDataOrNull with invalid secret returns null`() {
val uri = mockk<Uri> {
every { scheme } returns "otpauth"
every { host } returns "totp"
every { getQueryParameter("secret") } returns "1234567890qwertyuiop"
}
assertNull(uri.getTotpDataOrNull())
}
@Test
fun `getTotpDataOrNull with invalid digits returns null`() {
val uri = mockk<Uri> {
every { scheme } returns "otpauth"
every { host } returns "totp"
every { getQueryParameter("secret") } returns "secret"
every { getQueryParameter("digits") } returns "11"
}
assertNull(uri.getTotpDataOrNull())
}
@Test
fun `getTotpDataOrNull with invalid period returns null`() {
val uri = mockk<Uri> {
every { scheme } returns "otpauth"
every { host } returns "totp"
every { getQueryParameter("secret") } returns "secret"
every { getQueryParameter("digits") } returns "5"
every { getQueryParameter("period") } returns "0"
}
assertNull(uri.getTotpDataOrNull())
}
@Test
fun `getTotpDataOrNull with invalid algorithm returns null`() {
val uri = mockk<Uri> {
every { scheme } returns "otpauth"
every { host } returns "totp"
every { getQueryParameter("secret") } returns "secret"
every { getQueryParameter("digits") } returns "5"
every { getQueryParameter("period") } returns "10"
every { getQueryParameter("algorithm") } returns "sha22"
}
assertNull(uri.getTotpDataOrNull())
}
@Test @Test
fun `getTotpDataOrNull with minimum required values returns TotpData with defaults`() { fun `getTotpDataOrNull with minimum required values returns TotpData with defaults`() {
val secret = "secret" val secret = "secret"
@ -62,7 +111,7 @@ class TotpUriUtilsTest {
secret = secret, secret = secret,
digits = 6, digits = 6,
period = 30, period = 30,
algorithm = CryptoHashAlgorithm.SHA_1, algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
) )
assertEquals(expectedResult, uri.getTotpDataOrNull()) assertEquals(expectedResult, uri.getTotpDataOrNull())
@ -93,7 +142,7 @@ class TotpUriUtilsTest {
secret = secret, secret = secret,
digits = digits, digits = digits,
period = period, period = period,
algorithm = CryptoHashAlgorithm.SHA_256, algorithm = TotpData.CryptoHashAlgorithm.SHA_256,
) )
assertEquals(expectedResult, uri.getTotpDataOrNull()) assertEquals(expectedResult, uri.getTotpDataOrNull())