fix(seguranca): move algoritmo de geracao de chave do JS pro backend

O algoritmo (CHAVE_INTERNA + funcoes gerarReativacao/gerarInstalacao)
ficava 100% legivel no codigo-fonte da pagina — qualquer pessoa com
acesso ao painel conseguia abrir o DevTools e extrair a logica pra
gerar chaves de licenca fora do sistema.

Move tudo pra app/gerador_chave.py (roda so no servidor) e expoe via
POST /gerador-chave/gerar (autenticado, protegido por CSRF). A pagina
agora so manda os dados do formulario e mostra o resultado que volta
do backend. Algoritmo verificado byte-a-byte contra os 3 exemplos
reais conhecidos (cliente 000001, ALUGUEL). Bonus: o byte inicial
agora usa secrets.randbelow (CSPRNG) em vez de Math.random() do JS.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-14 10:03:19 -03:00
co-authored by Claude Sonnet 5
parent a3a84abd37
commit 5c577a644a
5 changed files with 253 additions and 76 deletions
+41
View File
@@ -0,0 +1,41 @@
from app.gerador_chave import gerar_instalacao, gerar_reativacao
def test_gerar_instalacao_bate_com_exemplo_real_byte_dc():
assert gerar_instalacao("000001", "ALUGUEL", byte_inicial=0xDC) == "DC45092C"
def test_gerar_instalacao_bate_com_exemplo_real_byte_2e():
assert gerar_instalacao("000001", "ALUGUEL", byte_inicial=0x2E) == "2E35083D"
def test_gerar_instalacao_bate_com_exemplo_real_byte_3d():
assert gerar_instalacao("000001", "ALUGUEL", byte_inicial=0x3D) == "3D25090C"
def test_gerar_instalacao_compra_usa_modalidade_00():
# Mesmo byte inicial, modalidade diferente -> cifrado diferente do ALUGUEL.
aluguel = gerar_instalacao("000001", "ALUGUEL", byte_inicial=0xDC)
compra = gerar_instalacao("000001", "COMPRA", byte_inicial=0xDC)
assert aluguel != compra
assert compra.startswith("DC") # byte inicial some no começo, sem cifrar
def test_gerar_instalacao_sem_byte_inicial_gera_aleatorio_valido():
chave = gerar_instalacao("000001", "ALUGUEL")
assert len(chave) == 8
assert chave[3:5].isdigit() # valor_cliente embutido no meio
def test_gerar_reativacao_e_deterministico_com_byte_fixo():
chave1 = gerar_reativacao("000123", 8, 2026, byte_inicial=0x10)
chave2 = gerar_reativacao("000123", 8, 2026, byte_inicial=0x10)
assert chave1 == chave2
assert len(chave1) == 22 # 11 bytes (1 aleatorio + 10 do texto) em hex
def test_gerar_reativacao_sem_byte_inicial_varia():
# Estatisticamente, gerar várias vezes sem byte fixo não deve sempre
# bater igual (chance de colisão é 1/256).
chaves = {gerar_reativacao("000123", 8, 2026) for _ in range(20)}
assert len(chaves) > 1
+81 -4
View File
@@ -8,18 +8,95 @@ app_teste = FastAPI()
app_teste.include_router(router_chave_manual.router)
app_teste.dependency_overrides[exigir_login] = lambda: "teste"
client = TestClient(app_teste)
CSRF_TESTE = "teste-csrf-token"
client.cookies.set("csrf_token", CSRF_TESTE)
def test_pagina_gerador_carrega():
resposta = client.get("/gerador-chave")
assert resposta.status_code == 200
assert "gerarReativacao" in resposta.text
assert "gerarInstalacao" in resposta.text
assert "Gerador de Chave" in resposta.text
# O algoritmo NUNCA pode voltar a aparecer no HTML/JS da página — ele
# roda só no backend (app/gerador_chave.py) desde que essa exposição
# foi corrigida.
assert "CHAVE_INTERNA" not in resposta.text
assert "function gerarReativacao" not in resposta.text
assert "function gerarInstalacao" not in resposta.text
def test_pagina_gerador_nao_grava_nada():
# Rota é GET-only: não existe POST /gerador-chave, então não há como
# este router gravar em Clientes_Chaves.
# GET /gerador-chave não recebe csrf_token no cookie/form combinando
# com nada que grave — só há dois POSTs possíveis nesse router:
# /gerador-chave (não existe, 405) e /gerador-chave/gerar (não grava
# em Clientes_Chaves, só calcula e devolve).
resposta = client.post("/gerador-chave")
assert resposta.status_code == 405
def test_gerar_reativacao_com_sucesso():
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "reativacao", "codigo": "123", "referencia": "2026-08"},
)
assert resposta.status_code == 200
corpo = resposta.json()
assert len(corpo["chave"]) == 22
assert all(c in "0123456789ABCDEF" for c in corpo["chave"])
def test_gerar_instalacao_com_sucesso_bate_com_exemplo_conhecido():
# Não dá pra fixar o byte aleatório via HTTP, mas confere que o valor do
# cliente (1*19+31=50) aparece embutido no meio da chave, como o
# algoritmo real exige.
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "instalacao", "codigo": "000001", "modalidade": "ALUGUEL"},
)
assert resposta.status_code == 200
corpo = resposta.json()
assert len(corpo["chave"]) == 8
assert corpo["chave"][3:5] == "50"
def test_gerar_codigo_invalido_bloqueia():
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "instalacao", "codigo": "abc123", "modalidade": "ALUGUEL"},
)
assert resposta.status_code == 400
assert "6 dígitos" in resposta.json()["erro"]
def test_gerar_reativacao_sem_referencia_bloqueia():
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "reativacao", "codigo": "123", "referencia": ""},
)
assert resposta.status_code == 400
assert "mês/ano" in resposta.json()["erro"]
def test_gerar_instalacao_modalidade_invalida_bloqueia():
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "instalacao", "codigo": "123", "modalidade": "X"},
)
assert resposta.status_code == 400
assert "Modalidade" in resposta.json()["erro"]
def test_gerar_tipo_invalido_bloqueia():
resposta = client.post(
"/gerador-chave/gerar",
data={"csrf_token": CSRF_TESTE, "tipo": "outracoisa", "codigo": "123"},
)
assert resposta.status_code == 400
assert "Tipo de chave" in resposta.json()["erro"]
def test_gerar_sem_csrf_bloqueia():
resposta = client.post(
"/gerador-chave/gerar",
data={"tipo": "instalacao", "codigo": "123", "modalidade": "ALUGUEL"},
)
assert resposta.status_code == 403