Extrair dados do cartão de cidadão

Neste artigo vou demostrar como se podem obter dados do cartão de cidadão utilizando a linguagem de programação JAVA e validar os dados de identificação.

Porquê Java? Não vejo muitos artigos a utilizar Java e é uma linguagem que continua a ser utilizada nas universidades, nas empresas de desenvolvimento de software e não só. Já vi em fóruns de comunidades pedidos de ajuda para obter os dados do cartão de cidadão, mas o pedido que mais encontro é como obter a fotografia. Este artigo foi feito a pensar nessas questões. Também inclui funções de validação do número de bilhete de identidade e cartão de cidadão, número de identificação fiscal, número de identificação da segurança social e número de identificação bancária.

Sobre o cartão de cidadão

O cartão de cidadão veio substituir vários cartões de identificação Portugueses para um único cartão de identificação mas este cartão não vem apenas reduzir a quantidade de cartões que o cidadão tem que transportar mas permite também fazer algumas operações utilizando os serviços público do estado português através da internet e assinar documentos eletrónicos. 

O cartão de cidadão tem incluído um chip ISO/IEC 7816, e é neste chip que está guardada toda a informação.

Cartão do cidadão: chip ISO/IEC 7816
Figura 1: Chip ISO/IEC 7816

Esta informação está bloqueada? Podemos utilizar?

Informação existente no cartão não está toda disponível. Existem casos como a morada e certificados privados que apenas podem ser acedidos com pins que foram fornecidos juntamente com o cartão.

A informação a que se tem acesso é:

ÁreaSobre o cartãoSobre o cidadão
DadosData de entrega, entidade que efetuou a entrega, local de pedido, número de série, número do cartão, país, tipo de documento, validade, versão, zona de leitura ótica.Altura, apelido, apelido da mãe, apelido do pai, certificados digitais (autenticação), data de nascimento, fotografia, indicações eventuais, nacionalidade, número de identificação civil, número de identificação fiscal, número da segurança social, número do sistema nacional de saúde, primeiro nome, primeiro nome da mãe, primeiro nome do pai, sexo.

Mas que podemos nós fazer alguma coisa com esta informação?

Com os dados existentes podemos criar vários projetos como por exemplo autenticação de utilizador no Windows Active Directory (https://www.cartaodecidadao.pt/documentos/CC_MW_Windows_Logon_V1.0.pdf) , assinar documentos, recrutamento e outros.

Material necessário para um projeto do cartão de cidadão 

Antes de iniciar um projeto é recomendado que se tenha disponível um leitor de cartões e um cartão de cidadão. Podemos fazê-lo de duas formas.

  1. Comprar um kit de desenvolvimento que está disponível na página de internet do kit de desenvolvimento do cartão de cidadão em https://www.kitcc.pt/ccidadao/kits.
  2. Comprar um leitor de cartão de cidadão e usar o nosso próprio cartão de cidadão.

A diferença entre os dois é que o kit de desenvolvimento já inclui cartões com estado válido, revogado, suspenso, expirado, não ativado. O nosso próprio cartão está sempre válido e se se enganarem no pin mais do que três vezes o cartão fica bloqueado e para algumas operações é necessário deslocar-se à sua junta de freguesia ou loja do cidadão para solicitar um novo pin que é enviado por via postal.

Neste artigo pode utilizar o seu próprio cartão de cidadão. Mas se criar um projeto de autenticação recomendo comprar um kit de desenvolvimento.

Middleware do cartão de cidadão

É sempre necessária a instalação da aplicação do Cartão de Cidadão disponível em https://www.cartaodecidadao.pt. Esta aplicação permite ver os dados existentes do cartão de cidadão que está a ser utilizado mas também atua como um middleware.

Para o desenvolvimento vai ser utilizado uma biblioteca com o nome de pteidlibj. Esta biblioteca está disponível para desenvolvimento de aplicações .NET e Java, em duas formas:

  • Na diretoria de instalação existe uma diretoria com o nome sdk.
  • Middleware que está disponível na área de downloads na página de internet do kit de desenvolvimento do cartão de cidadão.

Tanto um com o outro já inclui um projeto de exemplo para .NET e Java. Existe o manual técnico de middleware em: https://www.cartaodecidadao.pt/documentos/CC_Technical_Reference_1.60.0_PT.pdf  e também está disponível na área de downloads na página de internet do kit de desenvolvimento do cartão de cidadão.

Cartão de cidadão: pasta do cartão de cidadão com SDK
Figura 1: Pasta do cartão de cidadão com SDK

Obtenção da fotografia

As questões que mais encontro sobre a obtenção de dados do cartão de cidadão são “Como é possível obter a fotografia do cartão de cidadão?” e “Qual é o formato do cartão de cidadão?”. O formato de imagem do cartão de cidadão é JPEG2000 (http://jpeg.org/jpeg2000/) que é um padrão de alta definição e que permite comprimir a imagem. Para visualizar a foto é necessário ler a imagem com este formato que depois pode converter em memória para outro formato se pretende guardar. No projeto de Java vai ser utilizado o Java Advanced Imagin (Jai-imageio) versão 1.1. Nas versões superiores foi removido o suporte para JPEG2000.

Para criar a miniatura da fotografia vai ser utilizado o Java Image Scaling Library (imgscalr).

Nota: Se não carregarem a biblioteca Jai-imageio no projeto quando obtiverem a fotografia, não vão obter nenhum erro de falha de carregamento da biblioteca, apenas vão obter um valor nulo.

Vamos programar

Neste artigo vou utilizar o IDE NetBeans, mas pode utilizar outro IDE da sua escolha.

Vão ser criadas:

  • Uma classe com as caixas de diálogo estáticas para as mensagens de erro e de informação;
  • Uma classe para obtenção dos dados do cartão de cidadão e controlo de erros;
  • Um formulário JFrame que já inclui o método main e todos os elementos gráficos;
  • Uma classe para validação de números de identificação.

Crie um novo projeto Java Application com o nome à sua escolha sem a pasta de ficheiros para guardar as bibliotecas e classe main definidas. Depois crie um Java Package com o nome PaPCC. É neste package que vão estar todas as classes Java e JFrame.

Classe com as caixas de diálogo para as mensagens de erro e informação

Esta classe inclui as funções estáticas para mostrar as mensagens de informação e erro.

Crie uma nova classe Java com o nome JMessageDialog dentro do package PaPCC.

Incluía as seguintes importações uma vez que vão ser necessárias para as funções.

import javax.swing.JFrame;
import javax.swing.JOptionPane; 

E depois as seguintes funções estáticas para apresentar as mensagens.

/**
 * Mostra uma janela com informação do erro
 * @param title título do erro
 * @param message mensagem do erro
 */
public static void erroMsg(String title, String message) {
  JOptionPane.showMessageDialog(new JFrame(), message,
      title, JOptionPane.ERROR_MESSAGE);
}

/**
 * Mostra uma janela com informação
 * @param title título da informação
 * @param message mensagem da informação
 */
public static void infoMsg(String title, String message) {
  JOptionPane.showMessageDialog(new JFrame(), message,
      title, JOptionPane.INFORMATION_MESSAGE);
}

A classe está terminada.

Classe Obtenção dos dados do cartão de cidadão e controlo de erros

Esta classe inclui todas as variáveis e funções de leitura e dados do cartão de cidadão.

Crie uma nova classe Java com o nome CitizenCardPortuguese no package PaPCC e inclua os seguintes importações que vão ser necessárias para as funções.

import static PaPCC.JMessageDialog.erroMsg;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.JOptionPane;
import pteidlib.PTEID_ADDR;
import pteidlib.PTEID_ID;
import pteidlib.PTEID_PIC;
import pteidlib.PTEID_Pin;
import pteidlib.PteidException;
import pteidlib.pteid;

Primeiro vão ser definidos todas as variáveis dos dados do cartão, pins e tentativas.

private String birthDate;
private String deliveryEntity;
private String cardNumber;
private String cardNumberPAN;
private String cardVersion;
private String country;
private String documentType;
private String firstname;
private String firstnameFather;
private String firstnameMother;
private String height;
private String locale;
private String mrz1;
private String mrz2;
private String mrz3;
private String name;
private String nameFather;
private String nameMother;
private String nationality;
private String notes;
private String numBI;
private String numNIF;
private String numSNS;
private String sex;
private String validityDate;
private String numSS;
private String deliveryDate;
private String addrType;
private String street;
private String municipality;
private String addressF;
private String building;
private String buildingAbbr;
private String countryM;
private String countryDescF;
private String cp3;
private String cp4;
private String district;
private String districtDesc;
private String door;
private String floor;
private String freguesia;
private String freguesiaDesc;
private String locality;
private String localityF;
private String municipalityDesc;
private String numMor;
private String numMorF;
private String postal;
private String postalF;
private String place;
private String regioF;
private String side;
private String streettype;
private String streettypeAbbr;
private String cityF;
private int authPinTriesLeft;
private int sigPinTriesLeft;
private int addrPinTriesLeft;
private String addr = null;

A seguir inclua os getters das variáveis definidas. No NetBeans pode fazer esta operação automaticamente clicando no botão direito do rato e selecionado a opção Insert Code…, a seguir a opção Getter, selecione todas as variáveis e por fim clique no botão Generate.

A seguinte função é usada para carregar o middleware, e permite saber se a biblioteca foi carregada com sucesso ou se existe algum problema. Esse resultado é dado ou fornecido através de um valor booleano.

/**
 * Carrega a biblioteca pteidlibj
 *
 * @return um valor booleano se o middleware foi carregado com sucesso
 * @throws UnsatisfiedLinkError erro do middleware
 */
public static boolean loadPteidLib() throws UnsatisfiedLinkError {
  return ((Boolean) AccessController.doPrivileged(new PrivilegedAction() {
    @Override
    public Boolean run() {
      try {
        System.loadLibrary("pteidlibj");
        return true;
      } catch (UnsatisfiedLinkError t) {
        if (t.getMessage().contains("already loaded")) {
          JOptionPane.showMessageDialog(null, "Biblioteca do Cartão de Cidadão bloqueada.", "Biblioteca bloqueada", JOptionPane.ERROR_MESSAGE);
        } else {
          JOptionPane.showMessageDialog(null, "Middleware do Cartão de Cidadão não está instalado.", "Aplicação não está instalada", JOptionPane.ERROR_MESSAGE);
        }
        return false;
      }
    }
  }));
}

A obtenção dos dados do cartão de cidadão vai ser efetuado por partes, primeiro vão ser obtidos os dados sem necessitar de pin de acesso, depois os dados da morada que requer o pin e por fim a fotografia.

A seguinte função destina-se a obter os dados do cartão de cidadão sem pin de acesso e com um valor booleano vamos saber se foi obtido com sucesso.

/**
 * Obtenção dos dados do cidadão
 *
 * @return um valor booleano se os dados foram obtidos com sucesso
 * @throws PteidException erro do middleware
 */
public boolean getData()
    throws PteidException {
  try {
    pteid.Init("");

    pteid.SetSODChecking(false);

    PTEID_ID idData = pteid.GetID();
    if (null != idData) {
      this.deliveryEntity = idData.deliveryEntity;
      this.birthDate = idData.birthDate;
      this.cardNumber = idData.cardNumber;
      this.cardNumberPAN = idData.cardNumberPAN;
      this.cardVersion = idData.cardVersion;
      this.country = idData.country;
      this.documentType = idData.documentType;
      this.firstname = idData.firstname;
      this.firstnameFather = idData.firstnameFather;
      this.firstnameMother = idData.firstnameMother;
      this.height = idData.height;
      this.locale = idData.locale;
      this.mrz1 = idData.mrz1;
      this.mrz2 = idData.mrz2;
      this.mrz3 = idData.mrz3;
      this.name = idData.name;
      this.nameFather = idData.nameFather;
      this.nameMother = idData.nameMother;
      this.nationality = idData.nationality;
      this.notes = idData.notes;
      this.numBI = idData.numBI;
      this.numNIF = idData.numNIF;
      this.numSNS = idData.numSNS;
      this.sex = idData.sex;
      this.validityDate = idData.validityDate;
      this.numSS = idData.numSS;
      this.deliveryDate = idData.deliveryDate;

      PTEID_Pin[] pin = pteid.GetPINs();
      this.authPinTriesLeft = pin[0].triesLeft;
      this.sigPinTriesLeft = pin[1].triesLeft;
      this.addrPinTriesLeft = pin[2].triesLeft;
    }

  } catch (PteidException ex) {
    int errorNumber = Integer.parseInt(ex.getMessage().split("Error code : -")[1]);
    errorCC(errorNumber, ex.getMessage());
    return false;
  } finally {
    pteid.Exit(0);
  }
  return true;
}

A seguinte função permite obter a morada do cartão de cidadão. Se experimentar com o cartão de cidadão com versão superior ou igual a 004.003, o PIN público é 0000. Este pin apenas permite consultar a morada, não permite alterar os dados da morada.

/**
 * Obtenção da morada
 *
 * @return um valor booleano se os dados foram obtidos com sucesso
 * @throws PteidException erro do middleware
 */
public boolean getAddress() throws PteidException {
  try {
    pteid.Init("");
    pteid.SetSODChecking(false);
    PTEID_ADDR adData = pteid.GetAddr();
    if (null != adData) {
      if ("N".equals(adData.addrType)) {
        this.addrType = "Nacional";
        this.district = adData.district;
        this.districtDesc = adData.districtDesc;
        this.municipality = adData.municipality;
        this.municipalityDesc = adData.municipalityDesc;
        this.freguesia = adData.freguesia;
        this.freguesiaDesc = adData.freguesiaDesc;
        this.streettypeAbbr = adData.streettypeAbbr;
        this.streettype = adData.streettype;
        this.street = adData.street;
        this.buildingAbbr = adData.buildingAbbr;
        this.building = adData.building;
        this.door = adData.door;
        this.floor = adData.floor;
        this.side = adData.side;
        this.place = adData.place;
        this.locality = adData.locality;
        this.cp3 = adData.cp3;
        this.cp4 = adData.cp4;
        this.postal = adData.postal;
        this.country = adData.country;
        this.numMor = adData.numMor;
      } else {
        this.addrType = "Internacional";
        this.district = adData.district;
        this.districtDesc = adData.districtDesc;
        this.municipality = adData.municipality;
        this.municipalityDesc = adData.municipalityDesc;
        this.freguesia = adData.freguesia;
        this.freguesiaDesc = adData.freguesiaDesc;
        this.streettypeAbbr = adData.streettypeAbbr;
        this.streettype = adData.streettype;
        this.street = adData.street;
        this.buildingAbbr = adData.buildingAbbr;
        this.building = adData.building;
        this.door = adData.door;
        this.floor = adData.floor;
        this.side = adData.side;
        this.place = adData.place;
        this.locality = adData.localityF;
        this.cp3 = adData.cp3;
        this.cp4 = adData.cp4;
        this.postal = adData.postalF;
        this.country = adData.countryDescF;
        this.countryDescF = adData.countryDescF;
        this.addressF = adData.addressF;
        this.cityF = adData.cityF;
        this.numMor = adData.numMorF;
        this.regioF = adData.regioF;
      }
    }
  } catch (PteidException ex) {
    int errorNumber = Integer.parseInt(ex.getMessage().split("Error code : -")[1]);
    errorCC(errorNumber, ex.getMessage());
    return false;
  } finally {
    pteid.Exit(0);
  }
  return true;
}

A seguinte função é para obter a fotografia.

/**
 * Obtenção da fotografia
 *
 * @return um valor booleano se os dados foram obtidos com sucesso
 * @throws PteidException erro do middleware
 * @throws Exception erro do InputStream
 */
public BufferedImage getPhoto() throws PteidException, Exception {
  BufferedImage bimg = null;
  try {
    pteid.Init("");
    pteid.SetSODChecking(false);
    PTEID_PIC picData = pteid.GetPic();
    if (null != picData) {
      byte[] byteImg = picData.picture;
      Iterator iterator = ImageIO.getImageReadersByFormatName("jpeg2000");
      while (iterator.hasNext()) {
        try (ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(byteImg))) {
          ImageReader reader = (ImageReader) iterator.next();
          reader.setInput(iis);
          bimg = reader.read(0, reader.getDefaultReadParam());
        }
      }
    }
  } catch (PteidException ex) {
    int errorNumber = Integer.parseInt(ex.getMessage().split("Error code : -")[1]);
    errorCC(errorNumber, ex.getMessage());
  } finally {
    pteid.Exit(0);
  }
  return bimg;
}

As seguintes funções são para identificar o código de erro que o middleware retorna. Estes erros estão documentados no manual técnico, na secção Códigos de Erro.

/**
 * Lista de códigos de erro do middleware
 *
 * @param errorNumber código de erro
 * @param ex mensagem de erro auxiliar
 */
public void errorCC(int errorNumber, String ex) {
  String message;
  switch (errorNumber) {
    case 1101:
      message = ("Erro desconhecido - Problemas com o serviço de leitor de cartões \nMessage: " + ex);
      erroMsg(message);
      break;
    case 1104:
      message = ("Não foi possível aceder ao cartão.\nVerifique se está corretamente inserido no leitor");
      erroMsg(message);
      break;
    case 1109:
      message = ("Acão cancelada pelo utilizador");
      erroMsg(message);
      break;
    case 12109:
      message = ("Não é permitido.");
      erroMsg(message);
      break;
    case 1210:
      message = ("O cartão inserido não corresponde a um cartão de cidadão válido.");
      erroMsg(message);
      break;
    case 1212:
      message = ("Pin de morada bloqueado. Resta(m) 0 tentativa(s).\n" + ex);
      erroMsg(message);
      break;
    case 1214:
      message = ("Pin inválido, não tente novamente.\n" + ex);
      erroMsg(message);
      break;
    case 1304:
      message = ("Pin inválido, não tente novamente.\n" + ex);
      erroMsg(message);
      break;
    default:
      message = ("Erro desconhecido: " + ex);
      erroMsg(message);
      break;
  }
}

E temos a nossa classe terminada.

Publicado na edição 52 (PDF) da Revista PROGRAMAR.