/*
* Proyecto CCIDroid. Driver para utilizacion de tarjetas CCID en el sistema operativo
* Android.
*
* El proyecto CCIDroid es un conector para la comunicacion entre sistemas Android y
* lectores de SmartCard USB segun el estandar CCID. Diseno inicial desarrollado para
* su integracion con el Controlador Java de la Secretaria de Estado de Administraciones
* Publicas para el DNI electronico.
*
* Copyright (C) 2012 Instituto Nacional de las Tecnologias de la Comunicacion (INTECO)
*
* Este programa es software libre y utiliza un licenciamiento dual (LGPL 2.1+
* o EUPL 1.1+), lo cual significa que los usuarios podran elegir bajo cual de las
* licencias desean utilizar el codigo fuente. Su eleccion debera reflejarse
* en las aplicaciones que integren o distribuyan el Controlador, ya que determinara
* su compatibilidad con otros componentes.
*
* El Controlador puede ser redistribuido y/o modificado bajo los terminos de la
* Lesser GNU General Public License publicada por la Free Software Foundation,
* tanto en la version 2.1 de la Licencia, o en una version posterior.
*
* El Controlador puede ser redistribuido y/o modificado bajo los terminos de la
* European Union Public License publicada por la Comision Europea,
* tanto en la version 1.1 de la Licencia, o en una version posterior.
*
* Deberia recibir una copia de la GNU Lesser General Public License, si aplica, junto
* con este programa. Si no, consultelo en <http://www.gnu.org/licenses/>.
*
* Deberia recibir una copia de la European Union Public License, si aplica, junto
* con este programa. Si no, consultelo en <http://joinup.ec.europa.eu/software/page/eupl>.
*
* Este programa es distribuido con la esperanza de que sea util, pero
* SIN NINGUNA GARANTIA; incluso sin la garantia implicita de comercializacion
* o idoneidad para un proposito particular.
*/
package es.inteco.labs.android.usb;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.util.Log;
import es.gob.jmulticard.HexUtils;
import es.gob.jmulticard.apdu.CommandApdu;
import es.gob.jmulticard.apdu.ResponseApdu;
import es.gob.jmulticard.apdu.connection.ApduConnection;
import es.gob.jmulticard.apdu.connection.ApduConnectionException;
import es.gob.jmulticard.apdu.connection.ApduConnectionProtocol;
import es.gob.jmulticard.apdu.connection.CardConnectionListener;
import es.gob.jmulticard.apdu.connection.CardNotPresentException;
import es.gob.jmulticard.apdu.connection.UnavailableReaderException;
import es.gob.jmulticard.apdu.iso7816four.GetResponseApduCommand;
import es.inteco.labs.android.usb.device.SmartCardUsbDevice;
import es.inteco.labs.android.usb.device.exception.NotAvailableUSBDeviceException;
import es.inteco.labs.android.usb.device.exception.UsbDeviceException;
/** Conexión con lector de tarjetas inteligentes implementado sobre Android USB Host API.
* Basado en <code>es.gob.jmulticard.jse.smartcardio.SmartcardIoConnection</code>.
* @author Jose Luis Escanciano Garcia */
public final class AndroidCCIDConnection implements ApduConnection {
private final SmartCardUsbDevice ccidReader;
private static final boolean DEBUG = false;
/** Construye una conexión con lector de tarjetas inteligentes implementado sobre Android USB Host API.
* @param usbManager Gestor de dispositivos USB del sistema
* @param reader Dispositivo USB de tipo CCID (lector de tarjetas).
* @throws UsbDeviceException */
public AndroidCCIDConnection(final UsbManager usbManager, final UsbDevice reader) throws UsbDeviceException{
if (!isCardReader(reader)) {
throw new IllegalArgumentException(
"Debe proporcionarse un lector de tarjetas CCID" //$NON-NLS-1$
);
}
this.ccidReader = new SmartCardUsbDevice(usbManager, reader);
}
/** Indica si el dispositivo es un lector de tarjetas.
* @return <code>true</code> si es un dispositivo CCID, <code>false</code> en caso contrario */
private static boolean isCardReader(final UsbDevice device) {
if (device == null) {
return false;
}
// SmartCard Device Class: http://www.usb.org/developers/defined_class/#BaseClass00h
// SmartCard Interface Class: http://www.usb.org/developers/defined_class/#BaseClass0Bh
if(device.getDeviceClass() == 0x00 && device.getInterface(0).getInterfaceClass() == 0x0B) {
return true;
}
return false;
}
/** {@inheritDoc} */
@Override
public void open() throws ApduConnectionException {
try {
if(!isOpen()) {
this.ccidReader.open();
}
}
catch (final NotAvailableUSBDeviceException e) {
throw new UnavailableReaderException("No se puede acceder al dispositivo USB: " + e, e); //$NON-NLS-1$
}
}
/** {@inheritDoc} */
@Override
public void close() throws ApduConnectionException {
if(isOpen()) {
this.ccidReader.close();
}
}
/** Etiqueta que indica que es necesario recuperar el resultado del comando anterior. */
private static final byte TAG_RESPONSE_PENDING = 0x61;
/** Etiqueta que identifica si la respuesta tiene una longitud no valida. */
private static final byte TAG_RESPONSE_INVALID_LENGTH = 0x6C;
/** {@inheritDoc} */
@Override
public ResponseApdu transmit(final CommandApdu command) throws ApduConnectionException {
if(!isOpen()){
throw new UnavailableReaderException("No existe dispositivo USB asignado a la conexion"); //$NON-NLS-1$
}
try {
if(!this.ccidReader.isCardActive()){
if(!this.ccidReader.isCardPresent()){
//No hay tarjeta en el lector
throw new CardNotPresentException();
}
//Hay tarjeta, pero no esta activa
Log.i("es.gob.jmulticard", "La tarjeta del lector no esta activa, se reiniciara"); //$NON-NLS-1$ //$NON-NLS-2$
this.reset();
}
if (DEBUG) {
Log.d("es.gob.jmulticard", "APDU Enviada:\n" + HexUtils.hexify(command.getBytes(), true)); //$NON-NLS-1$ //$NON-NLS-2$
}
final ResponseApdu response;
try {
response = new ResponseApdu(this.ccidReader.transmit(command.getBytes()));
if (DEBUG) {
Log.d("es.gob.jmulticard", "APDU Recibida:\n" + HexUtils.hexify(response.getBytes(), true)); //$NON-NLS-1$ //$NON-NLS-2$
}
// Solicitamos el resultado de la operacion si es necesario
if (response.getStatusWord().getMsb() == TAG_RESPONSE_PENDING) {
// Si ya se ha devuelto parte de los datos, los concatenamos al resultado
if (response.getData().length > 0) {
final byte[] data = response.getData();
final byte[] additionalData = transmit(
new GetResponseApduCommand((byte) 0x00, response.getStatusWord().getLsb())
).getBytes();
final byte[] fullResponse = new byte[data.length + additionalData.length];
System.arraycopy(data, 0, fullResponse, 0, data.length);
System.arraycopy(additionalData, 0, fullResponse, data.length, additionalData.length);
return new ResponseApdu(fullResponse);
}
return transmit(new GetResponseApduCommand((byte) 0x00, response.getStatusWord().getLsb()));
}
// En caso de longitud esperada incorrecta reenviamos la APDU con la longitud esperada.
// Incluimos la condicion del CLA igual 0x00 para que no afecte a las APDUs cifradas
// (de eso se encargara la clase de conexion con canal seguro)
else if (response.getStatusWord().getMsb() == TAG_RESPONSE_INVALID_LENGTH && command.getCla() == (byte) 0x00) {
command.setLe(response.getStatusWord().getLsb());
return transmit(command);
}
return response;
}
catch (final UsbDeviceException e) {
//Ver por que motivo ha fallado la transmision
throw new ApduConnectionException("Error enviando APDU: " + e, e); //$NON-NLS-1$
}
}
catch(final NotAvailableUSBDeviceException e){
//Error al acceder al dispositivo
throw new UnavailableReaderException("No se puede acceder al dispositivo USB: " + e, e); //$NON-NLS-1$
}
}
/** {@inheritDoc} */
@Override
public byte[] reset() throws ApduConnectionException {
try {
return this.ccidReader.resetCCID().getAtrBytes();
}
catch (final UsbDeviceException e) {
throw new ApduConnectionException("Error al reiniciar tarjeta: " + e, e); //$NON-NLS-1$
}
catch (final NotAvailableUSBDeviceException e) {
//Error al acceder al dispositivo
throw new UnavailableReaderException("No se puede acceder al dispositivo USB: " + e, e); //$NON-NLS-1$
}
}
/** {@inheritDoc} */
@Override
public void addCardConnectionListener(final CardConnectionListener ccl) {
throw new UnsupportedOperationException("No soporta eventos de insercion o extraccion"); //$NON-NLS-1$
}
/** {@inheritDoc} */
@Override
public void removeCardConnectionListener(final CardConnectionListener ccl) {
throw new UnsupportedOperationException("No soporta eventos de insercion o extraccion"); //$NON-NLS-1$
}
/** {@inheritDoc} */
@Override
public long[] getTerminals(final boolean onlyWithCardPresent) throws ApduConnectionException {
if (onlyWithCardPresent) {
try {
if (!this.ccidReader.isCardPresent()) {
return new long[0];
}
}
catch (final NotAvailableUSBDeviceException e) {
Log.e("es.gob.jmulticard", "No se ha podido determinar si hay tarjeta en el lector, se devolverra una lista vacia: " + e); //$NON-NLS-1$ //$NON-NLS-2$
return new long[0];
}
}
return new long[] { 0 };
}
/** {@inheritDoc} */
@Override
public String getTerminalInfo(final int terminal) throws ApduConnectionException {
if (terminal != 0) {
throw new IllegalArgumentException("Solo se accede al terminal 0, y se indico el " + terminal); //$NON-NLS-1$
}
return this.ccidReader.getDeviceName();
}
/** {@inheritDoc} */
@Override
public void setTerminal(final int t) {
if (t != 0) {
throw new IllegalArgumentException("Solo se accede al terminal 0, y se indico el " + t); //$NON-NLS-1$
}
}
/** {@inheritDoc} */
@Override
public boolean isOpen() {
return this.ccidReader.isOpen();
}
@Override
public void setProtocol(final ApduConnectionProtocol p) {
// No hace nada
}
}