Autenticação
Senha e hashing (scrypt, PBKDF2, bcrypt, Argon2id), MFA, 2FA, TOTP, WebAuthn e passkeys, OAuth 2.0, OIDC, SAML, LDAP, JWT, tokens e sessão — com o veredito de cada um.
Autenticação responde quem é você. Esta página percorre os mecanismos usados na indústria e diz, para cada um, o que a linguagem entrega — e o que ela não entrega.
Senha: o hashing#
Guardar senha é guardar um derivado lento e salgado dela. A lentidão é o recurso: ela não incomoda quem faz um login e inviabiliza quem faz bilhões.
| Algoritmo | Estado aqui | O veredito |
|---|---|---|
| scrypt | o padrão, Crypto.hash_password | memory-hard: exige ~32 MB por tentativa, e memória é o que a GPU não tem em abundância por núcleo |
| PBKDF2-SHA256 | disponível, algoritmo := "pbkdf2" | só encadeia hash — barato de acelerar em GPU. Fica para compatibilidade e para senhas antigas |
| Argon2id | não existe | seria o melhor; em Python puro rodaria lento a ponto de exigir parâmetros fracos, o que o torna pior que o scrypt |
| bcrypt | não existe | exige uma implementação de Blowfish; o hashlib não a traz, e escrevê-la em Python é o tipo de código criptográfico que não se deve escrever |
adopt Arcane.Crypto as Crypto
// O sal e sorteado e vai DENTRO da string guardada: guarde-a inteira.
guardada := Crypto.hash_password("uma senha bem forte 2026")
out guardada[0:24] // scrypt$32768$8$1$...
assert Crypto.verify_password("uma senha bem forte 2026", guardada)
assert Crypto.verify_password("outra senha", guardada) is no
// Duas vezes a MESMA senha dao strings diferentes — e o sal e o
// motivo: sem ele, senhas iguais teriam o mesmo resumo, e uma
// tabela pronta quebraria todas de uma vez.
assert Crypto.hash_password("igual") isnt Crypto.hash_password("igual")Rotação: o único momento em que dá para atualizar#
Sem isto, um banco fica para sempre no algoritmo com que nasceu — ninguém sabe quais linhas estão velhas, e não há momento em que a senha em claro esteja disponível para regravar. Exceto um: o login bem-sucedido.
adopt Arcane.Crypto as Crypto
action entrar(senha, guardada):
given Crypto.verify_password(senha, guardada) is no:
yield {"ok": no}
// Aqui — e so aqui — a senha em claro existe e ja foi conferida.
given Crypto.precisa_rehash(guardada):
yield {"ok": yes, "regravar": Crypto.hash_password(senha)}
yield {"ok": yes}
// Uma senha guardada no formato antigo continua entrando...
antiga := Crypto.hash_password("segredo do usuario", algoritmo := "pbkdf2")
r := entrar("segredo do usuario", antiga)
assert r["ok"]
// ...e sai do login ja no formato novo.
assert "regravar" in keys(r)
out "o formato antigo entra, e e atualizado na saida"Força, política e vazamento#
“Senha fraca” não diz o que fazer. O que vai para a tela é a lista do que falta.
adopt Arcane.Seguranca as Seg
fraca := Seg.forca_da_senha("Abcdef1!")
out $"{fraca['rotulo']} ({fraca['nota']}/100)"
cycle problema in fraca["problemas"]:
out $" - {problema}"
// A politica devolve; 'exigir_politica' levanta com a lista em
// 'e.nota', para quem escreve o caminho feliz.
r := Seg.politica("Tr0vao#Azul7291!x")
assert r["ok"]
// E a entropia, que e uma medida do TEXTO e nao de como ele foi
// escolhido: por isso ela e um dos criterios, e nunca o unico.
out $"entropia: {r['entropia']} bits"Múltiplos fatores#
Um fator é algo que você sabe (senha), tem (telefone, chave física) ou é (biometria). MFA exige de categorias diferentes — duas senhas não são dois fatores.
| Fator | Estado aqui | Observação |
|---|---|---|
| TOTP (app autenticador) | existe, RFC 6238 completo | o segundo fator com melhor relação custo/benefício |
| HOTP (por contador) | existe, RFC 4226 | base do TOTP; serve para token físico de contador |
| Códigos de recuperação | existe | devolve o código e o resumo — guarde só o resumo |
| SMS | não há integração | desaconselhado: SIM swap e interceptação de SS7 |
| WebAuthn / passkeys / FIDO2 | não existe | é um protocolo com CBOR, COSE e atestação, não uma função |
| Chave física (YubiKey) | não existe | chega por WebAuthn |
adopt Arcane.Seguranca as Seg
// Os vetores do RFC 4226. Uma implementacao de OTP que nao os
// reproduz esta errada mesmo que "funcione": o autenticador do
// usuario vai discordar dela.
assert Seg.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 0) is "755224"
assert Seg.hotp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 1) is "287082"
// O cadastro: sorteie o segredo, mostre o QR, e guarde o segredo
// CIFRADO — ele vale tanto quanto a senha.
segredo := Seg.totp_segredo()
out Seg.totp_uri(segredo, "ana@loja.com", emissor := "Minha Loja")
// A conferencia aceita a janela vizinha: o relogio do telefone anda
// alguns segundos fora, e um codigo digitado no segundo 29 chega no
// 31. Sem tolerancia, uma fracao real dos logins legitimos falha —
// e o usuario aprende a desligar o segundo fator.
codigo := Seg.totp_agora(segredo)
assert Seg.totp_conferir(segredo, codigo)
assert Seg.totp_conferir(segredo, "000000") is no
// E os codigos de recuperacao, para quem perdeu o telefone.
r := Seg.codigos_de_recuperacao(10)
out $"{len(r['codigos'])} codigos; guarde apenas os {len(r['resumos'])} resumos"WebAuthn e passkeys — o veredito#
São hoje o mecanismo mais forte disponível: a chave privada nunca sai do dispositivo, e a assinatura é ligada ao domínio, o que torna o phishing estruturalmente impossível.
OAuth 2.0, OIDC, SAML, LDAP#
São integrações com sistemas externos, e não primitivas. O que a biblioteca traz são as peças locais de cada fluxo — as que envolvem criptografia e que são justamente onde as implementações erram.
| Protocolo | Para quê | Estado aqui |
|---|---|---|
| OAuth 2.0 | autorização delegada — acesso a um recurso | as peças locais: pkce, estado_de_oauth; o HTTP é Arcane.Malha |
| OpenID Connect | autenticação sobre OAuth — quem é a pessoa | a verificação do id_token é Crypto.jwt_verificar (HS\; para RS256 é preciso um serviço)* |
| SAML | SSO corporativo em XML | não existe — exige XML-DSig e canonicalização, cheios de armadilhas |
| LDAP / Active Directory | diretório corporativo | não existe — é um protocolo binário próprio |
adopt Arcane.Seguranca as Seg
// PKCE (RFC 7636) — o que o OAuth 2.0 exige hoje, e a peca que a
// biblioteca entrega. O 'code' volta pela URL do navegador; num app
// de celular ou numa SPA nao existe segredo do cliente para provar
// quem e, e quem interceptar o codigo o troca por um token.
par := Seg.pkce()
// Ao PEDIR o codigo, manda-se o desafio (o SHA-256 do verificador).
out $"code_challenge={par['desafio'][0:16]}... method={par['metodo']}"
// Ao TROCAR o codigo, manda-se o verificador. So quem sorteou o tem.
assert Seg.conferir_pkce(par["verificador"], par["desafio"])
assert Seg.conferir_pkce("outro-verificador-que-nao-e-o-certo-123456", par["desafio"]) is no
// E o 'state', que e a defesa de CSRF do fluxo: sem ele, um atacante
// inicia o proprio fluxo e faz a vitima completa-lo — a conta dela
// acaba ligada a conta dele no provedor.
estado := Seg.estado_de_oauth()
// guarde na sessao, e CONFIRA na volta. Guardar e nao conferir e o
// defeito mais comum deste fluxo.
assert len(estado) bigger 20
out "as pecas locais do OAuth, e a rede fica com o Arcane.Malha"JWT#
Um JWT é um objeto JSON assinado. Ele é útil quando o receptor precisa validar sem consultar um banco — e é exatamente por isso que ele é difícil de revogar.
adopt Arcane.Crypto as Crypto
chave := Crypto.random_hex(32)
token := Crypto.jwt_assinar({"sub": "ana", "papel": "editor"}, chave,
expira_em := 900)
// O ALGORITMO E DECIDIDO POR QUEM VERIFICA, e nao pelo token.
// Essa e a falha classica do JWT: aceitar o 'alg' que vem dentro
// permite 'alg: none' — e o token passa a ser texto assinado por
// ninguem. Aqui o algoritmo e argumento da verificacao.
r := Crypto.jwt_verificar(token, chave, algoritmo := "HS256")
assert r["valido"]
assert r["carga"]["sub"] is "ana"
// 'jwt_ler' le SEM verificar — para inspecionar, nunca para decidir.
out Crypto.jwt_ler(token)["papel"]
// Ele DEVOLVE o veredito em vez de levantar: o motivo entra no log,
// e quem chama decide o que responder ao cliente.
outra := Crypto.jwt_verificar(token, "outra chave", algoritmo := "HS256")
assert outra["valido"] is no
out $"recusado: {outra['motivo']}"| A armadilha | Como se fecha aqui |
|---|---|
alg: none | o algoritmo é argumento de quem verifica, nunca lido do token |
| confusão HS256/RS256 | idem — não há como o token escolher |
| token que não expira | expira_em é conferido na verificação |
| revogação | não tem solução no formato: use prazo curto + refresh |
| dado sensível no corpo | o corpo é base64, não é cifrado — qualquer um lê |
Tokens e sessão#
| Tipo | Vida | Onde guardar | Como revogar |
|---|---|---|---|
| acesso | 5–15 min | memória do cliente | não se revoga: expira |
| refresh | dias/semanas | cookie HttpOnly | no banco — e rotacione a cada uso |
| sessão | horas | cookie HttpOnly | apagar do armazém |
| API key | longa | cofre do cliente | no banco, por prefixo |
| de uso único (e-mail, senha) | minutos | não se guarda | propósito + prazo, e marcar usado |
adopt Arcane.Seguranca as Seg
chave := Seg.chave_de_assinatura()
// Um link de redefinir senha: proposito E prazo. Sem o proposito, o
// token que confirma um e-mail SERVE para trocar a senha — os dois
// sao assinados com a mesma chave.
link := Seg.assinar({"usuario": 42}, chave, proposito := "trocar-senha")
monitor:
Seg.ler_assinado(link, chave, prazo := 900, proposito := "confirmar-email")
assert no
handle SignatureError as e:
out "recusado: o proposito e outro"
lido := Seg.ler_assinado(link, chave, prazo := 900, proposito := "trocar-senha")
assert lido["valor"]["usuario"] is 42
// E a URL assinada, para download temporario: mudar QUALQUER
// parametro invalida a assinatura.
u := Seg.assinar_url("https://loja.com/baixar?id=9", chave, prazo := 60)
assert Seg.conferir_url(u, chave)["ok"]O cookie de sessão#
| Atributo | Porque |
|---|---|
HttpOnly | o JavaScript não lê — um XSS deixa de virar roubo de sessão |
Secure | não viaja em HTTP puro |
SameSite=Lax | não vai numa requisição de outro site (defesa de CSRF) |
Path=/, sem Domain | não vaza para subdomínio de terceiro |
| id novo após o login | impede fixação de sessão |