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:
- Abordagem simples – Seleção aleatória direta de um pool de caracteres.
- Abordagem com garantia de composição – Garante pelo menos um caractere de cada tipo.
- 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/randno Go,OsRngno Rust,cryptono Node.js,random_bytesno PHP esecretsno Python). - Evite
Math.random()erandpadrã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 umu32e 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
slicenã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.Seedestá deprecado desde o Go 1.20. O gerador global passou a ser semeado aleatoriamente por padrão, e chamarSeedhoje é 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 domath/randtorna omath/randadequado 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.