Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação

[333] Geradores de Senhas Seguras: Implementações em 5 Linguagens de Programação

Quinze implementações de gerador de senha em Go, Rust, JavaScript, PHP e Python, em três níveis: sorteio direto do pool, garantia de composição e fonte criptográfica. Serve também de catálogo de armadilhas — viés de módulo, embaralhamento por sort, base64 no lugar do alfabeto pedido e semente derivada do relógio.
DevOps

14 min de leitura

A geração de senhas fortes é um dos pilares da segurança da informação. Uma senha robusta deve conter letras maiúsculas, minúsculas, números e caracteres especiais, com comprimento adequado (neste caso, 12 caracteres).

Neste artigo, apresentamos 15 implementações diferentes de geradores de senhas distribuídas em cinco linguagens populares: Go, Rust, JavaScript, PHP e Python. Para cada linguagem foram desenvolvidas três abordagens distintas, demonstrando diferentes técnicas e níveis de segurança:

  1. Abordagem simples – Seleção aleatória direta de um pool de caracteres.
  2. Abordagem com garantia de composição – Garante pelo menos um caractere de cada tipo.
  3. Abordagem criptograficamente segura – Utiliza geradores de números aleatórios criptográficos.

Go (Golang)

1. Seleção simples de um pool de caracteres

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	pool := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
	for i := 0; i < 5; i++ {
		password := ""
		for j := 0; j < 12; j++ {
			index := rand.Intn(len(pool))
			password += string(pool[index])
		}
		fmt.Println(password)
	}
}

2. Garantia de pelo menos um caractere de cada tipo

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	upper := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	lower := "abcdefghijklmnopqrstuvwxyz"
	digit := "0123456789"
	special := "!@#$%^&*()"
	pool := upper + lower + digit + special

	for i := 0; i < 5; i++ {
		positions := rand.Perm(12)
		password := make([]byte, 12)
		password[positions[0]] = upper[rand.Intn(len(upper))]
		password[positions[1]] = lower[rand.Intn(len(lower))]
		password[positions[2]] = digit[rand.Intn(len(digit))]
		password[positions[3]] = special[rand.Intn(len(special))]

		for j := 0; j < 12; j++ {
			if password[j] == 0 {
				password[j] = pool[rand.Intn(len(pool))]
			}
		}
		fmt.Println(string(password))
	}
}

3. Versão criptograficamente segura com crypto/rand

package main

import (
	"crypto/rand"
	"fmt"
	"math/big"
)

func main() {
	pool := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()"
	poolLen := big.NewInt(int64(len(pool)))

	for i := 0; i < 5; i++ {
		password := ""
		for j := 0; j < 12; j++ {
			n, _ := rand.Int(rand.Reader, poolLen)
			password += string(pool[n.Int64()])
		}
		fmt.Println(password)
	}
}

Rust

1. Seleção simples com crate rand

use rand::Rng;

fn main() {
    let pool: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".chars().collect();
    let mut rng = rand::thread_rng();

    for _ in 0..5 {
        let mut password = String::new();
        for _ in 0..12 {
            let index = rng.gen_range(0..pool.len());
            password.push(pool[index]);
        }
        println!("{}", password);
    }
}

2. Garantia de composição com shuffle de posições

use rand::Rng;
use rand::seq::SliceRandom;

fn main() {
    let upper: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
    let lower: Vec<char> = "abcdefghijklmnopqrstuvwxyz".chars().collect();
    let digit: Vec<char> = "0123456789".chars().collect();
    let special: Vec<char> = "!@#$%^&*()".chars().collect();
    let pool: Vec<char> = upper.iter().chain(lower.iter()).chain(digit.iter()).chain(special.iter()).cloned().collect();
    let mut rng = rand::thread_rng();

    for _ in 0..5 {
        let mut indices: Vec<usize> = (0..12).collect();
        indices.shuffle(&mut rng);

        let mut password: Vec<char> = vec![' '; 12];
        password[indices[0]] = *rng.choose(&upper).unwrap();
        password[indices[1]] = *rng.choose(&lower).unwrap();
        password[indices[2]] = *rng.choose(&digit).unwrap();
        password[indices[3]] = *rng.choose(&special).unwrap();

        for i in 0..12 {
            if password[i] == ' ' {
                password[i] = *rng.choose(&pool).unwrap();
            }
        }
        println!("{}", password.into_iter().collect::<String>());
    }
}

3. Versão segura com OsRng

use rand::rngs::OsRng;
use rand::RngCore;

fn main() {
    let pool: Vec<u8> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()".as_bytes().to_vec();
    let pool_len = pool.len() as u32;

    for _ in 0..5 {
        let mut password = String::new();
        let mut bytes = [0u8; 12];
        OsRng.fill_bytes(&mut bytes);
        for &byte in bytes.iter() {
            let index = (byte as u32 % pool_len) as usize;
            password.push(pool[index] as char);
        }
        println!("{}", password);
    }
}

JavaScript (Node.js)

1. Implementação simples

const pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';

for (let i = 0; i < 5; i++) {
  let password = '';
  for (let j = 0; j < 12; j++) {
    const index = Math.floor(Math.random() * pool.length);
    password += pool[index];
  }
  console.log(password);
}

2. Com garantia de composição

const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const lower = 'abcdefghijklmnopqrstuvwxyz';
const digit = '0123456789';
const special = '!@#$%^&*()';
const pool = upper + lower + digit + special;

for (let i = 0; i < 5; i++) {
  const positions = Array.from({length: 12}, (_, index) => index).sort(() => Math.random() - 0.5);
  let password = new Array(12);
  password[positions[0]] = upper[Math.floor(Math.random() * upper.length)];
  password[positions[1]] = lower[Math.floor(Math.random() * lower.length)];
  password[positions[2]] = digit[Math.floor(Math.random() * digit.length)];
  password[positions[3]] = special[Math.floor(Math.random() * special.length)];

  for (let j = 0; j < 12; j++) {
    if (password[j] === undefined) {
      password[j] = pool[Math.floor(Math.random() * pool.length)];
    }
  }
  console.log(password.join(''));
}

3. Versão com módulo crypto

const crypto = require('crypto');

for (let i = 0; i < 5; i++) {
  const randomBytes = crypto.randomBytes(9);
  let password = randomBytes.toString('base64').slice(0, 12);
  console.log(password);
}

PHP

1. Versão simples

<?php
$pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';

for ($i = 0; $i < 5; $i++) {
    $password = '';
    for ($j = 0; $j < 12; $j++) {
        $index = rand(0, strlen($pool) - 1);
        $password .= $pool[$index];
    }
    echo $password . PHP_EOL;
}
?>

2. Com garantia de composição

<?php
$upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$lower = 'abcdefghijklmnopqrstuvwxyz';
$digit = '0123456789';
$special = '!@#$%^&*()';
$pool = $upper . $lower . $digit . $special;

for ($i = 0; $i < 5; $i++) {
    $positions = range(0, 11);
    shuffle($positions);
    $password = array_fill(0, 12, null);
    $password[$positions[0]] = $upper[rand(0, strlen($upper) - 1)];
    $password[$positions[1]] = $lower[rand(0, strlen($lower) - 1)];
    $password[$positions[2]] = $digit[rand(0, strlen($digit) - 1)];
    $password[$positions[3]] = $special[rand(0, strlen($special) - 1)];

    for ($j = 0; $j < 12; $j++) {
        if ($password[$j] === null) {
            $password[$j] = $pool[rand(0, strlen($pool) - 1)];
        }
    }
    echo implode('', $password) . PHP_EOL;
}
?>

3. Versão segura com random_bytes

<?php
$pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';

for ($i = 0; $i < 5; $i++) {
    $password = '';
    $bytes = random_bytes(12);
    for ($j = 0; $j < 12; $j++) {
        $index = ord($bytes[$j]) % strlen($pool);
        $password .= $pool[$index];
    }
    echo $password . PHP_EOL;
}
?>

Python

1. Versão simples com random

import random

pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()'

for _ in range(5):
    password = ''.join(random.choice(pool) for _ in range(12))
    print(password)

2. Com garantia de composição

import random

upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
lower = 'abcdefghijklmnopqrstuvwxyz'
digit = '0123456789'
special = '!@#$%^&*()'
pool = upper + lower + digit + special

for _ in range(5):
    positions = random.sample(range(12), 4)
    password = [''] * 12
    password[positions[0]] = random.choice(upper)
    password[positions[1]] = random.choice(lower)
    password[positions[2]] = random.choice(digit)
    password[positions[3]] = random.choice(special)

    for i in range(12):
        if not password[i]:
            password[i] = random.choice(pool)
    print(''.join(password))

3. Versão segura com módulo secrets (recomendada)

import secrets

pool = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()'

for _ in range(5):
    password = ''.join(secrets.choice(pool) for _ in range(12))
    print(password)

Conclusão e Melhores Práticas

  • Em produção, sempre prefira geradores criptograficamente seguros (crypto/rand no Go, OsRng no Rust, crypto no Node.js, random_bytes no PHP e secrets no Python).
  • Evite Math.random() e rand padrão em aplicações sensíveis.
  • Considere o uso de gerenciadores de senhas ou APIs de password hashing (Argon2, bcrypt).
  • Para maior segurança, aumente o comprimento para 16+ caracteres e inclua mais variedade de caracteres especiais.

Este conjunto de exemplos serve como base sólida para desenvolvedores que precisam implementar geradores de senhas em diferentes ecossistemas.

Exercícios

Exercício 1

As versões "seguras" de Rust e PHP fazem byte % pool_len para escolher o índice. O pool tem 72 caracteres e o byte vai de 0 a 255. Calcule quais caracteres saem favorecidos e em que proporção.

Ver resposta

✓ Resposta: É viés de módulo. Como 256 = 3 × 72 + 40, os índices de 0 a 39 são alcançados por quatro bytes cada, e os de 40 a 71 por apenas três.

Em probabilidade: 4/256 = 1,5625% contra 3/256 = 1,1719%. Os 40 primeiros caracteres do pool — todo o A-Z mais a até n — aparecem 33% mais vezes que os 32 restantes, entre eles todos os dígitos e todos os símbolos.

Vale medir o dano com honestidade, porque a intuição costuma exagerá-lo. A entropia por caractere cai de 6,170 para 6,156 bits — em doze caracteres, uma perda de cerca de 0,17 bit num total de 74. Ninguém quebra essa senha por causa disso.

O que condena o padrão é outra coisa:

  • Não há motivo para aceitá-lo. O Go, no mesmo artigo, faz certo com rand.Int(rand.Reader, poolLen), que rejeita e sorteia de novo — mesma quantidade de código, zero viés. Em Rust é rand::distributions::Uniform; em PHP, random_int(0, strlen($pool) - 1), que é uniforme e existe desde o PHP 7.
  • Escala muito pior. O viés depende de quanto o intervalo do gerador sobra sobre o tamanho do pool. Com 72 caracteres a partir de um byte, sobram 40. Reduza o pool para 62 (sem símbolos) e a distorção cresce; use o mesmo % para sortear em intervalos grandes a partir de um u32 e o resultado deixa de ser aceitável.

A regra a levar: gerador criptográfico + % não é sorteio uniforme. A parte cara — a fonte de entropia — está certa; é o último passo, o barato, que estraga.

Exercício 2

A versão 2 do JavaScript embaralha as posições com .sort(() => Math.random() - 0.5). Por que isso não produz uma permutação uniforme — e o que um atacante ganha sabendo disso?

Ver resposta

✓ Resposta: Porque o sort exige um comparador consistente — que responda sempre a mesma coisa para o mesmo par, e que seja transitivo. Um comparador aleatório viola as duas condições, e a especificação não define o resultado nesse caso: o que sai depende do algoritmo de ordenação da engine e do número de elementos.

Na prática o resultado não é uniforme e não é nem sequer o mesmo entre engines. O padrão típico é conhecido: os elementos tendem a permanecer perto da posição original, porque um algoritmo de ordenação faz muito menos comparações do que seriam necessárias para dispersá-los. Com 12 elementos, as 12! = 479 milhões de permutações possíveis saem com probabilidades bem diferentes entre si.

O que o atacante ganha: positions[0] é a posição da maiúscula, positions[1] a da minúscula, e assim por diante. Se positions tende a sair próximo de [0,1,2,3,...], então a senha tende a começar com maiúscula, seguida de minúscula, dígito e símbolo. Um ataque que testa primeiro esse formato acerta muito mais cedo do que a força bruta sobre 74 bits sugeriria — e o custo de tentar é o mesmo.

A correção é Fisher-Yates, que é curto e correto:

for (let i = posicoes.length - 1; i > 0; i--) {
  const j = Math.floor(Math.random() * (i + 1));
  [posicoes[i], posicoes[j]] = [posicoes[j], posicoes[i]];
}

Note que ele continua usando Math.random() — corrigir o embaralhamento não conserta a fonte. Num gerador de senhas de verdade, o sorteio precisa vir de crypto.getRandomValues().

Exercício 3

A versão 3 do JavaScript é crypto.randomBytes(9).toString('base64').slice(0, 12). Ela cumpre a promessa das outras duas implementações da mesma linguagem? Analise o .slice(0, 12), o alfabeto e a composição.

Ver resposta

✓ Resposta: A fonte de aleatoriedade é a melhor das três, mas a saída não é a senha que o artigo especificou.

Três observações, da menos para a mais importante:

  • O slice não faz nada. Base64 codifica 3 bytes em 4 caracteres; 9 bytes dão exatamente 12, sem preenchimento. A string já tem 12 caracteres antes do corte — o que é elegante por acidente, mas escreveria melhor com o número de bytes deduzido do comprimento desejado.
  • O alfabeto é outro. Base64 usa A-Z a-z 0-9 + /. Some 64 símbolos, não os 72 do pool declarado: nenhum dos especiais !@#$%^&*() pode aparecer, e em compensação surgem + e /, que nunca estiveram no pool. Uma política que exija "um caractere especial da lista X" rejeita essa senha, e a / ainda tende a causar problema em URL e em shell.
  • Não há garantia de composição nenhuma. Nada impede que saiam 12 letras minúsculas. Isso é razoável do ponto de vista criptográfico — mas contradiz frontalmente a versão 2, apresentada como o jeito de garantir os quatro tipos.

Sobre a força: 9 bytes são 72 bits de entropia, contra 74 bits de doze caracteres sorteados do pool de 72. Praticamente empatados — o problema não é a senha ser fraca, é ela não ser a senha pedida.

A implementação coerente com as outras usaria a mesma fonte para sortear do pool, e não um encoding de bytes:

const pool = 'ABC...!@#$%^&*()';
const senha = Array.from(crypto.randomBytes(12))
  .map(() => pool[crypto.randomInt(pool.length)])   // randomInt é uniforme
  .join('');

Fica a distinção que o exercício quer fixar: codificar bytes aleatórios e sortear de um alfabeto são coisas diferentes. A primeira é ótima para um token; a segunda é o que se pede quando existe uma política de caracteres.

Exercício 4

A conclusão recomenda "aumente o comprimento para 16+ caracteres e inclua mais variedade de caracteres especiais". Se você só pudesse fazer uma das duas, qual escolheria? Justifique com números.

Ver resposta

✓ Resposta: O comprimento, com folga. A entropia cresce linearmente com o número de caracteres e apenas logaritmicamente com o tamanho do alfabeto.

Os números, para senha sorteada uniformemente:

  • 12 caracteres, pool de 72: 12 × log₂(72) = 74,0 bits (o do artigo)
  • 12 caracteres, pool de 94 (todo o ASCII imprimível): 12 × 6,55 = 78,7 bits — quase 30% mais símbolos rendem 4,7 bits
  • 16 caracteres, pool de 36 (só minúsculas e dígitos): 16 × 5,17 = 82,7 bits
  • 16 caracteres, pool de 72: 16 × 6,17 = 98,7 bits

Repare na terceira linha: 16 caracteres de um alfabeto pobre batem 12 de um alfabeto rico — e ainda são mais fáceis de digitar. Quatro caracteres a mais valem 24,7 bits; dobrar a variedade de símbolos vale menos de 5.

Há um argumento que reforça o mesmo lado e não é matemático: a exigência de composição é o que empurra pessoas para Senha@2024 — o alfabeto nominal cresce, a senha real fica previsível. É por isso que o NIST SP 800-63B desaconselha regras de composição e recomenda exigir comprimento e comparar contra listas de senhas vazadas.

Isso reenquadra a versão 2 de todas as cinco linguagens: garantir "um de cada tipo" reduz o espaço de senhas possíveis — elimina justamente as que não têm símbolo — e o algoritmo usado ainda amostra esse espaço de forma não uniforme, ao fixar quatro posições sorteadas. Ela existe para satisfazer políticas de sistemas legados, não porque produza senha melhor.

Exercício 5

A primeira versão em Go abre com rand.Seed(time.Now().UnixNano()). Suponha que ela gere a senha inicial de contas num sistema que cria vários usuários em lote. Que ataque isso viabiliza?

Ver resposta

✓ Resposta: Reconstruir todas as senhas do lote a partir de uma estimativa do horário de execução.

O math/rand é determinístico: a semente define integralmente a sequência. Aqui a semente é o relógio em nanossegundos, e o atacante que sabe o dia e a hora aproximada precisa varrer um intervalo pequeno — um minuto de incerteza são 6×10¹⁰ sementes, e cada teste é uma execução trivial do mesmo gerador. Pior: a resolução real do relógio raramente é de nanossegundo, então o espaço efetivo é muito menor do que parece.

E o alvo não é uma senha, é o lote inteiro: uma única semente correta reproduz a sequência completa, na ordem em que os usuários foram criados. Basta reconhecer uma senha para confirmar a semente e ter todas as outras.

Um caminho ainda mais barato existe se alguma dessas senhas vazar: com a saída em mãos, dá para recuperar o estado interno do gerador e prever o que vem depois, sem sequer adivinhar a semente.

Dois detalhes de Go que valem registrar:

  • rand.Seed está deprecado desde o Go 1.20. O gerador global passou a ser semeado aleatoriamente por padrão, e chamar Seed hoje é o oposto do que se quer: troca uma semente imprevisível por uma derivada do relógio.
  • A troca certa não é mexer na semente, é mudar de pacote: crypto/rand, como na versão 3. Nenhuma semente do math/rand torna o math/rand adequado para segredos — a diferença entre os dois é de propósito, não de qualidade da semente.

O caso de uso do enunciado é o pior possível para esse erro, porque a senha inicial costuma sobreviver: muitos usuários nunca a trocam.

Comentários

Mais em DevOps

Navegando pelo Sistema de Arquivos com Confiança
Navegando pelo Sistema de Arquivos com Confiança

A árvore de diretórios do Linux e o que cada ramo guarda: /etc, /var, /home…

O Terminal Não é o Inimigo
O Terminal Não é o Inimigo

Por que um curso de DevOps começa por um mês inteiro de terminal: o que é o…

Estudar e realmente Aprender DevOps & Cloud
Estudar e realmente Aprender DevOps & Cloud

Um panorama de DevOps e Cloud: o problema real que deu origem à prática, por…