Manipulando Token de Segurança

qrcodeEsses dias tive um desafio bem bacana, que foi gravar e ler informações em um dispositivo de segurança programado em Java. O dispositivo é o Alladin eToken Pro 72K (Java).

eToken
eToken

Utilizei ele para autenticação de duas vias, conforme descrito nesse post, mas foi necessário armazenar algumas informações de segurança para aderir a requisição de um cliente na implantação dos requisitos impostos pelo PCI Security Standards Council na criptografia de dados, para não cair na mesma falha que a Playstation Network.

O maior problema que encontrei foi na integração com os drivers do token, mas graças ao departamento de uma universidade da Aústria, consegui encontrar um wrapper JNI para o driver do token: http://jce.iaik.tugraz.at.

Com isso, criei uma biblioteca que pode autenticar, listar, ler e escrever dados no token. Não posso entrar em detalhes gerais do funcionamento da solução completa, por questões de segurança e contrato de confidencialidade, mas posso mostrar como gravar e recuperar informações do token. Para facilitar minha vida, criei duas classes, uma responsável pela conexão com o token e outra responsável pela manipulação dos dados. As classes estão comentadas. É necessário ter o driver do token instalado e configurado na máquina. O procedimento descrito aqui é para Linux, mas o mesmo pode ser feito em windows, bastando apontar o caminho do arquivo .so para um arquivo .dll.

Faça o download do PKCS#11 Wrapper no site: https://jce.iaik.tugraz.at/sic/Products/Core-Crypto-Toolkits/PKCS-11-Wrapper. A biblioteca é gratuita, mas é necessário realizar um cadastro. Descompacte-o. Procure por um arquivo situado dentro da pasta release da sua plataforma. No meu caso a pasta é: ./native/platforms/linux_x64/release. O arquivo se chama libpkcs11wrapper.so para a versão linux. Copie ele para uma outra pasta, juntamente com o arquivo ./java/lib/iaikPkcs11Wrapper.jar. Será necessário esses arquivos para executar o programa.

A primeira classe é responsável pela conexão com o Token: ETokenConnection.

package br.com.thiagovespa.etoken.utils;
import iaik.pkcs.pkcs11.Module;
import iaik.pkcs.pkcs11.Slot;
import iaik.pkcs.pkcs11.Token;
import iaik.pkcs.pkcs11.TokenException;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Classe responsável pela conexão com tokens
 *
 * @author Thiago Galbiatti Vespa
 * @version 2.1
 */
public class ETokenConnection {
	private final static Logger logger = Logger
			.getLogger(ETokenConnection.class.getName());
	private Module pkcs11Module;
	/**
	 * Inicializa a conexão com o módulo do token
	 *
	 * @param libraryPath
	 *            caminho para o driver do token
	 * @throws Exception
	 */
	public ETokenConnection(String libraryPath) throws Exception {
		try {
			pkcs11Module = Module.getInstance(libraryPath);
			pkcs11Module.initialize(null);
		} catch (IOException ex) {
			logger.log(Level.SEVERE, null, ex);
			throw ex;
		} catch (TokenException ex) {
			logger.log(Level.SEVERE, null, ex);
			throw ex;
		}
	}
	/**
	 * Recupera todos os slots de token que estão com o token presente
	 *
	 * @return todos os slots de token que estão com o token presente
	 * @throws TokenException
	 */
	public Slot[] getTokenSlots() throws TokenException {
		return pkcs11Module.getSlotList(Module.SlotRequirement.TOKEN_PRESENT);
	}
	/**
	 * Recupera todos os slots de token
	 *
	 * @return todos os slots de token
	 * @throws TokenException
	 */
	public Slot[] getAllTokenSlots() throws TokenException {
		return pkcs11Module.getSlotList(Module.SlotRequirement.ALL_SLOTS);
	}
	/**
	 * Recupera o primeiro slot que possui um token conectado
	 *
	 * @return primeiro slot que possui um token conectado
	 * @throws TokenException
	 */
	public Slot getFirstTokenSlots() throws TokenException {
		Slot[] slots = getTokenSlots();
		if (slots.length < 0) {
			return slots[0];
		}
		return null;
	}
	/**
	 * Recupera o primeiro token no primeiro slot que possui um token conectado
	 *
	 * @return primeiro token no primeiro slot que possui um token conectado
	 * @throws TokenException
	 */
	public Token getFirstToken() throws TokenException {
		Slot slot = getFirstTokenSlots();
		if (slot != null) {
			return slot.getToken();
		}
		return null;
	}
	/**
	 * Finaliza a conexão com o móduglo
	 *
	 * @throws TokenException
	 */
	public void close() throws TokenException {
		pkcs11Module.finalize(this);
	}
}

No construtor é necessário passar o caminho do driver do token. Após abrir conexão, você pode obter informações sobre os slots do token e os tokens conectados, um exemplo de uso seria o seguinte:

		ETokenConnection conn = null;
		try {
			conn = new ETokenConnection("/usr/lib/libeTPkcs11.so");
			Slot slot = conn.getFirstTokenSlots();
			if (slot != null) {
				logger.info("Token conectado!");
				logger.info(slot.getSlotInfo().toString());
			} else {
				logger.warning("Token não conectado!");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (conn != null) {
					conn.close();
				}
			} catch (TokenException e) {
				// Nada
			}
		}

Esse código realiza a conexão com o token (linha 3), recupera o primeiro token (linha 4), mostra informações do slot que possui o token conectado (linha 7) e desconecta (linha 17).

Para manipular os dados do token, criei outra classe: ETokenDataManager.

package br.com.thiagovespa.etoken.utils;
import iaik.pkcs.pkcs11.Session;
import iaik.pkcs.pkcs11.Token;
import iaik.pkcs.pkcs11.TokenException;
import iaik.pkcs.pkcs11.TokenInfo;
import iaik.pkcs.pkcs11.objects.Data;
import iaik.pkcs.pkcs11.objects.X509AttributeCertificate;
import iaik.pkcs.pkcs11.objects.X509PublicKeyCertificate;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Manipula dados no token
 *
 * @author Thiago Galbiatti Vespa
 * @version 2.0
 */
public class ETokenDataManager {
	private final static Logger logger = Logger
			.getLogger(ETokenConnection.class.getName());
	private Token token;
	private Session session;
	/**
	 * Construtor para manipulação de dados no token
	 *
	 * @param token
	 *            token que terá os dados manipulados
	 * @throws TokenException
	 *             caso o token não esteja conectado
	 */
	public ETokenDataManager(Token token) throws TokenException {
		if (token == null) {
			logger.severe("Não há token conectado!");
			throw new TokenException("Token is not connected!");
		}
		this.token = token;
	}
	/**
	 * Abre uma sessão segura com o token
	 *
	 * @param userPIN
	 *            senha utilizada no token
	 * @throws TokenException
	 *             em caso de senha inválida ou login inválido
	 */
	public void openSession(String userPIN) throws TokenException {
		this.session = token.openSession(Token.SessionType.SERIAL_SESSION,
				Token.SessionReadWriteBehavior.RW_SESSION, null, null);
		TokenInfo tokenInfo = token.getTokenInfo();
		if (tokenInfo.isLoginRequired()) {
			if (tokenInfo.isProtectedAuthenticationPath()) {
				session.login(Session.UserType.USER, null);
			} else {
				session.login(Session.UserType.USER, userPIN.toCharArray());
			}
		}
	}
	/**
	 * Fecha a conexão com o token
	 *
	 * @throws TokenException
	 */
	public void closeSession() throws TokenException {
		session.logout();
		session.closeSession();
		session = null;
	}
	/**
	 * Realiza a leitura de dados do token
	 *
	 * @param application
	 *            aplicação que gravou os dados
	 * @param label
	 *            label que identifica os dados
	 * @return dados recuperados e nulo caso não encontrado
	 * @throws TokenException
	 */
	public byte[] readData(String application, String label)
			throws TokenException {
		if (session == null) {
			logger.severe("Sessão fechada!");
			throw new TokenException("Session is closed!");
		}
		// cria o template para busca
		Data dataObjectTemplate = new Data();
		if (application != null) {
			// se tiver aplicação atribui
			dataObjectTemplate.getApplication().setCharArrayValue(
					application.toCharArray());
		}
		// atribui o label
		dataObjectTemplate.getLabel().setCharArrayValue(label.toCharArray());
		logger.info(dataObjectTemplate.toString());
		// inicia a busca
		session.findObjectsInit(dataObjectTemplate);
		Object[] foundDataObjects = session.findObjects(1);
		Data dataObject;
		if (foundDataObjects.length > 0) {
			// Estamos considerando que só terá um objeto para o template
			// definido
			// Pode haver mais que um
			dataObject = (Data) foundDataObjects[0];
			logger.info("Achou um objeto: ");
			logger.info(dataObject.toString());
		} else {
			dataObject = null;
		}
		// Finaliza a busca
		session.findObjectsFinal();
		if (dataObject == null || dataObject.getValue() == null) {
			return null;
		}
		byte[] data = dataObject.getValue().getByteArrayValue();
		return data;
	}
	/**
	 * Realiza a gravação de dados no token
	 *
	 * @param data
	 *            dados a serem gravados
	 * @param application
	 *            aplicação responsável pela gravação
	 * @param label
	 *            label para identificação dos dados
	 * @param modifiable
	 *            verdadeiro se os dados forem modificáveis
	 * @param privateData
	 *            verdadeiro se os dados forem privados
	 * @throws TokenException
	 */
	public void writeData(byte[] data, String application, String label,
			Boolean modifiable, Boolean privateData) throws TokenException {
		if (session == null) {
			logger.severe("Sessão fechada!");
			throw new TokenException("Session is closed!");
		}
		logger.info("Gravando dados no token...");
		// cria o template para inserção
		Data dataObjectTemplate = new Data();
		if (application != null) {
			// se tiver aplicação atribui
			dataObjectTemplate.getApplication().setCharArrayValue(
					application.toCharArray());
		}
		// atribui o label
		dataObjectTemplate.getLabel().setCharArrayValue(label.toCharArray());
		// atribui o conteúdo
		dataObjectTemplate.getValue().setByteArrayValue(data);
		// torna o dado persistente no token
		dataObjectTemplate.getToken().setBooleanValue(Boolean.TRUE);
		// atribui se o objeto é modificável ou não
		dataObjectTemplate.getModifiable().setBooleanValue(modifiable);
		// atribui se o objeto é privado ou não
		dataObjectTemplate.getPrivate().setBooleanValue(privateData);
		// cria o objeto no token
		session.createObject(dataObjectTemplate);
		logger.info("Dados gravados!");
	}
	/**
	 * Grava o conteúdo do arquivo no token
	 *
	 * @param path
	 *            caminho da localização do arquivo
	 * @param application
	 *            aplicação responsável pela gravação
	 * @param label
	 *            label para identificação dos dados
	 * @param modifiable
	 *            verdadeiro se os dados forem modificáveis
	 * @param privateData
	 *            verdadeiro se os dados forem privados
	 * @throws IOException
	 *             caso o arquivo não exista ou não esteja acessível
	 * @throws TokenException
	 */
	public void writeFromFile(String path, String application, String label,
			Boolean modifiable, Boolean privateData) throws IOException,
			TokenException {
		File file = new File(path);
		InputStream is = new BufferedInputStream(new FileInputStream(file));
		try {
			byte[] dataArray = new byte[(int) file.length()];
			is.read(dataArray);
			writeData(dataArray, application, label, modifiable, privateData);
		} finally {
			is.close();
		}
	}
	/**
	 * Lista todos os objetos em um token convertendo para um objeto certificado
	 * se for um
	 *
	 * @return todos os objetos em um token
	 * @throws TokenException
	 */
	public List<Object> listObjects() throws TokenException {
		List<Object> objectsInToken = new ArrayList<Object>();
		session.findObjectsInit(null);
		iaik.pkcs.pkcs11.objects.Object[] objects = session.findObjects(1);
		CertificateFactory x509CertificateFactory = null;
		while (objects.length > 0) {
			Object object = objects[0];
			if (object instanceof X509PublicKeyCertificate) {
				try {
					byte[] encodedCertificate = ((X509PublicKeyCertificate) object)
							.getValue().getByteArrayValue();
					if (x509CertificateFactory == null) {
						x509CertificateFactory = CertificateFactory
								.getInstance("X.509");
					}
					Certificate certificate = x509CertificateFactory
							.generateCertificate(new ByteArrayInputStream(
									encodedCertificate));
					logger.info("Certificado lido");
					objectsInToken.add(certificate);
				} catch (Exception ex) {
					logger.log(Level.SEVERE,
							"Could not decode this X509PublicKeyCertificate";,
							ex);
				}
			} else if (object instanceof X509AttributeCertificate) {
				try {
					byte[] encodedCertificate = ((X509AttributeCertificate) object)
							.getValue().getByteArrayValue();
					if (x509CertificateFactory == null) {
						x509CertificateFactory = CertificateFactory
								.getInstance("X.509");
					}
					Certificate certificate = x509CertificateFactory
							.generateCertificate(new ByteArrayInputStream(
									encodedCertificate));
					logger.info("Certificado att lido");
					objectsInToken.add(certificate);
				} catch (Exception ex) {
					logger.log(Level.SEVERE,
							"Could not decode this X509AttributeCertificate",
							ex);
				}
			} else {
				logger.info("Objeto lido");
				objectsInToken.add(object);
			}
			objects = session.findObjects(1);
		}
		session.findObjectsFinal();
		return objectsInToken;
	}
}

O construtor recebe um token de parâmetro (linha 44), obtido no objeto de conexão e realiza operações para inserir dados (linha 160 e 217), consultar e listar os dados existentes no token (linha 98 e 239). O método openSession (linha 60) é responsável por abrir a sessão segura (através da senha do token).

Abaixo segue o exemplo de um código que consulta, insere e lista objetos em um token.

		ETokenConnection conn = null;
		try {
			conn = new ETokenConnection("/usr/lib/libeTPkcs11.so");
			Token token = conn.getFirstToken();
			if (token != null) {
				logger.info("Token conectado!");
				ETokenDataManager dataManager = new ETokenDataManager(token);
				dataManager.openSession("senhaSecreta");
				String msg = new String(dataManager.readData(null,
						"Teste Label"));
				logger.info("Dados recuperados: " + msg);
				byte[] data = dataManager.readData("TesteApp", "Secreto");
				if (data == null) {
					dataManager.writeData("Dados sigilosos".getBytes(),
							"TesteApp", "Secreto", Boolean.FALSE, Boolean.TRUE);
				} else {
					msg = new String(data);
					logger.info("Dados recuperados: " + msg);
				}
				System.out.println("Foram encontrados: "
						+ dataManager.listObjects().size() + " objetos");
				for (Object o : dataManager.listObjects()) {
					System.out.println(o);
					System.out.println("-------");
				}
				dataManager.closeSession();
			} else {
				logger.warning("Token não conectado!");
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				if (conn != null) {
					conn.close();
				}
			} catch (TokenException e) {
				// Nada
			}
		}

Agora é só utilizar. O próximo passo é criar uma aplicação gráfica para manipular tokens e seus dados. Quem tiver interesse é só me avisar.

Sobre: Thiago Galbiatti Vespa

Thiago Galbiatti Vespa é mestre em Ciências da Computação e Matemática Computacional pela USP e bacharel em Ciências da Computação pela UNESP. Coordenador de projetos do JavaNoroeste, membro do JCP (Java Community Process), consultor Oracle, arquiteto de software de empresas de médio e grande porte, palestrante de vários eventos e colaborador de projetos open source. Possui as certificações: Oracle Certified Master, Java EE 5 Enterprise Architect – Step 1, 2 and 3; Oracle WebCenter Portal 11g Certified Implementation Specialist; Oracle Service Oriented Architecture Infrastructure Implementation Certified Expert; Oracle Certified Professional, Java EE 5 Web Services Developer; Oracle Certified Expert, NetBeans Integrated Development Environment 6.1 Programmer; Oracle Certified Professional, Java Programmer; Oracle Certified Associate, Java SE 5/SE 6