Pular para o conteúdo

Serviço de autenticação

Cadastro, login com scrypt, bloqueio progressivo e token assinado com prazo.

Autenticação é onde um projeto pequeno mais facilmente fica inseguro: a senha guardada com SHA-256, o login que diz “usuário não existe” (e com isso confirma quais e-mails têm conta), e o token que nunca vence. Este projeto faz as três coisas do jeito certo, com o que a biblioteca já tem.

PeçaO que ela exercita
Crypto.hash_passwordscrypt, com sal por senha
Crypto.hmaco token assinado
tentativas por contao bloqueio progressivo
a mesma resposta nos dois errosnão confirmar quais contas existem

Estrutura#

text
auth/
  src/
    senhas.df      forca, hash, conferencia
    contas.df      cadastro e login
    tokens.df      emitir e conferir
  tests/
    login_test.df
forge.toml
[project]
name = "auth"
version = "0.1.0"
description = "Autenticação"
entry = "src/main.df"
dataforge = ">=1.1"

[dependencies]

[scripts]
start = "run src/main.df"
test = "test tests/"

O núcleo#

Este bloco roda sozinho — copie para um arquivo e rode dataforge run. Ele termina com assert, e é assim que esta página é conferida a cada build.

src/contas.df
adopt Arcane.Crypto as Crypto

steady CHAVE := "troque-por-uma-variavel-de-ambiente"
contas := {}
falhas := {}

action cadastrar(email, senha):
    given len(senha) smaller 10:
        trigger "a senha precisa de pelo menos 10 caracteres"
    given email in contas:
        trigger "nao foi possivel cadastrar"
    contas[email] := Crypto.hash_password(senha)

// A MESMA mensagem para "nao existe" e "senha errada": a diferenca e
// exatamente o que quem ataca quer descobrir.
action entrar(email, senha, agora):
    f := falhas[email] ?? {"n": 0, "ate": 0}
    given agora smaller f["ate"]:
        yield {"ok": no, "motivo": "tente mais tarde"}
    guardada := contas[email] ?? void
    given guardada is void or not Crypto.verify_password(senha, guardada):
        n := f["n"] + 1
        // 1, 2, 4, 8... segundos a partir da terceira falha
        espera := 0 given n smaller 3 otherwise 2 ** (n - 3)
        falhas[email] := {"n": n, "ate": agora + espera}
        yield {"ok": no, "motivo": "email ou senha incorretos"}
    falhas[email] := {"n": 0, "ate": 0}
    yield {"ok": yes, "token": emitir(email, agora + 3600)}

action emitir(email, vence):
    carga := $"{email}|{vence}"
    yield $"{carga}|{Crypto.hmac(CHAVE, carga)}"

action conferir(token, agora):
    partes := token.split("|")
    given len(partes) is not 3:
        yield void
    carga := $"{partes[0]}|{partes[1]}"
    given not Crypto.hmac_verify(CHAVE, carga, partes[2]):
        yield void
    given int(partes[1]) smaller_eq agora:
        yield void
    yield partes[0]

cadastrar("ana@exemplo.com", "cavalo-bateria-grampo")
ok := entrar("ana@exemplo.com", "cavalo-bateria-grampo", 1000)
assert ok["ok"]
assert conferir(ok["token"], 1001) is "ana@exemplo.com"
assert conferir(ok["token"], 99999) is void
assert conferir(ok["token"].replace("ana", "eva"), 1001) is void

a := entrar("ana@exemplo.com", "errada", 2000)
b := entrar("ninguem@exemplo.com", "errada", 2000)
assert a["motivo"] is b["motivo"]

cycle i in range(0, 4):
    entrar("ana@exemplo.com", "errada", 3000)
assert entrar("ana@exemplo.com", "cavalo-bateria-grampo", 3000)["motivo"] is "tente mais tarde"
assert entrar("ana@exemplo.com", "cavalo-bateria-grampo", 3100)["ok"]
out "autenticacao verde"

O teste#

No projeto, a regra mora em src/ e o teste a importa pelo caminho relativo — dataforge test tests/ descobre o arquivo sozinho.

tests/nucleo_test.df
adopt ../src/contas as C

crucible "login":
    trial "o token adulterado nao vale":
        C.cadastrar("bia@exemplo.com", "uma-senha-longa")
        t := C.entrar("bia@exemplo.com", "uma-senha-longa", 0)["token"]
        expect C.conferir(t + "x", 1) is void

As decisões#

DecisãoSem ela
scrypt, e não SHA-256uma GPU testa bilhões de SHA-256 por segundo
mesma resposta nos dois erroso formulário vira um oráculo de quais e-mails têm conta
espera progressiva, não bloqueio fixoquem ataca bloqueia a conta da vítima de propósito
hmac_verify, não iscomparar a assinatura com is vaza, pelo tempo, quantos caracteres acertaram
o prazo dentro da carga assinadatrocar o prazo não invalida a assinatura

Para ir além#

Volte para todos os tipos de projeto.