📬 Toda semana, um resumo de segurança, Clean Code e IA no seu e-mail Inscreva-se →

Exponenciação Modular Rápida e Segura: Square-and-Multiply na Prática

Já vi código em produção fazendo a ** e antes de aplicar o módulo, com e de várias centenas de bits. O processo travava, a memória disparava, e a primeira reação de quem escreveu foi “deve ser bug na lib”. Não era. Era matemática básica: número elevado a expoente grande antes de reduzir cresce exponencialmente, e nenhuma biblioteca aguenta isso de graça.

Se você vai implementar RSA, Diffie-Hellman ou qualquer coisa que dependa de ae mod n com expoentes grandes, esse artigo é sobre como fazer essa conta rápido — e sobre um detalhe que a maioria só descobre depois de apanhar: a própria otimização de performance pode abrir uma porta para ataque.

Por que a implementação ingênua não sobrevive

Método 1 — potência primeiro, módulo depois:

result = a ** e
result %= n

Simples de ler, inviável na prática — o valor intermediário a ** e explode antes de você conseguir reduzi-lo.

Método 2 — multiplicação iterativa:

result = 1
for _ in range(e):
    result = (result * a) % n

Melhor, porque mantém os números sempre dentro de uma faixa controlada — mas ainda exige uma multiplicação por unidade do expoente. Para um expoente de 2048 bits, isso é proibitivo.

Square-and-Multiply: de O(e) para O(log e)

A virada acontece quando você para de tratar o expoente como uma contagem e passa a tratá-lo como uma sequência de bits. Convertendo e para binário, dá para reduzir o número de multiplicações de O(e) para O(log e) — uma diferença gigantesca quando e tem centenas de bits.

Existem duas formas equivalentes de implementar isso.

Esquerda para direita

  1. Converta o expoente para binário (s+1 bits).
  2. Inicialize result = 1.
  3. Para cada bit, do mais significativo ao menos significativo:
  4. Eleve ao quadrado: result = (result * result) % n
  5. Se o bit for 1, multiplique: result = (result * a) % n

Direita para esquerda

  1. Converta o expoente para binário.
  2. Inicialize result = a se o bit menos significativo for 1, senão result = 1. Defina base = a.
  3. Para cada bit restante, do menos significativo ao mais significativo:
  4. Eleve ao quadrado a base: base = (base * base) % n
  5. Se o bit for 1, multiplique: result = (result * base) % n

As duas chegam no mesmo resultado — a diferença é só a ordem de varredura dos bits e qual variável carrega o acumulador. Na prática, a escolha entre uma e outra costuma depender de qual encaixa melhor na arquitetura onde o código vai rodar.

Rodando o exemplo: a²³

23 em binário é 10111. Usando esquerda-para-direita:

IteraçãoBitOperaçãoValor (pré-módulo)Resultado parcial
Initinicializa1
11quadrado + multiplicaaa
20quadrado
31quadrado + multiplicaa⁴ × a = a⁵a⁵
41quadrado + multiplicaa¹⁰ × a = a¹¹a¹¹
51quadrado + multiplicaa²² × a = a²³a²³

Economia real: 7 multiplicações contra 22 do método ingênuo — mais de 70% de operações a menos. Multiplica isso por um expoente de 2048 bits e a diferença deixa de ser acadêmica: é a diferença entre um handshake TLS instantâneo e um que trava o usuário na tela de carregamento.

O problema que a otimização introduz

Aqui está o ponto que costuma pegar quem implementou isso pensando só em performance: a etapa condicional — “multiplica só se o bit for 1” — cria uma diferença observável de comportamento dependendo do valor do bit. Essa diferença aparece em:

  • Tempo de execução (um branch a mais quando o bit é 1)
  • Consumo de energia (mais operações = mais corrente consumida)

Um atacante monitorando essas variáveis — timing attack ou power analysis — pode, com repetições suficientes, inferir os bits do expoente secreto. Ou seja: o mesmo algoritmo que você otimizou para ser rápido é, por construção, mais fácil de espionar do que uma versão “lenta” que sempre faz o mesmo trabalho independente do bit.

Isso não é motivo para descartar square-and-multiply — é motivo para não parar na primeira versão que funciona e passa nos testes de correção.

Como mitigar sem abrir mão da performance

  • Algoritmos de tempo constante: eliminar desvios condicionais que dependem do valor dos bits do expoente.
  • Montgomery Ladder: garante uma sequência fixa de quadrado-e-multiplica para todo bit, independente do valor — elimina a assimetria que o atacante exploraria. Vale um artigo à parte para detalhar a mecânica de redução por trás dela: “Redução de Montgomery: Otimizando a Multiplicação Modular em Hardware”.
  • Exponent blinding: randomizar o expoente antes da computação, de forma que o resultado final continue correto mas o padrão de bits observado não corresponda ao expoente real. O fundamento matemático dessa técnica — apoiado no Teorema de Euler — está detalhado em “Exponenciação Modular Randomizada: Como Blindar RSA Contra Ataques de Canal Lateral”.
  • Base blinding: adicionar máscaras aleatórias às entradas, removendo-as depois do cálculo.

Fechando

Exponenciação modular eficiente é o que viabiliza criptografia rápida e de baixo consumo de energia — mas “eficiente” e “seguro” não vêm de graça juntos. Square-and-multiply reduz drasticamente o custo computacional, só que, sem cuidado adicional, também abre uma janela de vazamento por canal lateral. Combinar técnicas de tempo constante com algoritmos como Montgomery Ladder é o que entrega as duas coisas ao mesmo tempo — velocidade e robustez.

Se você mantém código de criptografia em produção, vale parar agora e checar: sua implementação de exponenciação modular tem algum branch condicional dependente de bit secreto? Se a resposta for sim, os dois artigos linkados acima (Montgomery e randomização) são o próximo passo lógico. Comenta se você já teve que revisar código assim depois de um pentest — quero saber que tipo de vazamento apareceu na prática.

Leandro Rosendo Candido

Leandro Rosendo Candido

Engenheiro de Computação, mestre pela USP, 10+ anos com Embedded Linux. Escreve e grava sobre segurança, Clean Code e IA aplicada à programação.

Rolar para cima