Geração segura de números aleatórios

1. Por que Números Aleatórios Importam em Segurança?

A geração de números aleatórios é um dos pilares fundamentais da segurança computacional. Sem aleatoriedade genuína, sistemas criptográficos inteiros podem ser comprometidos. Números aleatórios são essenciais para:

  • Geração de chaves criptográficas: chaves RSA, AES, ECDSA dependem de números imprevisíveis
  • Nonces: números usados uma única vez em protocolos de autenticação
  • Salts: valores adicionados a senhas antes do hash para evitar ataques de dicionário
  • Vetores de inicialização (IVs): necessários para modos de operação como CBC
  • Tokens de sessão: identificadores que devem ser impossíveis de adivinhar

As consequências de aleatoriedade fraca são catastróficas. Em 2008, o gerador de números aleatórios do OpenSSL no Debian foi acidentalmente quebrado, tornando todas as chaves RSA e DSA geradas no sistema previsíveis. Estima-se que milhares de chaves SSH e certificados SSL foram comprometidos. Em 1999, o PokerStars usou um gerador pseudoaleatório fraco que permitiu que jogadores previssem as cartas do baralho, resultando em perdas milionárias.

2. Aleatoriedade Verdadeira vs Pseudoaleatoriedade

Existem duas categorias principais de geradores de números aleatórios:

Geradores Verdadeiros (TRNG - True Random Number Generators): utilizam fontes físicas de entropia como ruído térmico de semicondutores, movimentos do mouse, variações de temporização de discos rígidos ou flutuações atmosféricas. São imprevisíveis por natureza, mas geralmente lentos e com taxa de saída limitada.

Geradores Pseudoaleatórios (PRNG - Pseudo Random Number Generators): algoritmos determinísticos que produzem sequências numéricas baseadas em uma semente (seed) inicial. Embora pareçam aleatórios, são completamente previsíveis se a semente for conhecida. Exemplos incluem Mersenne Twister e a função rand() da biblioteca C.

O compromisso prático: sistemas operacionais modernos combinam ambos. Coletam entropia de fontes físicas (TRNG) para alimentar um PRNG criptográfico (CSPRNG). O Linux usa /dev/urandom e o Windows usa CryptGenRandom. Essa abordagem oferece o melhor dos dois mundos: imprevisibilidade real e alta taxa de saída.

3. CSPRNG: O Padrão Ouro para Segurança

CSPRNG (Cryptographically Secure Pseudo-Random Number Generator) é a classe de geradores projetada especificamente para aplicações de segurança. Suas características essenciais incluem:

  • Imprevisibilidade estatística: mesmo conhecendo parte da saída, é computacionalmente inviável prever bits futuros
  • Resistência a ataques de estado: se o estado interno for comprometido, a saída anterior não pode ser reconstruída (backtracking resistance)
  • Forward secrecy: comprometimento do estado atual não permite prever saídas futuras após re-semeamento

Algoritmos CSPRNG notáveis incluem:
- /dev/urandom e /dev/random (Linux)
- CryptGenRandom (Windows)
- Fortuna e Yarrow (projetados por Bruce Schneier e Niels Ferguson)
- Algoritmo de Blum-Blum-Shub (baseado em teoria dos números)

Comparação crítica: Mersenne Twister (MT19937) é rápido e tem boas propriedades estatísticas, mas NÃO é criptograficamente seguro. Com 624 valores consecutivos, é possível reconstruir completamente seu estado interno. A função rand() da biblioteca C é ainda pior, tipicamente usando um gerador linear congruencial previsível. Nenhum deles deve ser usado em contextos de segurança.

4. Geração Segura em Diferentes Linguagens

Python

# CORRETO: uso seguro
import secrets
import os

# Gera 32 bytes criptograficamente seguros
token = secrets.token_hex(32)  # 64 caracteres hexadecimais
chave = os.urandom(32)         # bytes brutos

# Gera um inteiro seguro entre 1 e 100
numero_seguro = secrets.randbelow(100) + 1

# ERRADO: NÃO use para segurança
import random
token_inseguro = ''.join(random.choices('abcdef0123456789', k=64))

Node.js

// CORRETO: uso seguro
const crypto = require('crypto');

// Gera 32 bytes aleatórios
const buffer = crypto.randomBytes(32);
const token = buffer.toString('hex');

// Gera inteiro seguro entre 1 e 100
const { randomInt } = require('crypto');
randomInt(1, 101, (err, n) => {
  if (!err) console.log(n);
});

// ERRADO: NÃO use para segurança
const token_inseguro = Math.random().toString(36).substring(2);

Java

// CORRETO: uso seguro
import java.security.SecureRandom;

SecureRandom secureRandom = new SecureRandom();
byte[] chave = new byte[32];
secureRandom.nextBytes(chave);

// Gera inteiro seguro entre 0 e 99
int numero = secureRandom.nextInt(100);

// ERRADO: NÃO use para segurança
import java.util.Random;
Random inseguro = new Random();
int token_inseguro = inseguro.nextInt();

Go

// CORRETO: uso seguro
package main

import (
    "crypto/rand"
    "encoding/hex"
    "math/big"
)

func main() {
    // Gera 32 bytes seguros
    chave := make([]byte, 32)
    rand.Read(chave)
    token := hex.EncodeToString(chave)

    // Gera inteiro seguro entre 1 e 100
    n, _ := rand.Int(rand.Reader, big.NewInt(100))
    numero := n.Int64() + 1
}

// ERRADO: NÃO use math/rand para segurança
import "math/rand"
token_inseguro := rand.Intn(1000000)

5. Entropia: O Combustível da Aleatoriedade

Entropia é uma medida de incerteza ou imprevisibilidade, geralmente expressa em bits. Quanto maior a entropia, mais imprevisível é o sistema. Em sistemas Linux, a entropia é coletada de fontes como:

  • Interrupções de hardware (teclado, mouse)
  • Temporização de discos e operações de I/O
  • Ruído de rede e variações de pacotes
  • Variações elétricas em sensores

Problemas comuns de entropia:
- Containers Docker: ambientes isolados podem ter acesso limitado a fontes de entropia
- VMs recém-criadas: sistemas virtualizados frequentemente iniciam com pools de entropia vazios
- Sistemas embarcados: dispositivos IoT podem carecer de hardware de geração de entropia
- Boot rápido: sistemas modernos inicializam tão rápido que o pool de entropia não tem tempo de se encher

Mitigações:
- haveged: daemon que gera entropia a partir de variações de temporização de CPU
- rng-tools: ferramentas para gerenciar fontes de entropia
- Hardware RNG: TPM (Trusted Platform Module), instrução Intel RDRAND, chips dedicados

6. Armadilhas e Anti-Padrões Comuns

  1. Usar rand() ou Math.random() para chaves ou tokens: esses geradores não são criptográficos e podem ser previstos com esforço mínimo.

  2. Reutilizar a mesma semente: usar sempre a mesma seed (como 0 ou 12345) produz exatamente a mesma sequência de números.

  3. Sementes previsíveis: usar timestamp, PID, ou endereço de memória como seed permite que atacantes repliquem a sequência.

  4. Entropy starvation: não reabastecer entropia em execuções longas pode fazer o gerador entrar em modo degradado.

  5. Gerar números antes da inicialização do CSPRNG: em alguns sistemas, o pool de entropia pode não estar pronto imediatamente após o boot.

7. Boas Práticas para Uso em Produção

  • Sempre use APIs específicas para segurança: secrets em Python, crypto/rand em Go, SecureRandom em Java
  • Nunca implemente seu próprio gerador criptográfico: confie no sistema operacional e em bibliotecas testadas
  • Teste a qualidade: ferramentas como dieharder, TestU01 e ent podem verificar propriedades estatísticas
  • Para alta performance: gere buffers de bytes grandes de uma vez, reutilize instâncias de SecureRandom com cautela
  • Considere HashiCorp Vault: delegue a geração de chaves e tokens a um sistema especializado que gerencia entropia centralizadamente

Lembre-se: a segurança de todo o seu sistema depende da imprevisibilidade dos números aleatórios que você gera. Um único ponto fraco pode comprometer criptografia, autenticação e integridade de dados.

Referências