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
+27 -65
View File
@@ -34,56 +34,11 @@
</div>
<script>
const CHAVE_INTERNA =
"YUQL23KL23DF90WI5E1JAS467NMCXXL6JAOAUWWMCL0AOMM4A4VZYW9KHJUI2347" +
"EJHJKDF3424SKL K3LAKDJSL9RTIKJ";
// ==========================
// REATIVAÇÃO — mês/cliente/ano cifrados por inteiro (22 caracteres)
// ==========================
function gerarReativacao(codigoCliente, mes, ano, byteInicial = null) {
const cliente = String(codigoCliente).padStart(6, '0');
const texto = String(mes).padStart(2, '0') + cliente + String(ano).slice(-2);
const aleatorio = byteInicial !== null ? byteInicial : Math.floor(Math.random() * 256);
let anterior = aleatorio;
const resultado = [aleatorio];
const bytes = [];
for (let i = 0; i < texto.length; i++) bytes.push(texto.charCodeAt(i) & 0xff);
for (let i = 0; i < bytes.length; i++) {
const valor = (bytes[i] + anterior) % 255;
const cifrado = valor ^ CHAVE_INTERNA.charCodeAt(i % CHAVE_INTERNA.length);
resultado.push(cifrado);
anterior = cifrado;
}
return resultado.map(v => v.toString(16).padStart(2, '0').toUpperCase()).join('');
}
// ==========================
// INSTALAÇÃO — só a modalidade ("30"=ALUGUEL / "00"=COMPRA) é cifrada
// (mesmo algoritmo de cifra da reativação, texto diferente); o valor
// derivado do código do cliente (cliente×19+31) entra sem cifrar, no
// meio dos 6 caracteres cifrados (3 antes + 3 depois).
// ==========================
function gerarInstalacao(codigoCliente, modalidade, byteInicial = null) {
const clienteNum = parseInt(String(codigoCliente), 10) || 0;
const valorCliente = clienteNum * 19 + 31;
const textoModalidade = modalidade === 'ALUGUEL' ? '30' : '00';
const aleatorio = byteInicial !== null ? byteInicial : Math.floor(Math.random() * 256);
let anterior = aleatorio;
const bytes = [aleatorio];
for (let i = 0; i < textoModalidade.length; i++) {
const valor = (textoModalidade.charCodeAt(i) + anterior) % 255;
const cifrado = valor ^ CHAVE_INTERNA.charCodeAt(i % CHAVE_INTERNA.length);
bytes.push(cifrado);
anterior = cifrado;
}
const criptografado = bytes.map(v => v.toString(16).padStart(2, '0').toUpperCase()).join('');
const parte1 = criptografado.substring(0, 3);
const parte2 = criptografado.substring(3, 6);
return `${parte1}${valorCliente}${parte2}`;
}
// O algoritmo de geração de chave roda só no backend (app/gerador_chave.py)
// — esta página só manda os dados do formulário pro endpoint e mostra o
// resultado. Nunca reintroduza a lógica de cifra aqui: qualquer pessoa com
// acesso à página consegue ler o código-fonte JS pelo navegador.
const CSRF_TOKEN = "{{ csrf_token }}";
const $tipo = document.getElementById('tipo');
const $codigo = document.getElementById('codigo');
@@ -117,25 +72,32 @@ function mostrarErro(msg) {
$copiar.style.display = 'none';
}
document.getElementById('gerar').addEventListener('click', () => {
document.getElementById('gerar').addEventListener('click', async () => {
$erro.style.display = 'none';
const codigoCliente = String($codigo.value || '').trim();
if (!/^[0-9]{1,6}$/.test(codigoCliente)) return mostrarErro('Código do cliente deve ter até 6 dígitos numéricos.');
const codigoFormatado = codigoCliente.padStart(6, '0');
let chave;
if ($tipo.value === 'reativacao') {
if (!/^\d{4}-\d{2}$/.test($referencia.value)) return mostrarErro('Selecione o mês/ano de referência.');
const [anoStr, mesStr] = $referencia.value.split('-');
const mesNum = Number(mesStr), anoNum = Number(anoStr);
if (!(mesNum >= 1 && mesNum <= 12)) return mostrarErro('Mês fora do intervalo 1-12.');
if (!(anoNum >= 2000 && anoNum <= 2100)) return mostrarErro('Ano inválido (use entre 2000 e 2100).');
chave = gerarReativacao(codigoFormatado, mesNum, anoNum);
} else {
chave = gerarInstalacao(codigoFormatado, $modalidade.value);
const corpo = new URLSearchParams({
csrf_token: CSRF_TOKEN,
tipo: $tipo.value,
codigo: $codigo.value,
referencia: $referencia.value,
modalidade: $modalidade.value,
});
let resposta, dados;
try {
resposta = await fetch('/gerador-chave/gerar', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: corpo,
});
dados = await resposta.json();
} catch (e) {
return mostrarErro('Não foi possível falar com o servidor — tente de novo.');
}
$resultado.textContent = chave;
if (!resposta.ok) return mostrarErro(dados.erro || 'Não foi possível gerar a chave.');
$resultado.textContent = dados.chave;
$resultado.style.display = 'block';
$copiar.style.display = 'inline-block';
});