Como gerar senha segura em código (sem Math.random)

Uma senha segura depende de duas coisas: um gerador aleatório criptográfico e um sorteio sem viés entre os caracteres. Este guia explica por que Math.random não serve, calcula a força de uma senha em bits e mostra como gerar senhas em JavaScript, Python e PHP com as funções seguras de cada linguagem.

Por Leonardo Gaertner · Publicado em

O gerador certo

Math.random no JavaScript, random no Python e rand no PHP são rápidos, mas previsíveis: quem observa alguns valores pode deduzir os próximos. Para senhas, tokens e códigos de recuperação, use o gerador criptográfico: crypto.getRandomValues no JavaScript, o módulo secrets no Python e random_int no PHP.

Para gerar uma senha agora, sem código, use o gerador de senha, que usa o gerador criptográfico do navegador.

Quão forte é uma senha

A força se mede em bits: tamanho vezes o logaritmo na base 2 do número de caracteres possíveis. Com o alfabeto dos exemplos (71 caracteres), cada caractere vale cerca de 6,15 bits.

  • 12 caracteres: cerca de 74 bits.
  • 16 caracteres: cerca de 98 bits, um bom padrão para senhas guardadas em gerenciador.
  • Aumentar o tamanho rende mais que aumentar o alfabeto: cada caractere a mais soma 6 bits, e dobrar o alfabeto soma só 1 bit por caractere.

O viés do módulo

O erro clássico é pegar um byte aleatório (0 a 255) e fazer byte % tamanhoDoAlfabeto. Com 71 caracteres, 256 não divide por 71: sobram 43 valores, e os 43 primeiros caracteres saem 4 vezes em 256 enquanto os outros saem 3. A senha continua parecendo aleatória, mas fica mais fácil de adivinhar.

A solução é descartar os bytes da sobra, como faz o exemplo em JavaScript. Python e PHP já sorteiam sem viés.

Em JavaScript

JavaScript
const ALFABETO = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
  'abcdefghijklmnopqrstuvwxyz' +
  '0123456789!@#$%&*-_';

function gerarSenha(tamanho = 16, alfabeto = ALFABETO) {
  // Descarta bytes acima do maior múltiplo do tamanho do alfabeto: sem isso, os
  // primeiros caracteres sairiam mais vezes (o viés do módulo)
  const limite = 256 - (256 % alfabeto.length);
  let senha = '';
  while (senha.length < tamanho) {
    // getRandomValues aceita no máximo 65536 bytes por chamada
    const bytes = crypto.getRandomValues(new Uint8Array(Math.min(tamanho * 2, 65536)));
    for (const b of bytes) {
      if (b < limite && senha.length < tamanho) {
        senha += alfabeto[b % alfabeto.length];
      }
    }
  }
  return senha;
}

Funciona no navegador e no Node.js. Os bytes descartados são repostos na próxima volta.

Em Python e PHP

Python
import secrets

ALFABETO = (
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    "abcdefghijklmnopqrstuvwxyz"
    "0123456789!@#$%&*-_"
)


def gerar_senha(tamanho: int = 16, alfabeto: str = ALFABETO) -> str:
    # secrets.choice usa o gerador seguro do sistema e sorteia sem viés
    return "".join(secrets.choice(alfabeto) for _ in range(tamanho))
PHP
<?php
const ALFABETO = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' .
    'abcdefghijklmnopqrstuvwxyz' .
    '0123456789!@#$%&*-_';

function gerarSenha(int $tamanho = 16, string $alfabeto = ALFABETO): string
{
    $senha = '';
    $max = strlen($alfabeto) - 1;
    for ($i = 0; $i < $tamanho; $i++) {
        $senha .= $alfabeto[random_int(0, $max)]; // random_int é seguro e sem viés
    }
    return $senha;
}

Cuidados

  • Nunca registre a senha gerada em logs, nem a envie por e-mail em texto. Se precisar entregar uma senha, prefira um link de definição de senha com validade curta.
  • Exigir "pelo menos um número e um símbolo" pouco acrescenta a uma senha aleatória longa, e pode forçar um novo sorteio. O tamanho importa mais.
  • Para guardar a senha no banco, nunca use o texto nem um hash simples. Veja como guardar senhas com hash.
  • Se a senha vai ser digitada à mão, tire caracteres parecidos (0 e O, 1, l e I) do alfabeto e compense com um caractere a mais.

Achou um erro neste guia? Avise por e-mail.

Perguntas frequentes

Math.random serve para gerar senha?

Não. O Math.random não é criptográfico: a sequência pode ser prevista a partir de alguns valores. Use crypto.getRandomValues no navegador e no Node.js.

Qual o tamanho ideal de uma senha?

Para senhas aleatórias guardadas em um gerenciador, 16 caracteres ou mais. Com letras, números e alguns símbolos, isso passa de 90 bits, fora do alcance de tentativas por força bruta.

O que é o viés do módulo?

É a diferença de frequência que aparece quando um número aleatório é reduzido com o resto da divisão por um valor que não divide o intervalo por igual. Alguns caracteres passam a sair mais que outros.

Como gerar senha em Python?

Use o módulo secrets: "".join(secrets.choice(alfabeto) for _ in range(16)). Ele usa o gerador seguro do sistema operacional e sorteia sem viés.