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.

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 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

Comentários

19 comentários para o artigo “Manipulando Token de Segurança”
  1. juniorsatanas disse:

    Muito Bom Thiago.. parabéns !

    jr

  2. José Sérgio Tempesta disse:

    Olá Thiago,

    Estou construindo uma ferramenta para assinatura digital multi-os, usando certificados A3, assim que finalizá-lo eu mando para você publicar.

    Um abraço

      • Olá Thiago, vc diz no principio do teu site que “Precisando é só avisar!”, pois eu te avisar agora, hehe…

        Estou trabalhando num projeto público que evita que mães carentes precisem ir ao cartório para registrar seus filhos, fazendo o registro na própria maternidade.

        A maternidade digitaliza os documentos da mãe e os envia ao cartório para análise.

        Estes documentos enviados são assinados digitalmente, por isso a necessidade de um assinador digital.

        Minha pergunta é, como relaciono o seu código acima, para realizar a assinatura de um arquivo no formato PKCS#7.

        Se vc tiver uma idéia ou qualquer luz que seja, por favor me envie.

        Muito obrigado

  3. Olá Thiago,

    Coloquei estou com o seguinte erro. Na hora que ele faz o pkcs11Module = Module.getInstance(libraryPath); ele lança uma exceção: java.io.IOException: Não foi possível encontrar o procedimento especificado.

    Sabe o que pode ser?
    Obrigado.

    • Olá Flávio. Pelo erro você está em ambiente Windows e você tem duas possibilidades: ou a versão da DLL utilizada não é compatível com seu Windows ou o software não está encontrando as DLLs e suas dependências. Você pode me enviar o código utilizado e onde estão localizadas as DLLs? Seu windows é 32 ou 64 bits?

  4. Cara, estou com outro problema. Uma vez que o o keystore é carregado com a senha certa, posso colocar a senha errada que ele continua como se estivesse válido. Sabe o que pode ser?
    Já tentei dar um
    Security.removeProvider(“SunPKCS11-eToken”);
    e um
    Security.getProvider(“SunPKCS11-eToken”).clear();
    e não adianta….

    Obrigado!

  5. Thiago disse:

    Olé Thiago (Xará), eu tenho uma pergunta sobre essa solução, ela serve tanto para sistemas web e desktop?

  6. Thiago disse:

    ola, estou tendo um problema na hora de ele ler a DLL no sistema, e nao estou conseguindo identificar o erro, vou passar o ST, se vc tiver um luz, obrigado.

    trying to connect to PKCS#11 module: C:\Users\luciano.silva.ext\Downloads\iaikPkcs11Wrapper1.2.17\native\platforms\win32\release\pkcs11wrapper.dlljava.io.IOException: Não foi possível encontrar o procedimento especificado.

    at iaik.pkcs.pkcs11.wrapper.PKCS11Implementation.connect(Native Method)
    at iaik.pkcs.pkcs11.wrapper.PKCS11Implementation.(PKCS11Implementation.java:166)
    at iaik.pkcs.pkcs11.wrapper.PKCS11Connector.connectToPKCS11Module(PKCS11Connector.java:75)
    at demo.pkcs.pkcs11.wrapper.SimpleTest.(SimpleTest.java:112)
    at demo.pkcs.pkcs11.wrapper.SimpleTest.main(SimpleTest.java:131)
    Enviado às 17:48 de quarta-feira

  7. Mário disse:

    Olá Thiago,
    bom artigo, parabéns.
    Sabe se é possível carregar e ler a livraria (libeTPkcs11.so) usar da memoria em vez de ser de uma path no disco?
    Originalmente ela se encontra dentro do JAR que vou distribuir ao meu cliente, e não quero copiar para o disco.

    Cumps

  8. tati disse:

    ola estou com um enorme problema , o token foi instaldo normalmente A3 mas , foi inicializado e perdeu os dados existentes nele , oq posso fazer para recuperar sou leiga no assunto .. fico no aguardo

    attt

  9. Tatiane disse:

    eu tenho o sistema de nfe.. sou de sc..
    quando eu vou no botão assinar nota fiscal, da um erro informando que nao tenho a aetpkss1.dll
    nao sei como instalar novamente esta dll, já pocurei por tudo. podes me ajudar?

Compartilhe Suas Idéias

Diga-nos o que você está pensando...
e se quiser que sua foto ou imagem apareça, acesse e cadastre um gravatar!