Base64 ou Base64URL : ce qui change et quand utiliser chacun
Base64URL, c'est le Base64 standard avec deux caractères remplacés, - pour + et _ pour /, et en général sans le padding =.
Les deux encodages sont définis dans la RFC 4648 : le Base64 standard dans la section 4, et la variante « URL and filename safe », base64url, dans la section 5. Ils découpent les données en groupes de 6 bits exactement de la même manière et partagent 62 de leurs 64 caractères. Seuls diffèrent les caractères des valeurs 62 et 63, ainsi que la convention de padding (remplissage). Passer de l'un à l'autre revient donc à échanger des caractères, sans réencoder les données, et les confondre est un bug facile à commettre qui passe souvent inaperçu.
Les deux alphabets côte à côte
| Base64 (RFC 4648, section 4) | Base64URL (RFC 4648, section 5) | |
|---|---|---|
| Valeurs 0 à 61 | A-Z a-z 0-9 | A-Z a-z 0-9 |
| Valeur 62 | + | - |
| Valeur 63 | / | _ |
Padding avec = | Obligatoire, sauf si le format qui l'utilise en dispose autrement | Généralement omis |
| Utilisable tel quel dans les URL et les noms de fichiers | Non | Oui |
Les mêmes octets sous les deux formes :
Bytes fb ff bf
Base64 +/+/
Base64URL -_-_
Text <<???>>
Base64 PDw/Pz8+Pg==
Base64URL PDw_Pz8-Pg
Les données binaires encodées, comme les hashs, les clés et les tokens aléatoires, contiennent très souvent un + ou un / quelque part : la différence n'a rien d'un cas marginal. La RFC 4648 précise elle-même que base64url ne doit pas être considéré comme le même encodage que base64, et que le code doit nommer la variante qu'il attend.
Pourquoi le Base64 standard casse dans les URL et les noms de fichiers
Chacun des trois caractères supplémentaires du Base64 standard a un sens ailleurs :
+est lu comme une espace lorsqu'une chaîne de requête est décodée comme des données de formulaire, ce que fontURLSearchParams,$_GETen PHP et la plupart des frameworks côté serveur. Un token commeab+carrive sous la formeab c./sépare les segments de chemin : une valeur Base64 placée dans un chemin crée des segments supplémentaires, et certains serveurs web refusent la forme encodée%2Fdans les chemins. Ce caractère est aussi interdit dans les noms de fichiers sous Windows, macOS et Linux.=sépare le nom d'un paramètre de sa valeur.
Encoder la valeur en pourcent (%2B, %2F, %3D) fonctionne si chaque morceau de code concerné le fait exactement une fois. En pratique, une couche oublie de le faire ou le fait deux fois : c'est pourquoi les formats conçus pour les URL utilisent Base64URL et évitent tout échappement.
Les règles de padding
Base64 encode 3 octets en 4 caractères. Quand la longueur des données n'est pas un multiple de 3, le dernier groupe est plus court, et le Base64 standard le complète jusqu'à 4 caractères avec = :
| Octets dans le dernier groupe | Base64 | Base64URL sans padding |
|---|---|---|
1, par exemple ff | /w== | _w |
2, par exemple fb ff | +/8= | -_8 |
3, par exemple fb ff bf | +/+/ | -_-_ |
La section 3.2 de la RFC 4648 impose le padding, sauf si la spécification qui s'y réfère indique explicitement le contraire. La section 5 ajoute que = devrait normalement être encodé en pourcent dans une URL, et que le padding peut être omis lorsque la longueur des données est de toute façon connue. Les formats fondés sur Base64URL profitent de cette possibilité : les JSON Web Signatures (RFC 7515), et donc les JWT, suppriment tous les = finaux, tout comme les code challenges PKCE (RFC 7636).
Sans padding, n octets produisent ⌈4n/3⌉ caractères : 16 octets donnent 22 caractères au lieu de 24, et 32 octets en donnent 43 au lieu de 44. Une longueur dont le reste de la division par 4 vaut 1 n'est jamais valide, quelle que soit la variante.
Où chacun est utilisé
| Contexte | Variante |
|---|---|
| En-tête, payload et signature d'un JWT | Base64URL sans padding |
Valeurs des JSON Web Keys, comme n et e | Base64URL sans padding |
code_challenge de PKCE (OAuth) | Base64URL sans padding |
Data URI, data:image/png;base64,... | Base64 standard (RFC 2397) |
| Pièces jointes d'e-mail (MIME) | Base64 standard, lignes de 76 caractères au maximum (RFC 2045) |
| Certificats et clés PEM | Base64 standard, lignes de 64 caractères (RFC 7468) |
| En-tête d'authentification HTTP Basic | Base64 standard (RFC 7617) |
| Tokens dans les URL, noms de fichiers, clés de cache | Base64URL est le choix sûr |
Les fonctions par défaut des bibliothèques suivent le Base64 standard : btoa, b64encode en Python, base64_encode en PHP et Base64.getEncoder() en Java produisent tous +, / et du padding. Base64URL doit être demandé explicitement.
Convertir de l'un à l'autre
atob et btoa ne connaissent que le Base64 standard : avec elles, c'est à vous de convertir les caractères et de corriger le padding.
// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);
Les navigateurs récents proposent aussi Uint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true }) et Uint8Array.fromBase64(s, { alphabet: 'base64url' }), qui travaillent directement sur les octets ; vérifiez leur prise en charge dans les navigateurs que vous ciblez avant de vous y fier. Les autres plateformes offrent un support direct ou une formule courte :
// Node.js
Buffer.from(data).toString('base64url'); // no padding
Buffer.from(s, 'base64url');
# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);
// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);
En Python, urlsafe_b64decode rejette une entrée sans padding, d'où l'ajout des signes =. En PHP, base64_decode accepte un padding manquant, même en mode strict.
Les pièges
- Des bugs qui passent les tests. Un décodeur standard n'échoue que sur une entrée Base64URL qui contient
-ou_. Pour des données aléatoires, cela concerne environ la moitié des valeurs de 16 octets et environ les trois quarts des valeurs de 32 octets, comme les empreintes SHA-256 (1 − (62/64) puissance 42 ≈ 74 %). Un test avec une ou deux valeurs d'exemple peut donc facilement passer. Pourquoi les décodeurs rejettent ces chaînes, et comment chaque langage réagit : tout est expliqué dans Chaîne Base64 invalide. - Les décodeurs permissifs masquent la confusion. Sous Node.js,
Buffer.from(s, 'base64')accepte les deux alphabets, ce qui est pratique mais signifie que le bug n'apparaît que dans un autre service. Leb64decodepar défaut de Python ignore silencieusement-et_, puis renvoie de mauvais octets ou une erreur trompeuse sur le nombre de caractères. - Base64URL n'est pas l'encodage URL. Encoder en pourcent une chaîne Base64 standard donne
%2B,%2Fet%3D: cela ne la transforme pas en Base64URL, et un décodeur qui attend l'un n'acceptera pas l'autre. - Aucune des deux variantes ne cache quoi que ce soit. Le payload d'un JWT n'est que du JSON encodé en Base64URL, lisible par quiconque détient le token. Collez-en un dans le décodeur JWT pour voir son en-tête et son payload.
L'encodeur Base64 de ce site produit du Base64 standard avec padding ; appliquez la substitution ci-dessus si vous avez besoin de Base64URL. Pour le décodage, le décodeur Base64 accepte les deux alphabets et ajoute lui-même le padding manquant : vous pouvez donc coller l'une ou l'autre forme.