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
+48 -7
View File
@@ -1,19 +1,60 @@
"""
Calculadora de chave de reativação, para uso manual (reenvio/correção
pontual). 100% client-side (mesmo algoritmo gerarReativacao já usado no
workflow n8n e em Gerar chave WEB/index.html) — esta rota só serve a
página; NUNCA grava em Clientes_Chaves.
Calculadora de chave de reativação/instalação, para uso manual (reenvio/
correção pontual). O algoritmo em si (app/gerador_chave.py) roda só aqui
no backend — a página não recebe mais nada além do resultado já pronto,
pra ninguém conseguir extrair o algoritmo pelo código-fonte da página.
NUNCA grava em Clientes_Chaves.
"""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
import re
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from ..auth import exigir_login
from ..csrf import exigir_csrf, token_do_contexto
from ..gerador_chave import gerar_instalacao, gerar_reativacao
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
_RE_CODIGO = re.compile(r"^[0-9]{1,6}$")
_RE_REFERENCIA = re.compile(r"^\d{4}-\d{2}$")
@router.get("/gerador-chave", response_class=HTMLResponse)
def pagina(request: Request, usuario: str = Depends(exigir_login)):
return templates.TemplateResponse(request, "gerador_chave.html", {})
return templates.TemplateResponse(request, "gerador_chave.html", {"csrf_token": token_do_contexto(request)})
@router.post("/gerador-chave/gerar", dependencies=[Depends(exigir_csrf)])
def gerar(
tipo: str = Form(...),
codigo: str = Form(...),
referencia: str = Form(""),
modalidade: str = Form(""),
usuario: str = Depends(exigir_login),
):
codigo = codigo.strip()
if not _RE_CODIGO.match(codigo):
return JSONResponse({"erro": "Código do cliente deve ter até 6 dígitos numéricos."}, status_code=400)
codigo_formatado = codigo.zfill(6)
if tipo == "reativacao":
if not _RE_REFERENCIA.match(referencia):
return JSONResponse({"erro": "Selecione o mês/ano de referência."}, status_code=400)
ano_str, mes_str = referencia.split("-")
mes, ano = int(mes_str), int(ano_str)
if not (1 <= mes <= 12):
return JSONResponse({"erro": "Mês fora do intervalo 1-12."}, status_code=400)
if not (2000 <= ano <= 2100):
return JSONResponse({"erro": "Ano inválido (use entre 2000 e 2100)."}, status_code=400)
chave = gerar_reativacao(codigo_formatado, mes, ano)
elif tipo == "instalacao":
if modalidade not in ("ALUGUEL", "COMPRA"):
return JSONResponse({"erro": "Modalidade inválida."}, status_code=400)
chave = gerar_instalacao(codigo_formatado, modalidade)
else:
return JSONResponse({"erro": "Tipo de chave inválido."}, status_code=400)
return JSONResponse({"chave": chave})