sexta-feira, 31 de março de 2017

Calculando o hash de arquivos para verificação de integridade com C# e VB.NET

Por André Lima em 29/03/2017

Uns meses atrás eu estava respondendo algumas dúvidas no fórum da MSDN e acabei me deparando com uma questão muito interessante. Imagine que você tem uma Web API que disponibiliza um arquivo para download e você quer, do lado do cliente, saber se o download foi realmente efetuado com sucesso ou não. Como podemos fazer isso? Simples: nós temos que calcular o hash* do arquivo original e comparar com o hash do arquivo baixado.
Mas, como é que podemos calcular o hash de arquivos no .NET? Não se preocupe, essa tarefa é bem simples. O .NET traz nativamente consigo a implementação dos principais algoritmos de cálculo de hash. No artigo de hoje eu vou mostrar para você como utilizar esses algoritmos para calcular o hash de arquivos com o intuito de verificar a sua integridade.

Hashes e integridade de arquivos

O cálculo de hashes de arquivos tem sido utilizado desde há muito tempo para checar se um arquivo está íntegro ou não. O mais famoso deles é o MD5, que você provavelmente já encontrou pela internet quando você estava prestes a baixar o instalador de uma aplicação.
A ideia é muito simples. Juntamente com o arquivo que será baixado, você disponibiliza também a representação em texto do hash daquele arquivo. Então, depois do download ser concluído, o usuário que baixou o arquivo pode utilizar algum utilitário para calcular o hash do arquivo baixado e, se ele bater com o hash disponibilizado na hora do download, isso significa que o arquivo foi baixado de forma íntegra. Além disso, essa metodologia serve para detectarmos que o arquivo não foi modificado maliciosamente no meio do caminho.
Uma falha no algoritmo do MD5 descoberta em 2012 fez com que esse tipo de hash fosse descartado para fins de criptografia e integridade. Apesar disso, ele ainda continua sendo muito utilizado. Por exemplo, se você algum dia precisar baixar o Apache Server, verá que eles ainda disponibilizam o hash MD5 para verificação da integridade dos arquivos:
O algoritmo que substituiu o MD5 nesse tipo de verificação foi o SHA (SHA-1 e SHA-256). Se observarmos, por exemplo, a página de downloads do Audacity (popular editor de áudio open source), veremos que eles disponibilizam o hash SHA-256 dos arquivos:
Independente do algoritmo de hash utilizado, a ideia é sempre a mesma. Em algum lugar junto com o download do arquivo nós temos o seu hash. Uma vez concluído o download, podemos então calcular o hash do arquivo baixado para verificarmos se ele bate com o disponibilizado pela fonte do download. E assim sabemos se o arquivo foi baixado integralmente ou não. Para mais informações sobre verificação de integridade de arquivos através de cálculos de hash, confira esta entrada na Wikipedia.
Existem diversas ferramentas que fazem o cálculo do hash de arquivos. Em ambientes Unix, a ferramenta mais conhecida é a hashdeep. Já no Windows, se você quiser uma ferramenta confiável, eu recomendo o Microsoft File Checksum Integrity Verifier. E se você não quiser instalar ferramenta nenhuma, você pode utilizar também o website HTML5 File Hash Online Calculator, que faz o cálculo dos hashes via web.
No exemplo deste artigo, vamos utilizar três arquivos de exemplo: imagem.jpgimagemCopia.jpg (que, como o próprio nome já diz, é uma cópia da imagem.jpg) e imagemDiferente.gif. Se calcularmos os hashes MD5 e SHA-256 desses três arquivos com o HTML5 File Hash Online Calculator, o resultado será este:
Agora vamos ver como podemos fazer o cálculo dos hashes na nossa aplicação?

Calculando hash de arquivos

Para entendermos como funciona o cálculo de hashes de arquivos no .NET, vamos criar um projeto do tipo “Console Application“. Uma vez criado o projeto, vá até o diretório bin/debug no Windows Explorer e copie as três imagens mencionadas na seção anterior.
O cálculo do hash é muito simples. Basta criarmos uma instância do algoritmo de hash desejado e, em seguida, chamamos o método ComputeHash passando a stream do arquivo que queremos calcular o hash. Todos os algoritmos de hash ficam dentro do namespace “System.Security.Cryptography“.
Vamos criar um método que receberá o caminho do arquivo, fará o cálculo do hash MD5 e retornará um array de bytes como resultado:
1
2
3
4
5
6
7
8
9
10
11
// C#
private static byte[] CalcularHash(string arquivo)
{
    using (var md5 = System.Security.Cryptography.MD5.Create())
    {
        using (var stream = System.IO.File.OpenRead(arquivo))
        {
            return md5.ComputeHash(stream);
        }
    }
}
1
2
3
4
5
6
7
8
' VB.NET
Private Function CalcularHash(Arquivo As String) As Byte()
    Using Md5 = System.Security.Cryptography.MD5.Create()
        Using Stream = System.IO.File.OpenRead(Arquivo)
            Return Md5.ComputeHash(Stream)
        End Using
    End Using
End Function
Ao chamarmos esse método passando o caminho do nosso arquivo, teremos um array de bytes do hash calculado. Porém, como é que nós fazemos para converter esse array de bytes em uma string no mesmo formato utilizado pelas ferramentas de cálculo de hash? Para isso, utilizamos a classe BitConverter.
Veja como fica o código para calcular o hash do arquivo “imagem.jpg“, converter para string e imprimir o resultado no console:
1
2
3
4
5
// C#
var imagem = "imagem.jpg";
var hashImagem = CalcularHash(imagem);
var hashImagemString = BitConverter.ToString(hashImagem).Replace("-", "").ToLower();
Console.WriteLine(hashImagemString);
1
2
3
4
5
' VB.NET
Dim Imagem = "imagem.jpg"
Dim HashImagem = CalcularHash(Imagem)
Dim HashImagemString = BitConverter.ToString(HashImagem).Replace("-", "").ToLower()
Console.WriteLine(HashImagemString)
E o resultado é este:
Note que o resultado é idêntico ao que foi calculado com a ferramenta web que utilizamos na seção anterior.

Comparando o hash de dois arquivos

Agora que já sabemos como calcular o hash de arquivos, vamos ver como podemos comparar o hash de diferentes arquivos para verificarmos se eles são iguais ou não. Essa comparação pode ser feita considerando tanto os bytes do hash quanto a sua representação string. Vamos começar com a comparação dos arrays de bytes dos hashes.
Primeiramente, vamos calcular os hashes dos três arquivos, armazenando-os em variáveis diferentes. Em seguida, fazemos a comparação dos hashes. Mas, como é que comparamos arrays de bytes no .NET? Não podemos utilizar o operador “==” nem o método “Equals“, pois isso fará uma comparação das instâncias, que sempre retornará falso, mesmo se o conteúdo dos hashes for idêntico. E agora?
Pois bem, para compararmos o conteúdo de dois arrays no .NET, nós utilizamos o método “SequenceEqual” da classe “Enumerable“, passando os dois arrays. Veja só como fica o código nesse caso:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// C#
var imagem = "imagem.jpg";
var imagemCopia = "imagemCopia.jpg";
var imagemDiferente = "imagemDiferente.gif";
 
var hashImagem = CalcularHash(imagem);
var hashImagemCopia = CalcularHash(imagemCopia);
var hashImagemDiferente = CalcularHash(imagemDiferente);
 
Console.WriteLine("Comparação com byte array");
 
// imagem == imagemCopia?
if (Enumerable.SequenceEqual(hashImagem, hashImagemCopia))
    Console.WriteLine("imagem.jpg = imagemCopia.jpg");
else
    Console.WriteLine("imagem.jpg != imagemCopia.jpg");
 
// imagem == imagemDiferente?
if (Enumerable.SequenceEqual(hashImagem, hashImagemDiferente))
    Console.WriteLine("imagem.jpg = imagemDiferente.gif");
else
    Console.WriteLine("imagem.jpg != imagemDiferente.gif");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
' VB.NET
Dim Imagem = "imagem.jpg"
Dim ImagemCopia = "imagemCopia.jpg"
Dim ImagemDiferente = "imagemDiferente.gif"
 
Dim HashImagem = CalcularHash(Imagem)
Dim HashImagemCopia = CalcularHash(ImagemCopia)
Dim HashImagemDiferente = CalcularHash(ImagemDiferente)
 
Console.WriteLine("Comparação com byte array")
 
' imagem == imagemCopia?
If Enumerable.SequenceEqual(HashImagem, HashImagemCopia) Then
    Console.WriteLine("imagem.jpg = imagemCopia.jpg")
Else
    Console.WriteLine("imagem.jpg != imagemCopia.jpg")
End If
 
' imagem == imagemDiferente?
If Enumerable.SequenceEqual(HashImagem, HashImagemDiferente) Then
    Console.WriteLine("imagem.jpg = imagemDiferente.gif")
Else
    Console.WriteLine("imagem.jpg != imagemDiferente.gif")
End If
Como mencionado anteriormente, uma outra maneira que podemos utilizar para compararmos os hashes é através da sua representação em string. Após termos convertido os hashes em string, fazemos a comparação através do método “string.Compare“:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C#
Console.WriteLine("Comparação com string");
 
var hashImagemString = BitConverter.ToString(hashImagem).Replace("-", "").ToLower();
var hashImagemCopiaString = BitConverter.ToString(hashImagemCopia).Replace("-", "").ToLower();
var hashImagemDiferenteString = BitConverter.ToString(hashImagemDiferente).Replace("-", "").ToLower();
 
// imagem == imagemCopia?
if (string.Compare(hashImagemString, hashImagemCopiaString, StringComparison.InvariantCulture) == 0)
    Console.WriteLine("imagem.jpg = imagemCopia.jpg");
else
    Console.WriteLine("imagem.jpg != imagemCopia.jpg");
 
// imagem == imagemDiferente?
if (string.Compare(hashImagemString, hashImagemDiferenteString, StringComparison.InvariantCulture) == 0)
    Console.WriteLine("imagem.jpg = imagemDiferente.gif");
else
    Console.WriteLine("imagem.jpg != imagemDiferente.gif");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
' VB.NET
Console.WriteLine("Comparação com string")
 
Dim HashImagemString = BitConverter.ToString(HashImagem).Replace("-", "").ToLower()
Dim HashImagemCopiaString = BitConverter.ToString(HashImagemCopia).Replace("-", "").ToLower()
Dim HashImagemDiferenteString = BitConverter.ToString(HashImagemDiferente).Replace("-", "").ToLower()
 
' imagem == imagemCopia?
If String.Compare(HashImagemString, HashImagemCopiaString, StringComparison.InvariantCulture) = 0 Then
    Console.WriteLine("imagem.jpg = imagemCopia.jpg")
Else
    Console.WriteLine("imagem.jpg != imagemCopia.jpg")
End If
 
' imagem == imagemDiferente?
If String.Compare(HashImagemString, HashImagemDiferenteString, StringComparison.InvariantCulture) = 0 Then
    Console.WriteLine("imagem.jpg = imagemDiferente.gif")
Else
    Console.WriteLine("imagem.jpg != imagemDiferente.gif")
End If
Ao executarmos o projeto, teremos o resultado esperado (imagem.jpg é igual a imagemCopia.jpg e imagem.jpg é diferente de imagemDiferente.gif):
E com isso você conferiu como fazer o cálculo e comparação do hash de arquivos com C# e VB.NET. Essa metodologia pode ser utilizada para, por exemplo, validar o download de arquivos através de uma API na sua aplicação.

Trocando o algoritmo de hash

Uma última alteração que podemos fazer nesse exemplo é trocarmos o algoritmo de hash de MD5 para SHA-256. Como mencionado anteriormente, apesar de ainda ser amplamente utilizado, o algoritmo MD5 não é mais criptograficamente confiável e deve ser substituído pelo SHA.
Para trocarmos o algoritmo de hash, basta fazermos uma alteração na hora de criarmos o hash, trocando o algoritmo de MD5 para SHA-256. Por exemplo, para calcularmos o hash com o algoritmo SHA-256 ao invés do MD5, o código ficaria assim:
1
2
3
4
5
6
7
8
9
10
11
// C#
private static byte[] CalcularHash(string arquivo)
{
    using (var algoritmoHash = System.Security.Cryptography.SHA256.Create())
    {
        using (var stream = System.IO.File.OpenRead(arquivo))
        {
            return algoritmoHash.ComputeHash(stream);
        }
    }
}
1
2
3
4
5
6
7
8
' VB.NET
Private Function CalcularHash(Arquivo As String) As Byte()
    Using AlgoritmoHash = System.Security.Cryptography.SHA256.Create()
        Using Stream = System.IO.File.OpenRead(Arquivo)
            Return AlgoritmoHash.ComputeHash(Stream)
        End Using
    End Using
End Function
Se quisermos ir um passo além, podemos transformar fazer uma alteração de forma que o método fique genérico:
1
2
3
4
5
6
7
8
9
10
11
// C#
private static byte[] CalcularHash<T>(string arquivo) where T: System.Security.Cryptography.HashAlgorithm
{
    using (var algoritmoHash = System.Security.Cryptography.HashAlgorithm.Create(typeof(T).ToString()))
    {
        using (var stream = System.IO.File.OpenRead(arquivo))
        {
            return algoritmoHash.ComputeHash(stream);
        }
    }
}
1
2
3
4
5
6
7
8
' VB.NET
Private Function CalcularHash(Of T As System.Security.Cryptography.HashAlgorithm)(Arquivo As String) As Byte()
    Using AlgoritmoHash = System.Security.Cryptography.HashAlgorithm.Create(GetType(T).ToString())
        Using Stream = System.IO.File.OpenRead(Arquivo)
            Return AlgoritmoHash.ComputeHash(Stream)
        End Using
    End Using
End Function
Com essa alteração, a chamada para esse método genérico deve passar o algoritmo de hash desejado. Por exemplo, se quiséssemos utilizar o algoritmo SHA-256, a chamada ficaria assim:
1
2
3
4
// C#
var hashImagem = CalcularHash<System.Security.Cryptography.SHA256>(imagem);
var hashImagemCopia = CalcularHash<System.Security.Cryptography.SHA256>(imagemCopia);
var hashImagemDiferente = CalcularHash<System.Security.Cryptography.SHA256>(imagemDiferente);
1
2
3
4
' VB.NET
Dim HashImagem = CalcularHash(Of System.Security.Cryptography.SHA256)(Imagem)
Dim HashImagemCopia = CalcularHash(Of System.Security.Cryptography.SHA256)(ImagemCopia)
Dim HashImagemDiferente = CalcularHash(Of System.Security.Cryptography.SHA256)(ImagemDiferente)

Baixe o projeto de exemplo

Para baixar o projeto de exemplo desse artigo, assine a minha newsletter. Ao fazer isso, além de ter acesso ao projeto, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário no final do artigo.

Conclusão

Implementar o cálculo de hashes de arquivos no .NET é uma tarefa muito simples, uma vez que já temos nativamente os principais algoritmos de hash implementados diretamente no .NET Framework. No artigo de hoje você aprendeu a calcular o hash de um arquivo utilizando o algoritmo MD5 e SHA-256. Essa implementação pode ser utilizada para comparar o hash de dois arquivos diferentes, detectando se eles são exatamente o mesmo arquivo ou não.
Como você pode conferir no artigo, a comparação dos hashes pode ser feita byte a byte através do método Enumerable.SequenceEqual. Outra opção é fazermos a comparação da representação em string do hash, que pode ser feita através do método string.Compare.
Por fim, você conferiu como trocar o algoritmo de cálculo do hash, bem como a transformação do método em genérico, possibilitando a definição do algoritmo desejado no momento da sua chamada.
Essa implementação serve principalmente para verificarmos se o download de um arquivo foi feito com sucesso e para garantirmos que ninguém alterou o arquivo durante a sua transmissão. Você já precisou implementar algo parecido com isso? Como é que você acabou resolvendo? Deixe os seus comentários logo abaixo.
Até a próxima!
André Lima
Image by Pixabay used under Creative Commons


*Hash - Uma função hash é um algoritmo que mapeia dados de comprimento variável para dados de comprimento fixo. Os valores retornados por uma função hash são chamados valores hash, códigos hash, somas hash (hash sums), checksums ou simplesmente hashes.