Como navegadores geram números aleatórios criptograficamente seguros
Math.random() não é aleatório. É uma função determinística que produz uma sequência de aparência aleatória a partir de um valor inicial chamado semente. Dê a mesma semente e você recebe a mesma sequência, sempre, para sempre.
Para um dado num jogo, tudo bem. Para qualquer coisa que um atacante ganharia adivinhando, é uma vulnerabilidade com longo histórico de exploração.
O que um PRNG faz de verdade
Um gerador pseudoaleatório mantém um bloco de estado interno e aplica uma função de mistura a ele a cada chamada. Motores JavaScript modernos usam xorshift128+ ou um parente próximo: um punhado de deslocamentos e XORs sobre 128 bits de estado, rápido o bastante para chamar num laço apertado.
A saída passa em testes estatísticos de aleatoriedade. Os valores são uniformemente distribuídos, não correlacionados e sem padrão óbvio. O que eles não são é imprevisíveis, porque a função é pública e o estado é finito.
Dadas saídas consecutivas suficientes, o estado interno pode ser reconstruído — o xorshift128+ já foi resolvido a partir de um número pequeno de valores observados. Uma vez conhecido o estado, toda saída futura também é conhecida. Não há fator de trabalho a vencer; é álgebra.
Isso não é um defeito. O Math.random() foi projetado para velocidade e qualidade estatística, e entrega as duas. Nunca foi projetado para resistir a um adversário, e a especificação nunca afirmou o contrário.
O que torna um CSPRNG diferente
Um gerador criptograficamente seguro acrescenta duas propriedades que o estatístico não tem:
Imprevisibilidade. Dadas todas as saídas até agora, prever o próximo bit tem de ser computacionalmente inviável — nada melhor que cara ou coroa.
Resistência a retrocesso. Se um atacante comprometer o estado interno agora, ele não pode reconstruir as saídas que você produziu antes.
Esses requisitos descartam funções de mistura simples. CSPRNGs são construídos sobre primitivas criptográficas — uma cifra de bloco em modo contador, ou uma construção baseada em hash — onde recuperar o estado significa quebrar a primitiva subjacente.
De onde vem a entropia
O gerador ainda precisa de uma semente, e é aqui que o sistema operacional faz o trabalho de verdade. O kernel mantém um reservatório de entropia alimentado por eventos físicos genuinamente imprevisíveis:
- Variação de tempo entre interrupções
- Movimento de mouse e intervalos entre teclas
- Tempos de E/S de disco e de rede
- Em CPUs modernas, uma fonte de ruído em hardware (
RDSEEDno x86) amostrando ruído térmico no silício
O reservatório é continuamente misturado, e o CSPRNG é semeado e ressemeado a partir dele. No navegador você nunca toca em nada disso; você chama uma função e a plataforma te entrega bytes vindos dessa cadeia.
A API
// Um inteiro sem sinal de 32 bits.
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
const n = buf[0];
// 32 bytes aleatórios, por exemplo para um token.
const bytes = crypto.getRandomValues(new Uint8Array(32));
// Um UUID v4, já formatado.
const id = crypto.randomUUID();
O crypto.getRandomValues preenche um array tipado no lugar e é síncrono. Está disponível em todo navegador atual, e no Node sob require('crypto') com a mesma semântica.
A armadilha do viés
Ter bytes seguros não basta — você ainda pode jogar a segurança fora ao mapeá-los para um intervalo.
A abordagem óbvia está errada:
// Enviesado.
const indice = bytes[0] % alfabeto.length;
Se o alfabeto tem 62 caracteres e bytes[0] vai de 0 a 255, então 256 não é múltiplo de 62. Valores de 0 a 7 podem ser produzidos por cinco valores de byte diferentes, enquanto de 8 a 61 só por quatro. Os oito primeiros caracteres do seu alfabeto saem cerca de 25% mais vezes que o resto.
Numa senha de 16 caracteres isso é uma redução mensurável do espaço de busca real — não catastrófica, mas totalmente evitável. A correção é amostragem por rejeição: descarte qualquer byte que caia na cauda desigual.
function indiceAleatorio(max) {
const limite = Math.floor(256 / max) * max; // maior múltiplo exato
const buf = new Uint8Array(1);
let valor;
do {
crypto.getRandomValues(buf);
valor = buf[0];
} while (valor >= limite);
return valor % max;
}
O laço parece desperdício e não é — para um alfabeto de 62 caracteres ele rejeita cerca de 3% dos sorteios.
Essa é a diferença entre um gerador seguro em princípio e um seguro na prática, e é por isso que um gerador de senhas vale mais que escrever o seu do zero toda vez.
Como identificar qual você está olhando
Lendo o gerador de outra pessoa, os sinais são rápidos:
Math.random()em qualquer ponto do caminho — não é seguro, por mais hash que venha depois. Hash de entrada previsível dá saída previsível.new Date().getTime()como semente — pior. O milissegundo atual é um espaço de busca de alguns milhares de valores para um atacante que saiba aproximadamente quando o token foi emitido.%aplicado direto sobre bytes aleatórios — fonte segura, resultado enviesado.crypto.getRandomValuesmais amostragem por rejeição — correto.
Onde isso importa
Use a fonte criptográfica para qualquer coisa cujo valor dependa de ser imprevisível: senhas, tokens de API, identificadores de sessão, links de redefinição de senha, tokens CSRF, códigos de convite e qualquer identificador que conceda acesso por ser conhecido.
O Math.random() continua sendo a escolha certa para embaralhar uma playlist, dar jitter num tempo de retentativa ou escolher uma imagem de exemplo. Ali velocidade importa e sigilo não.
A regra de decisão é uma pergunta: aconteceria algo ruim se alguém previsse este valor? Se sim, a resposta é crypto.getRandomValues. Se não, use o rápido e não pense mais nisso.