Manipulando Token de Segurança
Esses 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).
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.










Muito Bom Thiago.. parabéns !
jr
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
Valeu
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
Só um detalhe, todo o código para validação de um certificado digital X509 eu já tenho pronto, como lhe disse finalizando eu mando tudo para vc compartilhar com galera.
Um abraço
Você vai utilizar essa classe para assinar:
http://download.oracle.com/javase/6/docs/api/java/security/Signature.html
Utilize o método getInstance e depois o initSign passando a chave privada do token. Utilize o método update para colocar os dados a serem assinados e o método sign para pegar a assinatura.
Para verificar a assinatura é só utilizar o método verify passando a assinatura.
Vou ver se consigo criar um post disso em breve.
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?
Fala Thiago! Consegui resolver o problema!
Muito Obrigado pelo tutorial!
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!
Você está dando um close na session?
Olé Thiago (Xará), eu tenho uma pergunta sobre essa solução, ela serve tanto para sistemas web e desktop?
Desktop, mas utilizei em um servidor web
.
Dependendo de como você for utilizar serve para sistemas web também!
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
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
É só você carregar o arquivo assim getClass().getResourceAsStream(“libeTPkcs11.so”)
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
Infelizmente não há como você recuperar.
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?