/* ******************************************************************************* * Android U2F USB Bridge * (c) 2016 Ledger * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ package com.ledger.android.u2f.bridge; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.usb.*; import android.util.Log; import java.util.HashMap; import java.util.concurrent.LinkedBlockingQueue; import com.ledger.android.u2f.bridge.utils.Dump; public class U2FTransportAndroid { private boolean stopped; private UsbManager usbManager; private U2FTransportAndroidHID transport; private final LinkedBlockingQueue<Boolean> gotRights = new LinkedBlockingQueue<Boolean>(1); private static final String LOG_TAG = "U2FTransportAndroid"; private static final String ACTION_USB_PERMISSION = "USB_PERMISSION"; /** * Receives broadcast when a supported USB device is attached, detached or * when a permission to communicate to the device has been granted. */ private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); UsbDevice usbDevice = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); String deviceName = usbDevice.getDeviceName(); if (ACTION_USB_PERMISSION.equals(action)) { boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); // sync with connect gotRights.clear(); gotRights.add(permission); context.unregisterReceiver(mUsbReceiver); } } }; public U2FTransportAndroid(Context context) { usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); } public boolean isPluggedIn() { return getDevice(usbManager) != null; } public void markStopped() { stopped = true; } public U2FTransportAndroidHID getTransport() { return transport; } public boolean connect(final Context context, final U2FTransportFactoryCallback callback) { if (transport != null) { try { transport.close(); } catch (Exception e) { } } IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_USB_PERMISSION); context.registerReceiver(mUsbReceiver, filter); final UsbDevice device = getDevice(usbManager); final Intent intent = new Intent(ACTION_USB_PERMISSION); gotRights.clear(); usbManager.requestPermission(device, PendingIntent.getBroadcast(context, 0, intent, 0)); // retry because of InterruptedException while (true) { try { // gotRights.take blocks until the UsbManager gives us the rights via callback to the BroadcastReceiver // this might need an user interaction if (gotRights.take()) { if (!stopped) { transport = open(usbManager, device); callback.onConnected((transport != null ? true : false)); } return true; } else { if (!stopped) { callback.onConnected(false); } return true; } } catch (InterruptedException ignored) { } } } public static UsbDevice getDevice(UsbManager manager) { HashMap<String, UsbDevice> deviceList = manager.getDeviceList(); for (UsbDevice device : deviceList.values()) { if ((device.getDeviceClass() == UsbConstants.USB_CLASS_HID) || (device.getDeviceClass() == UsbConstants.USB_CLASS_PER_INTERFACE)) { return device; } } return null; } private static final int LIBUSB_REQUEST_GET_DESCRIPTOR = 0x06; private static final int LIBUSB_DT_REPORT = 0x22; private static final int LIBUSB_RECIPIENT_INTERFACE = 0x01; private static int getBytes(byte[] data, int length, int size, int cur) { int result = 0; if (cur + size >= length) { return 0; } switch(size) { case 0: break; case 1: result = (data[cur + 1] & 0xff); break; case 2: result = ((data[cur + 2] & 0xff) << 8) | (data[cur + 1] & 0xff); break; case 3: result = ((data[cur + 4] & 0xff) << 24) | ((data[cur + 3] & 0xff) << 16) | ((data[cur + 2] & 0xff) << 8) | (data[cur + 1] & 0xff); break; } return result; } private static int getUsage(byte[] report) { return getUsagePageOrUsage(report, report.length, true); } private static int getUsagePage(byte[] report) { return getUsagePageOrUsage(report, report.length, false); } private static int getUsagePageOrUsage(byte[] data, int size, boolean getUsage) { int result = -1; int i = 0; int size_code; int data_len, key_size; while (i < size) { int key = (data[i] & 0xff); int key_cmd = key & 0xfc; if ((key & 0xf0) == 0xf0) { if (i + 1 < size) { data_len = data[i + 1]; } else { data_len = 0; } key_size = 3; } else { size_code = key & 0x03; switch(size_code) { case 0: case 1: case 2: data_len = size_code; break; case 3: data_len = 4; break; default: data_len = 0; break; } key_size = 1; } if ((key_cmd == 0x04) && !getUsage) { result = getBytes(data, size, data_len, i); break; } if ((key_cmd == 0x08) && getUsage) { result = getBytes(data, size, data_len, i); break; } i += data_len + key_size; } return result; } public static U2FTransportAndroidHID open(UsbManager manager, UsbDevice device) { // Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html) // Important if enumerating, rather than being awaken by the intent notification for (int interfaceIndex=0; interfaceIndex<device.getInterfaceCount(); interfaceIndex++) { UsbInterface dongleInterface = device.getInterface(interfaceIndex); UsbEndpoint in = null; UsbEndpoint out = null; for (int i = 0; i < dongleInterface.getEndpointCount(); i++) { UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i); if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) { in = tmpEndpoint; } else { out = tmpEndpoint; } } UsbDeviceConnection connection = manager.openDevice(device); boolean claimed = connection.claimInterface(dongleInterface, true); byte[] descriptor = new byte[256]; try { int result = connection.controlTransfer(UsbConstants.USB_DIR_IN | LIBUSB_RECIPIENT_INTERFACE, LIBUSB_REQUEST_GET_DESCRIPTOR, (LIBUSB_DT_REPORT << 8), interfaceIndex, descriptor, descriptor.length, 2000); } catch(Exception e) { e.printStackTrace(); } if ((getUsage(descriptor) == FIDO_USAGE) && (getUsagePage(descriptor) == FIDO_USAGE_PAGE)) { return new U2FTransportAndroidHID(connection, dongleInterface, in, out, TIMEOUT); } else { connection.releaseInterface(dongleInterface); connection.close(); } } return null; } private static final int FIDO_USAGE = 0x01; private static final int FIDO_USAGE_PAGE = 0xf1d0; private static final int TIMEOUT = 20000; }