Por que o Base64 precisa de preenchimento
O Base64 converte 3 bytes em 4 caracteres (o algoritmo completo está em como o Base64 funciona). Quando o comprimento da entrada não é múltiplo de 3, o último bloco fica curto:
| Bytes no último bloco | Caracteres de dados | Preenchimento | Exemplo |
|---|---|---|---|
| 3 | 4 | nenhum | Man → TWFu |
| 2 | 3 | = | Ma → TWE= |
| 1 | 2 | == | M → TQ== |
O = mantém o comprimento da saída múltiplo de 4. Isso facilitava a vida dos decodificadores da era MIME, que podiam ler a entrada em grupos fixos de 4 caracteres e saber onde os dados terminam sem acompanhar o comprimento total.
A rigor, essa informação é redundante. Um grupo final de 3 caracteres só pode significar 2 bytes, e um grupo de 2 caracteres só pode significar 1 byte, então um decodificador que conta caracteres não precisa de = nenhum. É por isso que o Base64 sem preenchimento é possível e por que tantos formatos o usam.
Existe um caso em que o preenchimento carrega informação real: a concatenação. Se duas strings com preenchimento forem coladas, o = no meio marca onde a primeira termina. Mas os decodificadores lidam mal com isso. Passar TQ==TQ== (dois M codificados) para o decodificador padrão do Python retornou b'M\x04\xd0', o Buffer do Node retornou só M, e o atob lançou um erro. Decodifique cada pedaço separadamente.
Quando o preenchimento é opcional
A RFC 4648 §3.2 diz que os codificadores DEVEM acrescentar o preenchimento, a menos que a especificação que usa o Base64 diga explicitamente o contrário. Várias dizem:
- JWT e JOSE (RFC 7515) usam Base64URL sem o preenchimento. Um header de JWT como
eyJhbGciOiJIUzI1NiJ9não tem=mesmo quando o comprimento não é múltiplo de 4. Veja Base64 no JWT. - URLs e nomes de arquivo costumam levar Base64URL sem preenchimento, porque o
=tem significado em query strings e precisaria de escape como%3D. A página Base64 vs Base64URL explica a diferença completa. - WebAuthn e OAuth PKCE (o
code_challengeda RFC 7636) também usam Base64URL sem preenchimento.
O Base64 padrão em e-mail, arquivos PEM, data URIs e na maioria das APIs JSON tem preenchimento. Na dúvida, gere saída com preenchimento para o Base64 padrão e sem preenchimento para o Base64URL, e faça seu decodificador aceitar as duas.
Decodificadores que exigem preenchimento
O comportamento varia mais do que se imagina. Estes são os resultados de decodificar TWE (Ma sem preenchimento) com o decodificador padrão de cada biblioteca:
| Linguagem / ferramenta | Chamada | Resultado para TWE |
|---|---|---|
| Python 3.13 | base64.b64decode | binascii.Error: Incorrect padding |
| Python 3.13 | base64.urlsafe_b64decode | Incorrect padding também |
| .NET | Convert.FromBase64String | FormatException |
| Go | base64.StdEncoding | erro; use RawStdEncoding para entrada sem preenchimento |
| Go | base64.RawStdEncoding | Ma, mas rejeita TWE= |
| Ruby | Base64.strict_decode64 | ArgumentError: invalid base64 |
| Ruby | Base64.decode64, urlsafe_decode64 | Ma |
| Java 21 | Base64.getDecoder().decode | Ma |
| Node.js | Buffer.from(s, 'base64'), atob | Ma |
| PHP 8.4 | base64_decode (também no modo estrito) | Ma |
| GNU coreutils 9.7 | base64 -d | Ma |
| OpenSSL 3.5 | openssl base64 -d | saída vazia, código de saída 0 |
Repare na linha do OpenSSL: ele não imprimiu nada e ainda assim relatou sucesso, então um script que passa Base64 sem preenchimento pelo openssl base64 -d gera um arquivo vazio sem avisar. Nos mesmos testes, ele também não retornou nada para uma única linha sem quebra de linha no final; passar -A resolveu esse caso.
O Go é o mais rígido no sentido oposto: o RawStdEncoding recusa uma string que tem preenchimento. Se você aceita entrada de várias fontes, remova os = e use a codificação raw, ou acrescente o preenchimento e use a padrão.
Exemplos por linguagem estão nas páginas de código, por exemplo Base64 em Python e Base64 em Go.
Como restaurar o preenchimento que falta
A regra depende só do comprimento da string depois de remover os espaços em branco:
len % 4 | O que acrescentar | Significado |
|---|---|---|
| 0 | nada | blocos completos |
| 2 | == | o último bloco contém 1 byte |
| 3 | = | o último bloco contém 2 bytes |
| 1 | não tem conserto | truncada, ou um caractere a mais |
Resto 1 nunca é válido. Um caractere Base64 carrega 6 bits, o que não basta para um byte, então nenhuma quantidade de preenchimento torna a string decodificável. Se você vir isso, procure um caractere faltando, um caractere a mais ou uma quebra de linha que entrou na contagem.
A correção é uma expressão na maioria das linguagens:
import base64
s = "eyJzdWIiOiIxMjM0In0" # 19 chars, 19 % 4 == 3
padded = s + "=" * (-len(s) % 4) # adds one "="
print(base64.urlsafe_b64decode(padded))
# b'{"sub":"1234"}'
const s = "eyJzdWIiOiIxMjM0In0";
const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
// "eyJzdWIiOiIxMjM0In0="
O validador de Base64 informa o comprimento e o estado do preenchimento de uma string, e o Reparar Base64 acrescenta o = que falta, remove espaços em branco e aspas e converte caracteres URL-safe numa só etapa.
Codificações não canônicas
O preenchimento tem um parente mais discreto: os bits não usados do último caractere de dados. Quando Ma é codificado, seus 16 bits ocupam dois grupos de 6 bits e meio. O terceiro caractere carrega os últimos 4 bits de a mais 2 bits de enchimento, e a RFC 4648 §3.5 diz que o codificador deve zerar esses bits. Isso dá E (000100).
Um decodificador ignora os bits de enchimento, então TWF= (em que F é 000101, bits de enchimento 01) decodifica para o mesmo Ma. Python, Node, Java e PHP aceitaram TWF= e retornaram Ma nos nossos testes. Da mesma forma, TR== decodifica para M, assim como TQ==.
Strings como TWF= são chamadas de não canônicas. Elas importam em alguns lugares:
- Assinaturas e hashes sobre texto Base64. Duas strings diferentes decodificam para os mesmos bytes, então comparar strings Base64 para saber se os dados são iguais pode dar a resposta errada. Compare os bytes decodificados.
- Deduplicação e chaves de cache. O mesmo problema: normalize decodificando e codificando de novo.
- Parsers rígidos. A RFC 4648 permite que os decodificadores rejeitem bits de enchimento diferentes de zero. O
StdEncoding.Strict()do Go e obase64 -ddo GNU rejeitamTWF=.
Preenchimento a mais é uma esquisitice parecida. TWE== tem um = sobrando. O Python e o Buffer do Node aceitaram, e o atob lançou InvalidCharacterError.
Preenchimento ao codificar em pedaços
Se você codifica um arquivo grande em Base64 por partes, todas as partes, menos a última, precisam ter um múltiplo de 3 bytes. Caso contrário, cada parte ganha seu próprio preenchimento e a saída unida fica errada. Codificar hello world!! em pedaços de 4 bytes deu aGVsbA==byB3bw==cmxkIQ==IQ==; em pedaços de 3 bytes deu aGVsbG8gd29ybGQhIQ==, idêntico a codificar tudo de uma vez. Para a quebra de linha do MIME, 57 bytes por linha é o tamanho natural do pedaço, já que 57 bytes viram exatamente 76 caracteres.
Cole uma string com preenchimento ausente ou quebrado e receba Base64 válido de volta.
Perguntas frequentes
Posso simplesmente apagar os sinais = do Base64?
Os dados sobrevivem, porque o comprimento da string já diz ao decodificador quantos bytes o último bloco contém. Se o resultado ainda decodifica depende de quem recebe: o Base64URL dos JWTs espera ausência de preenchimento, enquanto o b64decode do Python e o Convert.FromBase64String do .NET rejeitam a string sem ele.
Por que minha string Base64 não tem = no final?
Ou o comprimento da entrada era múltiplo de 3 bytes, e o preenchimento não foi necessário, ou quem gerou a string usa Base64 sem preenchimento, o que é comum no Base64URL de tokens e URLs.
Como corrigir o erro Incorrect padding no Python?
Acrescente '=' * (-len(s) % 4) antes de decodificar. Se a string for URL-safe, use também base64.urlsafe_b64decode. Se o comprimento mod 4 for 1, o preenchimento não vai ajudar: a string está truncada ou tem um caractere a mais.
Uma string Base64 pode terminar com três sinais =?
Não. Um bloco final contém 1 ou 2 bytes restantes, que produzem == ou =. Três = significariam um bloco sem dados, o que não existe.