chromium/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/CableAuthenticator.java

// Copyright 2020 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.chrome.browser.webauth.authenticator;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbAccessory;
import android.os.Build;
import android.provider.Settings;
import android.util.Pair;

import org.jni_zero.CalledByNative;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskRunner;
import org.chromium.base.task.TaskTraits;
import org.chromium.blink.mojom.AuthenticatorStatus;
import org.chromium.blink.mojom.GetAssertionAuthenticatorResponse;
import org.chromium.blink.mojom.MakeCredentialAuthenticatorResponse;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.components.webauthn.AuthenticationContextProvider;
import org.chromium.components.webauthn.Fido2Api;
import org.chromium.components.webauthn.Fido2CredentialRequest;
import org.chromium.components.webauthn.FidoIntentSender;
import org.chromium.components.webauthn.GpmBrowserOptionsHelper;
import org.chromium.components.webauthn.WebauthnMode;
import org.chromium.components.webauthn.WebauthnModeProvider;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;
import org.chromium.url.Origin;

import java.nio.ByteBuffer;

/**
 * CableAuthenticator implements makeCredential and getAssertion operations on top of the Privileged
 * FIDO2 API.
 */
class CableAuthenticator implements AuthenticationContextProvider {
    private static final String TAG = "CableAuthenticator";

    private static final int REGISTER_REQUEST_CODE = 1;
    private static final int SIGN_REQUEST_CODE = 2;

    private static final int CTAP2_OK = 0;
    private static final int CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19;
    private static final int CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26;
    private static final int CTAP2_ERR_OPERATION_DENIED = 0x27;
    private static final int CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B;
    private static final int CTAP2_ERR_NO_CREDENTIALS = 0x2E;
    private static final int CTAP2_ERR_OTHER = 0x7F;

    private final Context mContext;
    private final CableAuthenticatorUI mUi;
    private final TaskRunner mTaskRunner;
    // mFCMEvent contains the serialized event data that was stored in the notification's
    // PendingIntent.
    private final byte[] mFCMEvent;
    // mServerLinkData contains the information passed from GMS Core in the event that
    // this is a SERVER_LINK connection.
    private final byte[] mServerLinkData;
    // mQRURI contains the contents of a QR code ("FIDO:/234"...), or null if
    // this is not a QR transaction.
    private final String mQRURI;
    // mLinkQR stores whether a QR transaction should send linking information.
    private boolean mLinkQR;
    // mAccessory contains the USB device, if operating in USB mode.
    private UsbAccessory mAccessory;

    // mHandle is the opaque ID returned by the native code to ensure that
    // |stop| doesn't apply to a transaction that this instance didn't create.
    private long mHandle;

    public enum Result {
        REGISTER_OK,
        REGISTER_ERROR,
        SIGN_OK,
        SIGN_ERROR,
        OTHER,
    }

    public enum RequestType {
        GET_ASSERTION,
        MAKE_CREDENTIAL,
    }

    public CableAuthenticator(
            Context context,
            CableAuthenticatorUI ui,
            long networkContext,
            long registration,
            byte[] secret,
            boolean isFcmNotification,
            UsbAccessory accessory,
            byte[] serverLink,
            byte[] fcmEvent,
            String qrURI) {
        mContext = context;
        mUi = ui;
        mFCMEvent = fcmEvent;
        mServerLinkData = serverLink;
        mQRURI = qrURI;
        mAccessory = accessory;

        // networkContext can only be used from the UI thread, therefore all
        // short-lived work is done on that thread.
        mTaskRunner = PostTask.createTaskRunner(TaskTraits.UI_USER_VISIBLE);
        ThreadUtils.assertOnUiThread();

        WebauthnModeProvider.getInstance().setGlobalWebauthnMode(WebauthnMode.CHROME);
        CableAuthenticatorJni.get().setup(registration, networkContext, secret);

        // Wait for |onTransportReady|.
    }

    // Calls from native code.

    // Called when an informative status update is available. The argument has the same values
    // as the Status enum from v2_authenticator.h.
    @CalledByNative
    public void onStatus(int code) {
        mUi.onStatus(code);
    }

    @CalledByNative
    public static BLEAdvert newBLEAdvert(byte[] payload) {
        return new BLEAdvert(payload);
    }

    @CalledByNative
    public void makeCredential(@JniType("std::vector<uint8_t>") byte[] serializedParams) {
        PublicKeyCredentialCreationOptions params =
                PublicKeyCredentialCreationOptions.deserialize(ByteBuffer.wrap(serializedParams));
        // The Chrome hybrid authenticator never supported creation-time
        // evaluation of PRFs and, by the time we added support in general, we
        // were already in the process of rolling out the hybrid authenticator
        // in Play Services and so it continued not to be supported.
        params.prfInput = null;

        final Fido2CredentialRequest request = new Fido2CredentialRequest(this);
        request.setIsHybridRequest(true);
        final Origin origin = Origin.create(new GURL("https://" + params.relyingParty.id));
        request.handleMakeCredentialRequest(
                params,
                params.challenge,
                GpmBrowserOptionsHelper.createDefaultBrowserOptions(),
                origin,
                origin,
                (status, response) -> {
                    mTaskRunner.postTask(
                            () ->
                                    CableAuthenticatorJni.get()
                                            .onAuthenticatorAttestationResponse(
                                                    CTAP2_OK,
                                                    response.attestationObject,
                                                    response.prf));
                    mUi.onAuthenticatorResult(Result.REGISTER_OK);
                },
                (status) -> {
                    final boolean isInvalidStateError =
                            status == AuthenticatorStatus.CREDENTIAL_EXCLUDED;

                    mTaskRunner.postTask(
                            () ->
                                    CableAuthenticatorJni.get()
                                            .onAuthenticatorAttestationResponse(
                                                    isInvalidStateError
                                                            ? CTAP2_ERR_CREDENTIAL_EXCLUDED
                                                            : CTAP2_ERR_OPERATION_DENIED,
                                                    null,
                                                    false));

                    mUi.onAuthenticatorResult(
                            isInvalidStateError ? Result.REGISTER_OK : Result.REGISTER_ERROR);
                });
    }

    @CalledByNative
    public void getAssertion(@JniType("std::vector<uint8_t>") byte[] serializedParams) {
        PublicKeyCredentialRequestOptions params =
                PublicKeyCredentialRequestOptions.deserialize(ByteBuffer.wrap(serializedParams));

        final Fido2CredentialRequest request = new Fido2CredentialRequest(this);
        request.setIsHybridRequest(true);
        final Origin origin = Origin.create(new GURL("https://" + params.relyingPartyId));
        request.handleGetAssertionRequest(
                params,
                /* maybeClientDataHash= */ params.challenge,
                origin,
                origin,
                /* payment= */ null,
                (status, response) -> {
                    response.info.clientDataJson = new byte[0];
                    ByteBuffer buffer = response.serialize();
                    byte[] serialized = new byte[buffer.remaining()];
                    buffer.get(serialized);
                    mTaskRunner.postTask(
                            () ->
                                    CableAuthenticatorJni.get()
                                            .onAuthenticatorAssertionResponse(
                                                    CTAP2_OK, serialized));
                    mUi.onAuthenticatorResult(Result.SIGN_OK);
                },
                (status) -> {
                    mTaskRunner.postTask(
                            () ->
                                    CableAuthenticatorJni.get()
                                            .onAuthenticatorAssertionResponse(
                                                    CTAP2_ERR_OPERATION_DENIED, null));
                    mUi.onAuthenticatorResult(Result.SIGN_ERROR);
                });
    }

    @Override
    public Context getContext() {
        return mContext;
    }

    @Override
    public RenderFrameHost getRenderFrameHost() {
        return null;
    }

    @Override
    public FidoIntentSender getIntentSender() {
        return mUi;
    }

    @Override
    public WebContents getWebContents() {
        return null;
    }

    /**
     * Called from native code when a network-based operation has completed.
     *
     * @param ok true if the transaction completed successfully. Otherwise it indicates some form of
     *     error that could include tunnel server errors, handshake failures, etc.
     * @param errorCode a value from cablev2::authenticator::Platform::Error.
     */
    @CalledByNative
    public void onComplete(boolean ok, int errorCode) {
        ThreadUtils.assertOnUiThread();
        mUi.onComplete(ok, errorCode);
    }

    void onActivityStop() {
        CableAuthenticatorJni.get().onActivityStop(mHandle);
    }

    void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i(TAG, "onActivityResult " + requestCode + " " + resultCode);

        boolean isMakeCredential;
        Result result;
        switch (requestCode) {
            case REGISTER_REQUEST_CODE:
                isMakeCredential = true;
                result = Result.REGISTER_ERROR;
                break;
            case SIGN_REQUEST_CODE:
                isMakeCredential = false;
                result = Result.SIGN_ERROR;
                break;
            default:
                Log.e(TAG, "Ignoring unknown request code " + requestCode);
                return;
        }

        int ctapStatus = CTAP2_ERR_OTHER;
        Object response = null;

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

            case Activity.RESULT_CANCELED:
                ctapStatus = CTAP2_ERR_OPERATION_DENIED;
                break;

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

        if (response == null) {
            // Use already set error code.
        } 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 : ""));

            switch (error.first) {
                case Fido2Api.INVALID_STATE_ERR:
                    if (isMakeCredential) {
                        ctapStatus = CTAP2_ERR_CREDENTIAL_EXCLUDED;
                    } else {
                        ctapStatus = CTAP2_ERR_NO_CREDENTIALS;
                    }
                    break;
                case Fido2Api.NOT_ALLOWED_ERR:
                    if (error.second != null
                            && error.second.equals(
                                    "Request doesn't have a valid list of allowed credentials.")) {
                        ctapStatus = CTAP2_ERR_NO_CREDENTIALS;
                    } else {
                        ctapStatus = CTAP2_ERR_OPERATION_DENIED;
                    }
                    break;
                case Fido2Api.NOT_SUPPORTED_ERR:
                    ctapStatus = CTAP2_ERR_UNSUPPORTED_OPTION;
                    break;
                default:
                    ctapStatus = CTAP2_ERR_OTHER;
                    break;
            }
        } else if (isMakeCredential) {
            if (response instanceof MakeCredentialAuthenticatorResponse) {
                MakeCredentialAuthenticatorResponse r =
                        (MakeCredentialAuthenticatorResponse) response;

                onAuthenticatorAttestationResponse(CTAP2_OK, r.attestationObject, r.prf);
                result = Result.REGISTER_OK;
            }
        } else {
            if (response instanceof GetAssertionAuthenticatorResponse) {
                GetAssertionAuthenticatorResponse gaResponse =
                        (GetAssertionAuthenticatorResponse) response;
                ByteBuffer buffer = gaResponse.serialize();
                byte[] serialized = new byte[buffer.remaining()];
                buffer.get(serialized);
                onAuthenticatorAssertionResponse(CTAP2_OK, serialized);
                result = Result.SIGN_OK;
            }
        }

        if (result != Result.REGISTER_OK && result != Result.SIGN_OK) {
            if (isMakeCredential) {
                onAuthenticatorAttestationResponse(ctapStatus, null, false);
            } else {
                onAuthenticatorAssertionResponse(ctapStatus, null);
            }
        }

        mUi.onAuthenticatorResult(result);
    }

    private void onAuthenticatorAttestationResponse(
            int ctapStatus, byte[] attestationObject, boolean prfEnabled) {
        mTaskRunner.postTask(
                () ->
                        CableAuthenticatorJni.get()
                                .onAuthenticatorAttestationResponse(
                                        ctapStatus, attestationObject, prfEnabled));
    }

    private void onAuthenticatorAssertionResponse(int ctapStatus, byte[] responseBytes) {
        mTaskRunner.postTask(
                () ->
                        CableAuthenticatorJni.get()
                                .onAuthenticatorAssertionResponse(ctapStatus, responseBytes));
    }

    // Calls from UI.

    void setQRLinking(boolean link) {
        mLinkQR = link;
    }

    /** Called to indicate that either USB or Bluetooth transports are ready for processing. */
    void onTransportReady() {
        ThreadUtils.assertOnUiThread();

        if (mServerLinkData != null) {
            mHandle = CableAuthenticatorJni.get().startServerLink(this, mServerLinkData);
        } else if (mQRURI != null) {
            mHandle = CableAuthenticatorJni.get().startQR(this, getName(), mQRURI, mLinkQR);
        } else if (mFCMEvent != null) {
            mHandle = CableAuthenticatorJni.get().startCloudMessage(this, mFCMEvent);
        } else {
            mHandle =
                    CableAuthenticatorJni.get()
                            .startUSB(this, new USBHandler(mContext, mTaskRunner, mAccessory));
        }
    }

    void close() {
        ThreadUtils.assertOnUiThread();
        CableAuthenticatorJni.get().stop(mHandle);
    }

    String getName() {
        final String name =
                Settings.Global.getString(
                        mContext.getContentResolver(), Settings.Global.DEVICE_NAME);
        if (name != null && name.length() > 0) {
            return name;
        }
        return Build.MANUFACTURER + " " + Build.MODEL;
    }

    /**
     * validateServerLinkData returns zero if |serverLink| is a valid argument for |startServerLink|
     * or else an error value from cablev2::authenticator::Platform::Error.
     */
    static int validateServerLinkData(byte[] serverLinkData) {
        return CableAuthenticatorJni.get().validateServerLinkData(serverLinkData);
    }

    /**
     * validateQRURI returns zero if |uri| is a valid FIDO QR code or else an error value from
     * cablev2::authenticator::Platform::Error.
     */
    static int validateQRURI(String uri) {
        return CableAuthenticatorJni.get().validateQRURI(uri);
    }

    @NativeMethods("cablev2_authenticator")
    interface Natives {
        /**
         * setup is called before any other functions in order for the native code to perform
         * one-time setup operations. It may be called several times, but subsequent calls are
         * ignored.
         */
        void setup(
                long registration,
                long networkContext,
                @JniType("std::vector<uint8_t>") byte[] secret);

        /**
         * Called to instruct the C++ code to start a new transaction using |usbDevice|. Returns an
         * opaque value that can be passed to |stop| to cancel this transaction.
         */
        long startUSB(CableAuthenticator cableAuthenticator, USBHandler usbDevice);

        /**
         * Called to instruct the C++ code to start a new transaction based on the contents of a QR
         * code. The given name will be transmitted to the peer in order to identify this device, it
         * should be human-meaningful. The qrURI must be a fido: URI. Returns an opaque value that
         * can be passed to |stop| to cancel this transaction.
         */
        long startQR(
                CableAuthenticator cableAuthenticator,
                @JniType("std::string") String authenticatorName,
                @JniType("std::string") String qrURI,
                boolean link);

        /**
         * Called to instruct the C++ code to start a new transaction based on the given link
         * information which has been provided by the server. Returns an opaque value that can be
         * passed to |stop| to cancel this transaction.
         */
        long startServerLink(
                CableAuthenticator cableAuthenticator,
                @JniType("std::vector<uint8_t>") byte[] serverLinkData);

        /**
         * Called when a GCM message is received and the user has tapped on the resulting
         * notification. fcmEvent contains a serialized event, as created by
         * |webauthn::authenticator::Registration::Event::Serialize|.
         */
        long startCloudMessage(
                CableAuthenticator cableAuthenticator,
                @JniType("std::vector<uint8_t>") byte[] fcmEvent);

        /**
         * Called to alert the C++ code to stop any ongoing transactions. Takes an opaque handle
         * value that was returned by one of the |start*| functions.
         */
        void stop(long handle);

        /**
         * validateServerLinkData returns zero if |serverLink| is a valid argument for
         * |startServerLink| or else an error value from cablev2::authenticator::Platform::Error.
         */
        int validateServerLinkData(@JniType("std::vector<uint8_t>") byte[] serverLinkData);

        /**
         * validateQRURI returns zero if |qrURI| is a valid fido: URI or else an error value from
         * cablev2::authenticator::Platform::Error.
         */
        int validateQRURI(@JniType("std::string") String qrURI);

        /**
         * onActivityStop is called when onStop() is called on the Activity. This is done in order
         * to record events because we want to know when users are abandoning the process.
         */
        void onActivityStop(long handle);

        /** Called to alert native code of a response to a makeCredential request. */
        void onAuthenticatorAttestationResponse(
                int ctapStatus,
                @JniType("std::vector<uint8_t>") byte[] attestationObject,
                boolean prfEnabled);

        /** Called to alert native code of a response to a getAssertion request. */
        void onAuthenticatorAssertionResponse(int ctapStatus, byte[] responseBytes);
    }
}