/* * Copyright (C) 2017 Schürmann & Breitmoser GbR * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.sufficientlysecure.keychain.securitytoken.usb; import java.io.IOException; import android.content.Context; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.securitytoken.CommandApdu; import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; import org.sufficientlysecure.keychain.securitytoken.Transport; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; import static org.bouncycastle.util.encoders.Hex.toHexString; /** * Based on USB CCID Specification rev. 1.1 * http://www.usb.org/developers/docs/devclass_docs/DWG_Smart-Card_CCID_Rev110.pdf * Implements small subset of these features */ public class UsbTransport implements Transport { // https://github.com/Yubico/yubikey-personalization/blob/master/ykcore/ykdef.h private static final int VENDOR_YUBICO = 4176; private static final int PRODUCT_YUBIKEY_NEO_OTP_CCID = 273; private static final int PRODUCT_YUBIKEY_NEO_CCID = 274; private static final int PRODUCT_YUBIKEY_NEO_U2F_CCID = 277; private static final int PRODUCT_YUBIKEY_NEO_OTP_U2F_CCID = 278; private static final int PRODUCT_YUBIKEY_4_5_CCID = 1028; private static final int PRODUCT_YUBIKEY_4_5_OTP_CCID = 1029; private static final int PRODUCT_YUBIKEY_4_5_FIDO_CCID = 1030; private static final int PRODUCT_YUBIKEY_4_5_OTP_FIDO_CCID = 1031; // https://www.nitrokey.com/de/documentation/installation#p:nitrokey-pro&os:linux private static final int VENDOR_NITROKEY = 8352; private static final int PRODUCT_NITROKEY_PRO = 16648; private static final int PRODUCT_NITROKEY_START = 16913; private static final int PRODUCT_NITROKEY_STORAGE = 16649; private static final int VENDOR_FSIJ = 9035; private static final int VENDOR_LEDGER = 11415; private static final int VENDOR_SECALOT = 4617; private static final int PRODUCT_SECALOT = 28672; private final UsbDevice usbDevice; private final UsbManager usbManager; private UsbDeviceConnection usbConnection; private UsbInterface usbInterface; private CcidTransportProtocol ccidTransportProtocol; private boolean allowUntestedUsbTokens; public static UsbTransport createUsbTransport(Context context, UsbDevice usbDevice) { UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); boolean allowUntestedUsbTokens = Preferences.getPreferences(context).getExperimentalUsbAllowUntested(); return new UsbTransport(usbDevice, usbManager, allowUntestedUsbTokens); } private UsbTransport(UsbDevice usbDevice, UsbManager usbManager, boolean allowUntestedUsbTokens) { this.usbDevice = usbDevice; this.usbManager = usbManager; this.allowUntestedUsbTokens = allowUntestedUsbTokens; } @Override public void release() { if (usbConnection != null) { usbConnection.releaseInterface(usbInterface); usbConnection.close(); usbConnection = null; } Timber.d("Usb transport disconnected"); } /** * Check if device is was connected to and still is connected * * @return true if device is connected */ @Override public boolean isConnected() { return usbConnection != null && usbManager.getDeviceList().containsValue(usbDevice) && usbConnection.getSerial() != null; } /** * Check if Transport supports persistent connections e.g connections which can * handle multiple operations in one session * * @return true if transport supports persistent connections */ @Override public boolean isPersistentConnectionAllowed() { return true; } /** * Connect to OTG device */ @Override public void connect() throws IOException { usbInterface = getSmartCardInterface(usbDevice); if (usbInterface == null) { throw new UsbTransportException("USB error: CCID mode must be enabled (no class 11 interface)"); } final Pair ioEndpoints = getIoEndpoints(usbInterface); UsbEndpoint usbBulkIn = ioEndpoints.first; UsbEndpoint usbBulkOut = ioEndpoints.second; if (usbBulkIn == null || usbBulkOut == null) { throw new UsbTransportException("USB error: invalid class 11 interface"); } usbConnection = usbManager.openDevice(usbDevice); if (usbConnection == null) { throw new UsbTransportException("USB error: failed to connect to device"); } boolean tokenTypeSupported = SecurityTokenInfo.SUPPORTED_USB_TOKENS.contains(getTokenTypeIfAvailable()); if (!allowUntestedUsbTokens && !tokenTypeSupported) { usbConnection.close(); usbConnection = null; throw new UnsupportedUsbTokenException(); } if (!usbConnection.claimInterface(usbInterface, true)) { throw new UsbTransportException("USB error: failed to claim interface"); } CcidDescription ccidDescription = CcidDescription.fromRawDescriptors(usbConnection.getRawDescriptors()); Timber.d("CCID Description: " + ccidDescription); CcidTransceiver transceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, ccidDescription); ccidTransportProtocol = ccidDescription.getSuitableTransportProtocol(); ccidTransportProtocol.connect(transceiver); } /** * Transmit and receive data * * @param data data to transmit * @return received data */ @Override public ResponseApdu transceive(CommandApdu data) throws UsbTransportException { byte[] rawCommand = data.toBytes(); if (Constants.DEBUG) { Timber.d("USB >> " + toHexString(rawCommand)); } byte[] rawResponse = ccidTransportProtocol.transceive(rawCommand); if (Constants.DEBUG) { Timber.d("USB << " + toHexString(rawResponse)); } return ResponseApdu.fromBytes(rawResponse); } @Override public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final UsbTransport that = (UsbTransport) o; return usbDevice != null ? usbDevice.equals(that.usbDevice) : that.usbDevice == null; } @Override public int hashCode() { return usbDevice != null ? usbDevice.hashCode() : 0; } @Override public TransportType getTransportType() { return TransportType.USB; } @Nullable @Override public TokenType getTokenTypeIfAvailable() { return getTokenTypeFromUsbDeviceInfo(usbDevice.getVendorId(), usbDevice.getProductId(), usbConnection.getSerial()); } @Nullable public static TokenType getTokenTypeFromUsbDeviceInfo(int vendorId, int productId, String serialNo) { switch (vendorId) { case VENDOR_YUBICO: { switch (productId) { case PRODUCT_YUBIKEY_NEO_OTP_CCID: case PRODUCT_YUBIKEY_NEO_CCID: case PRODUCT_YUBIKEY_NEO_U2F_CCID: case PRODUCT_YUBIKEY_NEO_OTP_U2F_CCID: return TokenType.YUBIKEY_NEO; case PRODUCT_YUBIKEY_4_5_CCID: case PRODUCT_YUBIKEY_4_5_OTP_CCID: case PRODUCT_YUBIKEY_4_5_FIDO_CCID: case PRODUCT_YUBIKEY_4_5_OTP_FIDO_CCID: return TokenType.YUBIKEY_4_5; } break; } case VENDOR_NITROKEY: { switch (productId) { case PRODUCT_NITROKEY_PRO: return TokenType.NITROKEY_PRO; case PRODUCT_NITROKEY_START: SecurityTokenInfo.Version gnukVersion = SecurityTokenInfo.parseGnukVersionString(serialNo); boolean versionGreaterEquals125 = gnukVersion != null && SecurityTokenInfo.Version.create("1.2.5").compareTo(gnukVersion) <= 0; return versionGreaterEquals125 ? TokenType.NITROKEY_START_1_25_AND_NEWER : TokenType.NITROKEY_START_OLD; case PRODUCT_NITROKEY_STORAGE: return TokenType.NITROKEY_STORAGE; } break; } case VENDOR_FSIJ: { SecurityTokenInfo.Version gnukVersion = SecurityTokenInfo.parseGnukVersionString(serialNo); boolean versionGreaterEquals125 = gnukVersion != null && SecurityTokenInfo.Version.create("1.2.5").compareTo(gnukVersion) <= 0; return versionGreaterEquals125 ? TokenType.GNUK_1_25_AND_NEWER : TokenType.GNUK_OLD; } case VENDOR_LEDGER: { return TokenType.LEDGER_NANO_S; } case VENDOR_SECALOT: { switch (productId) { case PRODUCT_SECALOT: return TokenType.SECALOT; } break; } } Timber.d("Unknown USB token. Vendor ID: %s, Product ID: %s", vendorId, productId); return null; } /** * Get first class 11 (Chip/Smartcard) interface of the device * * @param device {@link UsbDevice} which will be searched * @return {@link UsbInterface} of smartcard or null if it doesn't exist */ @Nullable private static UsbInterface getSmartCardInterface(UsbDevice device) { for (int i = 0; i < device.getInterfaceCount(); i++) { UsbInterface anInterface = device.getInterface(i); if (anInterface.getInterfaceClass() == UsbConstants.USB_CLASS_CSCID) { return anInterface; } } return null; } /** * Get device's bulk-in and bulk-out endpoints * * @param usbInterface usb device interface * @return pair of builk-in and bulk-out endpoints respectively */ @NonNull private static Pair getIoEndpoints(final UsbInterface usbInterface) { UsbEndpoint bulkIn = null, bulkOut = null; for (int i = 0; i < usbInterface.getEndpointCount(); i++) { final UsbEndpoint endpoint = usbInterface.getEndpoint(i); if (endpoint.getType() != UsbConstants.USB_ENDPOINT_XFER_BULK) { continue; } if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) { bulkIn = endpoint; } else if (endpoint.getDirection() == UsbConstants.USB_DIR_OUT) { bulkOut = endpoint; } } return new Pair<>(bulkIn, bulkOut); } }