chromium/chrome/android/features/cablev2_authenticator/java/src/org/chromium/chrome/browser/webauth/authenticator/USBHandler.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.content.Context;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.system.StructPollfd;

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 java.io.Closeable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

// USBHandler implements I/O and basic message framing for carrying CTAP2 over the Android Accessory
// protocol[1]. It forms a counterpart to the implementation in //device/fido/aoa. It is intended to
// be used with the Intent-based flow[2] for getting a handle to a {@link UsbAccessory}.
//
// [1] https://source.android.com/devices/accessories/aoa
// [2] https://developer.android.com/guide/topics/connectivity/usb/accessory#discover-a-intent
class USBHandler implements Closeable {
    // These two values must match up with the values in
    // android_accessory_device.h.
    private static final byte COAOA_SYNC = 119;
    private static final byte COAOA_MSG = 33;

    // These values must, implicitly, match the other implementation in
    // android_accessory_device.cc.
    private static final int SYNC_LENGTH = 17;
    private static final int MSG_HEADER_LENGTH = 5;

    private static final String TAG = "CableUSBHandler";

    private final UsbAccessory mAccessory;
    private final Context mContext;
    private final TaskRunner mTaskRunner;
    private final UsbManager mUsbManager;
    private final StructPollfd[] mPollFds;

    private ParcelFileDescriptor mFd;
    private FileInputStream mInput;
    private FileOutputStream mOutput;
    // mStopped should only be accessed via the synchronized functions
    // |haveStopped| and |setStopped| because it spans threads. See the comment
    // in |read| for motivation.
    private boolean mStopped;

    private byte[] mBuffer;
    private int mBufferUsed;
    private int mBufferOffset;

    USBHandler(Context context, TaskRunner taskRunner, UsbAccessory accessory) {
        mAccessory = accessory;
        mContext = context;
        mTaskRunner = taskRunner;
        mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
        mPollFds = new StructPollfd[1];
        mPollFds[0] = new StructPollfd();
    }

    @CalledByNative
    public void startReading() {
        ThreadUtils.assertOnUiThread();
        openAccessory(mAccessory);
    }

    @Override
    @CalledByNative
    public void close() {
        ThreadUtils.assertOnUiThread();

        setStopped();

        if (mFd != null) {
            try {
                mFd.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * Called by CableAuthenticator to write a deferred reply (e.g. to a makeCredential or
     * getAssertion request).
     */
    @CalledByNative
    public void write(@JniType("std::vector<uint8_t>") byte[] message) {
        ThreadUtils.assertOnUiThread();
        assert mOutput != null;

        doWrite(message);
    }

    private synchronized boolean haveStopped() {
        return mStopped;
    }

    private synchronized void setStopped() {
        mStopped = true;
    }

    private void openAccessory(UsbAccessory accessory) {
        ThreadUtils.assertOnUiThread();

        if (haveStopped()) {
            return;
        }

        mFd = mUsbManager.openAccessory(accessory);
        Log.i(TAG, "Accessory opened " + accessory);
        if (mFd == null) {
            Log.i(TAG, "Returned file descriptor is null");
            USBHandlerJni.get().onUSBData(null);
            return;
        }

        FileDescriptor fd = mFd.getFileDescriptor();
        mInput = new FileInputStream(fd);
        mOutput = new FileOutputStream(fd);

        // The Android documentation[1] suggests that reads with too small a
        // buffer will discard the extra like a datagram socket:
        //   > When reading [...] ensure that the buffer that you use is big enough to store the USB
        //   > packet data. The Android accessory protocol supports packet buffers up to 16384
        //   > bytes, so you can choose to always declare your buffer to be of this size for
        //   > simplicity.
        // The kernel source doesn't actually appear to do that but, in case that changes, a 16KiB
        // buffer is used and the usual, incremental, read operation is built on top of it.
        mBuffer = new byte[16384];
        mBufferUsed = 0;
        mBufferOffset = 0;

        PostTask.postTask(
                TaskTraits.BEST_EFFORT_MAY_BLOCK,
                () -> {
                    this.readLoop();
                });
    }

    /**
     * Implements a standard, incremental, read operation on top of the kernel's read operation,
     * which discards any data that doesn't fit into the provided buffer. Returns the number of
     * bytes read, or -1 on error.
     */
    private int read(byte[] buffer, int offset, int len) throws IOException {
        while (mBufferUsed == mBufferOffset) {
            // Refill the buffer so that there's some data. Android has a bug where closing an
            // accessory file descriptor does not unblock pending reads. Thus doing a simple read(2)
            // doesn't work because it'll end up stuck forever. Thus the file descriptor is polled
            // with a timeout and, each time the timeout triggers, a flag is inspected to see
            // whether the descriptor has been closed.
            mPollFds[0].fd = mFd.getFileDescriptor();
            mPollFds[0].events = (short) OsConstants.POLLIN;
            while (true) {
                int pollRet;
                try {
                    pollRet = Os.poll(mPollFds, 200);
                } catch (ErrnoException e) {
                    pollRet = -1;
                }

                if (pollRet < 0) {
                    return pollRet;
                } else if (pollRet == 0) {
                    // Timeout.
                    if (haveStopped()) {
                        return -1;
                    }
                    continue;
                }

                assert pollRet == 1;
                int n = mInput.read(mBuffer, 0, mBuffer.length);
                if (n <= 0) {
                    return -1;
                }
                mBufferUsed = n;
                mBufferOffset = 0;
                break;
            }
        }

        // Some data exists in the internal buffer. Return as much as we can.
        int todo = mBufferUsed - mBufferOffset;
        if (todo > len) {
            todo = len;
        }
        System.arraycopy(mBuffer, mBufferOffset, buffer, offset, todo);
        mBufferOffset += todo;
        return todo;
    }

    /** Utility function that builds on read() to completely fill a buffer */
    private boolean readAll(byte[] buffer) {
        int done = 0;
        while (done < buffer.length) {
            int n;
            try {
                n = read(buffer, done, buffer.length - done);
            } catch (IOException e) {
                return false;
            }
            if (n < 0) {
                return false;
            }
            done += n;
        }

        return true;
    }

    /* Reads a non-negative int32 from the given offset. */
    private static int getNonNegativeS32(byte[] message, int offset) {
        return (((int) message[offset + 0]) & 0xff)
                | ((((int) message[offset + 1]) & 0xff) << 8)
                | ((((int) message[offset + 2]) & 0xff) << 16)
                | ((((int) message[offset + 3]) & 0x7f) << 24);
    }

    /**
     * Reads messages from the USB peer forever. This consumes a thread in the thread-pool, which is
     * a little rude, but the browser isn't doing anything else while it's doing security key
     * operations. It'll exit when it hits an error which, if nothing else, will be triggered when
     * close() sets |mStopped|.
     */
    private void readLoopInner() {
        byte[] syncMessage = new byte[SYNC_LENGTH];
        byte[] msgHeader = new byte[MSG_HEADER_LENGTH];
        byte[] restOfSyncMessage = null;

        // It is possible for several transactions to occur over a USB accessory
        // connection. However, if the peer canceled an operation and stopped
        // reading, that operation may still have completed on the phone.
        // Therefore a stray reply might be sitting waiting to confuse a future
        // operation from the desktop. Desktops thus send a synchronisation
        // message containing a random nonce and discard data until the matching
        // synchronisation reply is found.
        if (!readAll(syncMessage)) {
            return;
        }

        for (; ; ) {
            // The next message must be a synchronisation message, either
            // because it's the first message, or because the loop below
            // encountered a message type other than |COAOA_MSG| and there's
            // only two valid message types.
            if (syncMessage[0] != COAOA_SYNC) {
                Log.i(TAG, "Found unexpected message type");
                return;
            }

            // Echo the synchronisation message (which contains a random nonce)
            // back to the peer so that it knows where the replies to its
            // messages begin.
            try {
                mOutput.write(syncMessage);
            } catch (IOException e) {
                Log.i(TAG, "Failed to write sync message");
                return;
            }

            // Read messages until EOF or desync.
            for (; ; ) {
                if (!readAll(msgHeader)) {
                    return;
                }
                if (msgHeader[0] != COAOA_MSG) {
                    if (restOfSyncMessage == null) {
                        restOfSyncMessage = new byte[SYNC_LENGTH - MSG_HEADER_LENGTH];
                    }
                    if (!readAll(restOfSyncMessage)) {
                        return;
                    }
                    System.arraycopy(msgHeader, 0, syncMessage, 0, msgHeader.length);
                    System.arraycopy(
                            restOfSyncMessage,
                            0,
                            syncMessage,
                            msgHeader.length,
                            restOfSyncMessage.length);
                    break;
                }

                int length = getNonNegativeS32(msgHeader, 1);
                // Enforce 1MB sanity limit on messages.
                if (length > (1 << 20)) {
                    Log.i(TAG, "Message too long");
                    return;
                }
                byte[] message = new byte[length];
                if (!readAll(message)) {
                    return;
                }
                mTaskRunner.postTask(() -> this.didRead(message));
            }
        }
    }

    /**
     * Wrap {@link readLoopInner} so that error paths can simply |return| rather than having to
     * remember to send a null message in every case.
     */
    private void readLoop() {
        readLoopInner();
        Log.i(TAG, "Read loop has exited.");
        mTaskRunner.postTask(() -> this.didRead(null));
    }

    /** Called with each message read from USB, or null on transport error. */
    private void didRead(byte[] buffer) {
        ThreadUtils.assertOnUiThread();

        if (haveStopped()) {
            return;
        }

        if (buffer == null) {
            Log.i(TAG, "Error reading from USB");
        }

        USBHandlerJni.get().onUSBData(buffer);
    }

    private void doWrite(byte[] buffer) {
        byte[] headerBytes = new byte[5];
        headerBytes[0] = COAOA_MSG;
        headerBytes[1] = (byte) buffer.length;
        headerBytes[2] = (byte) (buffer.length >>> 8);
        headerBytes[3] = (byte) (buffer.length >>> 16);
        headerBytes[4] = (byte) (buffer.length >>> 24);

        try {
            mOutput.write(headerBytes);
            mOutput.write(buffer);
        } catch (IOException e) {
            // It's assumed that any errors will be caught by the reading thread.
            Log.i(TAG, "USB write failed");
        }
    }

    @NativeMethods("cablev2_authenticator")
    interface Natives {
        // onUSBData is called when data is read from the USB data. If data is
        // null then an error occurred.
        void onUSBData(byte[] data);
    }
}