Gerador de Senhas Seguras

Construa um gerador de senhas seguras em Rust com configuração de caracteres, cálculo de entropia e medidor de força.

Senhas fracas são uma das maiores vulnerabilidades de segurança no mundo digital. Neste projeto, vamos construir um gerador de senhas robusto que permite configurar o comprimento, os conjuntos de caracteres e a quantidade de senhas geradas. Além disso, implementaremos um cálculo de entropia e um medidor de força para que o usuário saiba exatamente o quão segura é a senha gerada.

Este projeto é uma excelente introdução à geração de números aleatórios criptograficamente seguros em Rust, ao uso de iteradores para compor conjuntos de caracteres e ao design de interfaces CLI informativas.

O Que Vamos Construir

Nosso gerar-senha terá os seguintes recursos:

  • Geração de senhas com comprimento configurável
  • Seleção de conjuntos de caracteres (letras, números, símbolos)
  • Geração de múltiplas senhas de uma vez
  • Cálculo de entropia em bits
  • Medidor visual de força da senha
  • Modo frase-senha (passphrase) com palavras aleatórias
  • Opção para evitar caracteres ambíguos (0/O, 1/l/I)

Estrutura do Projeto

gerar-senha/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── cli.rs
    ├── gerador.rs
    └── avaliador.rs

Configurando o Projeto

cargo new gerar-senha
cd gerar-senha

Configure o Cargo.toml:

[package]
name = "gerar-senha"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
rand = "0.8"
colored = "2"

A crate rand fornece geração de números aleatórios criptograficamente seguros através do trait CryptoRng, essencial para gerar senhas que não possam ser previstas.

Passo 1: Interface de Linha de Comando

// src/cli.rs
use clap::{Parser, Subcommand};

#[derive(Parser, Debug)]
#[command(name = "gerar-senha")]
#[command(about = "Gerador de senhas seguras com cálculo de entropia")]
pub struct Cli {
    #[command(subcommand)]
    pub comando: Comando,
}

#[derive(Subcommand, Debug)]
pub enum Comando {
    /// Gera uma ou mais senhas aleatórias
    Gerar {
        /// Comprimento da senha
        #[arg(short = 'c', long, default_value_t = 16)]
        comprimento: usize,

        /// Quantidade de senhas a gerar
        #[arg(short = 'n', long, default_value_t = 1)]
        quantidade: usize,

        /// Incluir letras maiúsculas
        #[arg(short = 'M', long, default_value_t = true)]
        maiusculas: bool,

        /// Incluir letras minúsculas
        #[arg(short = 'm', long, default_value_t = true)]
        minusculas: bool,

        /// Incluir dígitos numéricos
        #[arg(short = 'd', long, default_value_t = true)]
        digitos: bool,

        /// Incluir símbolos especiais
        #[arg(short = 's', long, default_value_t = true)]
        simbolos: bool,

        /// Excluir caracteres ambíguos (0, O, l, 1, I)
        #[arg(long)]
        sem_ambiguos: bool,

        /// Caracteres personalizados para usar
        #[arg(long)]
        caracteres: Option<String>,
    },

    /// Gera uma frase-senha com palavras aleatórias
    Frase {
        /// Quantidade de palavras
        #[arg(short = 'p', long, default_value_t = 4)]
        palavras: usize,

        /// Separador entre palavras
        #[arg(short = 's', long, default_value = "-")]
        separador: String,

        /// Capitalizar primeira letra de cada palavra
        #[arg(short = 'c', long)]
        capitalizar: bool,

        /// Adicionar número aleatório ao final
        #[arg(short = 'n', long)]
        com_numero: bool,
    },

    /// Avalia a força de uma senha fornecida
    Avaliar {
        /// A senha a ser avaliada
        senha: String,
    },
}

Passo 2: Motor de Geração de Senhas

// src/gerador.rs
use rand::seq::SliceRandom;
use rand::Rng;

const MINUSCULAS: &str = "abcdefghijklmnopqrstuvwxyz";
const MAIUSCULAS: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITOS: &str = "0123456789";
const SIMBOLOS: &str = "!@#$%^&*()-_=+[]{}|;:,.<>?/~`";
const AMBIGUOS: &[char] = &['0', 'O', 'o', 'l', '1', 'I'];

/// Lista de palavras em português para gerar frases-senha
const PALAVRAS: &[&str] = &[
    "abacaxi", "banana", "cavalo", "diamante", "elefante",
    "floresta", "guitarra", "horizonte", "iguana", "janela",
    "lagarto", "montanha", "nuvem", "oceano", "palmeira",
    "quetzal", "raposa", "serpente", "trovao", "universo",
    "vulcao", "xadrez", "zebra", "ancora", "borboleta",
    "cascata", "duende", "estrela", "falcao", "girassol",
    "harpa", "inverno", "jardim", "lanterna", "morcego",
    "nogueira", "orquestra", "pirata", "raio", "sombra",
    "tempestade", "uva", "viagem", "cristal", "dragao",
    "espelho", "ferradura", "golfinho", "teclado", "relógio",
];

/// Opções de configuração para o gerador
pub struct ConfigGeracao {
    pub comprimento: usize,
    pub maiusculas: bool,
    pub minusculas: bool,
    pub digitos: bool,
    pub simbolos: bool,
    pub sem_ambiguos: bool,
    pub caracteres_personalizados: Option<String>,
}

/// Constrói o conjunto de caracteres com base na configuração
pub fn construir_charset(config: &ConfigGeracao) -> Vec<char> {
    if let Some(ref personalizado) = config.caracteres_personalizados {
        return personalizado.chars().collect();
    }

    let mut charset = String::new();

    if config.minusculas {
        charset.push_str(MINUSCULAS);
    }
    if config.maiusculas {
        charset.push_str(MAIUSCULAS);
    }
    if config.digitos {
        charset.push_str(DIGITOS);
    }
    if config.simbolos {
        charset.push_str(SIMBOLOS);
    }

    let mut chars: Vec<char> = charset.chars().collect();

    if config.sem_ambiguos {
        chars.retain(|c| !AMBIGUOS.contains(c));
    }

    chars
}

/// Gera uma senha aleatória usando o charset fornecido
pub fn gerar_senha(charset: &[char], comprimento: usize) -> String {
    let mut rng = rand::thread_rng();

    (0..comprimento)
        .map(|_| {
            let idx = rng.gen_range(0..charset.len());
            charset[idx]
        })
        .collect()
}

/// Gera uma frase-senha com palavras aleatórias
pub fn gerar_frase(
    num_palavras: usize,
    separador: &str,
    capitalizar: bool,
    com_numero: bool,
) -> String {
    let mut rng = rand::thread_rng();

    let palavras_escolhidas: Vec<String> = PALAVRAS
        .choose_multiple(&mut rng, num_palavras)
        .map(|&palavra| {
            if capitalizar {
                let mut chars = palavra.chars();
                match chars.next() {
                    None => String::new(),
                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
                }
            } else {
                palavra.to_string()
            }
        })
        .collect();

    let mut frase = palavras_escolhidas.join(separador);

    if com_numero {
        let numero: u32 = rng.gen_range(10..999);
        frase.push_str(separador);
        frase.push_str(&numero.to_string());
    }

    frase
}

/// Calcula a entropia da senha em bits
pub fn calcular_entropia(comprimento: usize, tamanho_charset: usize) -> f64 {
    if tamanho_charset == 0 {
        return 0.0;
    }
    comprimento as f64 * (tamanho_charset as f64).log2()
}

/// Calcula a entropia de uma frase-senha
pub fn calcular_entropia_frase(num_palavras: usize, tamanho_dicionario: usize) -> f64 {
    num_palavras as f64 * (tamanho_dicionario as f64).log2()
}

/// Retorna o tamanho do dicionário de palavras
pub fn tamanho_dicionario() -> usize {
    PALAVRAS.len()
}

Usamos rand::thread_rng() que fornece um gerador de números aleatórios criptograficamente seguro no modo padrão. A entropia é calculada como log2(tamanho_charset) * comprimento, que nos dá o número de bits de aleatoriedade na senha.

Passo 3: Avaliador de Força de Senha

// src/avaliador.rs
use colored::*;

/// Níveis de força da senha
#[derive(Debug)]
pub enum ForcaSenha {
    MuitoFraca,
    Fraca,
    Razoavel,
    Forte,
    MuitoForte,
}

/// Resultado da avaliação de uma senha
#[derive(Debug)]
pub struct Avaliacao {
    pub forca: ForcaSenha,
    pub entropia: f64,
    pub comprimento: usize,
    pub tem_minuscula: bool,
    pub tem_maiuscula: bool,
    pub tem_digito: bool,
    pub tem_simbolo: bool,
    pub tipos_caracteres: usize,
}

/// Avalia a força de uma senha
pub fn avaliar(senha: &str) -> Avaliacao {
    let comprimento = senha.len();
    let tem_minuscula = senha.chars().any(|c| c.is_ascii_lowercase());
    let tem_maiuscula = senha.chars().any(|c| c.is_ascii_uppercase());
    let tem_digito = senha.chars().any(|c| c.is_ascii_digit());
    let tem_simbolo = senha.chars().any(|c| !c.is_alphanumeric());

    let tipos_caracteres = [tem_minuscula, tem_maiuscula, tem_digito, tem_simbolo]
        .iter()
        .filter(|&&b| b)
        .count();

    // Estimar o tamanho do charset usado
    let mut tamanho_charset = 0;
    if tem_minuscula { tamanho_charset += 26; }
    if tem_maiuscula { tamanho_charset += 26; }
    if tem_digito { tamanho_charset += 10; }
    if tem_simbolo { tamanho_charset += 30; }

    let entropia = if tamanho_charset > 0 {
        comprimento as f64 * (tamanho_charset as f64).log2()
    } else {
        0.0
    };

    let forca = match entropia as u32 {
        0..=28 => ForcaSenha::MuitoFraca,
        29..=49 => ForcaSenha::Fraca,
        50..=69 => ForcaSenha::Razoavel,
        70..=99 => ForcaSenha::Forte,
        _ => ForcaSenha::MuitoForte,
    };

    Avaliacao {
        forca,
        entropia,
        comprimento,
        tem_minuscula,
        tem_maiuscula,
        tem_digito,
        tem_simbolo,
        tipos_caracteres,
    }
}

/// Exibe a avaliação formatada no terminal
pub fn exibir_avaliacao(avaliacao: &Avaliacao) {
    println!("\n{}", "Avaliação da Senha".bold().cyan());
    println!("  Comprimento:         {} caracteres", avaliacao.comprimento);
    println!("  Entropia:            {:.1} bits", avaliacao.entropia);
    println!("  Tipos de caracteres: {}/4", avaliacao.tipos_caracteres);

    let (texto_forca, barra) = match avaliacao.forca {
        ForcaSenha::MuitoFraca => ("Muito Fraca".red().bold(), "##..........".red()),
        ForcaSenha::Fraca => ("Fraca".red(), "####........".red()),
        ForcaSenha::Razoavel => ("Razoável".yellow(), "######......".yellow()),
        ForcaSenha::Forte => ("Forte".green(), "########....".green()),
        ForcaSenha::MuitoForte => ("Muito Forte".green().bold(), "############".green().bold()),
    };

    println!("  Força:               {} [{}]", texto_forca, barra);

    // Detalhes dos tipos de caracteres
    let sim = "Sim".green();
    let nao = "Não".red();
    println!("\n  Minúsculas:  {}", if avaliacao.tem_minuscula { &sim } else { &nao });
    println!("  Maiúsculas:  {}", if avaliacao.tem_maiuscula { &sim } else { &nao });
    println!("  Dígitos:     {}", if avaliacao.tem_digito { &sim } else { &nao });
    println!("  Símbolos:    {}", if avaliacao.tem_simbolo { &sim } else { &nao });

    // Estimativa de tempo para brute-force (1 bilhão de tentativas por segundo)
    let tentativas = 2.0_f64.powf(avaliacao.entropia);
    let segundos = tentativas / 1_000_000_000.0;

    let tempo = if segundos < 60.0 {
        "menos de 1 minuto".to_string()
    } else if segundos < 3600.0 {
        format!("{:.0} minutos", segundos / 60.0)
    } else if segundos < 86400.0 * 365.0 {
        format!("{:.1} dias", segundos / 86400.0)
    } else if segundos < 86400.0 * 365.0 * 1000.0 {
        format!("{:.0} anos", segundos / (86400.0 * 365.0))
    } else {
        format!("{:.2e} anos", segundos / (86400.0 * 365.0))
    };

    println!("\n  Tempo estimado para quebrar (brute-force): {}", tempo.bold());
}

O avaliador calcula a entropia baseada nos tipos de caracteres presentes e estima o tempo para quebrar a senha por força bruta considerando 1 bilhao de tentativas por segundo.

Passo 4: Integrando no main.rs

// src/main.rs
mod avaliador;
mod cli;
mod gerador;

use clap::Parser;
use cli::{Cli, Comando};
use colored::*;

fn main() {
    let cli = Cli::parse();

    match cli.comando {
        Comando::Gerar {
            comprimento,
            quantidade,
            maiusculas,
            minusculas,
            digitos,
            simbolos,
            sem_ambiguos,
            caracteres,
        } => {
            let config = gerador::ConfigGeracao {
                comprimento,
                maiusculas,
                minusculas,
                digitos,
                simbolos,
                sem_ambiguos,
                caracteres_personalizados: caracteres,
            };

            let charset = gerador::construir_charset(&config);

            if charset.is_empty() {
                eprintln!(
                    "{} Nenhum conjunto de caracteres selecionado.",
                    "ERRO:".red().bold()
                );
                std::process::exit(1);
            }

            let entropia = gerador::calcular_entropia(comprimento, charset.len());

            println!("{}", "Senhas geradas:".bold().cyan());
            println!(
                "  Charset: {} caracteres | Comprimento: {} | Entropia: {:.1} bits\n",
                charset.len(),
                comprimento,
                entropia
            );

            for i in 1..=quantidade {
                let senha = gerador::gerar_senha(&charset, comprimento);
                if quantidade > 1 {
                    println!("  {}. {}", i, senha.bold());
                } else {
                    println!("  {}", senha.bold());
                }
            }

            // Avaliar a primeira senha como referência
            println!();
            let primeira = gerador::gerar_senha(&charset, comprimento);
            let avaliacao = avaliador::avaliar(&primeira);
            avaliador::exibir_avaliacao(&avaliacao);
        }

        Comando::Frase {
            palavras,
            separador,
            capitalizar,
            com_numero,
        } => {
            let frase = gerador::gerar_frase(palavras, &separador, capitalizar, com_numero);
            let entropia = gerador::calcular_entropia_frase(
                palavras,
                gerador::tamanho_dicionario(),
            );

            println!("{}", "Frase-senha gerada:".bold().cyan());
            println!("\n  {}", frase.bold());
            println!(
                "\n  Palavras: {} | Entropia: {:.1} bits",
                palavras, entropia
            );

            let avaliacao = avaliador::avaliar(&frase);
            avaliador::exibir_avaliacao(&avaliacao);
        }

        Comando::Avaliar { senha } => {
            let avaliacao = avaliador::avaliar(&senha);
            avaliador::exibir_avaliacao(&avaliacao);
        }
    }
}

Como Executar

cargo build --release

Exemplos de uso:

# Gerar uma senha padrão de 16 caracteres
./target/release/gerar-senha gerar
# Senha: x$7KmP!qR2vN&fLw
# Entropia: 104.9 bits - Muito Forte

# Gerar 5 senhas de 20 caracteres
./target/release/gerar-senha gerar -c 20 -n 5

# Gerar senha sem caracteres ambíguos
./target/release/gerar-senha gerar -c 12 --sem-ambiguos

# Gerar senha apenas com letras e números (sem símbolos)
./target/release/gerar-senha gerar -c 16 --simbolos false

# Gerar frase-senha com 5 palavras
./target/release/gerar-senha frase -p 5
# Frase: estrela-dragao-montanha-raposa-cristal

# Frase-senha capitalizada com número
./target/release/gerar-senha frase -p 4 -c -n
# Frase: Falcao-Oceano-Trovao-Girassol-742

# Avaliar uma senha existente
./target/release/gerar-senha avaliar "MinhaSenh@123"
# Força: Forte [########....] - Entropia: 75.3 bits

Desafios para Expandir

  1. Cópia para a area de transferencia: Use a crate clipboard ou arboard para copiar a senha gerada automaticamente, evitando que o usuário precise selecioná-la manualmente.

  2. Verificação contra listas de vazamentos: Integre com a API “Have I Been Pwned” (usando hashing k-anonimity) para verificar se a senha ou padrão similar já apareceu em vazamentos de dados.

  3. Senhas pronunciáveis: Implemente um modo que gere senhas que alternem consoantes e vogais, produzindo senhas aleatórias mas que podem ser memorizadas mais facilmente (ex: “bopikaru42”).

  4. Perfis de configuração: Permita que o usuário salve perfis de configuração (ex: “banco”, “email”, “wifi”) com diferentes requisitos de complexidade em um arquivo TOML.

  5. Geração determinística com seed: Adicione a opção de fornecer um seed para gerar senhas reproduzíveis — útil para cenários de teste ou derivação de senhas a partir de uma master key.

Veja Também