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

// Copyright 2022 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.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ResultReceiver;
import android.util.Base64;
import android.util.Pair;

import androidx.annotation.Nullable;

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

import org.chromium.base.Log;
import org.chromium.blink.mojom.AttestationConveyancePreference;
import org.chromium.blink.mojom.AuthenticationExtensionsClientOutputs;
import org.chromium.blink.mojom.AuthenticatorAttachment;
import org.chromium.blink.mojom.AuthenticatorTransport;
import org.chromium.blink.mojom.CommonCredentialInfo;
import org.chromium.blink.mojom.GetAssertionAuthenticatorResponse;
import org.chromium.blink.mojom.MakeCredentialAuthenticatorResponse;
import org.chromium.blink.mojom.PrfValues;
import org.chromium.blink.mojom.PublicKeyCredentialCreationOptions;
import org.chromium.blink.mojom.PublicKeyCredentialDescriptor;
import org.chromium.blink.mojom.PublicKeyCredentialParameters;
import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
import org.chromium.blink.mojom.PublicKeyCredentialType;
import org.chromium.blink.mojom.ResidentKeyRequirement;
import org.chromium.blink.mojom.UserVerificationRequirement;
import org.chromium.blink.mojom.UvmEntry;
import org.chromium.mojo_base.mojom.TimeDelta;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

/**
 * Fido2Api contains functions for serialising/deserialising the structures that make up Play
 * Services' FIDO2 API.
 *
 * <p>These structures are made with the {@link Parcel} class. Parcel is a linear, binary format
 * that doesn't contain type information. I.e. it knows how to write strings, ints, byte[], etc, but
 * the reading side has to know what order the values come in rather than being able to generically
 * parse like, e.g., JSON. (Parcel can also write Bundles, which are a dict-like object, but that is
 * not used in this API.)
 *
 * <p>Building on top of {@link Parcel} is a format called SafeParcelable. It adds a
 * tag-length-value structure that allows for optional fields and future extensions. Tags and
 * lengths are encoded in one or two ints. The bottom 16 bits of the first int are the tag number.
 * The top 16 bits are either the length, or else the value 0xffff, which indicates that the length
 * is contained in a following int. The single-int format is never encoded by this code so that we
 * never have to shuffle data around in the case that it's longer than 0xfffe bytes.
 *
 * <p>The function {@link writeHeader} writes a tag and placeholder length, returning the offset of
 * the length that can be filled in with a later call to {@link writeLength}. Thus encoding is
 * one-pass: the code doesn't need to calculate lengths before starting to serialise.
 *
 * <p>Objects start with a tag and length where the tag is the special value {@link OBJECT_MAGIC}.
 * Then they contain a series of tag-length-value structures. The tag communicates the type of the
 * value. If the tag is unknown then the length permits it to be skipped over.
 *
 * <p>Since these structures come from Play Services, they should be trustworthy. However, things
 * are a little more dicey than one would hope. {@link Parcel} JNIs to native code and setting a
 * position is accepted without checking if it's in bounds[1]. (There's a check for values > 2^31,
 * to catch negative ints from Java, but that's not in all Android versions.) When reading, there's
 * a bounds check, not until after a potentially overflowing addition[2]. Therefore we are careful
 * when setting the data position and always add lengths parsed from the data using {@link
 * addLengthToParcelPosition}.
 *
 * <p>[1]
 * https://android.googlesource.com/platform/frameworks/native/+/3cf307284a620c67b9eb024439583fc1c42574ee/libs/binder/Parcel.cpp#370
 * [2]
 * https://android.googlesource.com/platform/frameworks/native/+/3cf307284a620c67b9eb024439583fc1c42574ee/libs/binder/Parcel.cpp#1545
 */
@JNINamespace("webauthn")
public final class Fido2Api {
    public interface Calls {
        /**
         * Serialize a browser's or an app's makeCredential request to a {@link Parcel}. Apps should
         * not set {@param origin}.
         *
         * @param options the options passed from the renderer.
         * @param origin the origin that the request should act as.
         * @param clientDataHash (optional) override the ClientDataJSON generated by Play Services.
         * @param browserOptions (optional) a bundle of browser-specific request data, like channel.
         * @param parcel the {@link Parcel} to append the output to.
         * @param resultReceiver the {@link ResultReceiver} to receive responses from GMSCore
         *     instead of the pending intent.
         * @throws NoSuchAlgorithmException when options requests an impossible-to-satisfy
         *     public-key algorithm.
         */
        void makeCredential(
                PublicKeyCredentialCreationOptions options,
                @Nullable Uri uri,
                @Nullable byte[] clientDataHash,
                @Nullable Bundle browserOptions,
                @Nullable ResultReceiver resultReceiver,
                Parcel parcel)
                throws NoSuchAlgorithmException;

        /**
         * Serialize a browser's or an app's getAssertion request to a {@link Parcel}. Apps should
         * not set {@param origin}.
         *
         * @param options the options passed from the renderer.
         * @param origin the origin that the request should act as.
         * @param clientDataHash (optional) override the ClientDataJSON generated by Play Services.
         * @param tunnelId (optional) used when making server-linked (i.e. accounts.google.com,
         *     phone-as-a-security-key) requests
         * @param resultReceiver the {@link ResultReceiver} to receive responses from GMSCore
         *     instead of the pending intent.
         * @param parcel the {@link Parcel} to append the output to.
         */
        void getAssertion(
                PublicKeyCredentialRequestOptions options,
                @Nullable Uri uri,
                @Nullable byte[] clientDataHash,
                @Nullable byte[] tunnelId,
                @Nullable ResultReceiver resultReceiver,
                Parcel parcel);
    }

    // Error codes returned by the API.
    public static final int SECURITY_ERR = 18;
    public static final int TIMEOUT_ERR = 23;
    public static final int ENCODING_ERR = 27;
    public static final int NOT_ALLOWED_ERR = 35;
    public static final int DATA_ERR = 30;
    public static final int NOT_SUPPORTED_ERR = 9;
    public static final int CONSTRAINT_ERR = 29;
    public static final int INVALID_STATE_ERR = 11;
    public static final int UNKNOWN_ERR = 28;

    // CREDENTIAL_EXTRA is the Intent key under which the serialised response is stored.
    public static final String CREDENTIAL_EXTRA = "FIDO2_CREDENTIAL_EXTRA";

    private static final double MIN_TIMEOUT_SECONDS = 10;
    private static final double MAX_TIMEOUT_SECONDS = 600;

    private static final String TAG = "Fido2Api";
    private static final int ECDSA_COSE_IDENTIFIER = -7;

    // OBJECT_MAGIC is a magic value used to indicate the start of an object when encoding with
    // Parcel.
    private static final int OBJECT_MAGIC = 20293;

    // VAL_PARCELABLE is the tag value for a Parcelable, as used by `Parcel.writeValue`.
    private static final int VAL_PARCELABLE = 4;

    // parcelUsesLengthPrefixes will be true if `Parcel.writeValue` uses length
    // prefixes. This was added in Android 13 and there's one case where an
    // array is sent directly as a Parcel, rather than as a SafeParcel. We
    // sadly need to care about this because the Parcel class supplied by the
    // system doesn't provide any way of reading arrays that isn't coupled to
    // ClassLoader-based assumptions.
    private static final boolean sParcelUsesLengthPrefixes = doesParcelUseLengthPrefix();

    private static boolean doesParcelUseLengthPrefix() {
        // See comment for `sParcelUsesLengthPrefixes`.
        Parcel parcel = Parcel.obtain();
        parcel.writeValue(new ArrayList());
        final boolean ret = parcel.dataPosition() == 12;
        parcel.recycle();
        return ret;
    }

    /**
     * Serialize a browser's makeCredential request to a {@link Parcel}.
     *
     * @param options the options passed from the renderer.
     * @param origin the origin that the request should act as.
     * @param clientDataHash (optional) override the ClientDataJSON generated by Play Services.
     * @param browserOptions (optional) a bundle of Chrome-specific request data, like channel.
     * @param resultReceiver the {@link ResultReceiver} to listen for responses from Play Services.
     * @param parcel the {@link Parcel} to append the output to.
     * @throws NoSuchAlgorithmException when options requests an impossible-to-satisfy public-key
     *     algorithm.
     */
    public static void appendBrowserMakeCredentialOptionsToParcel(
            PublicKeyCredentialCreationOptions options,
            Uri origin,
            @Nullable byte[] clientDataHash,
            @Nullable Bundle browserOptions,
            @Nullable ResultReceiver resultReceiver,
            Parcel parcel)
            throws NoSuchAlgorithmException {
        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 2: PublicKeyCredentialCreationOptions
        int z = writeHeader(2, parcel);
        appendMakeCredentialOptionsToParcel(options, resultReceiver, parcel);
        writeLength(z, parcel);

        // 3: origin
        z = writeHeader(3, parcel);
        origin.writeToParcel(parcel, /* flags= */ 0);
        writeLength(z, parcel);

        // 4: clientDataHash
        if (clientDataHash != null) {
            z = writeHeader(4, parcel);
            parcel.writeByteArray(clientDataHash);
            writeLength(z, parcel);
        }

        // 5: browserOptions
        if (browserOptions != null) {
            z = writeHeader(5, parcel);
            parcel.writeBundle(browserOptions);
            writeLength(z, parcel);
        }

        writeLength(a, parcel);
    }

    /**
     * Serialize an app's makeCredential request to a {@link Parcel}.
     *
     * @param options the options passed from the renderer.
     * @param resultReceiver the {@link ResultReceiver} to listen for responses from Play Services.
     * @param parcel the {@link Parcel} to append the output to.
     * @throws NoSuchAlgorithmException when options requests an impossible-to-satisfy public-key
     *     algorithm.
     */
    static void appendMakeCredentialOptionsToParcel(
            PublicKeyCredentialCreationOptions options,
            @Nullable ResultReceiver resultReceiver,
            Parcel parcel)
            throws NoSuchAlgorithmException {

        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 2: PublicKeyCredentialRpEntity

        int b = writeHeader(2, parcel);
        int c = writeHeader(OBJECT_MAGIC, parcel);

        int z = writeHeader(2, parcel);
        parcel.writeString(options.relyingParty.id);
        writeLength(z, parcel);

        z = writeHeader(3, parcel);
        parcel.writeString(options.relyingParty.name);
        writeLength(z, parcel);

        writeLength(c, parcel);
        writeLength(b, parcel);

        // 3: PublicKeyCredentialUserEntity

        b = writeHeader(3, parcel);
        c = writeHeader(OBJECT_MAGIC, parcel);

        z = writeHeader(2, parcel);
        parcel.writeByteArray(options.user.id);
        writeLength(z, parcel);

        z = writeHeader(3, parcel);
        parcel.writeString(options.user.name);
        writeLength(z, parcel);

        z = writeHeader(5, parcel);
        parcel.writeString(options.user.displayName);
        writeLength(z, parcel);

        writeLength(c, parcel);
        writeLength(b, parcel);

        // 4: challenge

        b = writeHeader(4, parcel);
        parcel.writeByteArray(options.challenge);
        writeLength(b, parcel);

        // 5: parameters

        b = writeHeader(5, parcel);

        boolean hasEcdsaP256 = false;
        // TODO(agl): we shouldn't filter here but GMS Core requires that no
        // unknown values be sent, defeating the point.
        for (PublicKeyCredentialParameters param : options.publicKeyParameters) {
            if (param.algorithmIdentifier == ECDSA_COSE_IDENTIFIER
                    && param.type == PublicKeyCredentialType.PUBLIC_KEY) {
                hasEcdsaP256 = true;
                break;
            }
        }

        if (!hasEcdsaP256 && options.publicKeyParameters.length != 0) {
            // The site only accepts algorithms that are not supported.
            throw new NoSuchAlgorithmException();
        }

        if (!hasEcdsaP256) {
            parcel.writeInt(0);
        } else {
            parcel.writeInt(1);
            c = startLength(parcel);
            int d = writeHeader(OBJECT_MAGIC, parcel);

            z = writeHeader(2, parcel);
            parcel.writeString(credentialTypeToString(PublicKeyCredentialType.PUBLIC_KEY));
            writeLength(z, parcel);

            z = writeHeader(3, parcel);
            parcel.writeInt(ECDSA_COSE_IDENTIFIER);
            writeLength(z, parcel);

            writeLength(d, parcel);
            writeLength(c, parcel);
        }

        writeLength(b, parcel);

        // 6: timeout
        if (options.timeout != null) {
            b = writeHeader(6, parcel);
            parcel.writeDouble(adjustTimeout(options.timeout));
            writeLength(b, parcel);
        }

        // 7: exclude list
        if (options.excludeCredentials != null && options.excludeCredentials.length != 0) {
            b = writeHeader(7, parcel);
            appendCredentialListToParcel(options.excludeCredentials, parcel);
            writeLength(b, parcel);
        }

        // 8: authenticator selection
        if (options.authenticatorSelection != null) {
            b = writeHeader(8, parcel);
            c = writeHeader(OBJECT_MAGIC, parcel);

            String attachment =
                    attachmentToString(options.authenticatorSelection.authenticatorAttachment);
            if (attachment != null) {
                z = writeHeader(2, parcel);
                parcel.writeString(attachment);
                writeLength(z, parcel);
            }

            z = writeHeader(3, parcel);
            parcel.writeInt(
                    options.authenticatorSelection.residentKey == ResidentKeyRequirement.REQUIRED
                            ? 1
                            : 0);
            writeLength(z, parcel);

            z = writeHeader(4, parcel);
            parcel.writeString(
                    userVerificationToString(options.authenticatorSelection.userVerification));
            writeLength(z, parcel);

            z = writeHeader(5, parcel);
            parcel.writeString(residentKeyToString(options.authenticatorSelection.residentKey));
            writeLength(z, parcel);

            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        // 11: attestation preference
        b = writeHeader(11, parcel);
        parcel.writeString(attestationPreferenceToString(options.attestation));
        writeLength(b, parcel);

        // 12: extensions
        if (options.isPaymentCredentialCreation || options.prfEnable) {
            b = writeHeader(12, parcel);
            appendMakeCredentialExtensionsToParcel(options, parcel);
            writeLength(b, parcel);
        }

        // 13: JSON serialisation of the request.
        //
        // Recent versions of Play Services will use this field and ignore the others. In time, we
        // should be able to skip serialising anything else in this function.
        b = writeHeader(13, parcel);
        parcel.writeString(
                Fido2CredentialRequestJni.get().createOptionsToJson(options.serialize()));
        writeLength(b, parcel);

        // 14: resultReceiver
        if (resultReceiver != null) {
            b = writeHeader(14, parcel);
            resultReceiver.writeToParcel(parcel, 0);
            writeLength(b, parcel);
        }

        writeLength(a, parcel);
    }

    /**
     * Serialize known extensions for an app's makeCredential request to a {@link Parcel}.
     *
     * @param options the options passed from the renderer.
     * @param parcel the {@link Parcel} to append the output to.
     */
    private static void appendMakeCredentialExtensionsToParcel(
            PublicKeyCredentialCreationOptions options, Parcel parcel) {
        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 10: GoogleThirdPartyPayment
        //
        // This only has effect if set to 'true' (i.e., omitting it and setting it to false are
        // equivalent), so we only include the 'true' case.
        if (options.isPaymentCredentialCreation) {
            final int b = writeHeader(10, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(1, parcel);
            parcel.writeInt(1);
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        // 11: PRF
        if (options.prfEnable) {
            final int b = writeHeader(11, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(1, parcel);
            if (options.prfInput != null) {
                // Two bytestrings for a single PRF input: the null credential ID and then the
                // hashed salts, concatenated.
                parcel.writeInt(2);
                writePrfInput(options.prfInput, /* prfInputsHashed= */ false, parcel);
            } else {
                // No PRF inputs.
                parcel.writeInt(0);
            }
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        writeLength(a, parcel);
    }

    /**
     * Serialize a browser's getAssertion request to a {@link Parcel}.
     *
     * @param options the options passed from the renderer.
     * @param origin the origin that the request should act as.
     * @param clientDataHash (optional) override the ClientDataJSON generated by Play Services.
     * @param resultReceiver the {@link ResultReceiver} to listen for responses from Play Services.
     * @param parcel the {@link Parcel} to append the output to.
     */
    public static void appendBrowserGetAssertionOptionsToParcel(
            PublicKeyCredentialRequestOptions options,
            Uri origin,
            byte[] clientDataHash,
            byte[] tunnelId,
            @Nullable ResultReceiver resultReceiver,
            Parcel parcel) {
        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 2: PublicKeyCredentialRequestOptions
        int z = writeHeader(2, parcel);
        appendGetAssertionOptionsToParcel(options, tunnelId, resultReceiver, parcel);
        writeLength(z, parcel);

        // 3: origin
        z = writeHeader(3, parcel);
        origin.writeToParcel(parcel, 0);
        writeLength(z, parcel);

        // 4: clientDataHash
        if (clientDataHash != null) {
            z = writeHeader(4, parcel);
            parcel.writeByteArray(clientDataHash);
            writeLength(z, parcel);
        }

        writeLength(a, parcel);
    }

    /**
     * Serialize an app's getAssertion request to a {@link Parcel}.
     *
     * @param options the options passed from the renderer.
     * @param resultReceiver the {@link ResultReceiver} to listen for responses from Play Services.
     * @param parcel the {@link Parcel} to append the output to.
     */
    public static void appendGetAssertionOptionsToParcel(
            PublicKeyCredentialRequestOptions options,
            byte[] tunnelId,
            @Nullable ResultReceiver resultReceiver,
            Parcel parcel) {
        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 2: challenge
        int z = writeHeader(2, parcel);
        parcel.writeByteArray(options.challenge);
        writeLength(z, parcel);

        // 3: timeout
        if (options.timeout != null) {
            z = writeHeader(3, parcel);
            parcel.writeDouble(adjustTimeout(options.timeout));
            writeLength(z, parcel);
        }

        // 4: RP ID
        z = writeHeader(4, parcel);
        parcel.writeString(options.relyingPartyId);
        writeLength(z, parcel);

        // 5: allow list
        if (options.allowCredentials != null) {
            z = writeHeader(5, parcel);
            appendCredentialListToParcel(options.allowCredentials, parcel);
            writeLength(z, parcel);
        }

        // 8: user verification
        z = writeHeader(8, parcel);
        parcel.writeString(userVerificationToString(options.userVerification));
        writeLength(z, parcel);

        // 9: extensions
        z = writeHeader(9, parcel);
        appendGetAssertionExtensionsToParcel(options, tunnelId, parcel);
        writeLength(z, parcel);

        // 11: JSON serialisation of the request.
        //
        // Recent versions of Play Services will use this field and ignore the others. In time, we
        // should be able to skip serialising anything else in this function.
        z = writeHeader(11, parcel);
        parcel.writeString(Fido2CredentialRequestJni.get().getOptionsToJson(options.serialize()));
        writeLength(z, parcel);

        // 12: resultReceiver
        if (resultReceiver != null) {
            z = writeHeader(12, parcel);
            resultReceiver.writeToParcel(parcel, 0);
            writeLength(z, parcel);
        }

        writeLength(a, parcel);
    }

    private static void appendGetAssertionExtensionsToParcel(
            PublicKeyCredentialRequestOptions options, byte[] tunnelId, Parcel parcel) {
        final int a = writeHeader(OBJECT_MAGIC, parcel);

        // 2: appId
        if (options.extensions.appid != null) {
            final int b = writeHeader(2, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(2, parcel);
            parcel.writeString(options.extensions.appid);
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        // 4: user verification methods
        if (options.extensions.userVerificationMethods) {
            final int b = writeHeader(4, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(1, parcel);
            parcel.writeInt(1);
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        // 11: PRF
        if (options.extensions.prf) {
            final int b = writeHeader(11, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(1, parcel);
            parcel.writeInt(2 * options.extensions.prfInputs.length);
            for (PrfValues input : options.extensions.prfInputs) {
                writePrfInput(input, options.extensions.prfInputsHashed, parcel);
            }
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        if (tunnelId != null) {
            final int b = writeHeader(9, parcel);
            final int c = writeHeader(OBJECT_MAGIC, parcel);
            final int d = writeHeader(1, parcel);
            parcel.writeString(Base64.encodeToString(tunnelId, Base64.NO_WRAP));
            writeLength(d, parcel);
            writeLength(c, parcel);
            writeLength(b, parcel);
        }

        writeLength(a, parcel);
    }

    private static void writePrfInput(PrfValues input, boolean prfInputsHashed, Parcel parcel) {
        parcel.writeByteArray(input.id);
        if (prfInputsHashed) {
            if (input.second == null) {
                parcel.writeByteArray(input.first);
            } else {
                parcel.writeByteArray(concat(input.first, input.second));
            }
        } else {
            parcel.writeByteArray(hashPrfInputs(input));
        }
    }

    /**
     * Hash a PRF input.
     *
     * <p>The WebAuthn spec says (https://w3c.github.io/webauthn/#prf-extension) that PRF inputs are
     * hashed with a prefix to provide domain separation. This function performs that transform.
     */
    private static byte[] hashPrfInput(MessageDigest h, byte[] input) {
        h.reset();
        h.update("WebAuthn PRF\u0000".getBytes(StandardCharsets.UTF_8));
        return h.digest(input);
    }

    /**
     * Convert PRF inputs from renderer to Play Services form.
     *
     * <p>The WebAuthn spec says (https://w3c.github.io/webauthn/#prf-extension) that PRF inputs are
     * hashed with a prefix to provide domain separation. Additionally, Play Services wants the
     * inputs concatenated. This function takes care of that.
     */
    private static byte[] hashPrfInputs(PrfValues input) {
        MessageDigest hash;
        try {
            hash = MessageDigest.getInstance("SHA256");
        } catch (NoSuchAlgorithmException e) {
            // It is unreasonable for the runtime not to provide SHA-256.
            throw new RuntimeException(e);
        }
        byte[] first = hashPrfInput(hash, input.first);
        if (input.second == null) {
            return first;
        }
        byte[] second = hashPrfInput(hash, input.second);
        return concat(first, second);
    }

    private static byte[] concat(byte[] a, byte[] b) {
        byte[] ret = new byte[a.length + b.length];
        System.arraycopy(a, 0, ret, 0, a.length);
        System.arraycopy(b, 0, ret, a.length, b.length);
        return ret;
    }

    private static void appendCredentialListToParcel(
            PublicKeyCredentialDescriptor[] creds, Parcel parcel) {
        parcel.writeInt(creds.length);
        for (PublicKeyCredentialDescriptor cred : creds) {
            int a = startLength(parcel);
            int b = writeHeader(OBJECT_MAGIC, parcel);

            int z = writeHeader(2, parcel);
            parcel.writeString(credentialTypeToString(cred.type));
            writeLength(z, parcel);

            z = writeHeader(3, parcel);
            parcel.writeByteArray(cred.id);
            writeLength(z, parcel);

            int c = writeHeader(4, parcel);
            parcel.writeInt(cred.transports.length);
            for (int transport : cred.transports) {
                z = startLength(parcel);
                parcel.writeString(transportToString(transport));
                writeLength(z, parcel);
            }
            writeLength(c, parcel);

            writeLength(b, parcel);
            writeLength(a, parcel);
        }
    }

    /**
     * Write a SafeParcelable-style header.
     *
     * <p>See the class comment for a description of this function works.
     *
     * @param tag the tag number to encode (<65536).
     * @param parcel the {@link Parcel} to append to.
     * @return the offset of the placeholder length.
     */
    private static int writeHeader(int tag, Parcel parcel) {
        assert tag < 0x10000;
        parcel.writeInt(0xffff0000 | tag);
        return startLength(parcel);
    }

    /**
     * Write a SafeParcelable-style length.
     *
     * <p>See the class comment for a description of this function works.
     *
     * @param parcel the {@link Parcel} to append to.
     * @return the offset of the placeholder length.
     */
    private static int startLength(Parcel parcel) {
        int pos = parcel.dataPosition();
        parcel.writeInt(0xdddddddd);
        return pos;
    }

    /**
     * Fill in a previous placeholder length.
     *
     * <p>See the class comment for a description of this function works.
     *
     * @param pos the return value from {@link writeHeader} or {@link writeLength}.
     * @param parcel the {@link Parcel} to append to.
     */
    private static void writeLength(int pos, Parcel parcel) {
        int totalLength = parcel.dataPosition();
        parcel.setDataPosition(pos);
        parcel.writeInt(totalLength - pos - 4);
        parcel.setDataPosition(totalLength);
    }

    private static String attachmentToString(int attachment) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert AuthenticatorAttachment.MAX_VALUE == AuthenticatorAttachment.CROSS_PLATFORM;

        switch (attachment) {
            case AuthenticatorAttachment.PLATFORM:
                return "platform";
            case AuthenticatorAttachment.CROSS_PLATFORM:
                return "cross-platform";
            case AuthenticatorAttachment.NO_PREFERENCE:
            default:
                return null;
        }
    }

    private static int stringToAttachment(String v) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert AuthenticatorAttachment.MAX_VALUE == AuthenticatorAttachment.CROSS_PLATFORM;

        if (v.equals("platform")) {
            return AuthenticatorAttachment.PLATFORM;
        } else if (v.equals("cross-platform")) {
            return AuthenticatorAttachment.CROSS_PLATFORM;
        }
        return AuthenticatorAttachment.MIN_VALUE - 1;
    }

    private static String credentialTypeToString(int credType) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert PublicKeyCredentialType.MIN_VALUE == PublicKeyCredentialType.MAX_VALUE;

        switch (credType) {
            case PublicKeyCredentialType.PUBLIC_KEY:
            default:
                return "public-key";
        }
    }

    private static String transportToString(int transport) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert AuthenticatorTransport.MAX_VALUE == AuthenticatorTransport.INTERNAL;

        switch (transport) {
            case AuthenticatorTransport.NFC:
                return "nfc";
            case AuthenticatorTransport.BLE:
                return "ble";
            case AuthenticatorTransport.INTERNAL:
                return "internal";
            case AuthenticatorTransport.HYBRID:
                return "cable";
            case AuthenticatorTransport.USB:
            default:
                return "usb";
        }
    }

    private static String userVerificationToString(int uv) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert UserVerificationRequirement.MAX_VALUE == UserVerificationRequirement.DISCOURAGED;

        switch (uv) {
            case UserVerificationRequirement.REQUIRED:
                return "required";
            case UserVerificationRequirement.DISCOURAGED:
                return "discouraged";
            case UserVerificationRequirement.PREFERRED:
            default:
                return "preferred";
        }
    }

    private static String residentKeyToString(int rk) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert ResidentKeyRequirement.MAX_VALUE == ResidentKeyRequirement.REQUIRED;

        switch (rk) {
            case ResidentKeyRequirement.REQUIRED:
                return "required";
            case ResidentKeyRequirement.PREFERRED:
                return "preferred";
            case ResidentKeyRequirement.DISCOURAGED:
            default:
                return "discouraged";
        }
    }

    private static String attestationPreferenceToString(int attestationPref) {
        // This is the closest one can get to a static assert that no new enumeration values have
        // been added.
        assert AttestationConveyancePreference.MAX_VALUE
                == AttestationConveyancePreference.ENTERPRISE;

        switch (attestationPref) {
            case AttestationConveyancePreference.INDIRECT:
                return "indirect";
            case AttestationConveyancePreference.DIRECT:
                return "direct";
            case AttestationConveyancePreference.ENTERPRISE:
                return "direct"; // cannot be represented in the GMS Core API.
            case AttestationConveyancePreference.NONE:
            default:
                return "none";
        }
    }

    /**
     * Read a SafeParcelable-style tag and length.
     *
     * @param parcel the {@link Parcel} to read from.
     * @return a (tag, length) pair.
     */
    private static Pair<Integer, Integer> readHeader(Parcel parcel) {
        int a = parcel.readInt();
        int tag = a & 0xffff;
        int length = (a >> 16) & 0xffff;
        if (length == 0xffff) {
            length = parcel.readInt();
        }

        return new Pair(tag, length);
    }

    /**
     * Read a FIDO API response from a {@link PendingIntent} result.
     *
     * @param data the Intent, as passed to {@link Activity.onActivityResult}.
     * @return see {@link parseResponse}.
     * @throws IllegalArgumentException if there was a parse error.
     */
    public static @Nullable Object parseIntentResponse(Intent data)
            throws IllegalArgumentException {
        byte[] responseBytes = data.getByteArrayExtra(CREDENTIAL_EXTRA);
        if (responseBytes == null) {
            Log.e(TAG, "FIDO2 PendingIntent missing response");
            throw new IllegalArgumentException();
        }

        final Object response = parseResponse(responseBytes);
        if (response == null) {
            Log.e(TAG, "Failed to parse FIDO2 API response");
            throw new IllegalArgumentException();
        }

        return response;
    }

    /**
     * Read a FIDO API response from a bytestring.
     *
     * @param responseBytes an encoded PublicKeyCredential object.
     * @return One of the following: 1) a Pair&lt;Integer, String&gt;, if the response is an error.
     *     (The first value is the error code, the second is an optional error message.) 2) a
     *     MakeCredentialAuthenticatorResponse. 3) a GetAssertionAuthenticatorResponse.
     * @throws IllegalArgumentException if there was a parse error.
     */
    static Object parseResponse(byte[] responseBytes) throws IllegalArgumentException {
        Parcel parcel = Parcel.obtain();
        parcel.unmarshall(responseBytes, 0, responseBytes.length);
        parcel.setDataPosition(0);

        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        MakeCredentialAuthenticatorResponse creationResponse = null;
        GetAssertionAuthenticatorResponse assertionResponse = null;
        Extensions extensions = null;
        int attachment = AuthenticatorAttachment.MIN_VALUE - 1;
        String jsonString = null;

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 4:
                    // Attestation response
                    creationResponse = parseAttestationResponse(parcel);
                    if (creationResponse == null) {
                        throw new IllegalArgumentException();
                    }
                    // The response may need to have attachment information included, which is in
                    // another field.
                    break;

                case 5:
                    // Sign response
                    assertionResponse = parseAssertionResponse(parcel);
                    if (assertionResponse == null) {
                        throw new IllegalArgumentException();
                    }
                    // An assertion response may need to be merged with
                    // extension information, which is in another field.
                    break;

                case 6:
                    // Error
                    return parseErrorResponse(parcel);

                case 7:
                    // Extension outputs
                    extensions = parseExtensionResponse(parcel);
                    if (extensions == null) {
                        throw new IllegalArgumentException();
                    }
                    break;

                case 8:
                    // authenticatorAttachment
                    attachment = stringToAttachment(parcel.readString());
                    break;

                case 9:
                    jsonString = parcel.readString();
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        if (creationResponse != null) {
            if (jsonString != null) {
                // If the JSON form was provided then we use that and ignore the
                // rest.
                byte[] responseSerialized =
                        Fido2CredentialRequestJni.get().makeCredentialResponseFromJson(jsonString);
                if (responseSerialized == null) {
                    Log.e(
                            TAG,
                            "Failed to convert response from JSON to Mojo object: %s",
                            jsonString);
                    throw new IllegalArgumentException();
                }
                MakeCredentialAuthenticatorResponse response;
                try {
                    response =
                            MakeCredentialAuthenticatorResponse.deserialize(
                                    ByteBuffer.wrap(responseSerialized));
                } catch (org.chromium.mojo.bindings.DeserializationException e) {
                    throw new IllegalArgumentException(e);
                }

                return response;
            }

            if (attachment >= AuthenticatorAttachment.MIN_VALUE) {
                creationResponse.authenticatorAttachment = attachment;
            }
            if (extensions != null) {
                if (extensions.hasCredProps) {
                    creationResponse.hasCredPropsRk = true;
                    creationResponse.credPropsRk = extensions.didCreateDiscoverableCredential;
                }
                if (extensions.prf != null) {
                    creationResponse.echoPrf = true;
                    creationResponse.prf = extensions.prf.first;
                    creationResponse.prfResults = extensions.getPrfResults();
                }
            }
            return creationResponse;
        }

        if (assertionResponse != null) {
            if (jsonString != null) {
                // If the JSON form was provided then we use that and ignore the
                // rest.
                byte[] responseSerialized =
                        Fido2CredentialRequestJni.get().getCredentialResponseFromJson(jsonString);
                if (responseSerialized == null) {
                    Log.e(
                            TAG,
                            "Failed to convert response from JSON to Mojo object: %s",
                            jsonString);
                    throw new IllegalArgumentException();
                }
                GetAssertionAuthenticatorResponse response;
                try {
                    response =
                            GetAssertionAuthenticatorResponse.deserialize(
                                    ByteBuffer.wrap(responseSerialized));
                } catch (org.chromium.mojo.bindings.DeserializationException e) {
                    throw new IllegalArgumentException(e);
                }

                return response;
            }

            if (extensions != null && extensions.userVerificationMethods != null) {
                assertionResponse.extensions.echoUserVerificationMethods = true;
                assertionResponse.extensions.userVerificationMethods = new UvmEntry[0];
                assertionResponse.extensions.userVerificationMethods =
                        extensions.userVerificationMethods.toArray(
                                assertionResponse.extensions.userVerificationMethods);
            }
            if (extensions != null && extensions.prf != null) {
                assertionResponse.extensions.echoPrf = true;
                assertionResponse.extensions.prfResults = extensions.getPrfResults();
            }
            if (attachment >= AuthenticatorAttachment.MIN_VALUE) {
                assertionResponse.authenticatorAttachment = attachment;
            }
            return assertionResponse;
        }

        throw new IllegalArgumentException();
    }

    private static MakeCredentialAuthenticatorResponse parseAttestationResponse(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        byte[] keyHandle = null;
        byte[] clientDataJson = null;
        byte[] attestationObject = null;
        int[] transports = new int[] {};

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 2:
                    keyHandle = parcel.createByteArray();
                    break;

                case 3:
                    clientDataJson = parcel.createByteArray();
                    break;

                case 4:
                    attestationObject = parcel.createByteArray();
                    break;

                case 5:
                    transports = parseTransports(parcel);
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        if (keyHandle == null || clientDataJson == null || attestationObject == null) {
            throw new IllegalArgumentException();
        }

        MakeCredentialAuthenticatorResponse ret = new MakeCredentialAuthenticatorResponse();
        CommonCredentialInfo info = new CommonCredentialInfo();

        AttestationObjectParts parts = new AttestationObjectParts();
        if (!Fido2ApiJni.get().parseAttestationObject(attestationObject, parts)) {
            // A failure to parse the attestation object is fatal to the request
            // on desktop and so the same behavior is used here.
            throw new IllegalArgumentException();
        }
        ret.publicKeyAlgo = parts.coseAlgorithm;
        info.authenticatorData = parts.authenticatorData;
        ret.publicKeyDer = parts.spki;
        ret.attestationObject = parts.attestationObject;
        ret.transports = transports;

        info.id = encodeId(keyHandle);
        info.rawId = keyHandle;
        info.clientDataJson = clientDataJson;
        ret.info = info;
        return ret;
    }

    private static int[] parseTransports(Parcel parcel) throws IllegalArgumentException {
        int numValues = parcel.readInt();
        int[] pending = new int[numValues];
        int j = 0;

        for (int i = 0; i < numValues; i++) {
            String transport = parcel.readString();

            if (transport.equals("usb")) {
                pending[j++] = AuthenticatorTransport.USB;
            } else if (transport.equals("nfc")) {
                pending[j++] = AuthenticatorTransport.NFC;
            } else if (transport.equals("ble")) {
                pending[j++] = AuthenticatorTransport.BLE;
            } else if (transport.equals("cable") || transport.equals("hybrid")) {
                pending[j++] = AuthenticatorTransport.HYBRID;
            } else if (transport.equals("internal")) {
                pending[j++] = AuthenticatorTransport.INTERNAL;
            }
        }

        if (j == numValues) {
            return pending;
        }

        int[] ret = new int[j];
        System.arraycopy(pending, 0, ret, 0, j);
        return ret;
    }

    private static GetAssertionAuthenticatorResponse parseAssertionResponse(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        byte[] keyHandle = null;
        byte[] clientDataJson = null;
        byte[] authenticatorData = null;
        byte[] signature = null;
        byte[] userHandle = null;

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 2:
                    keyHandle = parcel.createByteArray();
                    break;

                case 3:
                    clientDataJson = parcel.createByteArray();
                    break;

                case 4:
                    authenticatorData = parcel.createByteArray();
                    break;

                case 5:
                    signature = parcel.createByteArray();
                    break;

                case 6:
                    userHandle = parcel.createByteArray();
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        if (keyHandle == null
                || clientDataJson == null
                || authenticatorData == null
                || signature == null) {
            throw new IllegalArgumentException();
        }

        CommonCredentialInfo info = new CommonCredentialInfo();
        info.authenticatorData = authenticatorData;
        info.id = encodeId(keyHandle);
        info.rawId = keyHandle;
        info.clientDataJson = clientDataJson;

        GetAssertionAuthenticatorResponse response = new GetAssertionAuthenticatorResponse();
        response.info = info;
        response.signature = signature;
        response.userHandle = userHandle;
        response.extensions = new AuthenticationExtensionsClientOutputs();

        return response;
    }

    private static Pair<Integer, String> parseErrorResponse(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        Integer code = null;
        String message = null;

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 2:
                    code = parcel.readInt();
                    break;

                case 3:
                    message = parcel.readString();
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        if (code == null /* `message` is optional */) {
            throw new IllegalArgumentException();
        }

        return new Pair<>(code, message);
    }

    private static class Extensions {
        public ArrayList<UvmEntry> userVerificationMethods;
        public boolean hasCredProps;
        public boolean didCreateDiscoverableCredential;
        // prf contains an "enabled" flag and a bytestring that contains either
        // one or two 32-byte strings.
        public Pair<Boolean, byte[]> prf;

        PrfValues getPrfResults() {
            if (prf == null || prf.second == null) {
                return null;
            }

            PrfValues prfResults = new PrfValues();
            if (prf.second.length == 32) {
                prfResults.first = prf.second;
            } else {
                prfResults.first = new byte[32];
                prfResults.second = new byte[32];
                System.arraycopy(prf.second, 0, prfResults.first, 0, 32);
                System.arraycopy(prf.second, 32, prfResults.second, 0, 32);
            }

            return prfResults;
        }
    }

    private static Extensions parseExtensionResponse(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        Extensions ret = new Extensions();

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 1:
                    ret.userVerificationMethods = parseUvmEntries(parcel);
                    if (ret.userVerificationMethods == null) {
                        throw new IllegalArgumentException();
                    }
                    break;

                case 3:
                    ret.hasCredProps = true;
                    ret.didCreateDiscoverableCredential = parseCredPropsResponse(parcel);
                    break;

                case 4:
                    ret.prf = parsePrfResponse(parcel);
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        return ret;
    }

    private static ArrayList<UvmEntry> parseUvmEntries(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        ArrayList<UvmEntry> ret = new ArrayList<>();

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 1:
                    int num = parcel.readInt();
                    for (int i = 0; i < num; i++) {
                        int unusedLength = parcel.readInt();
                        UvmEntry entry = parseUvmEntry(parcel);
                        if (entry == null) {
                            throw new IllegalArgumentException();
                        }
                        ret.add(entry);
                    }
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        return ret;
    }

    private static UvmEntry parseUvmEntry(Parcel parcel) throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        UvmEntry ret = new UvmEntry();

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 1:
                    ret.userVerificationMethod = parcel.readInt();
                    break;

                case 2:
                    ret.keyProtectionType = (short) parcel.readInt();
                    break;

                case 3:
                    ret.matcherProtectionType = (short) parcel.readInt();
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        return ret;
    }

    private static boolean parseCredPropsResponse(Parcel parcel) throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        boolean ret = false;

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 1:
                    ret = parcel.readInt() != 0;
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        return ret;
    }

    private static Pair<Boolean, byte[]> parsePrfResponse(Parcel parcel)
            throws IllegalArgumentException {
        Pair<Integer, Integer> header = readHeader(parcel);
        if (header.first != OBJECT_MAGIC) {
            throw new IllegalArgumentException();
        }
        final int endPosition = addLengthToParcelPosition(header.second, parcel);

        boolean enabled = false;
        byte[] outputs = null;

        while (parcel.dataPosition() < endPosition) {
            header = readHeader(parcel);
            switch (header.first) {
                case 1:
                    enabled = parcel.readInt() != 0;
                    break;

                case 2:
                    outputs = parcel.createByteArray();
                    if (outputs.length != 32 && outputs.length != 64) {
                        throw new IllegalArgumentException("bad PRF output length");
                    }
                    break;

                default:
                    // unknown tag. Skip over it.
                    parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
            }
        }

        return Pair.create(enabled, outputs);
    }

    /**
     * Return a position that is `length` bytes after the current {@link Parcel} position.
     *
     * <p>This function safely adds an untrusted length to a {@link Parcel} position, watching for
     * overflow and for exceeding the bounds of the data.
     *
     * @throws IllegalArgumentException when running off the end of the data.
     */
    private static int addLengthToParcelPosition(int length, Parcel parcel)
            throws IllegalArgumentException {
        final int ret = length + parcel.dataPosition();
        if (length < 0 || ret < length || ret > parcel.dataSize()) {
            throw new IllegalArgumentException();
        }
        return ret;
    }

    /**
     * Base64 encodes the raw id.
     *
     * @param keyHandle the raw id (key handle of the credential).
     * @return Base64-encoded id.
     */
    private static String encodeId(byte[] keyHandle) {
        return Base64.encodeToString(
                keyHandle, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
    }

    /**
     * Adjusts a timeout between a reasonable minimum and maximum.
     *
     * @param timeout The unadjusted timeout as specified by the website. May be null.
     * @return The adjusted timeout in seconds.
     */
    private static double adjustTimeout(TimeDelta timeout) {
        if (timeout == null) return MAX_TIMEOUT_SECONDS;

        return Math.max(
                MIN_TIMEOUT_SECONDS,
                Math.min(
                        MAX_TIMEOUT_SECONDS,
                        TimeUnit.MICROSECONDS.toSeconds(timeout.microseconds)));
    }

    /** AttestationObjectParts groups together the return values of |parseAttestationObject|. */
    public static final class AttestationObjectParts {
        @CalledByNative("AttestationObjectParts")
        void setAll(
                byte[] authenticatorData,
                byte[] spki,
                int coseAlgorithm,
                byte[] attestationObject) {
            this.authenticatorData = authenticatorData;
            this.spki = spki;
            this.coseAlgorithm = coseAlgorithm;
            this.attestationObject = attestationObject;
        }

        public byte[] authenticatorData;
        public byte[] spki;
        public int coseAlgorithm;
        public byte[] attestationObject;
    }

    /**
     * Parse a {@link WebauthnCredentialDetails} list from a parcel.
     *
     * @param parcel the {@link parcel} with current position set to the beginning of the list.
     * @return The list of {@link WebauthnCredentialDetails} if successfully parsed.
     * @throws IllegalArgumentException if a parsing error is encountered.
     */
    public static ArrayList<WebauthnCredentialDetails> parseCredentialList(Parcel parcel)
            throws IllegalArgumentException {
        int numCredentials = parcel.readInt();
        ArrayList<WebauthnCredentialDetails> credentials = new ArrayList<>();
        for (int i = 0; i < numCredentials; i++) {
            WebauthnCredentialDetails details = new WebauthnCredentialDetails();

            // The array is as written by `Parcel.writeArray`. Each element of the array is prefixed
            // by the class name of that element. The class names will be
            // "com.google.android.gms.fido.fido2.api.common.DiscoverableCredentialInfo" but that
            // isn't checked here to avoid depending on the name of the class.
            if (parcel.readInt() != VAL_PARCELABLE) {
                throw new IllegalArgumentException();
            }
            if (sParcelUsesLengthPrefixes) {
                parcel.readInt(); // discard length prefix.
            }
            parcel.readString(); // ignore class name
            Pair<Integer, Integer> header = readHeader(parcel);
            if (header.first != OBJECT_MAGIC) {
                throw new IllegalArgumentException();
            }
            final int endPosition = addLengthToParcelPosition(header.second, parcel);

            // The original version of this API returned only discoverable credentials, not usable
            // for Secure Payment Confirmation. If the tags are missing, this is the default.
            details.mIsDiscoverable = true;
            details.mIsPayment = false;

            while (parcel.dataPosition() < endPosition) {
                header = readHeader(parcel);
                switch (header.first) {
                    case 1:
                        details.mUserName = parcel.readString();
                        break;
                    case 2:
                        details.mUserDisplayName = parcel.readString();
                        break;
                    case 3:
                        details.mUserId = parcel.createByteArray();
                        break;
                    case 4:
                        details.mCredentialId = parcel.createByteArray();
                        break;
                    case 5:
                        details.mIsDiscoverable = parcel.readInt() != 0;
                        break;
                    case 6:
                        details.mIsPayment = parcel.readInt() != 0;
                        break;
                    default:
                        // unknown tag. Skip over it.
                        parcel.setDataPosition(addLengthToParcelPosition(header.second, parcel));
                }
            }
            if (details.mCredentialId == null) {
                throw new IllegalArgumentException();
            }
            if (details.mIsDiscoverable
                    && (details.mUserName == null
                            || details.mUserDisplayName == null
                            || details.mUserId == null)) {
                throw new IllegalArgumentException();
            }
            credentials.add(details);
        }
        return credentials;
    }

    @NativeMethods
    interface Natives {
        // parseAttestationObject parses a CTAP2 attestation[1] and extracts the
        // parts that the browser provides via Javascript API [2].
        //
        // [1] https://www.w3.org/TR/webauthn/#attestation-object
        // [2] https://w3c.github.io/webauthn/#sctn-public-key-easy
        boolean parseAttestationObject(byte[] attestationObject, AttestationObjectParts result);
    }
}