From: uazo Date: Thu, 9 Oct 2025 09:11:01 +0000 Subject: Add setting to enable Credman for passkeys Enables Credential Manager for passkey management on A14+ devices via a switch in the settings. By default, the feature is disabled. In other versions of Android, webauthn appears to be enabled on websites but always returns a timeout error as per specifications. Conditional immediate support is disabled. License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html --- .../settings/PasswordSettings.java | 27 +++++++++++++++++++ .../touch_to_fill_controller.cc | 2 ++ ...etting-to-enable-Credman-for-passkeys.grdp | 12 +++++++++ .../webauthn/AuthenticatorImpl.java | 24 +++++++---------- .../components/webauthn/WebauthnFeatures.java | 2 ++ .../cred_man/CredManSupportProvider.java | 25 +++++------------ .../webauthn/android/webauthn_feature_map.cc | 1 + components/webauthn/features.cc | 4 +++ components/webauthn/features.h | 3 +++ device/fido/public/features.cc | 1 + .../authentication_credentials_container.cc | 5 ++++ 11 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Add-setting-to-enable-Credman-for-passkeys.grdp diff --git a/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java --- a/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java @@ -11,6 +11,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; @@ -68,6 +69,10 @@ import org.chromium.chrome.browser.ui.messages.snackbar.INeedSnackbarManager; import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar; import org.chromium.chrome.browser.lifetime.ApplicationLifetime; +import org.chromium.chrome.browser.flags.CromiteNativeUtils; +import org.chromium.components.webauthn.WebauthnFeatureMap; +import org.chromium.components.webauthn.WebauthnFeatures; + /** * The "Passwords" screen in Settings, which allows the user to enable or disable password saving, * to view saved passwords (just the username and URL), and to delete saved passwords. @@ -334,6 +339,7 @@ public class PasswordSettings extends ChromeBaseSettingsFragment createSavePasswordsSwitch(); createAutoSignInCheckbox(); createEnableAndroidAutofillSwitch(); + createCredManSwitch(); PasswordManagerHandlerProvider.getForProfile(getProfile()) .getPasswordManagerHandler() @@ -687,6 +693,27 @@ public class PasswordSettings extends ChromeBaseSettingsFragment getPrefService().getBoolean(Pref.CREDENTIALS_ENABLE_AUTOSIGNIN)); } + private void createCredManSwitch() { + ChromeSwitchPreference credManSwitch = + new ChromeSwitchPreference(getStyledContext(), null); + credManSwitch.setTitle(R.string.enable_android_credman_title); + credManSwitch.setOrder(0); + credManSwitch.setSummary(R.string.enable_android_credman_summary); + credManSwitch.setChecked( + WebauthnFeatureMap.getInstance().isEnabled(WebauthnFeatures.WEBAUTHN_ANDROID_PASSKEY)); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + credManSwitch.setSummary(R.string.enable_android_credman_disabled_summary); + credManSwitch.setEnabled(false); + } + credManSwitch.setOnPreferenceChangeListener((preference, newValue) -> { + CromiteNativeUtils.setFlagEnabled("WebAuthenticationAndroidPasskey", (boolean)newValue); + if (!mSnackbarManagerSupplier.get().isShowing()) + mSnackbarManagerSupplier.get().showSnackbar(mSnackbar); + return true; + }); + getPreferenceScreen().addPreference(credManSwitch); + } + private void displayManageAccountLink() { SyncService syncService = SyncServiceFactory.getForProfile(getProfile()); if (syncService == null || !syncService.isEngineInitialized()) { diff --git a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc --- a/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc +++ b/chrome/browser/touch_to_fill/password_manager/touch_to_fill_controller.cc @@ -312,6 +312,7 @@ void TouchToFillController::Reset() { } visibility_controller_->Reset(); credentials_.clear(); + cred_man_delegate_ = nullptr; } void TouchToFillController::ActionCompleted() { @@ -319,6 +320,7 @@ void TouchToFillController::ActionCompleted() { visibility_controller_->SetShown(); } ttf_delegate_.reset(); + cred_man_delegate_ = nullptr; } DisplayTarget TouchToFillController::GetResponsibleDisplayTarget( diff --git a/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Add-setting-to-enable-Credman-for-passkeys.grdp b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Add-setting-to-enable-Credman-for-passkeys.grdp new file mode 100644 --- /dev/null +++ b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Add-setting-to-enable-Credman-for-passkeys.grdp @@ -0,0 +1,12 @@ + + + + Enable Android Credential Manager + + + Enable support for Android Credential Manager + + + Not supported on your device (require A14+) + + diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/AuthenticatorImpl.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/AuthenticatorImpl.java --- a/components/webauthn/android/java/src/org/chromium/components/webauthn/AuthenticatorImpl.java +++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/AuthenticatorImpl.java @@ -30,6 +30,8 @@ import org.chromium.blink.mojom.WebAuthnClientCapability; import org.chromium.build.annotations.NullMarked; import org.chromium.build.annotations.Nullable; import org.chromium.components.password_manager.BrowserAssistedLoginType; +import org.chromium.components.webauthn.CredManSupport; +import org.chromium.components.webauthn.cred_man.CredManSupportProvider; import org.chromium.components.ukm.UkmRecorder; import org.chromium.content_public.browser.RenderFrameHost; import org.chromium.content_public.browser.Visibility; @@ -167,8 +169,7 @@ public final class AuthenticatorImpl implements Authenticator, AuthenticationCon mIsPaymentRequest = options.isPaymentCredentialCreation; mRequestCallback = requestCallback; mRequestCallback.setCompletionCallback(this::cleanupRequest); - if (!GmsCoreUtils.isWebauthnSupported() - || (!isChrome(mWebContents) && !GmsCoreUtils.isResultReceiverSupported())) { + if (CredManSupportProvider.getCredManSupport() == CredManSupport.DISABLED) { mRequestCallback.onComplete( WebauthnRequestResponse.forFailedMakeCredential( AuthenticatorStatus.NOT_IMPLEMENTED, @@ -254,8 +255,7 @@ public final class AuthenticatorImpl implements Authenticator, AuthenticationCon && DeviceFeatureMap.isEnabled( DeviceFeatureList .WEBAUTHN_AUTHENTICATOR_PASSWORDS_ONLY_IMMEDIATE_REQUESTS); - if (!GmsCoreUtils.isWebauthnSupported() - || (!isChrome(mWebContents) && !GmsCoreUtils.isResultReceiverSupported()) + if (CredManSupportProvider.getCredManSupport() == CredManSupport.DISABLED || (options.publicKey == null && !isPasswordOnlyFlux)) { RequestMetrics metrics = new RequestMetrics.Builder() @@ -300,12 +300,11 @@ public final class AuthenticatorImpl implements Authenticator, AuthenticationCon } private boolean couldSupportConditionalMediation() { - return GmsCoreUtils.isWebauthnSupported() && isChrome(mWebContents); + return isChrome(mWebContents); } private boolean couldSupportUvpaa() { - return GmsCoreUtils.isWebauthnSupported() - && (isChrome(mWebContents) || GmsCoreUtils.isResultReceiverSupported()); + return isChrome(mWebContents); } @Override @@ -379,22 +378,19 @@ public final class AuthenticatorImpl implements Authenticator, AuthenticationCon capabilities.add( createWebAuthnClientCapability( AuthenticatorConstants.CAPABILITY_CONDITIONAL_GET, - couldSupportConditionalMediation() && isUvpaa)); + true)); capabilities.add( createWebAuthnClientCapability( AuthenticatorConstants.CAPABILITY_UVPAA, - couldSupportUvpaa() && isUvpaa)); + true)); capabilities.add( createWebAuthnClientCapability( AuthenticatorConstants.CAPABILITY_CONDITIONAL_CREATE, - isUvpaa && couldSupportConditionalMediation())); + true)); capabilities.add( createWebAuthnClientCapability( AuthenticatorConstants.CAPABILITY_IMMEDIATE_GET, - DeviceFeatureMap.isEnabled( - DeviceFeatureList - .WEBAUTHN_IMMEDIATE_GET) - && isUvpaa)); + false)); capabilities.add( createWebAuthnClientCapability( AuthenticatorConstants diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/WebauthnFeatures.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/WebauthnFeatures.java --- a/components/webauthn/android/java/src/org/chromium/components/webauthn/WebauthnFeatures.java +++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/WebauthnFeatures.java @@ -19,4 +19,6 @@ public abstract class WebauthnFeatures { public static final String WEBAUTHN_ANDROID_CRED_MAN_FOR_DEV = "WebAuthnAndroidCredManForDev"; public static final String WEBAUTHN_ANDROID_CRED_MAN_REQUEST_EXTRA_BUNDLE = "WebAuthenticationAndroidCredManRequestExtraBundle"; + public static final String WEBAUTHN_ANDROID_PASSKEY = + "WebAuthenticationAndroidPasskey"; } diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManSupportProvider.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManSupportProvider.java --- a/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManSupportProvider.java +++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManSupportProvider.java @@ -19,11 +19,11 @@ import org.chromium.base.version_info.VersionInfo; import org.chromium.build.annotations.NullMarked; import org.chromium.build.annotations.Nullable; import org.chromium.components.webauthn.CredManSupport; -import org.chromium.components.webauthn.GmsCoreUtils; import org.chromium.components.webauthn.WebauthnFeatureMap; import org.chromium.components.webauthn.WebauthnFeatures; import org.chromium.components.webauthn.WebauthnMode; import org.chromium.components.webauthn.WebauthnModeProvider; +import org.chromium.components.webauthn.WebauthnLogger; @NullMarked public class CredManSupportProvider { @@ -79,12 +79,12 @@ public class CredManSupportProvider { } if (getAndroidVersion() < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { sCredManSupport = CredManSupport.DISABLED; - log(TAG, "Disabled because of Android version."); + WebauthnLogger.logError(TAG, "Disabled because of Android version."); return sCredManSupport; } - if (notSkippedBecauseInTests() && hasOldGmsVersion()) { + if (!WebauthnFeatureMap.getInstance().isEnabled(WebauthnFeatures.WEBAUTHN_ANDROID_PASSKEY)) { sCredManSupport = CredManSupport.DISABLED; - log(TAG, "Disabled because of old GMS version."); + WebauthnLogger.logError(TAG, "Disabled because of user."); return sCredManSupport; } if (notSkippedBecauseInTests() @@ -92,14 +92,14 @@ public class CredManSupportProvider { == null) { sCredManSupport = CredManSupport.DISABLED; recordCredManAvailability(/* available= */ false); - log(TAG, "Disabled because CredentialManager is not available."); + WebauthnLogger.logError(TAG, "Disabled because CredentialManager is not available."); return sCredManSupport; } recordCredManAvailability(/* available= */ true); final CredManUiRecommender recommender = ServiceLoaderUtil.maybeCreate(CredManUiRecommender.class); - boolean customUiRecommended = recommender != null && recommender.recommendsCustomUi(); + boolean customUiRecommended = true; boolean gpmInCredMan = sOverrideForcesGpm != null ? sOverrideForcesGpm : customUiRecommended; boolean isChrome3pPwmMode = @@ -138,13 +138,6 @@ public class CredManSupportProvider { "WebAuthentication.Android.CredManAvailability", available); } - private static boolean hasOldGmsVersion() { - assert sOverrideAndroidVersion == null : "Don't use in testing!"; - // The check works for unavailable and low GMS versions. `getGmsCoreVersion()` is -1 if the - // GMS version can't be retrieved. Chrome assumes an insufficient GMS availability then. - return GmsCoreUtils.getGmsCoreVersion() < getMinGmsVersionForCurrentChannel(); - } - private static int getAndroidVersion() { return sOverrideAndroidVersion == null ? Build.VERSION.SDK_INT : sOverrideAndroidVersion; } @@ -152,10 +145,4 @@ public class CredManSupportProvider { private static boolean notSkippedBecauseInTests() { return sOverrideForcesGpm == null && sOverrideAndroidVersion == null; } - - private static int getMinGmsVersionForCurrentChannel() { - return (VersionInfo.isBetaBuild() || VersionInfo.isStableBuild()) - ? GMSCORE_MIN_VERSION_BETA_STABLE - : GMSCORE_MIN_VERSION_CANARY_DEV; - } } diff --git a/components/webauthn/android/webauthn_feature_map.cc b/components/webauthn/android/webauthn_feature_map.cc --- a/components/webauthn/android/webauthn_feature_map.cc +++ b/components/webauthn/android/webauthn_feature_map.cc @@ -18,6 +18,7 @@ const base::Feature* const kFeaturesExposedToJava[] = { &kWebAuthnAndroidPasskeyCacheMigration, &kWebAuthnAndroidCredManForDev, &kWebAuthnAndroidCredManRequestExtraBundle, + &kWebAuthnAndroidPasskey, }; // static diff --git a/components/webauthn/features.cc b/components/webauthn/features.cc --- a/components/webauthn/features.cc +++ b/components/webauthn/features.cc @@ -34,6 +34,10 @@ BASE_FEATURE(kWebAuthnAndroidCredManRequestExtraBundle, BASE_FEATURE(kWebAuthnConditionalUiSuppressedOnWebView, base::FEATURE_ENABLED_BY_DEFAULT); +CROMITE_FEATURE(kWebAuthnAndroidPasskey, + "WebAuthenticationAndroidPasskey", + base::FEATURE_DISABLED_BY_DEFAULT); + #endif // BUILDFLAG(IS_ANDROID) #if !BUILDFLAG(IS_ANDROID) diff --git a/components/webauthn/features.h b/components/webauthn/features.h --- a/components/webauthn/features.h +++ b/components/webauthn/features.h @@ -27,6 +27,9 @@ BASE_DECLARE_FEATURE(kWebAuthnAndroidCredManForDev); COMPONENT_EXPORT(WEBAUTHN) extern const base::FeatureParam kWebAuthnAndroidCredManForDevMode; +COMPONENT_EXPORT(WEBAUTHN) +BASE_DECLARE_FEATURE(kWebAuthnAndroidPasskey); + // Whether to send a custom bundle values for the CredMan requests. The extras // are used by Google Password Manager. COMPONENT_EXPORT(WEBAUTHN) diff --git a/device/fido/public/features.cc b/device/fido/public/features.cc --- a/device/fido/public/features.cc +++ b/device/fido/public/features.cc @@ -183,6 +183,7 @@ BASE_FEATURE_PARAM(int, BASE_FEATURE(kWebAuthnImmediateGet, "WebAuthenticationImmediateGet", base::FEATURE_ENABLED_BY_DEFAULT); +SET_CROMITE_FEATURE_DISABLED(kWebAuthnImmediateGet); BASE_FEATURE_PARAM(int, kWebAuthnImmediateMediationTimeoutMilliseconds, diff --git a/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc b/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc --- a/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc +++ b/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc @@ -1157,6 +1157,11 @@ DOMException* AuthenticatorStatusToDOMException( const WebAuthnDOMExceptionDetailsPtr& dom_exception_details) { DCHECK_EQ(status != AuthenticatorStatus::ERROR_WITH_DOM_EXCEPTION_DETAILS, dom_exception_details.is_null()); + if (status == AuthenticatorStatus::UNKNOWN_ERROR || + status == AuthenticatorStatus::NOT_IMPLEMENTED) { + status = AuthenticatorStatus::NOT_ALLOWED_ERROR; + base::PlatformThread::Sleep(base::Milliseconds(base::RandInt(1000, 6000))); + } switch (status) { case AuthenticatorStatus::SUCCESS: NOTREACHED(); --