.NET - Armazenando senhas de forma segura
 Neste artigo veremos como armazenar senhas de forma segura em aplicações na plataforma .NET.


Armazenar senhas com segurança é uma das coisas que devemos implementar no desenvolvimento de aplicativos de hoje.

 



 

Infelizmente muitos desenvolvedores ainda continuam armazenando senhas como texto puro e esta é a pior forma de armazenar informações de segurança em sistemas.

 

Usando esta abordagem qualquer um que tiver acesso ao banco de dados ou meio de armazenamento onde as senhas estão inseridas vai ter acesso todas as informações para realizar um grande estrago.

 

Assim se você armazena as senhas em uma tabela como no formato abaixo:

 

usuario email senha
admin admin@gmail.com admin
usuario usuario@gmail.com 123456

 

O seu nível de segurança é zero e assim NUNCA armazena senhas como texto puro.

 

Cifrando a informação crítica
 

Armazenar as senhas ou outra informação criptografadas no banco de dados é melhor do que armazenar senhas em formato de texto simples.

Existem dois tipos de criptografia usados: criptografia reversível e criptografia não reversível.  Se a senha for criptografada usando criptografia reversível, podemos recuperar a senha real descriptografando-a.

A desvantagem da criptografia reversível é que, se o invasor puder adivinhar a lógica da criptografia, ele poderá recuperar facilmente as senhas reais.

 

A seguir temos as informações de usuário e senha armazenadas desta vez a senha esta cifrada

 

usuario email senha cifrada
admin admin@gmail.com Yp2GMnemcPbOSsJ08OyNbw==#weuey
usuario usuario@gmail.com Yp2GMnemcPbOSsJ08OyNbw==#weuey

 

A primeira vista seria muito difícil para as pessoas comuns saberem o valor de uma senha criptografada. No entanto, se prestarmos atenção, admin e usuario têm o mesmo valor de senha cifrada. Se um deles vazar, podemos supor que todos os usuários que têm a mesma senha também vazaram.

 

Cifrando a senha usando uma função de Hash

 

Usar uma função de hash para armazenar senhas no banco de dados é o mais comum. Hashes são criptografia não reversível; mesmo que alguém tenha acesso ao banco de dados e conheça o algoritmo de hash usado, não poderá descriptografar o hash da senha. Os algoritmos de Hash comumente usados são SHA256 e SHA512.

Embora teoricamente o hash não possa ser descriptografado, as funções de hash não são totalmente seguras.

Existem várias técnicas para obter a senha original de um hash como : brute-force, dictionary, e rainbow-table.


A tabela abaixo mostra os mesmos usuários e as as senhas cifradas usando um Hash :

 

usuario email senha cifrada com Hash
admin admin@gmail.com

8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918

usuario usuario@gmail.com 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918


Embora o hash não pode ser revertido a senha pode ser 'inferida' e se o usuário usar uma senha fraca ela via ser descoberta com facilidade mesmo cifrada com um Hash.


Segundo publicações sobre o assunto as dez senhas mais usadas no mundo são :

  1. 123456
  2. 123456789
  3. 12345
  4. qwerty
  5. password
  6. 12345678
  7. 111111
  8. 123123
  9. 1234567890
  10. 1234567

Assim se você googar 'SHA256 for admin' vai encontrar neste site um recurso onde vai poder obter o hash a partir da senha.Veja abaixo a obtenção do Hash para admin :
 



Por isso é muito importante aplicar restrições de senha aos aplicativos que criamos, como por exemplo, exigir uma senha mínima de 8 caracteres contendo letras maiúsculas, letras minúsculas, números e símbolos. Desta forma teremos uma senha forte que será difícil de ser hackeada.

 

Melhor prática: Hash + Salt + Pepper + Iteration

A melhor maneira de armazenar senhas no banco de dados é adicionar salt, pepper e iteração.

 

O que isto significa ?

O Salt é um “condimento” adicionado ao processo de hash para que o valor do hash seja diferente mesmo que a senha original seja a mesma. Salt é um valor aleatório exclusivo para cada usuário e pode ser armazenado no banco de dados sem precisar ser criptografado. Adicionar salt ao processo de hash tornará impossível obter o hash por meio de mecanismos de pesquisa ou de um aplicativo de descriptografia de hash. O valor do salt deve ser alterado toda vez que houver alteração nos dados do usuário, por exemplo, alteração de senha ou email.

O Pepper também é um “condimento” adicionado ao processo de hashing, e é comum a todos os usuários; todos os usuários do aplicativo usarão a mesma peper, e, ele pode ser armazenado na configuração do aplicativo, variáveis ​​de ambiente ou cofre de chaves.

A Iteration é o número de iterações realizadas no processo de hash. Quanto mais iterações, mais difícil será para o atacante adivinhar.

 

Implementando a senha com uma função de hash

 

A seguir vamos mostrar na prática como implementar a cifragem de uma senha usando Hash com salt e pepper e iteration em um projeto ASP.NET Core Web API no .NET 6 usando o VS 2022 e criando um projeto com o nome ApiSenhaHash.

 

Vamos incluir no projeto o pacote : Microsoft.EntityFrameworkCore.Sqlite

 

A seguir vamos criar a pasta Entities e nesta pasta a classe Usuario :

 

public class Usuario
{
    public int Id { get; set; }
    public string? Nome { get; set; }
    public string? Email { get; set; }
    public string? SenhaSalt { get; set; }
    public string? SenhaHash { get; set; }
}

 

Crie a pasta Data e nesta pasta a classe AppDbContext :

 

using ApiSenhaHash.Entities;
using Microsoft.EntityFrameworkCore;
namespace ApiSenhaHash.Data;
public sealed class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }
    public DbSet<Usuario>? Usuarios { get; set; }
}

 

Crie uma pasta Resources no projeto e nesta pasta cria 3 records :

 

1- LoginResource

public sealed record LoginResource(string Nome, string Senha);

2- RegistroResource

public sealed record RegistroResource(string Nome, string Email, string Senha);

3- UsuarioResource

public sealed record UsuarioResource(int Id, string Nome, string Email);

Agora crie uma pasta Services e defina a interface IUsuarioService a classe concreta UsuarioService e a classe SenhaHash :

 

1- SenhaHash

 

using System.Security.Cryptography;
using System.Text;
namespace ApiSenhaHash.Services;
public class SenhaHash
{
    public static string ComputeHash(string senha, string salt, 
                                                   string pepper, int iteration)
    {
        if (iteration <= 0) return senha;
        using var sha256 = SHA256.Create();
        var passwordSaltPepper = $"{senha}{salt}{pepper}";
        var byteValue = Encoding.UTF8.GetBytes(passwordSaltPepper);
        var byteHash = sha256.ComputeHash(byteValue);
        var hash = Convert.ToBase64String(byteHash);
        return ComputeHash(hash, salt, pepper, iteration - 1);
    }
    public static string GenerateSalt()
    {
        using var rng = RandomNumberGenerator.Create();
        var byteSalt = new byte[16];
        rng.GetBytes(byteSalt);
        var salt = Convert.ToBase64String(byteSalt);
        return salt;
    }
}

Na SenhaHash temos dois métodos :  ComputeHash() e GenerateSalt().

O método ComputeHash() é um método recursivo usado para gerar um hash. O algoritmo de hash usado é SHA256.

O processo de combinação de senha, salt e pepper ocorre nesta linha de código:

var passwordSaltPepper = $"{senha}{salt}{pepper}";

Semelhante ao cozimento, o processo de adição de sal e pimenta pode ser ajustado de acordo com o gosto até atingir a complexidade desejada.

O resultado de hash na matriz de bytes é convertido em uma string de base 64, que é reinserida como um parâmetro de senha na função ComputeHash() até que o processo de iteração seja concluído.

O método GenerateSalt() é usado para gerar um salt de bytes aleatórios e convertê-lo como uma string de base 64.

2- IUsuarioService

 

using ApiSenhaHash.Resources;
namespace ApiSenhaHash.Services;
public interface IUsuarioService
{
    Task<UsuarioResource> Registro(RegistroResource resource, 
                                      CancellationToken cancellationToken);

    Task<UsuarioResource> Login(LoginResource resource, 
                                      CancellationToken cancellationToken);
}

 

3- UsuarioService

 

using ApiSenhaHash.Data;
using ApiSenhaHash.Entities;
using ApiSenhaHash.Resources;
using Microsoft.EntityFrameworkCore;
namespace ApiSenhaHash.Services;
public class UsuarioService : IUsuarioService
{
    private readonly AppDbContext _context;
    private readonly string _pepper;
    private readonly int _iteration = 3;
    private readonly IConfiguration _config;
    public UsuarioService(AppDbContext context, IConfiguration config)
    {
        _context = context;
        _config = config;
        _pepper = _config["Hash:pepper"]; 
    }
    public async Task<UsuarioResource> Registro(RegistroResource resource, 
                                                        CancellationToken cancellationToken)
    {
        var usuario = new Usuario
        {
            Nome = resource.Nome,
            Email = resource.Email,
            SenhaSalt = SenhaHash.GenerateSalt()
        };

        usuario.SenhaHash = SenhaHash.ComputeHash(resource.Senha, 
                                          usuario.SenhaSalt, _pepper, _iteration);

        await _context.Usuarios.AddAsync(usuario, cancellationToken);
        await _context.SaveChangesAsync(cancellationToken);
        return new UsuarioResource(usuario.Id, usuario.Nome, usuario.Email);
    }
    public async Task<UsuarioResource> Login(LoginResource resource, 
                                                                 CancellationToken cancellationToken)
    {
        var usuario = await _context.Usuarios
                        .FirstOrDefaultAsync(x => x.Nome == resource.Nome, 
                                                                            cancellationToken);
        if (usuario == null)
            throw new Exception("Nome e senha não conferem.");
        var passwordHash = SenhaHash.ComputeHash(resource.Senha, 
                                                     usuario.SenhaSalt, _pepper, _iteration);
        if (usuario.SenhaHash != passwordHash)
            throw new Exception("Nome e senha não conferem.");
        return new UsuarioResource(usuario.Id, usuario.Nome, usuario.Email);
    }
}

 

A classe UsuarioService possui dois métodos: Register() e Login().

No método Register(), ocorre um processo de criação de salt, que é então armazenado na coluna PasswordSalt da tabela Usuario:

PasswordSalt = PasswordHasher.GenerateSalt()

Em seguida, o processo de hash de senha usando sal, pimenta e iteração:

usuario.SenhaHash = SenhaHash.ComputeHash(resource.Senha, usuario.SenhaSalt, _pepper, _iteration);


O valor de _pepper é recuperado das do arquivo appsettings.json e o valor de _iteration é definido como 3.


No arquivo appsettings.json vamos definir a string de conexão e a variável de ambiente pepper com o valor numsey que iremos usar para gerar a senha.

 

{
  "ConnectionStrings": {
    "SqliteDataContext": "Data Source=Usuarios.db"
  },
  "Hash": {
    "pepper": "numsey"
  },
    "Logging": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.AspNetCore": "Warning"
      }
    },
    "AllowedHosts": "*"
  }


No arquivo Program vamos registrar o contexto e o serviço :

 

using ApiSenhaHash.Data;
using ApiSenhaHash.Services;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlite(builder.Configuration.GetConnectionString("SqliteDataContext")));
builder.Services.AddScoped<IUsuarioService, UsuarioService>();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();


Criando o controlador UsuarioController

Na pasta Controllers vamos criar o controlador UsuarioController onde vamos definir os endpoints da nossa API:

using ApiSenhaHash.Resources;
using ApiSenhaHash.Services;
using Microsoft.AspNetCore.Mvc;
namespace ApiSenhaHash.Controllers;
[Route("api/[controller]")]
[ApiController]
public class UsuarioController : ControllerBase
{
    private readonly IUsuarioService _usuarioService;
    public UsuarioController(IUsuarioService usuarioService)
    {
        _usuarioService = usuarioService;
    }
    [HttpPost("registro")]
    public async Task<IActionResult> Registro([FromBody] RegistroResource resource,
                                                  CancellationToken cancellationToken)
    {
        try
        {
            var response = await _usuarioService.Registro(resource, cancellationToken);
            return Ok($"Regitsro realizado com sucesso - {response}");
        }
        catch (Exception e)
        {
            return BadRequest(new { ErrorMessage = e.Message });
        }
    }
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginResource resource, 
                                                            CancellationToken cancellationToken)
    {
        try
        {
            var response = await _usuarioService.Login(resource, cancellationToken);
            return Ok($"Login realizado com sucesso - {response}");
        }
        catch (Exception e)
        {
            return BadRequest(new { ErrorMessage = e.Message });
        }
    }
}

Pronto !!!

Executando o projeto teremos a exibição dos endpoints na interface do Swagger:

Vamos registrar um usuário com o nome admin e senha "Numsey#2022" :

Como resultado teremos o seguinte:

Agora vamos fazer o login usando este usuário :

Como vemos o login foi feito com sucesso.

Vamos incluir no controlador o método Action GetUsuarios para retornar os usuários cadastrados e as senhas :

    [HttpGet("usuarios")]
    public async Task<ActionResult<List<Usuario>>> GetUsuarios()
    {
          try 
          {
               var response = await _usuarioService.GetUsuarios();
               return Ok(response);
          }
          catch
          {
 	    throw;
           }
    }

Executando novamente  o projeto teremos agora o endpoint usuarios que podemos acessar para obter os usuários e senhas cadastradas:

Acionando o endpoint teremos o resultado abaixo:



Observe que temos as senhas cifradas e o Salt usado.

E estamos conversados  ... 

Pegue o projeto aqui :    ApiSenhaHash.zip

"E a vós outros, que estáveis mortos pelas vossas transgressões e pela incircuncisão da vossa carne, vos deu vida juntamente com ele, perdoando todos os nossos delitos;"
Colossenses 2:13

Referências:


José Carlos Macoratti