Por qué Base64 necesita relleno
Base64 convierte 3 bytes en 4 caracteres (el algoritmo completo está en cómo funciona Base64). Cuando la longitud de la entrada no es múltiplo de 3, el último bloque queda incompleto:
| Bytes en el último bloque | Caracteres de datos | Relleno | Ejemplo |
|---|---|---|---|
| 3 | 4 | ninguno | Man → TWFu |
| 2 | 3 | = | Ma → TWE= |
| 1 | 2 | == | M → TQ== |
El = mantiene la longitud de la salida en un múltiplo de 4. Eso facilitaba la vida a los decodificadores de la época de MIME, que podían leer la entrada en grupos fijos de 4 caracteres y saber dónde terminan los datos sin llevar la cuenta de la longitud total.
En sentido estricto, la información es redundante. Un último grupo de 3 caracteres solo puede significar 2 bytes, y uno de 2 caracteres solo puede significar 1 byte, así que a un decodificador que cuenta caracteres no le hace falta ningún =. Por eso es posible el Base64 sin relleno y por eso lo usan muchos formatos.
Hay un caso en el que el relleno sí aporta información real: la concatenación. Si se pegan dos cadenas con relleno, el = del medio marca dónde termina la primera. Aun así, los decodificadores lo manejan mal. Al pasar TQ==TQ== (dos M codificadas) al decodificador por defecto de Python, devolvió b'M\x04\xd0', Buffer de Node devolvió solo M y atob lanzó un error. Decodifica cada trozo por separado.
Cuándo el relleno es opcional
RFC 4648 §3.2 dice que los codificadores DEBEN añadir relleno salvo que la especificación que usa Base64 diga explícitamente lo contrario. Varias lo hacen:
- JWT y JOSE (RFC 7515) usan Base64URL sin relleno. Una cabecera JWT como
eyJhbGciOiJIUzI1NiJ9no tiene=aunque la longitud no sea múltiplo de 4. Consulta Base64 en JWT. - URLs y nombres de archivo suelen llevar Base64URL sin relleno, porque
=tiene un significado en las cadenas de consulta y habría que escaparlo como%3D. La página Base64 vs Base64URL explica la diferencia completa. - WebAuthn y OAuth PKCE (el
code_challengede RFC 7636) también usan Base64URL sin relleno.
El Base64 estándar del correo, los archivos PEM, las data URI y la mayoría de las APIs JSON lleva relleno. En caso de duda, genera salida con relleno para el Base64 estándar y sin relleno para Base64URL, y haz que tu decodificador acepte ambas.
Decodificadores que exigen relleno
El comportamiento varía más de lo que se suele pensar. Estos son los resultados de decodificar TWE (Ma sin relleno) con el decodificador por defecto de cada biblioteca:
| Lenguaje / herramienta | Llamada | Resultado para TWE |
|---|---|---|
| Python 3.13 | base64.b64decode | binascii.Error: Incorrect padding |
| Python 3.13 | base64.urlsafe_b64decode | también Incorrect padding |
| .NET | Convert.FromBase64String | FormatException |
| Go | base64.StdEncoding | error; usa RawStdEncoding para entradas sin relleno |
| Go | base64.RawStdEncoding | Ma, pero rechaza 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 (también en modo estricto) | Ma |
| GNU coreutils 9.7 | base64 -d | Ma |
| OpenSSL 3.5 | openssl base64 -d | salida vacía, código de salida 0 |
Fíjate en la fila de OpenSSL: no imprimió nada y aun así informó de éxito, así que un script que pase Base64 sin relleno por openssl base64 -d genera sin avisar un archivo vacío. En las mismas pruebas tampoco devolvió nada con una única línea sin salto de línea final; pasar -A resolvió ese caso.
Go es el más estricto en el sentido contrario: RawStdEncoding rechaza una cadena que sí tiene relleno. Si aceptas entradas de varias fuentes, quita los = y usa la codificación raw, o añade el relleno y usa la estándar.
Hay ejemplos para cada lenguaje en las páginas de código, por ejemplo Base64 en Python y Base64 en Go.
Cómo restaurar el relleno que falta
La regla depende solo de la longitud de la cadena tras quitar los espacios en blanco:
len % 4 | Qué añadir | Significado |
|---|---|---|
| 0 | nada | bloques completos |
| 2 | == | el último bloque tiene 1 byte |
| 3 | = | el último bloque tiene 2 bytes |
| 1 | no tiene arreglo | cortada, o con un carácter de más |
Un resto de 1 nunca es válido. Un carácter Base64 lleva 6 bits, que no bastan para un byte, así que ningún relleno lo hace decodificable. Si te encuentras con esto, busca un carácter que falte, uno que sobre o un salto de línea que se haya contado.
El arreglo es una sola expresión en la mayoría de los lenguajes:
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="
El validador Base64 indica la longitud y el estado del relleno de una cadena, y Reparar Base64 añade los = que faltan, quita espacios y comillas y convierte los caracteres URL-safe en un solo paso.
Codificaciones no canónicas
El relleno tiene un pariente más discreto: los bits sobrantes del último carácter de datos. Al codificar Ma, sus 16 bits llenan dos grupos y medio de 6 bits. El tercer carácter lleva los últimos 4 bits de a más 2 bits de relleno, y RFC 4648 §3.5 dice que el codificador debe poner esos bits a cero. Eso da E (000100).
Un decodificador ignora esos bits, así que TWF= (donde F es 000101, con bits sobrantes 01) se decodifica como el mismo Ma. Python, Node, Java y PHP aceptaron TWF= y devolvieron Ma en nuestras pruebas. Del mismo modo, TR== se decodifica como M, igual que TQ==.
Las cadenas como TWF= se llaman no canónicas. Importan en algunos casos:
- Firmas y hashes sobre texto Base64. Dos cadenas distintas se decodifican en los mismos bytes, así que comparar cadenas Base64 para saber si los datos son iguales puede dar una respuesta incorrecta. Compara los bytes decodificados.
- Deduplicación y claves de caché. El mismo problema: normaliza decodificando y volviendo a codificar.
- Analizadores estrictos. RFC 4648 permite a los decodificadores rechazar bits sobrantes distintos de cero.
StdEncoding.Strict()de Go ybase64 -dde GNU rechazanTWF=.
El relleno de más es una rareza relacionada. TWE== tiene un = de sobra. Python y Buffer de Node lo aceptaron; atob lanzó InvalidCharacterError.
Relleno al codificar por trozos
Si codificas en Base64 un archivo grande por partes, todos los trozos salvo el último deben medir un múltiplo de 3 bytes. Si no, cada trozo recibe su propio relleno y la salida unida es incorrecta. Codificar hello world!! en trozos de 4 bytes dio aGVsbA==byB3bw==cmxkIQ==IQ==; en trozos de 3 bytes dio aGVsbG8gd29ybGQhIQ==, idéntico a codificarlo todo de una vez. Para dividir en líneas al estilo MIME, 57 bytes por línea es el tamaño natural del trozo, ya que 57 bytes se convierten exactamente en 76 caracteres.
Pega una cadena con el relleno ausente o roto y obtén un Base64 válido.
Preguntas frecuentes
¿Puedo borrar sin más los signos = de un Base64?
Los datos se conservan, porque la longitud de la cadena ya le dice al decodificador cuántos bytes tiene el último bloque. Que el resultado se siga decodificando depende del receptor: el Base64URL de los JWT espera que no haya relleno, mientras que b64decode de Python y Convert.FromBase64String de .NET rechazan la cadena sin él.
¿Por qué mi cadena Base64 no tiene = al final?
O la longitud de la entrada era múltiplo de 3 bytes y no hacía falta relleno, o quien la generó usa Base64 sin relleno, algo habitual en el Base64URL de tokens y URLs.
¿Cómo arreglo un error Incorrect padding en Python?
Añade '=' * (-len(s) % 4) antes de decodificar. Si la cadena es URL-safe, usa además base64.urlsafe_b64decode. Si la longitud módulo 4 es 1, el relleno no ayudará: la cadena está cortada o tiene un carácter de más.
¿Puede una cadena Base64 terminar con tres signos =?
No. El último bloque contiene 1 o 2 bytes sobrantes, que producen == o =. Tres = significarían un bloque sin datos, y eso no existe.