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 | 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 | 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 | 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 :
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: