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

// Copyright 2021 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 android.content.Context;

import androidx.annotation.VisibleForTesting;

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

import org.chromium.blink.mojom.AuthenticatorStatus;
import org.chromium.blink.mojom.PaymentOptions;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.Origin;

import java.nio.ByteBuffer;

/**
 * Acts as a bridge from InternalAuthenticator declared in
 * //components/webauthn/android/internal_authenticator_android.h to AuthenticatorImpl.
 *
 * <p>The origin associated with requests on InternalAuthenticator should be set by calling
 * setEffectiveOrigin() first.
 */
@JNINamespace("webauthn")
public class InternalAuthenticator {
    private long mNativeInternalAuthenticatorAndroid;
    private final AuthenticatorImpl mAuthenticator;

    private InternalAuthenticator(
            long nativeInternalAuthenticatorAndroid,
            Context context,
            WebContents webContents,
            FidoIntentSender intentSender,
            RenderFrameHost renderFrameHost,
            Origin topOrigin) {
        mNativeInternalAuthenticatorAndroid = nativeInternalAuthenticatorAndroid;
        WebauthnModeProvider.getInstance().setGlobalWebauthnMode(WebauthnMode.CHROME);
        mAuthenticator =
                new AuthenticatorImpl(
                        context,
                        webContents,
                        intentSender,
                        /* createConfirmationUiDelegate= */ null,
                        renderFrameHost,
                        topOrigin);
    }

    public static InternalAuthenticator createForTesting(
            Context context,
            FidoIntentSender intentSender,
            RenderFrameHost renderFrameHost,
            Origin topOrigin) {
        return new InternalAuthenticator(
                -1, context, /* webContents= */ null, intentSender, renderFrameHost, topOrigin);
    }

    @CalledByNative
    public static InternalAuthenticator create(
            long nativeInternalAuthenticatorAndroid, RenderFrameHost renderFrameHost) {
        final WebContents webContents = WebContentsStatics.fromRenderFrameHost(renderFrameHost);
        final WindowAndroid window = webContents.getTopLevelNativeWindow();
        final Context context = window.getActivity().get();
        final Origin topOrigin = webContents.getMainFrame().getLastCommittedOrigin();
        return new InternalAuthenticator(
                nativeInternalAuthenticatorAndroid,
                context,
                webContents,
                new AuthenticatorImpl.WindowIntentSender(window),
                renderFrameHost,
                topOrigin);
    }

    @CalledByNative
    public void clearNativePtr() {
        mNativeInternalAuthenticatorAndroid = 0;
    }

    @CalledByNative
    public void setEffectiveOrigin(Origin origin) {
        mAuthenticator.setEffectiveOrigin(origin);
    }

    @CalledByNative
    public void setPaymentOptions(ByteBuffer payment) {
        mAuthenticator.setPaymentOptions(PaymentOptions.deserialize(payment));
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public void makeCredential(ByteBuffer optionsByteBuffer) {
        mAuthenticator.makeCredential(
                PublicKeyCredentialCreationOptions.deserialize(optionsByteBuffer),
                (status, response, domExceptionDetails) -> {
                    // DOMExceptions can only be passed through the webAuthenticationProxy
                    // extensions API, which doesn't exist on Android.
                    assert status != AuthenticatorStatus.ERROR_WITH_DOM_EXCEPTION_DETAILS
                            && domExceptionDetails == null;
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get()
                                .invokeMakeCredentialResponse(
                                        mNativeInternalAuthenticatorAndroid,
                                        status,
                                        response == null ? null : response.serialize());
                    }
                });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public void getAssertion(ByteBuffer optionsByteBuffer) {
        mAuthenticator.getAssertion(
                PublicKeyCredentialRequestOptions.deserialize(optionsByteBuffer),
                (status, response, domExceptionDetails) -> {
                    // DOMExceptions can only be passed through the webAuthenticationProxy
                    // extensions API, which doesn't exist on Android.
                    assert status != AuthenticatorStatus.ERROR_WITH_DOM_EXCEPTION_DETAILS
                            && domExceptionDetails == null;
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get()
                                .invokeGetAssertionResponse(
                                        mNativeInternalAuthenticatorAndroid,
                                        status,
                                        response == null ? null : response.serialize());
                    }
                });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process. The response will be passed through
     * |invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse()|.
     */
    @CalledByNative
    public void isUserVerifyingPlatformAuthenticatorAvailable() {
        mAuthenticator.isUserVerifyingPlatformAuthenticatorAvailable(
                (isUVPAA) -> {
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get()
                                .invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse(
                                        mNativeInternalAuthenticatorAndroid, isUVPAA);
                    }
                });
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process.
     */
    @CalledByNative
    public boolean isGetMatchingCredentialIdsSupported() {
        return GmsCoreUtils.isGetMatchingCredentialIdsSupported();
    }

    /**
     * Called by InternalAuthenticatorAndroid, which facilitates WebAuthn for processes that
     * originate from the browser process. The response will be passed through
     * |invokeGetMatchingCredentialIdsResponse()|.
     */
    @CalledByNative
    public void getMatchingCredentialIds(
            String relyingPartyId, byte[][] credentialIds, boolean requireThirdPartyPayment) {
        mAuthenticator.getMatchingCredentialIds(
                relyingPartyId,
                credentialIds,
                requireThirdPartyPayment,
                (matchingCredentialIds) -> {
                    if (mNativeInternalAuthenticatorAndroid != 0) {
                        InternalAuthenticatorJni.get()
                                .invokeGetMatchingCredentialIdsResponse(
                                        mNativeInternalAuthenticatorAndroid,
                                        matchingCredentialIds.toArray(new byte[0][]));
                    }
                });
    }

    @CalledByNative
    public void cancel() {
        mAuthenticator.cancel();
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    @NativeMethods
    public interface Natives {
        void invokeMakeCredentialResponse(
                long nativeInternalAuthenticatorAndroid, int status, ByteBuffer byteBuffer);

        void invokeGetAssertionResponse(
                long nativeInternalAuthenticatorAndroid, int status, ByteBuffer byteBuffer);

        void invokeIsUserVerifyingPlatformAuthenticatorAvailableResponse(
                long nativeInternalAuthenticatorAndroid, boolean isUVPAA);

        void invokeGetMatchingCredentialIdsResponse(
                long nativeInternalAuthenticatorAndroid, byte[][] matchingCredentialIds);
    }
}