chromium/components/webauthn/android/java/src/org/chromium/components/webauthn/Fido2CredentialRequest.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.webauthn;

import static org.chromium.components.webauthn.WebauthnModeProvider.is;
import static org.chromium.components.webauthn.WebauthnModeProvider.isChrome;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcel;
import android.os.ResultReceiver;
import android.os.SystemClock;
import android.util.Pair;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import com.google.android.gms.tasks.Task;

import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.blink.mojom.AuthenticatorStatus;
import org.chromium.blink.mojom.AuthenticatorTransport;
import org.chromium.blink.mojom.GetAssertionAuthenticatorResponse;
import org.chromium.blink.mojom.MakeCredentialAuthenticatorResponse;
import org.chromium.blink.mojom.PaymentOptions;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialDescriptor;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.blink.mojom.PublicKeyCredentialType;
import org.chromium.blink.mojom.ResidentKeyRequirement;
import org.chromium.components.webauthn.Fido2ApiCall.Fido2ApiCallParams;
import org.chromium.components.webauthn.cred_man.CredManHelper;
import org.chromium.components.webauthn.cred_man.CredManSupportProvider;
import org.chromium.content_public.browser.ClientDataJson;
import org.chromium.content_public.browser.ClientDataRequestType;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.net.GURLUtils;
import org.chromium.url.Origin;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Uses the Google Play Services Fido2 APIs. Holds the logic of each request. */
@JNINamespace("webauthn")
public class Fido2CredentialRequest
        implements Callback<Pair<Integer, Intent>>, WebauthnBrowserBridge.Provider {
    private static final String TAG = "Fido2Request";
    static final String NON_EMPTY_ALLOWLIST_ERROR_MSG =
            "Authentication request must have non-empty allowList";
    static final String NON_VALID_ALLOWED_CREDENTIALS_ERROR_MSG =
            "Request doesn't have a valid list of allowed credentials.";
    static final String NO_SCREENLOCK_ERROR_MSG = "The device is not secured with any screen lock";
    static final String CREDENTIAL_EXISTS_ERROR_MSG =
            "One of the excluded credentials exists on the local device";
    static final String LOW_LEVEL_ERROR_MSG = "Low level error 0x6a80";

    // mPlayServicesAvailable caches whether the Play Services FIDO API is
    // available.
    private final boolean mPlayServicesAvailable;
    private final AuthenticationContextProvider mAuthenticationContextProvider;
    private GetAssertionResponseCallback mGetAssertionCallback;
    private MakeCredentialResponseCallback mMakeCredentialCallback;
    private FidoErrorResponseCallback mErrorCallback;
    private CredManHelper mCredManHelper;
    private Barrier mBarrier;
    // mFrameHost is null in makeCredential requests. For getAssertion requests
    // it's non-null for conditional requests and may be non-null in other
    // requests.
    private boolean mAppIdExtensionUsed;
    private boolean mEchoCredProps;
    private WebauthnBrowserBridge mBrowserBridge;
    // mIsHybridRequest is true if this request comes from a hybrid (i.e. cross-device) flow rather
    // than a WebContents. Handling the hybrid protocol can be delegated to Chrome (by Play
    // Services).
    private boolean mIsHybridRequest;

    public enum ConditionalUiState {
        NONE,
        WAITING_FOR_RP_ID_VALIDATION,
        WAITING_FOR_CREDENTIAL_LIST,
        WAITING_FOR_SELECTION,
        REQUEST_SENT_TO_PLATFORM,
        CANCEL_PENDING,
        CANCEL_PENDING_RP_ID_VALIDATION_COMPLETE,
    }

    private ConditionalUiState mConditionalUiState = ConditionalUiState.NONE;

    // Not null when the GMSCore-created ClientDataJson needs to be overridden or when using the
    // CredMan API.
    @Nullable private byte[] mClientDataJson;

    /**
     * Constructs the object.
     *
     * @param intentSender Interface for starting {@link Intent}s from Play Services.
     */
    public Fido2CredentialRequest(AuthenticationContextProvider authenticationContextProvider) {
        mAuthenticationContextProvider = authenticationContextProvider;
        boolean playServicesAvailable;
        try {
            playServicesAvailable = Fido2ApiCallHelper.getInstance().arePlayServicesAvailable();
        } catch (Exception e) {
            playServicesAvailable = false;
        }
        mPlayServicesAvailable = playServicesAvailable;
        mCredManHelper =
                new CredManHelper(mAuthenticationContextProvider, this, mPlayServicesAvailable);
        mBarrier = new Barrier(this::returnErrorAndResetCallback);
    }

    private void returnErrorAndResetCallback(int error) {
        assert mErrorCallback != null;
        if (mErrorCallback == null) return;
        mErrorCallback.onError(error);
        mErrorCallback = null;
        mGetAssertionCallback = null;
        mMakeCredentialCallback = null;
    }

    private Barrier.Mode getBarrierMode() {
        @CredManSupport int support = CredManSupportProvider.getCredManSupport();
        if (support != CredManSupport.DISABLED && mIsHybridRequest) {
            return Barrier.Mode.ONLY_CRED_MAN;
        }
        switch (support) {
            case CredManSupport.DISABLED:
                return Barrier.Mode.ONLY_FIDO_2_API;
            case CredManSupport.IF_REQUIRED:
                if (mIsHybridRequest) {
                    return Barrier.Mode.ONLY_CRED_MAN;
                }
                return Barrier.Mode.ONLY_FIDO_2_API;
            case CredManSupport.FULL_UNLESS_INAPPLICABLE:
                return Barrier.Mode.ONLY_CRED_MAN;
            case CredManSupport.PARALLEL_WITH_FIDO_2:
                return Barrier.Mode.BOTH;
        }
        assert support == CredManSupport.NOT_EVALUATED : "All `CredManMode`s must be handled!";
        return Barrier.Mode.ONLY_FIDO_2_API;
    }

    /**
     * Process a WebAuthn create() request.
     *
     * @param context The context used for both Play Services and CredMan calls.
     * @param options The arguments to create()
     * @param maybeClientDataHash The SHA-256 of the ClientDataJSON. Must be non-null iff frameHost
     *     from mAuthenticationContextProvider.frameHost is null.
     * @param maybeBrowserOptions Optional set of browser-specific data, like channel or incognito.
     * @param origin The origin that made the WebAuthn call.
     * @param topOrigin The origin of the main frame.
     * @param callback Success callback.
     * @param errorCallback Failure callback.
     */
    @SuppressWarnings("NewApi")
    public void handleMakeCredentialRequest(
            PublicKeyCredentialCreationOptions options,
            byte[] maybeClientDataHash,
            Bundle maybeBrowserOptions,
            Origin origin,
            Origin topOrigin,
            MakeCredentialResponseCallback callback,
            FidoErrorResponseCallback errorCallback) {
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        assert (frameHost != null) ^ (maybeClientDataHash != null);
        assert mMakeCredentialCallback == null && mErrorCallback == null;
        mMakeCredentialCallback = callback;
        mErrorCallback = errorCallback;

        if (frameHost != null) {
            frameHost.performMakeCredentialWebAuthSecurityChecks(
                    options.relyingParty.id,
                    origin,
                    options.isPaymentCredentialCreation,
                    (result) -> {
                        if (result.securityCheckResult != AuthenticatorStatus.SUCCESS) {
                            returnErrorAndResetCallback(result.securityCheckResult);
                            return;
                        }
                        continueMakeCredentialRequestAfterRpIdValidation(
                                options,
                                maybeClientDataHash,
                                maybeBrowserOptions,
                                origin,
                                topOrigin,
                                result.isCrossOrigin);
                    });
        } else {
            continueMakeCredentialRequestAfterRpIdValidation(
                    options,
                    maybeClientDataHash,
                    maybeBrowserOptions,
                    origin,
                    topOrigin,
                    /* isCrossOrigin= */ false);
        }
    }

    @SuppressWarnings("NewApi")
    private void continueMakeCredentialRequestAfterRpIdValidation(
            PublicKeyCredentialCreationOptions options,
            byte[] maybeClientDataHash,
            Bundle maybeBrowserOptions,
            Origin origin,
            Origin topOrigin,
            boolean isCrossOrigin) {
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        final boolean rkDiscouraged =
                options.authenticatorSelection == null
                        || options.authenticatorSelection.residentKey
                                == ResidentKeyRequirement.DISCOURAGED;
        mEchoCredProps = options.credProps;

        byte[] clientDataHash = maybeClientDataHash;
        if (clientDataHash == null) {
            assert options.challenge != null;
            final String callerOriginString = convertOriginToString(origin);
            clientDataHash =
                    buildClientDataJsonAndComputeHash(
                            ClientDataRequestType.WEB_AUTHN_CREATE,
                            callerOriginString,
                            options.challenge,
                            isCrossOrigin,
                            /* paymentOptions= */ null,
                            options.relyingParty.name,
                            topOrigin);
            if (clientDataHash == null) {
                returnErrorAndResetCallback(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                return;
            }
        }

        if (!isChrome(mAuthenticationContextProvider.getWebContents())) {
            if (CredManSupportProvider.getCredManSupportForWebView() == CredManSupport.DISABLED) {
                if (!mPlayServicesAvailable) {
                    Log.e(TAG, "Google Play Services' Fido2 API is not available.");
                    returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
                    return;
                }
                try {
                    Fido2ApiCallHelper.getInstance()
                            .invokeFido2MakeCredential(
                                    mAuthenticationContextProvider,
                                    options,
                                    Uri.parse(convertOriginToString(origin)),
                                    clientDataHash,
                                    maybeBrowserOptions,
                                    getMaybeResultReceiver(),
                                    this::onGotPendingIntent,
                                    this::onBinderCallException);
                } catch (NoSuchAlgorithmException e) {
                    returnErrorAndResetCallback(AuthenticatorStatus.ALGORITHM_UNSUPPORTED);
                    return;
                }
                return;
            }
            int result =
                    mCredManHelper.startMakeRequest(
                            options,
                            convertOriginToString(origin),
                            mClientDataJson,
                            clientDataHash,
                            mMakeCredentialCallback,
                            this::returnErrorAndResetCallback);
            if (result != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(result);
            return;
        }

        // If the PRF option is requested over hybrid then send the request
        // directly to Play Services. PRF evaluation points come over hybrid
        // pre-hashed, so it's not possible to send them via CredMan because
        // the JSON form of the PRF extension needs them unhashed. Thus, for
        // getAssertion, PRF requests go directly to Play Services. Because of
        // that, makeCredential requests with PRF are also sent directly to
        // Play Services so that users don't create a credential in a 3rd-party
        // password manager that they cannot then assert.
        final boolean prfOverHybrid = frameHost == null && options.prfEnable;

        // Send requests to Android 14+ CredMan if CredMan is enabled and
        // `gpm_in_cred_man` parameter is enabled.
        //
        // residentKey=discouraged requests are often for the traditional,
        // non-syncing platform authenticator on Android. A number of sites use
        // this and, so as not to disrupt them with Android 14, these requests
        // continue to be sent directly to Play Services.
        //
        // Otherwise these requests are for security keys, and Play Services is
        // currently the best answer for those requests too.
        //
        // Payments requests are also routed to Play Services since we haven't
        // defined how SPC works in CredMan yet.
        if (!rkDiscouraged
                && !options.isPaymentCredentialCreation
                && !prfOverHybrid
                && getBarrierMode() == Barrier.Mode.ONLY_CRED_MAN) {
            int result =
                    mCredManHelper.startMakeRequest(
                            options,
                            convertOriginToString(origin),
                            mClientDataJson,
                            clientDataHash,
                            mMakeCredentialCallback,
                            this::returnErrorAndResetCallback);
            if (result != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(result);
            return;
        }

        if (!mPlayServicesAvailable) {
            Log.e(TAG, "Google Play Services' Fido2PrivilegedApi is not available.");
            returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
            return;
        }

        try {
            Fido2ApiCallHelper.getInstance()
                    .invokeFido2MakeCredential(
                            mAuthenticationContextProvider,
                            options,
                            Uri.parse(convertOriginToString(origin)),
                            clientDataHash,
                            maybeBrowserOptions,
                            getMaybeResultReceiver(),
                            this::onGotPendingIntent,
                            this::onBinderCallException);
        } catch (NoSuchAlgorithmException e) {
            returnErrorAndResetCallback(AuthenticatorStatus.ALGORITHM_UNSUPPORTED);
            return;
        }
    }

    private void onBinderCallException(Exception e) {
        Log.e(TAG, "FIDO2 API call failed", e);
        returnErrorAndResetCallback(AuthenticatorStatus.NOT_ALLOWED_ERROR);
    }

    /**
     * Process a WebAuthn get() request.
     *
     * @param options The arguments to get(). If `isConditional` is true then `frameHost` must be
     *     non-null.
     * @param maybeClientDataHash The SHA-256 of the ClientDataJSON. Must be non-null iff frameHost
     *     from mAuthenticationContextProvider.frameHost is null.
     * @param origin The origin that made the WebAuthn call.
     * @param topOrigin The origin of the main frame.
     * @param payment Options for Secure Payment Confirmation. May only be non-null if `frameHost`
     *     is non-null.
     * @param callback Success callback.
     * @param errorCallback Failure callback.
     */
    @SuppressWarnings("NewApi")
    public void handleGetAssertionRequest(
            PublicKeyCredentialRequestOptions options,
            byte[] maybeClientDataHash,
            Origin origin,
            Origin topOrigin,
            PaymentOptions payment,
            GetAssertionResponseCallback callback,
            FidoErrorResponseCallback errorCallback) {
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        assert (frameHost != null) ^ (maybeClientDataHash != null);
        assert payment == null || frameHost != null;
        assert !options.isConditional || frameHost != null;
        assert mGetAssertionCallback == null && mErrorCallback == null;
        mGetAssertionCallback = callback;
        mErrorCallback = errorCallback;

        if (frameHost != null) {
            mConditionalUiState = ConditionalUiState.WAITING_FOR_RP_ID_VALIDATION;
            frameHost.performGetAssertionWebAuthSecurityChecks(
                    options.relyingPartyId,
                    origin,
                    payment != null,
                    (results) -> {
                        if (mConditionalUiState
                                == ConditionalUiState.CANCEL_PENDING_RP_ID_VALIDATION_COMPLETE) {
                            // This request was canceled while waiting for RP ID validation to
                            // complete.
                            returnErrorAndResetCallback(AuthenticatorStatus.ABORT_ERROR);
                            return;
                        }
                        mConditionalUiState = ConditionalUiState.NONE;
                        if (results.securityCheckResult != AuthenticatorStatus.SUCCESS) {
                            returnErrorAndResetCallback(results.securityCheckResult);
                            return;
                        }
                        continueGetAssertionRequestAfterRpIdValidation(
                                options,
                                maybeClientDataHash,
                                origin,
                                topOrigin,
                                payment,
                                results.isCrossOrigin);
                    });
            return;
        }

        continueGetAssertionRequestAfterRpIdValidation(
                options,
                maybeClientDataHash,
                origin,
                topOrigin,
                payment,
                /* isCrossOrigin= */ false);
    }

    @SuppressWarnings("NewApi")
    private void continueGetAssertionRequestAfterRpIdValidation(
            PublicKeyCredentialRequestOptions options,
            byte[] maybeClientDataHash,
            Origin origin,
            Origin topOrigin,
            PaymentOptions payment,
            boolean isCrossOrigin) {
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        boolean hasAllowCredentials =
                options.allowCredentials != null && options.allowCredentials.length != 0;

        if (!hasAllowCredentials) {
            // No UVM support for discoverable credentials.
            options.extensions.userVerificationMethods = false;
        }

        if (options.extensions.appid != null) {
            mAppIdExtensionUsed = true;
        }

        final String callerOriginString = convertOriginToString(origin);
        byte[] clientDataHash = maybeClientDataHash;
        if (clientDataHash == null) {
            assert options.challenge != null;
            clientDataHash =
                    buildClientDataJsonAndComputeHash(
                            (payment != null)
                                    ? ClientDataRequestType.PAYMENT_GET
                                    : ClientDataRequestType.WEB_AUTHN_GET,
                            callerOriginString,
                            options.challenge,
                            isCrossOrigin,
                            payment,
                            options.relyingPartyId,
                            topOrigin);
            if (clientDataHash == null) {
                returnErrorAndResetCallback(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                return;
            }
        } else {
            assert payment == null;
        }

        if (!isChrome(mAuthenticationContextProvider.getWebContents())) {
            if (options.isConditional) {
                returnErrorAndResetCallback(AuthenticatorStatus.NOT_IMPLEMENTED);
                return;
            }
            if (CredManSupportProvider.getCredManSupportForWebView() == CredManSupport.DISABLED) {
                if (!mPlayServicesAvailable) {
                    Log.e(TAG, "Google Play Services' Fido2 Api is not available.");
                    returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
                    return;
                }
                maybeDispatchGetAssertionRequest(options, callerOriginString, clientDataHash, null);
                return;
            }
            int result =
                    mCredManHelper.startGetRequest(
                            options,
                            callerOriginString,
                            mClientDataJson,
                            clientDataHash,
                            mGetAssertionCallback,
                            this::returnErrorAndResetCallback,
                            /* ignoreGpm= */ false);
            if (result != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(result);
            return;
        }

        // Payments should still go through Google Play Services. Also, if the request has
        // pre-hashed PRF inputs then we cannot represent that in JSON and so can only forward to
        // Play Services.
        final byte[] finalClientDataHash = clientDataHash;
        if (payment == null
                && !options.extensions.prfInputsHashed
                && getBarrierMode() == Barrier.Mode.ONLY_CRED_MAN) {
            if (options.isConditional) {
                mBarrier.resetAndSetWaitStatus(Barrier.Mode.ONLY_CRED_MAN);
                mCredManHelper.startPrefetchRequest(
                        options,
                        convertOriginToString(origin),
                        mClientDataJson,
                        clientDataHash,
                        mGetAssertionCallback,
                        this::returnErrorAndResetCallback,
                        mBarrier,
                        /* ignoreGpm= */ false);
            } else if (hasAllowCredentials && mPlayServicesAvailable) {
                // If the allowlist contains non-discoverable credentials then
                // the request needs to be routed directly to Play Services.
                checkForMatchingCredentials(options, origin, clientDataHash);
            } else {
                // WebauthnMode.CHROME_3PP_ENABLED will keep using CredMan's no credentials UI.
                if (is(mAuthenticationContextProvider.getWebContents(), WebauthnMode.CHROME)) {
                    mCredManHelper.setNoCredentialsFallback(
                            () ->
                                    this.maybeDispatchGetAssertionRequest(
                                            options,
                                            convertOriginToString(origin),
                                            finalClientDataHash,
                                            /* credentialId= */ null));
                } else {
                    mCredManHelper.setNoCredentialsFallback(null);
                }
                int response =
                        mCredManHelper.startGetRequest(
                                options,
                                convertOriginToString(origin),
                                mClientDataJson,
                                clientDataHash,
                                mGetAssertionCallback,
                                this::returnErrorAndResetCallback,
                                /* ignoreGpm= */ false);
                if (response != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(response);
            }
            return;
        }

        if (!mPlayServicesAvailable) {
            Log.e(TAG, "Google Play Services' Fido2PrivilegedApi is not available.");
            returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
            return;
        }

        // Conditional requests for Chrome 3rd party PWM mode when CredMan not enabled is not
        // defined yet.
        WebContents webContents = mAuthenticationContextProvider.getWebContents();
        if (options.isConditional && is(webContents, WebauthnMode.CHROME_3PP_ENABLED)) {

            returnErrorAndResetCallback(AuthenticatorStatus.NOT_IMPLEMENTED);
            return;
        }

        // Enumerate credentials from Play Services so that we can show the picker in Chrome UI.
        // Chrome 3rd party mode does not support enumeration in Chrome UI, hence use FIDO 2
        // enumeration for them.
        if (frameHost != null
                && (options.isConditional || !hasAllowCredentials)
                && is(webContents, WebauthnMode.CHROME)) {
            if (getBarrierMode() == Barrier.Mode.BOTH) {
                mBarrier.resetAndSetWaitStatus(Barrier.Mode.BOTH);
                mCredManHelper.startPrefetchRequest(
                        options,
                        callerOriginString,
                        mClientDataJson,
                        clientDataHash,
                        mGetAssertionCallback,
                        this::returnErrorAndResetCallback,
                        mBarrier,
                        /* ignoreGpm= */ true);
            } else {
                mBarrier.resetAndSetWaitStatus(Barrier.Mode.ONLY_FIDO_2_API);
            }
            mConditionalUiState = ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST;
            long conditionalUiCredentialListInitialTimeMs = SystemClock.elapsedRealtime();
            Fido2ApiCallHelper.getInstance()
                    .invokeFido2GetCredentials(
                            mAuthenticationContextProvider,
                            options.relyingPartyId,
                            (credentials) ->
                                    mBarrier.onFido2ApiSuccessful(
                                            () ->
                                                    onWebauthnCredentialDetailsListReceived(
                                                            options,
                                                            callerOriginString,
                                                            finalClientDataHash,
                                                            credentials,
                                                            conditionalUiCredentialListInitialTimeMs)),
                            (e) ->
                                    mBarrier.onFido2ApiFailed(
                                            AuthenticatorStatus.NOT_ALLOWED_ERROR));
            return;
        }

        if (hasAllowCredentials
                && !options.isConditional
                && getBarrierMode() == Barrier.Mode.BOTH) {
            checkForMatchingCredentials(options, origin, clientDataHash);
            return;
        }
        maybeDispatchGetAssertionRequest(options, callerOriginString, clientDataHash, null);
    }

    public void cancelConditionalGetAssertion() {
        mCredManHelper.cancelConditionalGetAssertion();

        switch (mConditionalUiState) {
            case WAITING_FOR_RP_ID_VALIDATION:
                mConditionalUiState = ConditionalUiState.CANCEL_PENDING_RP_ID_VALIDATION_COMPLETE;
                break;
            case WAITING_FOR_CREDENTIAL_LIST:
                mConditionalUiState = ConditionalUiState.CANCEL_PENDING;
                mBarrier.onFido2ApiCancelled();
                break;
            case WAITING_FOR_SELECTION:
                getBridge().cleanupRequest(mAuthenticationContextProvider.getRenderFrameHost());
                mConditionalUiState = ConditionalUiState.NONE;
                mBarrier.onFido2ApiCancelled();
                break;
            case REQUEST_SENT_TO_PLATFORM:
                // If the platform successfully completes the getAssertion then cancelation is
                // ignored, but if it returns an error then CANCEL_PENDING removes the option to
                // try again.
                mConditionalUiState = ConditionalUiState.CANCEL_PENDING;
                break;
            default:
                // No action
        }
    }

    public void handleIsUserVerifyingPlatformAuthenticatorAvailableRequest(
            IsUvpaaResponseCallback callback) {
        boolean chromeRequest = isChrome(mAuthenticationContextProvider.getWebContents());
        if ((!chromeRequest
                        && CredManSupportProvider.getCredManSupportForWebView()
                                == CredManSupport.FULL_UNLESS_INAPPLICABLE)
                || (chromeRequest && getBarrierMode() == Barrier.Mode.ONLY_CRED_MAN)) {
            callback.onIsUserVerifyingPlatformAuthenticatorAvailableResponse(true);
            return;
        }

        if (!mPlayServicesAvailable) {
            Log.e(TAG, "Google Play Services' Fido2PrivilegedApi is not available.");
            // Note that |IsUserVerifyingPlatformAuthenticatorAvailable| only returns
            // true or false, making it unable to handle any error status.
            // So it callbacks with false if Fido2PrivilegedApi is not available.
            callback.onIsUserVerifyingPlatformAuthenticatorAvailableResponse(false);
            return;
        }

        Fido2ApiCallParams params =
                WebauthnModeProvider.getInstance()
                        .getFido2ApiCallParams(mAuthenticationContextProvider.getWebContents());
        Fido2ApiCall call = new Fido2ApiCall(mAuthenticationContextProvider.getContext(), params);
        Fido2ApiCall.BooleanResult result = new Fido2ApiCall.BooleanResult();
        Parcel args = call.start();
        args.writeStrongBinder(result);

        Task<Boolean> task =
                call.run(
                        WebauthnModeProvider.getInstance()
                                .getFido2ApiCallParams(
                                        mAuthenticationContextProvider.getWebContents())
                                .mIsUserVerifyingPlatformAuthenticatorAvailableMethodId,
                        Fido2ApiCall.TRANSACTION_ISUVPAA,
                        args,
                        result);
        task.addOnSuccessListener(
                (isUVPAA) -> {
                    callback.onIsUserVerifyingPlatformAuthenticatorAvailableResponse(isUVPAA);
                });
        task.addOnFailureListener(
                (e) -> {
                    Log.e(TAG, "FIDO2 API call failed", e);
                    callback.onIsUserVerifyingPlatformAuthenticatorAvailableResponse(false);
                });
    }

    public void handleGetMatchingCredentialIdsRequest(
            String relyingPartyId,
            byte[][] allowCredentialIds,
            boolean requireThirdPartyPayment,
            GetMatchingCredentialIdsResponseCallback callback,
            FidoErrorResponseCallback errorCallback) {
        assert mErrorCallback == null;
        mErrorCallback = errorCallback;

        if (!mPlayServicesAvailable) {
            Log.e(TAG, "Google Play Services' Fido2PrivilegedApi is not available.");
            returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
            return;
        }

        Fido2ApiCallHelper.getInstance()
                .invokeFido2GetCredentials(
                        mAuthenticationContextProvider,
                        relyingPartyId,
                        (credentials) ->
                                onGetMatchingCredentialIdsListReceived(
                                        credentials,
                                        allowCredentialIds,
                                        requireThirdPartyPayment,
                                        callback),
                        this::onBinderCallException);
        return;
    }

    private void onGetMatchingCredentialIdsListReceived(
            List<WebauthnCredentialDetails> retrievedCredentials,
            byte[][] allowCredentialIds,
            boolean requireThirdPartyPayment,
            GetMatchingCredentialIdsResponseCallback callback) {
        List<byte[]> matchingCredentialIds = new ArrayList<>();
        for (WebauthnCredentialDetails credential : retrievedCredentials) {
            if (requireThirdPartyPayment && !credential.mIsPayment) continue;

            for (byte[] allowedId : allowCredentialIds) {
                if (Arrays.equals(allowedId, credential.mCredentialId)) {
                    matchingCredentialIds.add(credential.mCredentialId);
                    break;
                }
            }
        }
        callback.onResponse(matchingCredentialIds);
    }

    public void setIsHybridRequest(boolean isHybridRequest) {
        mIsHybridRequest = isHybridRequest;
    }

    public void overrideBrowserBridgeForTesting(WebauthnBrowserBridge bridge) {
        mBrowserBridge = bridge;
    }

    public void setCredManHelperForTesting(CredManHelper helper) {
        mCredManHelper = helper;
    }

    public void setBarrierForTesting(Barrier barrier) {
        mBarrier = barrier;
    }

    private void onWebauthnCredentialDetailsListReceived(
            PublicKeyCredentialRequestOptions options,
            String callerOriginString,
            byte[] clientDataHash,
            List<WebauthnCredentialDetails> credentials,
            long conditionalUiCredentialListInitialTimeMs) {
        assert mConditionalUiState == ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST
                || mConditionalUiState == ConditionalUiState.CANCEL_PENDING;

        boolean hasAllowCredentials =
                options.allowCredentials != null && options.allowCredentials.length != 0;
        boolean isConditionalRequest = options.isConditional;
        assert isConditionalRequest || !hasAllowCredentials;

        if (!credentials.isEmpty()) {
            RecordHistogram.recordTimesHistogram(
                    "WebAuthentication.CredentialFetchDuration.GmsCore",
                    SystemClock.elapsedRealtime() - conditionalUiCredentialListInitialTimeMs);
        }

        if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
            // The request was completed synchronously when the cancellation was received,
            // so no need to return an error to the renderer.
            mConditionalUiState = ConditionalUiState.NONE;
            return;
        }

        List<WebauthnCredentialDetails> discoverableCredentials = new ArrayList<>();
        for (WebauthnCredentialDetails credential : credentials) {
            if (!credential.mIsDiscoverable) continue;

            if (!hasAllowCredentials) {
                discoverableCredentials.add(credential);
                continue;
            }

            for (PublicKeyCredentialDescriptor descriptor : options.allowCredentials) {
                if (Arrays.equals(credential.mCredentialId, descriptor.id)) {
                    discoverableCredentials.add(credential);
                    break;
                }
            }
        }

        if (!isConditionalRequest
                && discoverableCredentials.isEmpty()
                && getBarrierMode() != Barrier.Mode.BOTH) {
            mConditionalUiState = ConditionalUiState.NONE;
            // When no passkeys are present for a non-conditional request, pass the request
            // through to GMSCore. It will show an error message to the user, but can offer the
            // user alternatives to use external passkeys.
            // If the barrier mode is BOTH, the no passkeys state is handled by Chrome. Do not pass
            // the request to GMSCore.
            maybeDispatchGetAssertionRequest(options, callerOriginString, clientDataHash, null);
            return;
        }

        Runnable hybridCallback = null;
        if (GmsCoreUtils.isHybridClientApiSupported()) {
            hybridCallback =
                    () ->
                            dispatchHybridGetAssertionRequest(
                                    options, callerOriginString, clientDataHash);
        }

        mConditionalUiState = ConditionalUiState.WAITING_FOR_SELECTION;
        getBridge()
                .onCredentialsDetailsListReceived(
                        mAuthenticationContextProvider.getRenderFrameHost(),
                        discoverableCredentials,
                        isConditionalRequest,
                        (selectedCredentialId) ->
                                maybeDispatchGetAssertionRequest(
                                        options,
                                        callerOriginString,
                                        clientDataHash,
                                        selectedCredentialId),
                        hybridCallback);
    }

    /**
     * Check whether a get() request needs routing to Play Services for a credential.
     *
     * <p>This function is called if a non-payments, non-conditional get() call with an allowlist is
     * received.
     *
     * <p>When Barrier.Mode is`ONLY_CRED_MAN`, all discoverable credentials are available in
     * CredMan. If any of the elements of the allowlist are non-discoverable credentials in the
     * local platform authenticator then the request should be sent directly to Play Services. It is
     * not required to dispatch the request to CredMan.
     *
     * <p>When Barrier.Mode is `BOTH`, some discoverable credentials may also be in Play Services.
     * If any of the elements of the allowlist match the credentials in the local platform
     * authenticator then the request should be sent directly to Play Services.
     */
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private void checkForMatchingCredentials(
            PublicKeyCredentialRequestOptions options, Origin callerOrigin, byte[] clientDataHash) {
        assert options.allowCredentials != null;
        assert options.allowCredentials.length > 0;
        assert !options.isConditional;
        assert mPlayServicesAvailable;
        Barrier.Mode mode = getBarrierMode();
        assert mode == Barrier.Mode.ONLY_CRED_MAN || mode == Barrier.Mode.BOTH;

        Fido2ApiCallHelper.getInstance()
                .invokeFido2GetCredentials(
                        mAuthenticationContextProvider,
                        options.relyingPartyId,
                        (credentials) ->
                                checkForMatchingCredentialsReceived(
                                        options, callerOrigin, clientDataHash, credentials),
                        (e) -> {
                            Log.e(
                                    TAG,
                                    "FIDO2 call to enumerate credentials failed. Dispatching to"
                                            + " CredMan. Barrier.Mode = "
                                            + mode,
                                    e);
                            mCredManHelper.startGetRequest(
                                    options,
                                    convertOriginToString(callerOrigin),
                                    mClientDataJson,
                                    clientDataHash,
                                    mGetAssertionCallback,
                                    this::returnErrorAndResetCallback,
                                    mode == Barrier.Mode.BOTH);
                        });
    }

    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private void checkForMatchingCredentialsReceived(
            PublicKeyCredentialRequestOptions options,
            Origin callerOrigin,
            byte[] clientDataHash,
            List<WebauthnCredentialDetails> retrievedCredentials) {
        assert options.allowCredentials != null;
        assert options.allowCredentials.length > 0;
        assert !options.isConditional;
        assert mPlayServicesAvailable;
        Barrier.Mode mode = getBarrierMode();
        assert mode == Barrier.Mode.ONLY_CRED_MAN || mode == Barrier.Mode.BOTH;

        for (WebauthnCredentialDetails credential : retrievedCredentials) {
            // In ONLY_CRED_MAN mode, all discoverable credentials are handled by CredMan. It is not
            // required to check for discoverable credentials.
            if (mode == Barrier.Mode.ONLY_CRED_MAN && credential.mIsDiscoverable) {
                continue;
            }

            for (PublicKeyCredentialDescriptor allowedId : options.allowCredentials) {
                if (allowedId.type != PublicKeyCredentialType.PUBLIC_KEY) {
                    continue;
                }

                if (Arrays.equals(allowedId.id, credential.mCredentialId)) {
                    // This get() request can be satisfied by Play Services with
                    // a non-discoverable credential so route it there.
                    maybeDispatchGetAssertionRequest(
                            options,
                            convertOriginToString(callerOrigin),
                            clientDataHash,
                            /* credentialId= */ null);
                    return;
                }
            }
        }

        mCredManHelper.setNoCredentialsFallback(
                () ->
                        this.maybeDispatchGetAssertionRequest(
                                options,
                                convertOriginToString(callerOrigin),
                                clientDataHash,
                                /* credentialId= */ null));

        // No elements of the allowlist are local, non-discoverable credentials
        // so route to CredMan.
        mCredManHelper.startGetRequest(
                options,
                convertOriginToString(callerOrigin),
                mClientDataJson,
                clientDataHash,
                mGetAssertionCallback,
                this::returnErrorAndResetCallback,
                mode == Barrier.Mode.BOTH);
    }

    private void maybeDispatchGetAssertionRequest(
            PublicKeyCredentialRequestOptions options,
            String callerOriginString,
            byte[] clientDataHash,
            byte[] credentialId) {
        assert mConditionalUiState == ConditionalUiState.NONE
                || mConditionalUiState == ConditionalUiState.REQUEST_SENT_TO_PLATFORM
                || mConditionalUiState == ConditionalUiState.WAITING_FOR_SELECTION;

        // If this is called a second time while the first sign-in attempt is still outstanding,
        // ignore the second call.
        if (mConditionalUiState == ConditionalUiState.REQUEST_SENT_TO_PLATFORM) {
            Log.e(TAG, "Received a second credential selection while the first still in progress.");
            return;
        }

        mConditionalUiState = ConditionalUiState.NONE;
        if (credentialId != null) {
            if (credentialId.length == 0) {
                if (options.isConditional) {
                    // An empty credential ID means an error from native code, which can happen if
                    // the embedder does not support Conditional UI.
                    Log.e(TAG, "Empty credential ID from account selection.");
                    getBridge().cleanupRequest(mAuthenticationContextProvider.getRenderFrameHost());
                    returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
                    return;
                }
                // For non-conditional requests, an empty credential ID means the user dismissed
                // the account selection dialog.
                returnErrorAndResetCallback(AuthenticatorStatus.NOT_ALLOWED_ERROR);
                return;
            }
            PublicKeyCredentialDescriptor selected_credential = new PublicKeyCredentialDescriptor();
            selected_credential.type = PublicKeyCredentialType.PUBLIC_KEY;
            selected_credential.id = credentialId;
            selected_credential.transports = new int[] {AuthenticatorTransport.INTERNAL};
            options.allowCredentials = new PublicKeyCredentialDescriptor[] {selected_credential};
        }

        if (options.isConditional) {
            mConditionalUiState = ConditionalUiState.REQUEST_SENT_TO_PLATFORM;
        }

        Fido2ApiCallHelper.getInstance()
                .invokeFido2GetAssertion(
                        mAuthenticationContextProvider,
                        options,
                        Uri.parse(callerOriginString),
                        clientDataHash,
                        getMaybeResultReceiver(),
                        this::onGotPendingIntent,
                        this::onBinderCallException);
    }

    private void dispatchHybridGetAssertionRequest(
            PublicKeyCredentialRequestOptions options,
            String callerOriginString,
            byte[] clientDataHash) {
        assert mConditionalUiState == ConditionalUiState.NONE
                || mConditionalUiState == ConditionalUiState.REQUEST_SENT_TO_PLATFORM
                || mConditionalUiState == ConditionalUiState.WAITING_FOR_SELECTION;

        if (mConditionalUiState == ConditionalUiState.REQUEST_SENT_TO_PLATFORM) {
            Log.e(TAG, "Received a second credential selection while the first still in progress.");
            return;
        }
        mConditionalUiState = ConditionalUiState.REQUEST_SENT_TO_PLATFORM;

        Fido2ApiCallParams params =
                WebauthnModeProvider.getInstance()
                        .getFido2ApiCallParams(mAuthenticationContextProvider.getWebContents());
        Fido2ApiCall call = new Fido2ApiCall(mAuthenticationContextProvider.getContext(), params);
        Parcel args = call.start();
        String callbackDescriptor =
                WebauthnModeProvider.getInstance()
                        .getFido2ApiCallParams(mAuthenticationContextProvider.getWebContents())
                        .mCallbackDescriptor;
        Fido2ApiCall.PendingIntentResult result =
                new Fido2ApiCall.PendingIntentResult(callbackDescriptor);
        args.writeStrongBinder(result);
        args.writeInt(1); // This indicates that the following options are present.
        Fido2Api.appendBrowserGetAssertionOptionsToParcel(
                options,
                Uri.parse(callerOriginString),
                clientDataHash,
                /* tunnelId= */ null,
                /* resultReceiver= */ null,
                args);
        Task<PendingIntent> task =
                call.run(
                        Fido2ApiCall.METHOD_BROWSER_HYBRID_SIGN,
                        Fido2ApiCall.TRANSACTION_HYBRID_SIGN,
                        args,
                        result);
        task.addOnSuccessListener(this::onGotPendingIntent);
        task.addOnFailureListener(this::onBinderCallException);
    }

    // Handles a PendingIntent from the GMSCore FIDO library.
    private void onGotPendingIntent(PendingIntent pendingIntent) {
        if (pendingIntent == null) {
            Log.e(TAG, "Didn't receive a pending intent.");
            returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
            return;
        }

        if (!mAuthenticationContextProvider.getIntentSender().showIntent(pendingIntent, this)) {
            Log.e(TAG, "Failed to send intent to FIDO API");
            returnErrorAndResetCallback(AuthenticatorStatus.UNKNOWN_ERROR);
            return;
        }
    }

    @Nullable
    private ResultReceiver getMaybeResultReceiver() {
        // The FIDO API traditionally returned a PendingIntent, which the calling app was expected
        // to invoke and then receive the result from the Activity it launched.
        //
        // However in a WebView context this is problematic because the WebView does not control the
        // app's activity to get the result. Thus support for using a ResultReceiver was added to
        // the API. Since we don't want to immediately increase the minimum GMS Core version needed
        // to run Chromium on Android, this code supports both methods.
        //
        // In time, once the GMS Core update has propagated sufficiently, we could consider removing
        // support for anything except the ResultReceiver.
        if (isChrome(mAuthenticationContextProvider.getWebContents())) return null;
        return new ResultReceiver(new Handler(Looper.getMainLooper())) {
            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {
                onResultReceiverResult(resultData);
            }
        };
    }

    private void onResultReceiverResult(Bundle resultData) {
        int errorCode = AuthenticatorStatus.UNKNOWN_ERROR;
        Object response = null;
        byte[] responseBytes = resultData.getByteArray(Fido2Api.CREDENTIAL_EXTRA);
        if (responseBytes != null) {
            try {
                response = Fido2Api.parseResponse(responseBytes);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Failed to parse FIDO2 API response from ResultReceiver", e);
                response = null;
            }
        }

        handleFido2Response(errorCode, response);
    }

    // Handles the result.
    @Override
    public void onResult(Pair<Integer, Intent> result) {
        final int resultCode = result.first;
        final Intent data = result.second;
        int errorCode = AuthenticatorStatus.UNKNOWN_ERROR;
        Object response = null;

        assert mConditionalUiState == ConditionalUiState.NONE
                || mConditionalUiState == ConditionalUiState.REQUEST_SENT_TO_PLATFORM
                || mConditionalUiState == ConditionalUiState.CANCEL_PENDING;

        switch (resultCode) {
            case Activity.RESULT_OK:
                if (data == null) {
                    errorCode = AuthenticatorStatus.NOT_ALLOWED_ERROR;
                } else {
                    try {
                        response = Fido2Api.parseIntentResponse(data);
                    } catch (IllegalArgumentException e) {
                        response = null;
                    }
                }
                break;

            case Activity.RESULT_CANCELED:
                errorCode = AuthenticatorStatus.NOT_ALLOWED_ERROR;
                break;

            default:
                Log.e(TAG, "FIDO2 PendingIntent resulted in code: " + resultCode);
                break;
        }

        handleFido2Response(errorCode, response);
    }

    private void handleFido2Response(int errorCode, Object response) {
        RenderFrameHost frameHost = mAuthenticationContextProvider.getRenderFrameHost();
        if (mConditionalUiState != ConditionalUiState.NONE) {
            if (response == null || response instanceof Pair) {
                if (response != null) {
                    Pair<Integer, String> error = (Pair<Integer, String>) response;
                    Log.e(
                            TAG,
                            "FIDO2 API call resulted in error: "
                                    + error.first
                                    + " "
                                    + (error.second != null ? error.second : ""));
                    errorCode = convertError(error);
                }

                if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                    mConditionalUiState = ConditionalUiState.NONE;
                    getBridge().cleanupRequest(frameHost);
                    mBarrier.onFido2ApiCancelled();
                } else {
                    // The user can try again by selecting another conditional UI credential.
                    mConditionalUiState = ConditionalUiState.WAITING_FOR_SELECTION;
                }
                return;
            }
            mConditionalUiState = ConditionalUiState.NONE;
            getBridge().cleanupRequest(frameHost);
        }

        if (response == null) {
            // Use the error already set.
        } else if (response instanceof Pair) {
            Pair<Integer, String> error = (Pair<Integer, String>) response;
            Log.e(
                    TAG,
                    "FIDO2 API call resulted in error: "
                            + error.first
                            + " "
                            + (error.second != null ? error.second : ""));
            errorCode = convertError(error);
        } else if (mMakeCredentialCallback != null) {
            if (response instanceof MakeCredentialAuthenticatorResponse) {
                MakeCredentialAuthenticatorResponse creationResponse =
                        (MakeCredentialAuthenticatorResponse) response;
                if (mEchoCredProps) {
                    // The other credProps fields will have been set by
                    // `parseIntentResponse` if Play Services provided credProps
                    // information.
                    creationResponse.echoCredProps = true;
                }
                if (mClientDataJson != null) {
                    creationResponse.info.clientDataJson = mClientDataJson;
                }
                mMakeCredentialCallback.onRegisterResponse(
                        AuthenticatorStatus.SUCCESS, creationResponse);
                mMakeCredentialCallback = null;
                return;
            }
        } else if (mGetAssertionCallback != null) {
            if (response instanceof GetAssertionAuthenticatorResponse) {
                GetAssertionAuthenticatorResponse r = (GetAssertionAuthenticatorResponse) response;
                if (mClientDataJson != null) {
                    r.info.clientDataJson = mClientDataJson;
                    if (frameHost != null) {
                        frameHost.notifyWebAuthnAssertionRequestSucceeded();
                    }
                }
                r.extensions.echoAppidExtension = mAppIdExtensionUsed;
                mGetAssertionCallback.onSignResponse(AuthenticatorStatus.SUCCESS, r);
                mGetAssertionCallback = null;
                return;
            }
        }

        returnErrorAndResetCallback(errorCode);
    }

    /**
     * Helper method to convert AuthenticatorErrorResponse errors.
     *
     * @param errorCode
     * @return error code corresponding to an AuthenticatorStatus.
     */
    private static int convertError(Pair<Integer, String> error) {
        final int errorCode = error.first;
        @Nullable final String errorMsg = error.second;

        switch (errorCode) {
            case Fido2Api.SECURITY_ERR:
                // AppId or rpID fails validation.
                return AuthenticatorStatus.INVALID_DOMAIN;
            case Fido2Api.TIMEOUT_ERR:
                return AuthenticatorStatus.NOT_ALLOWED_ERROR;
            case Fido2Api.ENCODING_ERR:
                // Error encoding results (after user consent).
                return AuthenticatorStatus.UNKNOWN_ERROR;
            case Fido2Api.NOT_ALLOWED_ERR:
                // The implementation doesn't support resident keys.
                if (errorMsg != null
                        && (errorMsg.equals(NON_EMPTY_ALLOWLIST_ERROR_MSG)
                                || errorMsg.equals(NON_VALID_ALLOWED_CREDENTIALS_ERROR_MSG))) {
                    return AuthenticatorStatus.EMPTY_ALLOW_CREDENTIALS;
                }
                // The request is not allowed, possibly because the user denied permission.
                return AuthenticatorStatus.NOT_ALLOWED_ERROR;
            case Fido2Api.DATA_ERR:
                // Incoming requests were malformed/inadequate. Fallthrough.
            case Fido2Api.NOT_SUPPORTED_ERR:
                // Request parameters were not supported.
                return AuthenticatorStatus.ANDROID_NOT_SUPPORTED_ERROR;
            case Fido2Api.CONSTRAINT_ERR:
                if (errorMsg != null && errorMsg.equals(NO_SCREENLOCK_ERROR_MSG)) {
                    return AuthenticatorStatus.USER_VERIFICATION_UNSUPPORTED;
                }
                return AuthenticatorStatus.UNKNOWN_ERROR;
            case Fido2Api.INVALID_STATE_ERR:
                if (errorMsg != null && errorMsg.equals(CREDENTIAL_EXISTS_ERROR_MSG)) {
                    return AuthenticatorStatus.CREDENTIAL_EXCLUDED;
                }
                // else fallthrough.
            case Fido2Api.UNKNOWN_ERR:
                if (errorMsg != null && errorMsg.equals(LOW_LEVEL_ERROR_MSG)) {
                    // The error message returned from GmsCore when the user attempted to use a
                    // credential that is not registered with a U2F security key.
                    return AuthenticatorStatus.NOT_ALLOWED_ERROR;
                }
                // fall through
            default:
                return AuthenticatorStatus.UNKNOWN_ERROR;
        }
    }

    @VisibleForTesting
    public static String convertOriginToString(Origin origin) {
        // Wrapping with GURLUtils.getOrigin() in order to trim default ports.
        return GURLUtils.getOrigin(
                origin.getScheme() + "://" + origin.getHost() + ":" + origin.getPort());
    }

    private byte[] buildClientDataJsonAndComputeHash(
            @ClientDataRequestType int clientDataRequestType,
            String callerOrigin,
            byte[] challenge,
            boolean isCrossOrigin,
            PaymentOptions paymentOptions,
            String relyingPartyId,
            Origin topOrigin) {
        String clientDataJson =
                ClientDataJson.buildClientDataJson(
                        clientDataRequestType,
                        callerOrigin,
                        challenge,
                        isCrossOrigin,
                        paymentOptions,
                        relyingPartyId,
                        topOrigin);
        if (clientDataJson == null) {
            return null;
        }
        mClientDataJson = clientDataJson.getBytes();
        MessageDigest messageDigest;
        try {
            messageDigest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
        messageDigest.update(mClientDataJson);
        return messageDigest.digest();
    }

    @Override
    public WebauthnBrowserBridge getBridge() {
        if (!isChrome(mAuthenticationContextProvider.getWebContents())) {
            return null;
        }
        if (mBrowserBridge == null) {
            mBrowserBridge = new WebauthnBrowserBridge();
        }
        return mBrowserBridge;
    }

    protected void destroyBridge() {
        if (mBrowserBridge == null) return;
        mBrowserBridge.destroy();
        mBrowserBridge = null;
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {
        String createOptionsToJson(ByteBuffer serializedOptions);

        byte[] makeCredentialResponseFromJson(String json);

        String getOptionsToJson(ByteBuffer serializedOptions);

        byte[] getCredentialResponseFromJson(String json);
    }
}