diff --git a/app/gerador_chave.py b/app/gerador_chave.py new file mode 100644 index 0000000..b7ce02f --- /dev/null +++ b/app/gerador_chave.py @@ -0,0 +1,56 @@ +""" +Algoritmo de geração de chave (reativação e instalação), reverso-projetado +do sistema legado (Delphi). Fica só aqui no backend — NUNCA deve voltar a +ser exposto em JavaScript de página, porque isso permite qualquer pessoa +com acesso ao painel extrair o algoritmo e gerar chaves fora do sistema. + +Verificado byte-a-byte contra 3 exemplos reais de chave de instalação +(cliente 000001, ALUGUEL): byte inicial 0xDC -> "DC45092C", +0x2E -> "2E35083D", 0x3D -> "3D25090C" (ver testes). +""" +import secrets + +CHAVE_INTERNA = ( + "YUQL23KL23DF90WI5E1JAS467NMCXXL6JAOAUWWMCL0AOMM4A4VZYW9KHJUI2347" + "EJHJKDF3424SKL K3LAKDJSL9RTIKJ" +) + + +def _cifrar(texto: str, byte_inicial: int) -> list[int]: + """Cifra por feedback de byte anterior + XOR com CHAVE_INTERNA rotativa. + Retorna a lista de bytes cifrados (sem o byte_inicial na frente).""" + anterior = byte_inicial + cifrados = [] + for i, ch in enumerate(texto): + valor = (ord(ch) + anterior) % 255 + cifrado = valor ^ ord(CHAVE_INTERNA[i % len(CHAVE_INTERNA)]) + cifrados.append(cifrado) + anterior = cifrado + return cifrados + + +def gerar_reativacao(codigo_cliente: str, mes: int, ano: int, byte_inicial: int | None = None) -> str: + """Chave de reativação — mês/cliente/ano cifrados por inteiro (22 caracteres hex).""" + cliente = str(codigo_cliente).zfill(6) + texto = str(mes).zfill(2) + cliente + str(ano)[-2:] + aleatorio = byte_inicial if byte_inicial is not None else secrets.randbelow(256) + cifrados = _cifrar(texto, aleatorio) + return "".join(f"{v:02X}" for v in [aleatorio, *cifrados]) + + +def gerar_instalacao(codigo_cliente: str, modalidade: str, byte_inicial: int | None = None) -> str: + """Chave de instalação — só a modalidade ("30"=ALUGUEL / "00"=COMPRA) é + cifrada; o valor derivado do código do cliente (cliente*19+31) entra sem + cifrar, no meio dos 6 caracteres cifrados (3 antes + 3 depois).""" + try: + cliente_num = int(str(codigo_cliente)) + except ValueError: + cliente_num = 0 + valor_cliente = cliente_num * 19 + 31 + texto_modalidade = "30" if modalidade == "ALUGUEL" else "00" + + aleatorio = byte_inicial if byte_inicial is not None else secrets.randbelow(256) + cifrados = _cifrar(texto_modalidade, aleatorio) + criptografado = "".join(f"{v:02X}" for v in [aleatorio, *cifrados]) + parte1, parte2 = criptografado[0:3], criptografado[3:6] + return f"{parte1}{valor_cliente}{parte2}" diff --git a/app/routers/chave_manual.py b/app/routers/chave_manual.py index 56abdbe..2b53043 100644 --- a/app/routers/chave_manual.py +++ b/app/routers/chave_manual.py @@ -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}) diff --git a/app/templates/gerador_chave.html b/app/templates/gerador_chave.html index 75fd13a..3437a31 100644 --- a/app/templates/gerador_chave.html +++ b/app/templates/gerador_chave.html @@ -34,56 +34,11 @@