domingo, 19 de dezembro de 2010

Acessando Smart Cards Usando Java

Nesse artigo demonstrarei o quanto é simples acessar Smart Cards utilizando o Java SE.
O material necessário:
  • Cartão Java Card de Desenvolvimento.
  • Leitor/Gravador padrão PC/SC ISO7816.
  • IDE Netbeans / Eclipse.
  • Ter completado com sucesso os 3 Artigos Hello World Java Card.
Como já havia dito tenho uma preferência pelo IDE Netbeans, mas você poderá utilizar tranquilamente o Eclipse se desejar.
Sem muitos rodeios vamos ao que interessa o Código:
import java.util.List;
import javax.smartcardio.*;

public class SmartCardTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        //"Fabrica" de Terminais PC/SC
        TerminalFactory factory;
        //Lista de Leitores PC/SC
        List terminals;
        //Terminal PC/SC
        CardTerminal terminal;
        //Smart Card
        Card card;
        //Smart Card ATR
        ATR cardATR;
        //Canal de Comunicação com o Smart Card
        CardChannel cardChannel;
        //APDU de Comando
        CommandAPDU commandAPDU;
        //APDU de Resposta
        ResponseAPDU responseAPDU;
        //Buffer de Auxilio
        byte[] buffer;

        try {
            //Adquire Fabrica de Leitores
            factory = TerminalFactory.getDefault();

            //Adquire Lista de Leitores PC/SC no Sistema
            terminals = factory.terminals().list();
            System.out.println("Lista : " + terminals);

            //Adquire Primeiro Terminal da Lista
            terminal = (CardTerminal)terminals.get(0);
            System.out.println("Terminal Selecionado: "
                    + terminal.getName());

            //Estabelece Conexão com o Cartão na Leitora
            card = terminal.connect("*");
            System.out.println("card: " + card);

            //Adquire ATR do Cartão
            cardATR = card.getATR();
            buffer = cardATR.getBytes();
            System.out.println("ATR : "
                    + formatBuffer(buffer, buffer.length));

            //Adquire Canal de Comunicação
            cardChannel = card.getBasicChannel();

            //AID do HelloWorld
            buffer = new byte[]{0x01, 0x02, 0x03, 0x04,
                        0x05, 0x06, 0x07, 0x08,
                        0x09, 0x00, 0x00};

            //Monta APDU de Envio
            commandAPDU = new CommandAPDU(
                    0x00,       //CLA
                    0xA4,       //INS - SELECT
                    0x04,       //P1
                    0x00,       //P2
                    buffer);    //AID

            //Imprime Comando
            System.out.println("\n[SELECT COMMAND]");
            System.out.println("=> " + formatBuffer(
                    commandAPDU.getBytes(),
                    commandAPDU.getBytes().length));

            //Trasnmite e Recebe
            responseAPDU = cardChannel.transmit(commandAPDU);

            //Verifica Resposta
            if (responseAPDU.getSW() != 0x9000) {
                throw new Exception("Falha ao Selecionar : "
                        + String.format("0x%04X",
                          responseAPDU.getSW()));
            }

            //Imprime Resposta
            System.out.println("<= " + formatBuffer(
                    responseAPDU.getBytes(),
                    responseAPDU.getBytes().length));

            //Bytes de Testes
            buffer =
                new byte[]{0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F};

            //Monta APDU de Envio
            commandAPDU = new CommandAPDU(
                    0x00, //CLA
                    0x00, //INS
                    0x00, //P1
                    0x00, //P2
                    buffer); //Teste

            //Imprime Comando
            System.out.println("\n[TESTE BYTES]");
            System.out.println("=> " + formatBuffer(
                    commandAPDU.getBytes(),
                    commandAPDU.getBytes().length));

            //Trasnmite e Recebe
            responseAPDU = cardChannel.transmit(commandAPDU);

            //Verifica Resposta
            if (responseAPDU.getSW() != 0x9000) {
                throw new Exception("Falha ao Selecionar : "
                        + String.format("0x%04X",
                          responseAPDU.getSW()));
            }

            //Imprime Resposta
            System.out.println("<= " + formatBuffer(
                    responseAPDU.getBytes(),
                    responseAPDU.getBytes().length));

        } catch (Exception e) {
            e.printStackTrace(System.out);
        }
    }

    /**
     * Converte Buffer bytes para String
     * @param buffer
     * @param length
     * @return String
     */
    public static String formatBuffer(byte[] buffer,int length)
    {
        StringBuilder strBuff = new StringBuilder("");
        for (int i = 0; i < length; i++) {
            strBuff.append(String.format("%02X", buffer[i]));
        }
        return strBuff.toString();
    }
}
O Resultado ao executar deve ser similar a isso:
Lista : [PC/SC terminal ACS ACR38U 0]
Terminal Selecionado: ACS ACR38U 0
card: PC/SC card in ACS ACR38U 0, protocol T=1, state OK
ATR : 3BF81300008131FE454A434F5076323431B7

[SELECT COMMAND]
=> 00A404000B0102030405060708090000
<= 9000

[TESTE BYTES]
=> 00000000060A0B0C0D0E0F
<= 00000000060A0B0C0D0E0F9000
Vamos a analise do código: 
A partir do Java 6, a Sun disponibilizou uma API intitulada "Java Smart Card I/O" que tem como finalidade prover acesso a Smart Cards.

Na linha 11 criamos uma variável factory que instância um objeto TerminalFactory, esse objeto implementa uma pilha de leitores que pode variar conforme a implementação, no nosso caso usaremos o padrão getDefault() que implementa leitores PC/SC, lembrem-se existem diversos leitores com diversos tipos de padrões.

Na linha 13 criamos uma lista terminals que armazena um ou mais terminais (leitor PC/SC) plugados no sistema, que é retornado pelo factory, caso não exista nenhum leitor plugado factory retornará uma exceção.

Na linha 15 criamos uma variável terminal que instância um objeto CardTerminal, esse objeto representa um terminal (leitor PC/SC).

Na linha 17 criamos uma variável card que instância um objeto Card, esse objeto representa o cartão que se encontra inserido no Leitor representado por terminal.

Na linha 19 criamos uma variável cardATR que instância um objeto ATR, esse objeto representa o ATR (Answer to Reset) retornado pelo cartão representado por card. 

Na linha 21 criamos uma variável cardChannel que instância um objeto CardChannel, esse objeto provê o canal de comunicação que nossa aplicação utilizará para trocar APDUs com o cartão, esse canal é retornado pelo cartão representado por card. 

Na linha 23 criamos uma variável commandAPDU que instância um objeto CommandAPDU, esse objeto representa um APDU de envio do host para o cartão através do canal de comunicação representado por cardChannel.

Na linha 25 criamos uma variável responseAPDU que instância um objeto ResponseAPDU, esse objeto representa um APDU de resposta do cartão para o host e é retornado através do canal de comunicação representado por cardChannel.

Na linha 31 iniciamos factory com uma "Fábrica de Terminais"  padrão que no caso é o PC/SC.

Na linha 34 preenchemos a lista terminals com terminais "fabricado" por factory, caso não exista nenhum leitor PC/SC plugado no sistema ou ocorra um erro factory lançará uma exceção, do contrario a lista possuirá ao menos um leitor.

Na linha 38 iniciamos terminal instanciando o primeiro leitor contido na lista terminals.

Na linha 43 estabelecemos conexão com o cartão presente na leitora através do comando terminal.connect("*") o parâmetro * informa para auto detectar o protocolo do cartão esse parâmetro também pode ser explicitamente informado como "T=0", "T=1" ou "T=CL", em caso de sucesso um objeto Card é retornado e card representará o cartão conectado na leitora, caso contrário terminal lançará uma exceção.

Na linha 47 requisitamos o ATR (Answer To Reset) que se trata de um objeto ATR retornado pelo método getATR da instância card.

Na linha 53 abrimos um canal de comunicação entre Host e Cartão, através da instância cardChannel retornado pelo método getBasicChannel de card.

Na linha 61 criamos um APDU a ser enviado para o cartão através do objeto CommandAPDU, esse APDU trata-se do comando de select padrão ISO7816 do nosso applet hello world instalado no cartão.

Na linha 75 transmitimos o APDU para o cartão através do método transmit de cardChannel, esse método sempre retorna um objeto ResponseAPDU, que sempre possui a resposta (Status World) da operação do comando enviado e ocasionalmente dados retornados pelo cartão, em caso de erros durante a transmissão o método lançará uma exceção.

Na linha 94 transmitimos o APDU de teste para o Applet do cartão o qual responde uma cópia exata do APDU de envio seguido do Status World 0x9000(OK).


Bem como podem ver é extremamente simples acessar Smart Cards utilizando a tecnologia Java SE.
Coloco a disposição para download o projeto escrito no NetBeans, que pode ser tranquilamente compilado em outras IDE's como Eclipse e até no Prompt de Comando (DOS) pois o exemplo é Pure Java não dependendo de bibliotecas externas:

Smart Card Test (Link)

Caso queira se aprofundar mais na API Java Smart Card IO segue o link para estudos:

Java Smart Card IO API (Link)

Em artigos futuros, postarei um artigo idêntico porém escrito em C/C++, nesse caso não é tão simples como no caso do Java porém ainda assim é muito similar.

Um grande abraço a todos e até a próxima!
 

10 comentários:

  1. Uma grande contribuição para os neofitos desta área, muito me ajudou.
    Gostaria de saber se poderias futuramente, postar alguns exemplos com diretorios e arquivos: DF, EF ...
    Um forte Abraço,
    Francis

    ResponderExcluir
  2. Olá Francis,
    No caso o Java Card comum não possui estrutura de diretórios, no caso o GlobalPlatform é o responsável pela carga/instalação/listagem/exclusão dos Applets, canal seguro, gerenciamento de chaves, etc.
    Cartões que possuem essa caracteristicas são os Java Cards especificos para GSM, Familia ACOS entre outros.
    Abraços.

    ResponderExcluir
  3. Boa Noite ... Ricardo

    Então os cartões comercializados pela SONSUN, JCOP 21, não tem estrutura de diretório?
    Trabalharei com estes, com uma estrutura TLV ?
    Desde já agradeço, e muito, tua atenção !!

    Abraço,
    Francis

    ResponderExcluir
  4. Olá Francis,
    Tanto o JCOP como qualquer outro Java Card comum não possue estrutura de diretórios.
    Vc apenas faz o Upload do Applet pro cartão e em seguida o instala.
    Após isso vc poderá selecionar o Applet pelo AID e trocar dados com ele.
    A troca de dados pode sim ser feita através de TLV nativamente nas versões Java Card 2.2.2, nas anteriores vc terá que implementar o TLV.

    Abraços,
    Ricardo Massao

    ResponderExcluir
  5. ola ricardo, gostaria saber se vc vai postar mesmo o codigo em C/C++, estou aguardando..

    ResponderExcluir
  6. Oi Ricardo gostaria de saber onde compro leitor/gravador compativel com java card e tb o cartao de desenvolvimento.
    Obrigado

    ResponderExcluir
  7. Oi por favor poderia me enviar por email nome de fornecedores de smart cards, leitores e gravadores para eu começar a testar de verdade. Muito obrigado e parabéns pelo blog.

    Meu nome é Luiz e meu email é lhmatias@gmail.com

    ResponderExcluir
  8. É possível acessar smartcard sem applet pelo navegador?

    ResponderExcluir
  9. Como o applet vai disponibilizar as informações do cartão para o lado servidor?

    Eu capturar estas informações em qualquer linguagem server-side, tipo PHP, Ruby, etc. ?

    ResponderExcluir
  10. Ola Ricardo eu gostaria de saber se tem como eu remover um applet do cartão ao digitar o PIN dele errado mais que o limite permitido, estou tentando remover um aqui mais da forma normal não estou conseguindo e o applet não tem o metodo pra resetar o PIN.
    Se possivel envie a possivel solução para o meu email welisson.oliveiraa@gmail.com
    Desde ja, obrigado!

    ResponderExcluir

Observação: somente um membro deste blog pode postar um comentário.