SmartCard + Java

Quinta-feira, Junho 26, 2008

Comecei a trabalhar com Java e a utilizar SmartCards no final de 2006, no mesmo período que comecei a utilizar Gnu/Linux efetivamente. Sim, emprego novo. =p

Mas… o que é um SmartCard?
Bom, basicamente é um cartão de plástico com um chip “embedado”.

Ah, então eu já vi um desses! É aqueles cartões bancários certo?
Isso, também. Mas há outros tipos, não apenas bancários.
Você tem celular? É GSM? Sabe aquele chipzinho que você insere nele?

Então, é um SmartCard também =]

A diferença entre eles é a estrutura criada internamente. Tem também a história das chaves de autenticação e outros detalhes que complicam bastante, dependendo do que precisa ser feito.

Como conheço um pouco sobre isso e já percebi que algumas pessoas ainda têm dúvidas, resolvi criar um pequeno tutorial com códigos em java para acesso ao SmartCard, para envio de alguns comando simples, apenas para começar a brincadeira.

Aqui considero que você já possui uma leitora com os drivers instalados, possui o JDK 1.6 (java) instalado (utilizaremos a classe SmartCardIO, que está disponível a partir dessa versão) e corretamente configurado, além de conhecimento sobre o funcionamento e comunicação com SmartCards (informações podem ser obtidas aqui e também aqui).

A comunicação com o cartão se dá através de comandos APDU (definida na ISO 7816-4).
Em java, antes de enviar o comando é necessário alguns passos importantes:

Criar uma “fábrica” de leitoras:

factory = TerminalFactory.getDefault();

Preencher uma lista com as leitoras conectadas e iniciadas:

terminals = factory.terminals().list();

Realizar a conexão com o cartão:

card = terminal.connect("t=0");

Abrir um canal para comunicação com o cartão:

channel = card.getBasicChannel();

Após esses passos é possível recuperar o ATR do cartão, que é sua resposta quando o mesmo é energizado:

atr = card.getATR();

Antes de enviar o comando APDU é desejável que seja feita a verificação para garantir que há um cartão na leitora e evitar uma exception

if(!terminal.isCardPresent()) {
}

Após a verificação, podemos enviar o comando desejado através do canal criado, recebendo a resposta do cartão:

lastResponse = channel.transmit(new CommandAPDU(myCommand));

Certo, esse é o básico. Mas temos um problema: A comunicação é feita através de array de bytes, e vai dar muito trabalho para converter toda hora que for preciso enviar um comando ou receber resposta e exibí-la. Basta criar uma classe para fazer a conversão =D Lembrando, valores HEXADECIMAIS.

A seguir uma classe simples que criei para demonstrar os passos citados e sua utilização.

import java.util.List;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;
import javax.swing.JComboBox;
import javax.swing.JOptionPane;

/**
 * Classe para comunicação com SmartCards
 * @author Luiz Gustavo Silva Marcondes
 * http://lgmarcondes.wordpress.com
 */
public class MySmartCardTest {

private TerminalFactory factory;
private List<CardTerminal> terminals;
private CardTerminal terminal;
private Card card;
private CardChannel channel;
private ResponseAPDU lastResponse;

public MySmartCardTest() {
    try {
        if(initialization("t=0")) {
            //Se conseguiu inicializar, vamos
            //recuperar informações do ATR
            //(Answer to Response) do cartão
            System.out.println(getAtr());
        }
    } catch (CardException ex) {
        ex.printStackTrace();
    }
}

/**
 * Inicializa a seção, criando um canal para se
 * comunicar com o SmartCard.
 * @param protocol o procoloco a ser utilizado
 * ("T=0", "T=1", or "T=CL"), ou "*" para se
 * conectar usando qualquer protocolo disponível
 * @return true em caso de inicialização correta.
 */
public boolean initialization(String protocol)
        throws CardException {
    //Criar uma "Fábrica" de Terminais (leitoras)
    factory = TerminalFactory.getDefault();
    //Preencher a lista de Terminais (leitoras)
    terminals = factory.terminals().list();

    //Exibe mensagem e retorna se não achar leitoras
    if(terminals.size() == 0) {
        JOptionPane.showMessageDialog(null, "Erro,"+
                " nenhuma leitora foi encontrada.");
        return false;
    }

    //Box contendo a lista das leitoras
    JComboBox readersList;
    readersList = new JComboBox(terminals.toArray());

    int opcao = JOptionPane.showConfirmDialog(null,
            readersList, "Escolha a leitora: ",
            JOptionPane.OK_CANCEL_OPTION);

    if (opcao == JOptionPane.OK_OPTION) {
        terminal = terminals.
                get(readersList.getSelectedIndex());
    } else {
        return false;
    }

    //Ligação com o cartão utilizando protocolo de
    //comunicação T=0
    card = terminal.connect("t=0");

    //Abre um canal com um cartão
    channel = card.getBasicChannel();

    return true;
}

/**
 * Retorna o ATR - Answer to Reset - Resposta do
 * cartão ao reset
 * @return o ATR
 */
public String getAtr() {

    //Armazena o ATR do cartão em um array de bytes
    byte[] atr = card.getATR().getBytes();
    String response = "";

    //Converte a resposta de array de bytes para
    //strings hexadecimais em letras maiúsculas
    for (int i = 0; i < atr.length; i++) {
        if (atr[i] < 0) {
            response += Integer.toHexString(256 +
                    atr[i]).toUpperCase();
        } else {
            if (atr[i] < 16)
                response += "0";
            response += Integer.toHexString(atr[i])
                    .toUpperCase();
        }
    }

    return response;
}

/**
 * Envia o comando APDU para o SmartCard e retorna a
 * APDU de resposta.
 * @param apdu uma string que contém o comando APDU.
 * @return uma string que contém a APDU de resposta.
 * @throws CardException
 */
public String sendApdu(String apdu)
        throws CardException {
    //Se não houver cartão na leitora retorna
    if(!terminal.isCardPresent())
        return "";

    //Exibe o comando a ser enviado
    System.out.println("Command APDU: " + apdu);

    //Converte a string contendo o comando para um
    //array de bytes
    byte[] buffer = strToHexByte(apdu);

    //Envia a APDU e guarda resposta em lastResponse
    lastResponse = channel.transmit(
            new CommandAPDU(buffer));

    String response = "";
    //Armazena a resposta (SW) em forma de string
    response = Integer.toHexString(
            lastResponse.getSW());

    response += " ";

    byte[] data = lastResponse.getData();

    for (int i = 0; i < data.length; i++) {
        if (data[i] < 0) {
            response += Integer.toHexString(256 +
                    data[i]).toUpperCase();
        } else {
            if (data[i] < 16)
                response += "0";
            response += Integer.toHexString(data[i])
                    .toUpperCase();
        }
    }

    return response.toUpperCase();
}

/**
 * Converte uma string hexadecimal em array de bytes
 * @param str a string hexadecimal a ser convertida.
 * @return um array de bytes com a strign convertida.
 */
public byte[] strToHexByte(String string) {
    byte[] b = new byte[string.length() / 2];

    try {
        String s1;

        int j=0;
        for(int i = 0; i < string.length(); i += 2) {
            s1 = string.substring(i, i + 2);
            b[j] = (byte)Integer.parseInt(s1, 16);
            j++;
        }
    } catch(NumberFormatException ex) {
        ex.printStackTrace();
    } catch(StringIndexOutOfBoundsException ex) {
        ex.printStackTrace();
    }

    return b;
}

public static void main(String args[]) {
        MySmartCardTest myTest;
        myTest = new MySmartCardTest();

        try {
            String myAPDU, myAnswer;
            myAPDU = "00A40400023F00";
            myAnswer = myTest.sendApdu(myAPDU);
            System.out.println("Resp: " + myAnswer);
        } catch(CardException ex) {
            ex.printStackTrace();
        }
}
}

Bom, como o Corinthians apenas empatou ontem, por hoje é só! =p