314 lines
12 KiB
Java
314 lines
12 KiB
Java
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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 androidx.annotation.NonNull;
|
|
import androidx.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<UsbEndpoint, UsbEndpoint> 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<UsbEndpoint, UsbEndpoint> 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);
|
|
}
|
|
}
|