chromium/components/webauthn/android/java/src/org/chromium/components/webauthn/cred_man/CredManGetCredentialRequestHelper.java

// Copyright 2023 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.cred_man;

import static org.chromium.components.webauthn.cred_man.CredManHelper.CRED_MAN_PREFIX;
import static org.chromium.components.webauthn.cred_man.CredManHelper.TYPE_PASSKEY;

import android.credentials.CredentialOption;
import android.credentials.GetCredentialRequest;
import android.os.Build;
import android.os.Bundle;

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

import org.chromium.content_public.browser.RenderFrameHost;

/**
 * This class is responsible for holding the arguments to create a valid {@link
 * GetCredentialRequest}. The request can be formed using the `getGetCredentialRequest` method.
 */
class CredManGetCredentialRequestHelper {
    private static CredManGetCredentialRequestHelper sInstanceForTesting;

    // Auto-select means that, when an allowlist is present and one of the providers matches with
    // it, the account selector can be skipped. (However, if two or more providers match with the
    // allowlist then the selector will, sadly, still be shown.)
    private static final String IS_AUTO_SELECT_ALLOWED =
            CRED_MAN_PREFIX + "BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED";
    private static final String TYPE_PASSWORD_CREDENTIAL =
            "android.credentials.TYPE_PASSWORD_CREDENTIAL";

    private String mRequestAsJson;
    private byte[] mClientDataHash;
    private boolean mPreferImmediatelyAvailable;
    private boolean mAllowAutoSelect;
    private boolean mRequestPasswords;

    @Nullable private String mOrigin;
    private boolean mPlayServicesAvailable;
    private boolean mIgnoreGpm;
    @Nullable private RenderFrameHost mRenderFrameHost;

    static class Builder {
        private CredManGetCredentialRequestHelper mHelper;

        Builder(
                String requestAsJson,
                byte[] clientDataHash,
                boolean preferImmediatelyAvailable,
                boolean allowAutoSelect,
                boolean requestPasswords) {
            mHelper = CredManGetCredentialRequestHelper.getInstance();
            mHelper.mRequestAsJson = requestAsJson;
            mHelper.mClientDataHash = clientDataHash;
            mHelper.mPreferImmediatelyAvailable = preferImmediatelyAvailable;
            mHelper.mAllowAutoSelect = allowAutoSelect;
            mHelper.mRequestPasswords = requestPasswords;
        }

        Builder setOrigin(String origin) {
            mHelper.mOrigin = origin;
            return this;
        }

        Builder setPlayServicesAvailable(boolean playServicesAvailable) {
            mHelper.mPlayServicesAvailable = playServicesAvailable;
            return this;
        }

        Builder setIgnoreGpm(boolean ignoreGpm) {
            mHelper.mIgnoreGpm = ignoreGpm;
            return this;
        }

        Builder setRenderFrameHost(RenderFrameHost renderFrameHost) {
            mHelper.mRenderFrameHost = renderFrameHost;
            return this;
        }

        CredManGetCredentialRequestHelper build() {
            if (sInstanceForTesting != null) return sInstanceForTesting;
            return mHelper;
        }
    }

    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    GetCredentialRequest getGetCredentialRequest(@Nullable CredManRequestDecorator decorator) {
        final Bundle requestBundle = getGetCredentialRequestBundle(decorator);
        var builder = new GetCredentialRequest.Builder(requestBundle);
        final CredentialOption publicKeyCredentialOption = getPublicKeyCredentialOption(decorator);
        final CredentialOption passwordCredentialOption = getPasswordCredentialOption(decorator);
        if (decorator != null) {
            decorator.updateGetCredentialRequestBuilder(builder, this);
        }
        builder.addCredentialOption(publicKeyCredentialOption);
        if (passwordCredentialOption != null) builder.addCredentialOption(passwordCredentialOption);
        return builder.build();
    }

    boolean getPreferImmediatelyAvailable() {
        return mPreferImmediatelyAvailable;
    }

    @Nullable
    String getOrigin() {
        return mOrigin;
    }

    boolean getPlayServicesAvailable() {
        return mPlayServicesAvailable;
    }

    boolean getIgnoreGpm() {
        return mIgnoreGpm;
    }

    @Nullable
    RenderFrameHost getRenderFrameHost() {
        return mRenderFrameHost;
    }

    private Bundle getGetCredentialRequestBundle(@Nullable CredManRequestDecorator decorator) {
        Bundle bundle = getBaseGetCredentialRequestBundle();
        if (decorator != null) {
            decorator.updateGetCredentialRequestBundle(bundle, this);
        }
        return bundle;
    }

    private Bundle getBaseGetCredentialRequestBundle() {
        Bundle getCredentialRequestBundle = new Bundle();
        getCredentialRequestBundle.putBoolean(
                CRED_MAN_PREFIX + "BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS",
                mPreferImmediatelyAvailable);
        return getCredentialRequestBundle;
    }

    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private CredentialOption getPublicKeyCredentialOption(
            @Nullable CredManRequestDecorator decorator) {
        Bundle publicKeyCredentialOptionBundle = getBasePublicKeyCredentialOptionBundle();
        if (decorator != null) {
            decorator.updatePublicKeyCredentialOptionBundle(publicKeyCredentialOptionBundle, this);
        }
        CredentialOption publicKeyCredentialOption =
                new CredentialOption.Builder(
                                TYPE_PASSKEY,
                                publicKeyCredentialOptionBundle,
                                publicKeyCredentialOptionBundle)
                        .build();
        return publicKeyCredentialOption;
    }

    private Bundle getBasePublicKeyCredentialOptionBundle() {
        Bundle publicKeyCredentialOptionBundle = new Bundle();
        publicKeyCredentialOptionBundle.putString(
                CRED_MAN_PREFIX + "BUNDLE_KEY_SUBTYPE",
                CRED_MAN_PREFIX + "BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION");
        publicKeyCredentialOptionBundle.putString(
                CRED_MAN_PREFIX + "BUNDLE_KEY_REQUEST_JSON", mRequestAsJson);
        publicKeyCredentialOptionBundle.putByteArray(
                CRED_MAN_PREFIX + "BUNDLE_KEY_CLIENT_DATA_HASH", mClientDataHash);
        publicKeyCredentialOptionBundle.putBoolean(IS_AUTO_SELECT_ALLOWED, mAllowAutoSelect);
        return publicKeyCredentialOptionBundle;
    }

    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    private @Nullable CredentialOption getPasswordCredentialOption(
            @Nullable CredManRequestDecorator decorator) {
        if (!mRequestPasswords) return null;
        Bundle passwordCredentialOptionBundle = new Bundle();
        if (decorator != null) {
            decorator.updatePasswordCredentialOptionBundle(passwordCredentialOptionBundle, this);
        }
        var builder =
                new CredentialOption.Builder(
                        TYPE_PASSWORD_CREDENTIAL,
                        passwordCredentialOptionBundle,
                        passwordCredentialOptionBundle);
        if (decorator != null) {
            decorator.updatePasswordCredentialOptionBuilder(builder, this);
        }
        return builder.build();
    }

    private static CredManGetCredentialRequestHelper getInstance() {
        if (sInstanceForTesting != null) return sInstanceForTesting;
        return new CredManGetCredentialRequestHelper();
    }

    public static void setInstanceForTesting(CredManGetCredentialRequestHelper instanceForTesting) {
        sInstanceForTesting = instanceForTesting;
    }

    private CredManGetCredentialRequestHelper() {}
}