Gerar corretamente uma chave secreta de aplicação
Publicado a 12/08/2026 · 13 min de leitura · Ferramentas de texto e idioma
Daniel Okonkwo — Programador front-end e redator de Tecnologia na OneKitly
Desempenho web · Formatos de ficheiro
Verificado a partir de 4 fontes
Três perguntas resolvem a questão. Uma: que fonte aleatória chama? Um gerador sólido chama a aleatoriedade criptográfica da plataforma — crypto.getRandomValues num navegador, secrets em Python, random_bytes em PHP. Um duvidoso chama Math.random, documentado como impróprio para segurança: o V8 implementa-o com xorshift128+, e um punhado de saídas consecutivas chega para reconstruir o estado interno e prever o resto. Esta ferramenta passa: o seu randStr chama rngInt, que enche um Uint32Array com crypto.getRandomValues; o ramo Math.random só corre se faltar o crypto, o que nenhum navegador faz. Duas: quanta entropia carrega a saída? Multiplique o comprimento pelo logaritmo de base dois do tamanho do alfabeto. O conjunto Django é aqui exatamente os cinquenta carateres que o get_random_secret_key do Django usa — minúsculas, algarismos e !@#$%^&*(-_=+) — portanto uma chave de 50 carateres carrega 50 × log2(50) = 282 bits, contra os cerca de 128 bits habitualmente tomados como piso. A opção alfanumérica dá 297 bits. O lado WordPress gera as oito constantes do wp-config com 64 carateres de um conjunto de 92, que é byte a byte o wp_generate_password com as duas bandeiras de carateres especiais: 418 bits cada uma. Três: para onde vai a chave a seguir? Nenhuma das duas respostas serve depois de ela ser colada numa conversa, num ticket ou num commit — um segredo partilhado é um segredo gasto, e o único remédio é gerar outro e fazer a rotação.
Como distinguir um gerador de chaves secretas sólido de um duvidoso, com este como exemplo trabalhado: que fonte aleatória chama, como calcular a entropia sozinho e o que se parte mesmo no dia em que se roda a chave.
Primeira pergunta: que fonte aleatória
Cada linguagem tem dois geradores de números aleatórios e não são intermutáveis. Um é rápido, determinista a partir de uma semente e pensado para baralhar uma lista de reprodução ou desfasar uma nova tentativa. O outro é mais lento, bebe da reserva de entropia do sistema operativo e serve para tudo aquilo que um atacante gostaria de adivinhar. Em JavaScript são Math.random e crypto.getRandomValues; em Python, random e secrets; em PHP, mt_rand e random_bytes. Usar o primeiro onde cabe o segundo é a forma mais comum de errar um gerador de chaves.
O que desqualifica o Math.random não é a saída parecer regular — não parece. É que o gerador é uma pequena máquina determinista cujo estado se pode recuperar. O V8, o motor do Chrome e do Node, usa xorshift128+, um estado de 128 bits avançado por deslocamentos e ou-exclusivos. A partir de uma sequência curta de saídas consecutivas, esse estado reconstrói-se, e a partir daí todos os valores futuros são conhecidos e todos os passados podem ser repetidos. O motor também não tem obrigação de o semear de forma imprevisível, e os navegadores semearam-no historicamente a partir de coisas que um atacante pode observar ou influenciar.
Esta ferramenta chama o correto. Lendo o código do botão para dentro: o manipulador de geração constrói uma cadeia carácter a carácter com randStr, o randStr escolhe cada índice com rngInt, e o rngInt reserva um Uint32Array de um elemento e enche-o com crypto.getRandomValues. Existe um recuo para Math.random, mas está atrás de um teste de existência de crypto e de crypto.getRandomValues, e o getRandomValues está disponível em qualquer contexto de navegação, incluindo os não seguros — ao contrário do crypto.subtle. Na prática, esse recuo é código inalcançável.
Segunda pergunta: quantos bits, e como contá-los sozinho
A aritmética cabe numa linha. Se cada carácter é extraído de forma independente e uniforme de um alfabeto de n carateres, cada carácter carrega log2(n) bits, e uma chave de comprimento L carrega L × log2(n). É tudo. Não há correção por «leva símbolos» nem por «parece complicada»: o tamanho do alfabeto e o comprimento são as únicas entradas, e é por isso que uma chave longa em minúsculas ganha a uma curta cheia de pontuação.
Aplique-a à predefinição. O conjunto Django é formado pelas vinte e seis minúsculas, pelos dez algarismos e pelos catorze carateres !@#$%^&*(-_=+) — cinquenta ao todo, sem repetidos, o que importa porque um carácter repetido encolheria em silêncio o alfabeto efetivo. log2(50) vale 5,644 bits por carácter, portanto a chave de 50 carateres por omissão carrega 282,2 bits. Passar à opção alfanumérica sobe o alfabeto para sessenta e dois e a chave para 297,7 bits; a opção completa sobe-o para setenta e seis e 312,4 bits. As três são enormes ao lado dos 128 bits normalmente citados como suficientes — vinte e três carateres do conjunto Django já os ultrapassariam.
O lado WordPress mede-se do mesmo modo. Emite as oito constantes do wp-config — as quatro chaves e os quatro sais — com 64 carateres cada, tiradas de um conjunto de 92: as sessenta e duas letras e algarismos, depois !@#$%^&*() e por fim -_ []{}<>~`+=,.;:/?|. É carácter a carácter o que produz o próprio wp_generate_password do WordPress com os seus dois argumentos de carateres especiais, e 64 × log2(92) dá 417,5 bits por constante. Dois pormenores desse conjunto merecem atenção: contém um espaço, o que é legal e um pouco surpreendente, e não contém nem plica nem barra invertida, e é por isso que o valor gerado cai em define('AUTH_KEY', '…'); sem qualquer escape.
O viés do módulo, dito com honestidade
Há um defeito na implementação e vale a pena enunciá-lo com precisão, porque uma formulação vaga seria mais alarmante do que o próprio defeito. Para transformar um número aleatório de 32 bits num índice dentro de um alfabeto de 50 carateres, o código tira o resto: índice igual a valor módulo 50. Dois elevado a trinta e dois é 4 294 967 296, e isso não se divide por cinquenta. O resto é quarenta e seis, portanto quarenta e seis dos cinquenta carateres podem sair de 85 899 346 dos valores possíveis e os outros quatro de 85 899 345.
Os carateres favorecidos são, portanto, mais prováveis do que os outros numa proporção de uma parte em 85 899 345, cerca de 1,2 por cem milhões. Para o conjunto WordPress de 92 carateres o resto é doze e a proporção cerca de 2,1 por cem milhões. Um gerador que corrigisse isto usaria amostragem por rejeição — tirar de novo sempre que o valor cai na cauda irregular — e ganharia uma diferença que nenhuma medição sobre uma chave de 50 carateres conseguiria detetar. É uma nota de rodapé, não um aviso.
O viés desaparece por completo quando o tamanho do alfabeto é uma potência de dois, porque então a divisão é exata: um alfabeto de 64 carateres ou de 256 valores não tem nenhum. É essa a razão elegante pela qual boa parte do material criptográfico se exprime em hexadecimal ou em base64: a aritmética é exata e não sobra nada para discutir.
Para que serve a chave, o que decide o que a rotação parte
A documentação do Django enumera exatamente o que depende de SECRET_KEY: cada sessão, a menos que use o backend de sessão por cache e tenha substituído o hash de autenticação de sessão por omissão; cada mensagem guardada num cookie; cada token de reposição de palavra-passe; e cada uso do framework de assinatura que não forneça a sua própria chave. A consequência está escrita na mesma página: quando uma chave deixa de ser SECRET_KEY e não consta de SECRET_KEY_FALLBACKS, tudo isso é invalidado de uma vez.
Na prática isso significa que toda a gente fica desligada e que cada ligação de reposição já numa caixa de correio deixa de funcionar — exatamente o que se quer depois de uma fuga, e exatamente o que não se quer numa terça-feira à tarde sem razão. O SECRET_KEY_FALLBACKS é a maneira documentada de ter as duas coisas: ponha a chave nova em SECRET_KEY, mantenha a antiga na lista de recuo o tempo suficiente para as sessões e as ligações pendentes expirarem, e depois retire-a. O Django é explícito quanto a que as palavras-passe dos utilizadores não derivam da chave e não são afetadas em caso algum.
O WordPress não oferece qualquer período de graça equivalente, e a sua própria documentação di-lo numa frase: pode mudar as chaves a qualquer momento para invalidar todos os cookies existentes, e isso significa que cada utilizador terá de entrar de novo. Não há lista de recuo; a mudança é imediata e total. A mesma página nota que as quatro chaves são obrigatórias e os quatro sais apenas recomendados, porque o WordPress gera ele próprio os sais se nenhum estiver definido — mais uma razão para colar as oito e não metade.
Onde uma chave bem gerada se queima na mesma
O gerador é a parte fácil. Quase todos os incidentes reais começam depois. Uma chave acaba num ficheiro de configuração que é commitado, numa captura de ecrã colada num ticket, numa mensagem a um colega, num registo de compilação, numa variável de ambiente impressa por um endpoint de depuração, ou numa camada de imagem de contentor que sobrevive ao ramo de onde veio. Nenhum desses casos é uma falha criptográfica; todos acabam com a utilidade da chave com igual firmeza.
O caso do controlo de versões merece frase própria, porque o instinto está errado. Apagar a linha e commitar a supressão não retira a chave: continua no histórico, continua em cada clone que alguém tenha feito e continua em cada fork e cada espelho do repositório. Reescrever o histórico só ajuda se também invalidar cada cópia já obtida, o que não pode. A única resposta sólida é dar a chave por pública e rodá-la.
Gerar no navegador está muito bem, e melhor do que parece. A chave é produzida pela página que já tem aberta, a partir da aleatoriedade do próprio sistema operativo, e só existe nesse separador até a copiar. Nada é enviado para lado nenhum, porque nada precisa de ser: tirar cinquenta índices e juntar cinquenta carateres cabe em poucas linhas, e um servidor só acrescentaria um sítio, não inspecionável, onde a chave teria existido.
| Definição | Alfabeto | Entropia |
|---|---|---|
| Django por omissão, 50 carateres | 50 — minúsculas, algarismos, !@#$%^&*(-_=+) | 282,2 bits — byte a byte o get_random_secret_key do Django |
| Django, alfanumérico, 50 carateres | 62 — letras das duas caixas e algarismos | 297,7 bits — seguro numa variável de shell, nada a proteger |
| Django, conjunto completo, 50 carateres | 76 — o conjunto alfanumérico mais os catorze símbolos | 312,4 bits — 30 bits mais do que a predefinição, sem ganho prático |
| Django, 100 carateres | 50 — o mesmo conjunto, o dobro do comprimento | 564,4 bits — a definição existe, a necessidade não |
| WordPress conjunto completo, 64 carateres, oito constantes | 92 — igual ao wp_generate_password com as duas bandeiras | 417,5 bits cada uma; contém um espaço, nem plica nem barra invertida |
| WordPress só alfanumérico, 64 carateres | 62 — letras e algarismos | 381,1 bits — 36 bits a menos, e nada a escapar em lado nenhum |
| Qualquer gerador que chame Math.random | O que disser | Efetivamente nula — o estado recupera-se a partir de umas poucas saídas |
Perguntas frequentes
- Uma chave gerada no meu navegador é tão segura como uma gerada num servidor?
- Mais segura, se acaso. O crypto.getRandomValues bebe da mesma entropia do sistema operativo que um servidor usaria, portanto a aleatoriedade é da mesma qualidade. A diferença é o trajeto da chave: gerada na página, existe num separador e não vai a lado nenhum até a copiar, ao passo que um gerador do lado do servidor significa que a chave existiu numa máquina que não controla, num processo que não pode inspecionar, talvez numa linha de registo que nunca verá. Recarregue a página ou feche o separador e o valor desaparece. A única coisa de que um navegador não o pode proteger é a área de transferência, e aquilo em que a colar a seguir.
- Que comprimento precisa mesmo de ter uma chave secreta?
- Pegue no valor por omissão do framework e deixe de pensar nisso. A função do Django devolve 50 carateres, que com o seu alfabeto de 50 são 282 bits; o WordPress emite 64 carateres de um alfabeto de 92, que são 418. O limiar habitualmente citado para um segredo simétrico ronda os 128 bits, e 23 carateres do alfabeto Django já o ultrapassam. O comprimento extra não custa nada e não traz nada, e as definições de 80 ou 100 carateres estão lá por exaustividade, não porque alguém precise delas. O comprimento só se torna a alavanca errada quando o alfabeto é minúsculo: uma chave de 20 carateres feita só de algarismos carrega 66 bits, menos de metade do que carrega uma de 20 carateres do conjunto Django.
- O viés do módulo significa que não devo usar este gerador?
- Não. O viés é real e vale 1,2 por cem milhões no alfabeto Django: quarenta e seis dos cinquenta carateres são alcançáveis a partir de 85 899 346 dos quatro mil milhões de valores de 32 bits possíveis, e os outros quatro a partir de 85 899 345. Repartido por cinquenta extrações independentes, move a entropia de uma chave numa quantidade muito abaixo de qualquer unidade que alguém imprimisse. Aquilo para que o viés serve mesmo é como marcador quando lê o gerador de outra pessoa: ver um módulo nu diz-lhe que ninguém pensou em amostragem por rejeição, o que é uma pista sobre o resto do código, e ver amostragem por rejeição diz-lhe que alguém pensou. Nenhuma das duas coisas é razão para rejeitar uma chave já com 282 bits.
- O que se parte no momento em que mudo a chave?
- No Django: cada sessão que não esteja no backend de cache, cada mensagem guardada num cookie e cada token de reposição de palavra-passe, incluindo as ligações que já estão nas caixas de correio. A documentação di-lo com clareza e dá a mitigação: mova a chave antiga para SECRET_KEY_FALLBACKS, deixe-a lá o tempo suficiente para as sessões e as ligações pendentes expirarem sozinhas, e depois apague-a. As palavras-passe dos utilizadores não derivam da chave e ficam intactas. No WordPress não há lista de recuo: mudar qualquer uma das oito constantes invalida de imediato todos os cookies existentes, e a documentação diz por palavras suas que todos os utilizadores terão de entrar de novo. Programe uma mudança de chave para uma hora calma, a não ser que esteja a responder a uma fuga, caso em que faça-o já e aceite as desconexões.
- Fiz commit da minha chave no git. Basta apagar a linha?
- Não. Um commit que apaga uma linha deixa o commit anterior intacto, portanto a chave continua no histórico e continua em cada clone, fork e espelho que já existe. Reescrever o histórico retira-a da sua cópia e de quem obtiver depois, mas não de quem obteve antes — e num repositório público há que assumir que isso inclui scanners automáticos, que vigiam os commits novos precisamente para isto. Dê a chave por publicada: gere outra, ponha a antiga na lista de recuo se o seu framework tiver uma, e retire-a passada a janela de transição. Depois torne a fuga estruturalmente impossível guardando o valor numa variável de ambiente ou num cofre de segredos e commitando apenas um ficheiro que nomeie a variável.
Artigos que podem interessar-lhe
Todos os guias →Ferramentas relacionadas
Tudo o que se segue descreve o que estas quatro ferramentas fazem hoje, verificado executando o seu próprio código sobre as entradas exatas reproduzidas em cada artigo, e não o que uma norma as obrigue a fazer. Quando uma ferramenta erra num caso, fica escrito com clareza em vez de contornado, e nada foi alterado para um artigo ler melhor. Duas consequências. Passe qualquer transformação primeiro por uma cópia e compare as duas pontas: uma ferramenta de texto que apaga alguma coisa não o anuncia. E dê um segredo por partilhado assim que ele sai da página: colá-lo numa conversa, num ticket ou num repositório queima-o, por melhor que tenha sido gerado.
Fontes
- Django Software Foundation — Settings reference — SECRET_KEY and SECRET_KEY_FALLBACKS
- Django (source) — django/core/management/utils.py — get_random_secret_key(), the 50-character alphabet
- WordPress — wp-config.php — Security Keys: changing them invalidates all existing cookies
- MDN Web Docs — Crypto.getRandomValues() — cryptographically strong values, and why Math.random() is not
Detetaste um erro neste artigo?