/* * Controlador Java de la Secretaria de Estado de Administraciones Publicas * para el DNI electronico. * * El Controlador Java para el DNI electronico es un proveedor de seguridad de JCA/JCE * que permite el acceso y uso del DNI electronico en aplicaciones Java de terceros * para la realizacion de procesos de autenticacion, firma electronica y validacion * de firma. Para ello, se implementan las funcionalidades KeyStore y Signature para * el acceso a los certificados y claves del DNI electronico, asi como la realizacion * de operaciones criptograficas de firma con el DNI electronico. El Controlador ha * sido disenado para su funcionamiento independiente del sistema operativo final. * * Copyright (C) 2012 Direccion General de Modernizacion Administrativa, Procedimientos * e Impulso de la Administracion Electronica * * 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.gob.jmulticard.jse.smartcardio; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; import javax.smartcardio.Card; import javax.smartcardio.CardChannel; import javax.smartcardio.CardException; import javax.smartcardio.CardTerminal; import javax.smartcardio.CommandAPDU; import javax.smartcardio.TerminalFactory; 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.ApduConnectionOpenedInExclusiveModeException; 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.LostChannelException; import es.gob.jmulticard.apdu.connection.NoReadersFoundException; import es.gob.jmulticard.apdu.iso7816four.GetResponseApduCommand; /** Conexión con lector de tarjetas inteligentes implementado sobre * JSR-268 SmartCard I/O. * @author Tomás García-Merás */ public final class SmartcardIoConnection implements ApduConnection { private static final boolean DEBUG = false; /** Constante para la indicación de que se ha detectado un reinicio del canal * con la tarjeta. */ private final static String SCARD_W_RESET_CARD = "SCARD_W_RESET_CARD"; //$NON-NLS-1$ private static final Logger LOGGER = Logger.getLogger("es.gob.jmulticard"); //$NON-NLS-1$ private int terminalNumber = -1; private CardChannel canal = null; private Card card = null; private boolean exclusive = false; private ApduConnectionProtocol protocol = ApduConnectionProtocol.ANY; /** JSR-268 no soporta eventos de inserción o extracción. */ @Override public void addCardConnectionListener(final CardConnectionListener ccl) { throw new UnsupportedOperationException("JSR-268 no soporta eventos de insercion o extraccion"); //$NON-NLS-1$ } /** {@inheritDoc} */ @Override public void close() throws ApduConnectionException { if (this.card != null) { try { this.card.disconnect(false); } catch (final Exception e) { throw new ApduConnectionException( "Error intentando cerrar el objeto de tarjeta inteligente, la conexion puede quedar abierta pero inutil: " + e, e //$NON-NLS-1$ ); } this.card = null; } this.canal = null; } /** {@inheritDoc} */ @Override public String getTerminalInfo(final int terminal) throws ApduConnectionException { try { final List<CardTerminal> terminales = TerminalFactory.getDefault().terminals().list(); if (terminal < terminales.size()) { final CardTerminal cardTerminal = terminales.get(terminal); if (cardTerminal != null) { return cardTerminal.getName(); } } return null; } catch (final Exception ex) { throw new ApduConnectionException("Error recuperando la lista de lectores de tarjetas del sistema: " + ex, ex); //$NON-NLS-1$ } } /** {@inheritDoc} */ @Override public long[] getTerminals(final boolean onlyWithCardPresent) throws ApduConnectionException { final List<CardTerminal> terminales; try { terminales = TerminalFactory.getDefault().terminals().list(); } catch(final CardException e) { LOGGER.warning("No se ha podido recuperar la lista de lectores del sistema: " + e); //$NON-NLS-1$ return new long[0]; } try { // Listamos los indices de los lectores que correspondan segun si tienen o no tarjeta insertada final ArrayList<Long> idsTerminales = new ArrayList<Long>(terminales.size()); for (int idx = 0; idx < terminales.size(); idx++) { if (onlyWithCardPresent) { if (terminales.get(idx).isCardPresent()) { idsTerminales.add(Long.valueOf(idx)); } } else { idsTerminales.add(Long.valueOf(idx)); } } final long[] ids = new long[idsTerminales.size()]; for (int i = 0; i < ids.length; i++) { ids[i] = idsTerminales.get(i).longValue(); } return ids; } catch (final Exception ex) { throw new ApduConnectionException("Error recuperando la lista de lectores de tarjetas del sistema: " + ex, ex); //$NON-NLS-1$ } } /** {@inheritDoc} */ @Override public boolean isOpen() { return this.card != null; } /** {@inheritDoc} */ @Override public void open() throws ApduConnectionException { // Desactivamos las respuestas automaticas para evitar los problemas con el canal seguro System.setProperty("sun.security.smartcardio.t0GetResponse", "false"); //$NON-NLS-1$ //$NON-NLS-2$ System.setProperty("sun.security.smartcardio.t1GetResponse", "false"); //$NON-NLS-1$ //$NON-NLS-2$ if (isExclusiveUse() && isOpen()) { throw new ApduConnectionOpenedInExclusiveModeException(); } final List<CardTerminal> terminales; try { terminales = TerminalFactory.getDefault().terminals().list(); } catch(final Exception e) { throw new NoReadersFoundException( "No se han podido listar los lectores del sistema: " + e, e //$NON-NLS-1$ ); } try { if (terminales.size() < 1) { throw new NoReadersFoundException(); } if (this.terminalNumber == -1) { final long[] cadsWithCard = getTerminals(true); if (cadsWithCard.length > 0) { this.terminalNumber = (int) cadsWithCard[0]; } else { throw new ApduConnectionException( "En el sistema no hay ningun terminal con tarjeta insertada" //$NON-NLS-1$ ); } } if (terminales.size() <= this.terminalNumber) { throw new ApduConnectionException( "No se detecto el lector de tarjetas numero " + Integer.toString(this.terminalNumber) //$NON-NLS-1$ ); } this.card = terminales.get(this.terminalNumber).connect(this.protocol.toString()); } catch(final javax.smartcardio.CardNotPresentException e) { throw new CardNotPresentException(e); } catch (final CardException e) { throw new ApduConnectionException( "No se ha podido abrir la conexion con el lector de tarjetas numero " + Integer.toString(this.terminalNumber) + ": " + e, e //$NON-NLS-1$ //$NON-NLS-2$ ); } if (this.exclusive) { try { this.card.beginExclusive(); } catch (final CardException e) { throw new ApduConnectionException( "No se ha podido abrir la conexion exclusiva con el lector de tarjetas numero " + Integer.toString(this.terminalNumber) + ": " + e, e //$NON-NLS-1$ //$NON-NLS-2$ ); } } this.canal = this.card.getBasicChannel(); } /** JSR-268 no soporta eventos de inserción o extracción. */ @Override public void removeCardConnectionListener(final CardConnectionListener ccl) { throw new UnsupportedOperationException("JSR-268 no soporta eventos de insercion o extraccion"); //$NON-NLS-1$ } /** {@inheritDoc} */ @Override public byte[] reset() throws ApduConnectionException { if (this.card != null) { try { this.card.disconnect(true); } catch (final CardException e) { LOGGER.warning("Error reiniciando la tarjeta: " + e); //$NON-NLS-1$ } } this.card = null; open(); if (this.card != null) { return this.card.getATR().getBytes(); } throw new ApduConnectionException("Error indefinido reiniciando la conexion con la tarjeta"); //$NON-NLS-1$ } /** Establece si la conexión se debe abrir en modo exclusivo. Solo * puede establecerse si la conexión aun no ha sido abierta. * @param ex * <code>true</code> para abrir la conexión en modo * exclusivo, <code>false</code> para abrirla en modo no * exclusivo. */ public void setExclusiveUse(final boolean ex) { if (this.card == null) { this.exclusive = ex; } else { SmartcardIoConnection.LOGGER.warning( "No se puede cambiar el modo de acceso a la tarjeta con la conexion abierta, se mantendra el modo EXCLUSIVE=" + Boolean.toString(this.exclusive) //$NON-NLS-1$ ); } } /** Establece el Protocolo de conexión con la tarjeta. * Por defecto, si no se establece ninguno, se indica <i>*</i> para que sea el API subyancente el * que detecte el apropiado. * @param p Protocolo de conexión con la tarjeta. */ @Override public void setProtocol(final ApduConnectionProtocol p) { if (p == null) { SmartcardIoConnection.LOGGER.warning( "El protocolo de conexion no puede ser nulo, se usara T=0" //$NON-NLS-1$ ); this.protocol = ApduConnectionProtocol.T0; return; } this.protocol = p; } /** {@inheritDoc} */ @Override public void setTerminal(final int terminalN) { if (this.terminalNumber == terminalN) { return; } final boolean wasOpened = isOpen(); if (wasOpened) { try { close(); } catch (final Exception e) { SmartcardIoConnection.LOGGER.warning( "Error intentando cerrar la conexion con el lector: " + e); //$NON-NLS-1$ } } this.terminalNumber = terminalN; if (wasOpened) { try { open(); } catch (final Exception e) { SmartcardIoConnection.LOGGER.warning("Error intentando abrir la conexion con el lector: " + e); //$NON-NLS-1$ } } } /** Tag que identifica que es necesario recuperar el resultado del comando anterior. */ private static final byte TAG_RESPONSE_PENDING = 0x61; private static final byte TAG_RESPONSE_INVALID_LENGTH = 0x6C; /** {@inheritDoc} */ @Override public ResponseApdu transmit(final CommandApdu command) throws ApduConnectionException { if (this.canal == null) { throw new ApduConnectionException( "No se puede transmitir sobre una conexion cerrada" //$NON-NLS-1$ ); } if (command == null) { throw new IllegalArgumentException( "No se puede transmitir una APDU nula" //$NON-NLS-1$ ); } if (DEBUG) { Logger.getLogger("es.gob.jmulticard").info( //$NON-NLS-1$ "Enviada APDU:\n" + //$NON-NLS-1$ HexUtils.hexify(command.getBytes(), true) ); } try { final ResponseApdu response = new ResponseApdu(this.canal.transmit(new CommandAPDU(command.getBytes())).getBytes()); // 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); } if (DEBUG) { Logger.getLogger("es.gob.jmulticard").info( //$NON-NLS-1$ "Respuesta:\n" + //$NON-NLS-1$ HexUtils.hexify(response.getBytes(), true) ); } return response; } catch (final CardException e) { final Throwable t = e.getCause(); if (t != null && SCARD_W_RESET_CARD.equals(t.getMessage())) { throw new LostChannelException(t.getMessage()); } throw new ApduConnectionException( "Error de comunicacion con la tarjeta tratando de transmitir la APDU " + //$NON-NLS-1$ HexUtils.hexify(command.getBytes(), true) + " al lector " + Integer.toString(this.terminalNumber) + //$NON-NLS-1$ " en modo EXCLUSIVE=" + //$NON-NLS-1$ Boolean.toString(this.exclusive) + " con el protocolo " + this.protocol.toString(), e //$NON-NLS-1$ ); } catch (final Exception e) { e.printStackTrace(); throw new ApduConnectionException( "Error tratando de transmitir la APDU " + HexUtils.hexify(command.getBytes(), true) + //$NON-NLS-1$ " al lector " + Integer.toString(this.terminalNumber) + //$NON-NLS-1$ " en modo EXCLUSIVE=" + //$NON-NLS-1$ Boolean.toString(this.exclusive) + " con el protocolo " + this.protocol.toString(), e //$NON-NLS-1$ ); } } /** Devuelve el protocolo de conexión con la tarjeta usado actualmente * @return Un objeto de tipo enumerado <code>ConnectionProtocol</code>. */ public ApduConnectionProtocol getProtocol() { return this.protocol; } /** Indica si la conexión con la tarjeta se ha establecido en modo exclusivo o no * @return <code>true</code> si la conexión está establecida en modo exclusivo. */ public boolean isExclusiveUse() { return this.exclusive; } }