PM-20150, PM-20151: Remove single tap passkey feature flags (#5585)

This commit is contained in:
David Perez
2025-07-25 13:05:18 -05:00
committed by GitHub
parent 8589a37e5a
commit 91f1180be7
11 changed files with 63 additions and 153 deletions

View File

@@ -16,8 +16,6 @@ import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
import com.x8bit.bitwarden.data.credentials.processor.GET_PASSWORD_INTENT
import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlin.random.Random
@@ -27,7 +25,6 @@ import kotlin.random.Random
class CredentialEntryBuilderImpl(
private val context: Context,
private val intentManager: IntentManager,
private val featureFlagManager: FeatureFlagManager,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
) : CredentialEntryBuilder {
@@ -91,10 +88,7 @@ class CredentialEntryBuilderImpl(
.also { builder ->
if (!isUserVerified) {
builder.setBiometricPromptDataIfSupported(
cipher = biometricsEncryptionManager
.getOrCreateCipher(userId),
isSingleTapAuthEnabled = featureFlagManager
.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication),
cipher = biometricsEncryptionManager.getOrCreateCipher(userId),
)
}
}

View File

@@ -53,18 +53,16 @@ object CredentialProviderModule {
dispatcherManager: DispatcherManager,
intentManager: IntentManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
featureFlagManager: FeatureFlagManager,
clock: Clock,
): CredentialProviderProcessor =
CredentialProviderProcessorImpl(
context,
authRepository,
bitwardenCredentialManager,
intentManager,
clock,
biometricsEncryptionManager,
featureFlagManager,
dispatcherManager,
context = context,
authRepository = authRepository,
bitwardenCredentialManager = bitwardenCredentialManager,
intentManager = intentManager,
clock = clock,
biometricsEncryptionManager = biometricsEncryptionManager,
dispatcherManager = dispatcherManager,
)
@Provides
@@ -108,12 +106,10 @@ object CredentialProviderModule {
fun provideCredentialEntryBuilder(
@ApplicationContext context: Context,
intentManager: IntentManager,
featureFlagManager: FeatureFlagManager,
biometricsEncryptionManager: BiometricsEncryptionManager,
): CredentialEntryBuilder = CredentialEntryBuilderImpl(
context = context,
intentManager = intentManager,
featureFlagManager = featureFlagManager,
biometricsEncryptionManager = biometricsEncryptionManager,
)

View File

@@ -33,8 +33,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -60,7 +58,6 @@ class CredentialProviderProcessorImpl(
private val intentManager: IntentManager,
private val clock: Clock,
private val biometricsEncryptionManager: BiometricsEncryptionManager,
private val featureFlagManager: FeatureFlagManager,
dispatcherManager: DispatcherManager,
) : CredentialProviderProcessor {
@@ -186,9 +183,9 @@ class CredentialProviderProcessorImpl(
.Builder(
accountName = accountName,
pendingIntent = intentManager.createFido2CreationPendingIntent(
CREATE_PASSKEY_INTENT,
userId,
requestCode.getAndIncrement(),
action = CREATE_PASSKEY_INTENT,
userId = userId,
requestCode = requestCode.getAndIncrement(),
),
)
.setDescription(
@@ -202,9 +199,7 @@ class CredentialProviderProcessorImpl(
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)
if (isVaultUnlocked &&
featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation)
) {
if (isVaultUnlocked) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }

View File

@@ -14,12 +14,8 @@ import javax.crypto.Cipher
*/
fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher?,
isSingleTapAuthEnabled: Boolean,
): PublicKeyCredentialEntry.Builder =
if (isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
cipher != null &&
isSingleTapAuthEnabled
) {
if (isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) && cipher != null) {
setBiometricPromptData(
biometricPromptData = buildPromptDataWithCipher(cipher),
)

View File

@@ -24,8 +24,6 @@ sealed class FlagKey<out T : Any> {
EmailVerification,
CredentialExchangeProtocolImport,
CredentialExchangeProtocolExport,
SingleTapPasskeyCreation,
SingleTapPasskeyAuthentication,
RestrictCipherItemDeletion,
UserManagedPrivilegedApps,
RemoveCardPolicy,
@@ -67,22 +65,6 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable single tap passkey creation.
*/
data object SingleTapPasskeyCreation : FlagKey<Boolean>() {
override val keyName: String = "single-tap-passkey-creation"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable single tap passkey authentication.
*/
data object SingleTapPasskeyAuthentication : FlagKey<Boolean>() {
override val keyName: String = "single-tap-passkey-authentication"
override val defaultValue: Boolean = false
}
/**
* Data object holding the feature flag key to enable the restriction of cipher item deletion
*/

View File

@@ -29,8 +29,6 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.CredentialExchangeProtocolImport,
FlagKey.CredentialExchangeProtocolExport,
FlagKey.CipherKeyEncryption,
FlagKey.SingleTapPasskeyCreation,
FlagKey.SingleTapPasskeyAuthentication,
FlagKey.RestrictCipherItemDeletion,
FlagKey.UserManagedPrivilegedApps,
FlagKey.RemoveCardPolicy,
@@ -79,11 +77,6 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.CredentialExchangeProtocolImport -> stringResource(BitwardenString.cxp_import)
FlagKey.CredentialExchangeProtocolExport -> stringResource(BitwardenString.cxp_export)
FlagKey.CipherKeyEncryption -> stringResource(BitwardenString.cipher_key_encryption)
FlagKey.SingleTapPasskeyCreation -> stringResource(BitwardenString.single_tap_passkey_creation)
FlagKey.SingleTapPasskeyAuthentication -> {
stringResource(BitwardenString.single_tap_passkey_authentication)
}
FlagKey.RestrictCipherItemDeletion -> stringResource(BitwardenString.restrict_item_deletion)
FlagKey.UserManagedPrivilegedApps -> {
stringResource(BitwardenString.user_trusted_privileged_app_management)

View File

@@ -13,8 +13,6 @@ import com.bitwarden.fido.Fido2CredentialAutofillView
import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherListViewType
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.util.mockBuilder
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherListView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
@@ -59,14 +57,12 @@ class CredentialEntryBuilderTest {
)
} returns mockGetPasswordCredentialIntent
}
private val mockFeatureFlagManager = mockk<FeatureFlagManager>()
private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
private val mockBeginGetPublicKeyOption = mockk<BeginGetPublicKeyCredentialOption>()
private val mockBeginGetPasswordOption = mockk<BeginGetPasswordOption>()
private val credentialEntryBuilder = CredentialEntryBuilderImpl(
context = mockContext,
intentManager = mockIntentManager,
featureFlagManager = mockFeatureFlagManager,
biometricsEncryptionManager = mockBiometricsEncryptionManager,
)
private val mockPublicKeyCredentialEntry = mockk<PublicKeyCredentialEntry>(relaxed = true)
@@ -144,9 +140,6 @@ class CredentialEntryBuilderTest {
createMockFido2CredentialAutofillView(number = 1),
)
every {
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
} returns false
every {
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
} returns null
@@ -183,11 +176,8 @@ class CredentialEntryBuilderTest {
createMockFido2CredentialAutofillView(number = 1),
)
// Verify biometric prompt data is not set when flag is false, buildVersion is at least 35,
// Verify biometric prompt data is not set when buildVersion is at least 35
// and cipher is null.
every {
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
} returns false
every {
mockBiometricsEncryptionManager.getOrCreateCipher("userId")
} returns null
@@ -204,11 +194,7 @@ class CredentialEntryBuilderTest {
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
}
// Verify biometric prompt data is not set when flag is true, buildVersion is below 35, and
// cipher is null.
every {
mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
} returns true
// Verify biometric prompt data is not set when buildVersion is below 35 and cipher is null.
credentialEntryBuilder
.buildPublicKeyCredentialEntries(
userId = "userId",
@@ -221,7 +207,7 @@ class CredentialEntryBuilderTest {
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
}
// Verify biometric prompt data is not set when flag is true, buildVersion is at least 35,
// Verify biometric prompt data is not set when buildVersion is at least 35
// and cipher is null
every { isBuildVersionAtLeast(any()) } returns true
credentialEntryBuilder
@@ -250,7 +236,7 @@ class CredentialEntryBuilderTest {
anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
}
// Verify biometric prompt data is set when flag is true, buildVersion is >= 35, cipher is
// Verify biometric prompt data is set when buildVersion is >= 35, cipher is
// not null, and user is not verified
credentialEntryBuilder
.buildPublicKeyCredentialEntries(

View File

@@ -32,9 +32,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import io.mockk.coEvery
import io.mockk.every
@@ -74,10 +72,6 @@ class CredentialProviderProcessorTest {
private val intentManager: IntentManager = mockk()
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk()
private val featureFlagManager: FeatureFlagManager = mockk {
every { getFeatureFlag(FlagKey.SingleTapPasskeyCreation) } returns false
every { getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication) } returns false
}
private val cancellationSignal: CancellationSignal = mockk()
private val clock = FIXED_CLOCK
@@ -85,14 +79,13 @@ class CredentialProviderProcessorTest {
@BeforeEach
fun setUp() {
credentialProviderProcessor = CredentialProviderProcessorImpl(
context,
authRepository,
bitwardenCredentialManager,
intentManager,
clock,
biometricsEncryptionManager,
featureFlagManager,
dispatcherManager,
context = context,
authRepository = authRepository,
bitwardenCredentialManager = bitwardenCredentialManager,
intentManager = intentManager,
clock = clock,
biometricsEncryptionManager = biometricsEncryptionManager,
dispatcherManager = dispatcherManager,
)
mockkStatic(::isBuildVersionAtLeast)
@@ -144,9 +137,9 @@ class CredentialProviderProcessorTest {
every { callback.onError(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onError(any()) }
@@ -173,9 +166,9 @@ class CredentialProviderProcessorTest {
every { callback.onError(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onError(any()) }
@@ -201,9 +194,9 @@ class CredentialProviderProcessorTest {
every { callback.onError(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onError(any()) }
@@ -228,11 +221,14 @@ class CredentialProviderProcessorTest {
every { context.getString(any(), any()) } returns "mockDescription"
every {
intentManager.createFido2CreationPendingIntent(
any(),
any(),
any(),
action = any(),
userId = any(),
requestCode = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { request.candidateQueryData } returns candidateQueryData
every {
@@ -241,9 +237,9 @@ class CredentialProviderProcessorTest {
every { callback.onResult(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
@@ -275,21 +271,20 @@ class CredentialProviderProcessorTest {
every { callback.onResult(capture(captureSlot)) } just runs
every {
intentManager.createFido2CreationPendingIntent(
any(),
any(),
any(),
action = any(),
userId = any(),
requestCode = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk<Cipher>()
every { featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation) } returns true
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
@@ -324,24 +319,13 @@ class CredentialProviderProcessorTest {
// Verify entries have no biometric prompt data when cipher is null
every { biometricsEncryptionManager.getOrCreateCipher(any()) } returns null
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
assertTrue(
captureSlot.captured.createEntries.all { it.biometricPromptData == null },
) { "Expected all entries to have null biometric prompt data." }
// Disable single tap feature flag to verify all entries do not have biometric prompt data
every { featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyCreation) } returns false
credentialProviderProcessor.processCreateCredentialRequest(
request,
cancellationSignal,
callback,
)
assertTrue(
captureSlot.captured.createEntries.all { it.biometricPromptData == null },
) { "Expected all entries to not have biometric prompt data." }
}
@Test
@@ -362,9 +346,9 @@ class CredentialProviderProcessorTest {
every { callback.onError(capture(captureSlot)) } just runs
credentialProviderProcessor.processGetCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onError(any()) }
@@ -412,9 +396,9 @@ class CredentialProviderProcessorTest {
)
credentialProviderProcessor.processGetCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 0) { callback.onError(any()) }
@@ -461,9 +445,9 @@ class CredentialProviderProcessorTest {
} returns Result.failure(Exception("Error decrypting credentials."))
credentialProviderProcessor.processGetCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onError(any()) }
@@ -503,9 +487,9 @@ class CredentialProviderProcessorTest {
every { callback.onResult(capture(captureSlot)) } just runs
credentialProviderProcessor.processGetCredentialRequest(
request,
cancellationSignal,
callback,
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
assertEquals(1, captureSlot.captured.credentialEntries.size)

View File

@@ -25,14 +25,6 @@ class FlagKeyTest {
FlagKey.CipherKeyEncryption.keyName,
"cipher-key-encryption",
)
assertEquals(
FlagKey.SingleTapPasskeyCreation.keyName,
"single-tap-passkey-creation",
)
assertEquals(
FlagKey.SingleTapPasskeyAuthentication.keyName,
"single-tap-passkey-authentication",
)
assertEquals(
FlagKey.RestrictCipherItemDeletion.keyName,
"pm-15493-restrict-item-deletion-to-can-manage-permission",
@@ -54,8 +46,6 @@ class FlagKeyTest {
FlagKey.EmailVerification,
FlagKey.CredentialExchangeProtocolImport,
FlagKey.CredentialExchangeProtocolExport,
FlagKey.SingleTapPasskeyCreation,
FlagKey.SingleTapPasskeyAuthentication,
FlagKey.CipherKeyEncryption,
FlagKey.RestrictCipherItemDeletion,
FlagKey.UserManagedPrivilegedApps,

View File

@@ -147,8 +147,6 @@ private val DEFAULT_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
FlagKey.EmailVerification to true,
FlagKey.CredentialExchangeProtocolImport to true,
FlagKey.CredentialExchangeProtocolExport to true,
FlagKey.SingleTapPasskeyCreation to true,
FlagKey.SingleTapPasskeyAuthentication to true,
FlagKey.RestrictCipherItemDeletion to true,
FlagKey.UserManagedPrivilegedApps to true,
FlagKey.RemoveCardPolicy to true,
@@ -158,8 +156,6 @@ private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = persistentMapOf
FlagKey.EmailVerification to false,
FlagKey.CredentialExchangeProtocolImport to false,
FlagKey.CredentialExchangeProtocolExport to false,
FlagKey.SingleTapPasskeyCreation to false,
FlagKey.SingleTapPasskeyAuthentication to false,
FlagKey.RestrictCipherItemDeletion to false,
FlagKey.UserManagedPrivilegedApps to false,
FlagKey.RemoveCardPolicy to false,

View File

@@ -865,8 +865,6 @@ Do you want to switch to this account?</string>
<string name="you_ll_only_need_to_set_up_authenticator_key">Youll only need to set up Authenticator Key for logins that require two-factor authentication with a code. The key will continuously generate six-digit codes you can use to log in.</string>
<string name="coachmark_3_of_3">3 OF 3</string>
<string name="you_must_add_a_web_address_to_use_autofill_to_access_this_account">You must add a web address to use autofill to access this account.</string>
<string name="single_tap_passkey_creation">Single tap passkey creation</string>
<string name="single_tap_passkey_authentication">Single tap passkey sign-on</string>
<string name="learn_about_new_logins">Learn about new logins</string>
<string name="we_ll_walk_you_through_the_key_features_to_add_a_new_login">We\'ll walk you through the key features to add a new login.</string>
<string name="explore_the_generator">Explore the generator</string>