Existem ocasiões onde armazenar ou transportar dados se torna um problema devido à sua importância. Inúmeras tentativas de invasão ou falhas de sistema podem ocasionar acesso indevido às informações sigilosas, acarretando em sérios problemas. Para evitar tais situações é preciso dificultar de alguma maneira a leitura desses dados utilizando técnicas de criptografia.

Assegurar a incorruptibilidade ou o acesso de dados se torna imprescindível quando tais são sigilosos. Imagine se um sistema de Internet Banking de um banco não utilizasse técnicas de criptografia para dificultar de todas as maneiras possíveis o acesso à conexão ou até mesmo aos dados tão pessoais do cliente logado. Pense como seria fácil um hacker (ou cracker como alguns preferem dizer) ter acesso às informações do cliente e até mesmo efetuar operações como depósitos, transferências, pagamentos de títulos sem nenhum conhecimento do titular da conta. Com certeza haveria sérios prejuízos ao cliente e ao banco. Este artigo trará uma explicação breve sobre criptografia e os tipos de criptografia utilizados atualmente, porém o foco será a criptografia hash e sua implementação em .NET através de um exemplo prático.

Mas o que é criptografia?

Criptografia são técnicas que visam tornar ilegíveis informações que possuem acesso restrito, formando o chamado >ciphertext. O >ciphertext ou texto cifrado é formado por uma codificação aleatória que pode ou não ser decodificada para seu valor original, podendo ser lido ou compreendido por seres humanos.

Basicamente as criptografias se diferem pela maneira que se utilizam para codificar a mensagem que é passada. A seguir serão listados os três tipos de criptografia utilizada atualmente:

  1. Criptografia simétrica: Esse tipo de criptografia utiliza somente uma chave criptográfica que é utilizada para cifrar e decifrar uma mensagem, como mostra a Figura 1.
    Esquema da criptografia simétrica
    Figura 1. Esquema da criptografia simétrica
  2. Criptografia assimétrica: A criptografia assimétrica utiliza duas chaves relacionadas matematicamente que podem codificar ou decodificar uma mensagem. Uma das chaves é chamada chave pública e a outra é chamada chave privada. A chave privada deve ser mantida em sigilo e a chave pública pode ser lida e utilizada por qualquer um. Se uma mensagem foi cifrada pela chave privada, pode ser decifrada pela chave pública e vice-versa, como mostra a Figura 2.
    Esquema da criptografia assimétrica
    Figura 2. Esquema da criptografia assimétrica
  3. Criptografia hash: A criptografia >hash é conhecida também como >hash code ou >message-digest. Esse tipo de criptografia não utiliza nenhuma chave criptográfica e é irreversível, ou seja, ao ser criptografada a mensagem não pode ser decodificada para sua forma original, como mostra a Figura 3.
    Não é possível decifrar o hash code
    Figura 3. Não é possível decifrar o hash code ao contrário das outras técnicas

Entendendo o hashing

Pode parecer não ser útil à primeira vista, mas a criptografia hash é extremamente importante e muito utilizada em conjunto com as outras formas criptográficas, principalmente para confirmar a integridade ou autenticidade de uma mensagem através do uso de certificados digitais. Para melhor entendimento de um certificado digital e do próprio hashing, iremos utilizar o exemplo do Internet Banking novamente. Para que você possa ter a certeza que ao acessar o portal do seu banco, você realmente esteja acessando o portal do seu banco, um certificado digital intermedia a transação utilizando um algoritmo hash juntamente com um algoritmo de criptografia assimétrica. Basicamente o que ocorre é:

  • Você com o seu browser acessa o Internet Banking;
  • Ao estabelecer a conexão, o servidor do portal bancário solicita o certificado utilizado pelo seu browser;
  • Seu browser, utilizando um algoritmo criptográfico assimétrico (geralmente o RSA) gera o par de chaves assimétricas e uma mensagem (geralmente os dados do cliente). Esta mensagem é criptografada com o algoritmo hash e depois é criptografa utilizando-se a chave privada. Esse procedimento é chamado assinatura digital. A chave pública é então enviada ao servidor do banco;
  • Nesse tempo, o servidor bancário também faz o mesmo procedimento, gerando as chaves criptográficas e enviando a chave pública ao seu browser;
  • Ao receber a mensagem mais a sua assinatura digital, o servidor bancário utiliza a sua chave pública para decifrar a assinatura digital e revelar o hash code da mensagem. Depois, utilizando o mesmo algoritmo de hashing (geralmente o SHA256) ele cifra a mensagem recebida e compara se as duas são idênticas. Se forem, a conexão se torna segura para troca de mensagens entre você e o servidor;
  • O seu browser efetua o mesmo processo utilizando os dados vindos do servidor. Se tudo estiver OK, a conexão está segura e um cadeado verde surge na barra de endereços do seu navegador.

Podemos acompanhar esses passos na Figura 4.

Definição da assinatura digital
Figura 4. Definição da assinatura digital

É importante enfatizar duas coisas sobre o hash:

  • um livro de mil páginas ou um simples caractere, ao serem passados sobre o hash, ficam com 256 caracteres (utilizando o algoritmo SHA256, por isso o nome);
  • se apenas uma letra do livro for alterada e gerado um novo hash code, este será completamente diferente do antigo. Isto será importante entender para que o exemplo prático em C# se torne mais compreensível.

Cenário

Em um sistema onde seja necessário armazenar senhas no banco de dados, não é agradável armazená-las em plain text (texto sem criptografia). Uma invasão bem sucedida a uma base de dados resultaria em nenhum esforço de um hacker em conseguir acesso às senhas e liberar o acesso às informações mais confidenciais. Por isso é necessário criptografar esses dados.

Aqui aplicaremos o uso da criptografia no sistema exemplo de login e senha. O objetivo será criar o hash da senha cadastrada no sistema e persistir não a senha, mas o hash code da mesma. Ao logar o usuário, o sistema irá comparar o hash code da senha digitada no campo do formulário com o hash code da senha original no banco de dados e irá liberar ou não o acesso. Na Figura 5 temos a tela de login.

Tela de login
Figura 5. Tela de login

Com a interface gráfica construída, é preciso modular e criar a tabela onde serão persistidos os dados de cada funcionário cadastrado. Na Figura 6 está o modelo da tabela >Funcionario.

Tabela Funcionario
Figura 6. Tabela Funcionario

Você pode estar se perguntando: por que o campo senha é >varchar(128)? Ou, qual o significado da tupla >UserGuid? Essas perguntas serão respondidas em breve.

Mãos à obra!

Definidos a interface gráfica e os campos da tabela >Funcionario, partiremos para os códigos. Em .NET, existe uma biblioteca onde se encontram os algoritmos de criptografia e segurança. Seu nome é >System.Security.Cryptography e a utilizaremos para termos acesso ao algoritmo de hash SHA512, que será utilizado no exemplo.

O primeiro passo é criar uma classe que irá encapsular o algoritmo hash. Ela terá um método chamado >CriptografarSenha que irá criar o hash da senha, retornando uma string que conterá o hash code. No construtor da classe é passado um parâmetro do tipo >HashAlgorithm, que é uma classe abstrata. Desta herdam todos os algoritmos de hash do .NET. Na Listagem 1 se encontra o código da classe.


public class Hash   
{   
      private HashAlgorithm _algoritmo;   
 
      public Hash(HashAlgorithm algoritmo)   
      {   
          _algoritmo = algoritmo;   
      }   
 
      public string CriptografarSenha(string senha)   
      {   
        var encodedValue = Encoding.UTF8.GetBytes(senha);   
        var encryptedPassword = _algoritmo.ComputeHash(encodedValue);   

        var sb = new StringBuilder();   
        foreach (var caracter in encryptedPassword)   
        {   
            sb.Append(caracter.ToString("X2"));   
        }   

        return sb.ToString();   
      }   
 
      public bool VerificarSenha(string senhaDigitada, string senhaCadastrada)   
      {   
        if (string.IsNullOrEmpty(senhaCadastrada))   
            throw new NullReferenceException("Cadastre uma senha.");   
           
        var encryptedPassword = _algoritmo.ComputeHash(Encoding.UTF8.GetBytes(senhaDigitada));   

        var sb = new StringBuilder();   
        foreach (var caractere in encryptedPassword)   
        {   
            sb.Append(caractere.ToString("X2"));   
        }   

        return sb.ToString() == senhaCadastrada;   
      }   
    }   
}
Listagem 1. Classe Hash

Este código possui algumas instruções que são fundamentais para entendimento do hash. O construtor da classe Hash recebe um parâmetro do tipo >HashAlgorithm e o atribui à variável interna >_algoritmo. Esta técnica é chamada injeção de dependência, pois não acopla a classe Hash a um único algoritmo de hash. Neste exemplo é utilizado o algoritmo >SHA512, mas se for necessário, pode ser utilizado todos os algoritmos de hash que o .NET Framework possui, fazendo bom uso da classe Hash, não replicando código. Isso é viável porque >HashAlgorithm é uma classe abstrata e todos os algoritmos hash .NET implementam essa classe.

O método >CriptografarSenha faz uso da variável interna >_algoritmo. Neste método é passada como parâmetro a senha a ser criptografada. A linha 29 da Listagem 1 é a principal linha do método, onde é chamado o método >ComputeHash. Este método recebe como parâmetro um vetor de bytes contendo o valor a ser criptografado. A classe >Encoding codifica a senha em um vetor de bytes e o passa como argumento para o método >ComputeHash que irá retornar um vetor de bytes contendo o hash code. Para

finalizar, o método transforma o hash em uma string, onde cada caractere é transformado em um dígito hexadecimal que será retornado. Como o algoritmo de hash utilizado é o >SHA512, o hash code gerado possui 64 caracteres. Com a transformação em string, o >ciphertext terá 128 caracteres. Isso aumenta o poder de criptografia do hash que será persistido no campo senha da tabela >Funcionario. Por isso este campo possui o tipo >varchar(128).

O método >VerificarSenha é bem similar, porém este recebe dois parâmetros: a senha digitada no formulário de login da Figura 2 e a senha cadastrada no banco de dados. O que este método faz é criar o hash code da senha digitada e comparar seu valor com o hash code da senha cadastrada no banco, retornado um valor booleano indicando se as senhas são iguais ou não. Aqui poderia ser chamado o método >CriptografarSenha, mas para fins didáticos foi criado um código semelhante.

Para transporte e armazenamento dos dados do funcionário na memória, foi construída a classe Funcionario como mostra a Listagem 2.


public class Funcionario : APessoa   
{   
    private string _senha;   
    private int _prioridade;   
    private string _nomeFuncao;   

    public string NomeFuncao   
    {   
        get { return _nomeFuncao; }   
        set { _nomeFuncao = value; }   
    }   
    public int Prioridade   
    {   
        get { return _prioridade; }   
        set { _prioridade = value; }   
    }   
    public string Senha   
    {   
        get { return _senha; }   
        set { _senha = value; }   
    }   
}
Listagem 2. Classe Funcionario

Na classe >Funcionario foi realizado o mapeamento de todos os campos da tabela. Alguns campos não estão presentes porque estão sendo herdados da classe abstrata >APessoa. Há uma exceção que é o campo >UserGuid que será abordado logo a seguir.

Para o acesso ao banco foi criada uma classe DAO (Data Access Object) chamada >FuncionarioDAO que ficará responsável por realizar operações de inserção, exclusão, seleção e alteração, o chamado >CRUD. O objetivo do artigo é mostrar o funcionamento do hash e como utilizar em .NET, portanto somente alguns métodos serão abordados.

Na classe >FuncionarioDAO, O método Gravar ficará responsável por inserir um novo funcionário no banco de dados. Ele possui algumas linhas interessantes que devem ser abordadas, como mostra a Listagem 3.


var hash = new Hash(SHA512.Create()); 
 
public override async Task Gravar(object value)   
{   
  Funcionario funcionario = value as Funcionario;   

  if (string.IsNullOrEmpty(funcionario.CPF) || 
  string.IsNullOrEmpty(funcionario.Nome) ||   
    string.IsNullOrEmpty(funcionario.Senha))   
    throw new Exception("Valores digitados estão vazios.");   

  try   
  {   
    Guid guid = Guid.NewGuid();   

    using (var sqlConnection = CreateConnection() as SqlConnection)   
    {   
      SqlTransaction transaction;   
      await sqlConnection.OpenAsync();   

      transaction = sqlConnection.BeginTransaction("CadastroFuncionario");   
      using (SqlCommand command = new 
      SqlCommand("InsereNovoFuncionario", sqlConnection, transaction))   
      {   
        command.CommandType = CommandType.StoredProcedure;   

        command.Parameters.Add(new SqlParameter("@CPF", funcionario.CPF));   
        command.Parameters.Add(new 
        SqlParameter("@ID_Funcao", funcionario.Prioridade));   
        command.Parameters.Add(new SqlParameter("@Nome", funcionario.Nome));   
        command.Parameters.Add(new SqlParameter("@Senha", 
      hash.CriptografarSenha(funcionario.Senha + guid.ToString())));   
        command.Parameters.Add(new 
        SqlParameter("@UserGuid", guid.ToString()));   

        await command.ExecuteNonQueryAsync();   
        transaction.Commit();   
      }   
    }   
  }   
  catch (InvalidOperationException e)   
  {   
    throw new InvalidOperationException
    ("Problemas ao se conectar com o banco.", e);   
  }   
  catch (SqlException)   
  {   
    throw;   
  }   
  catch (Exception e)   
  {   
    throw new Exception("Problemas ao se conectar com o banco.", e); ;   
  }   
}
Listagem 3. O método Gravar da classe FuncionarioDAO

Focando somente nas linhas relevantes ao tema do artigo, preste atenção nas linhas 11 e 26. Na linha 11 está criando um objeto do tipo Guid. Na linha 26 o objeto privado da classe >FuncionarioDAO está sendo utilizado, chamando o método >CriptografarSenha. Neste método é passada por parâmetro uma concatenação do Guid com a senha localizada no objeto >Funcionario, recebido como argumento do método Gravar. Se tudo correr bem, nas linhas 31 e 32, os dados do funcionário são salvos, juntamente com o hash code da senha mais o guid. Mas afinal de contas, por que executar este procedimento? Por que concatenar o guid com a senha?

A criptografia hash é bastante segura. É praticamente impossível através do hash code decodificar e voltar ao plain text. Mas como todos nós sabemos, hackers são bem persistentes e espertos e com certeza não hesitarão em tentar burlar a segurança de algum sistema se realmente quiserem. O que profissionais em segurança fazem e dificultar de todas as maneiras o acesso indevido às bases de informações com técnicas de criptografia.

Existem várias maneiras em que um hacker pode decifrar um hash, entre elas estão a técnicas de dicionário de hash e a força bruta. O dicionário de hash utiliza um arquivo onde um grande número de senhas mais escolhidas e seus respectivos hash codes gerados de diferentes algoritmos se encontram. O que fazem é comparar cada hash code do seu acervo com o hash code armazenado no banco invadido. Se forem iguais, a senha será aquela equivalente ao hash code aprovado. A força bruta é bem parecida, mas não possui um acervo de senhas com seus hashs. Simplesmente é gerada uma senha e criado o seu hash e comparado. Existem outras técnicas, porém estas são as mais utilizadas.

O que pode se fazer para evitar uma quebra do sigilo, é utilizar uma técnica chamada password salt. O que ela faz é gerar um valor aleatório e concatená-lo à senha formando um hash diferente do que seria com a senha somente. Por fim armazena este valor juntamente com a senha para que se possa depois autenticar o usuário.

O que foi utilizado na Listagem 3 foi um tipo de password salt simples para fins didáticos. O guid gerado é um valor aleatório que é concatenado com a senha na linha 26, gerando um hash code diferente, aumentando a força do >ciphertext. Se realmente for preciso algum dia utilizar esta técnica, utilize a classe >RNGCryptoServiceProvider. Esta classe possui um método chamado >GetBytes que recebe um parâmetro do tipo >byte[]. É neste parâmetro que será atribuído o valor gerado, podendo ser usado depois para aplicar o password salt, como mostra a Listagem 4.


private static string CreateSalt(int size)   
{   
    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();   
    byte[] buff = new byte[size];   
    rng.GetBytes(buff);   
    return Convert.ToBase64String(buff);   
}
Listagem 4. Exemplo da classe RNGCryptoServiceProvider

Depois que código da Listagem 3 for executado, a tabela >Funcionario irá abrigar o hash da senha.

Para finalizar, é preciso verificar a senha quando o usuário fizer o login. Para isso, a Listagem 5 irá abordar o método >VerificarUsuario, da classe >FuncionarioDAO.


public async Task<bool> VerificarUsuario(string CPF, string senha, Funcionario funcionario)   
{   
   if (string.IsNullOrEmpty(senha) || string.IsNullOrEmpty(CPF))   
   {   
     throw new ArgumentNullException();   
   }   

   string hashTxtSenha = null;   

   try   
   {   
     using (var sqlConnection = CreateConnection() as SqlConnection)   
     {   
       SqlTransaction transaction;   
       await sqlConnection.OpenAsync();   

       transaction = sqlConnection.BeginTransaction("VerificaFuncionario");   
       using (SqlCommand command = new SqlCommand("VerificaFuncionario", 
       sqlConnection, transaction))   
       {   
         command.CommandType = CommandType.StoredProcedure;   
         command.Parameters.Add(new SqlParameter("@CPF", CPF));   

         using (var reader = await command.ExecuteReaderAsync())   
         {   
           if (await reader.ReadAsync())   
           {   
               funcionario.Senha = reader["Senha"] as string;   
               funcionario.Nome = (reader["Nome"] as string).Trim();   
               var guid = reader["UserGuid"] as string;   
               funcionario.NomeFuncao = reader["Funcao"] as string;   
               funcionario.Prioridade = Convert.ToInt32(reader["ID_Prioridade"]);   

               hashTxtSenha = hash.CriptografarSenha(senha + guid);   
             }   
             else   
             {   
                 return false;   
             }   
           }   

           transaction.Commit();   

           if (hash.VerificarSenha(hashTxtSenha, funcionario.Senha)) 
           {   
               return true;   
           }   
           return false;   
       }   
     }   
   }   
   catch (InvalidOperationException e)   
   {   
     throw new InvalidOperationException
     ("Problemas ao se conectar com o banco.", e);   
   }   
   catch (SqlException)   
   {   
     throw;   
   }   
   catch (Exception e)   
   {   
     throw new Exception("Problemas para verificar usuário.", e); 
   }   
}
Listagem 5. Método VerificarUsuario

Na linha 8 é criada uma string chamada >hashTxtSenha, que irá armazenar o hash code da senha digitada no formulário de login (Figura 5), utilizando o mesmo objeto hash privado da classe >FuncionarioDAO. Na linha 33 é realizado o mesmo procedimento de password salt com o guid recuperado do banco. Com isso na linha 43, o método >VerificarSenha é chamado e ele ficará responsável em dizer se a senha digitada é a mesma da senha cadastrada. Se for o método >VerificarUsuario retorna true e libera o acesso.

O hash juntamente com as outras formas de criptografia são bastante eficazes quando é preciso manipular dados sigilosos. O .NET Framework oferece inúmeras ferramentas para quem necessita utilizar meios que venham a dificultar o acesso indevido às informações secretas que são utilizadas pelo sistema. Inclusive existe uma classe chamada >SecureString que automaticamente codifica e trata strings de uma maneira diferente caso um hacker decida burlar as strings contidas na memória, para que possa descobrir a senha antes mesmo da mesma ser salva no banco.

Juntamente em anexo ficará disponível um exemplo simples que você poderá baixar e ver com mais detalhes como funciona o hash em seu computador. Até a próxima e obrigado.

Referências: