Décodage Base64 en Java : un guide complet
Il arrive dans un ticket de support, une réponse API, un secret Kubernetes, ou enfoui au milieu d'une URL : une longue suite de lettres et de chiffres, avec de-ci de-là un +, un /, un - ou un _, et parfois un ou deux = en traîne à la fin. Quelqu'un affirme que c'est du Base64 et qu'il contient ce qu'il vous faut : un mot de passe, un payload JSON, un certificat, une photo. Ce guide est la recette Java pour le récupérer. Repères rapides, car la page d'accueil détaille le format en profondeur : Base64 réécrit chaque trio d'octets de données en quatre caractères tirés d'un alphabet de 64 lettres, et accroche un ou deux = à la fin quand le dernier bloc est court. Décoder, c'est le sens qui réduit dans cet échange : quatre caractères entrent, trois octets sortent, si bien que le résultat a toujours besoin d'environ un quart d'espace de moins que l'entrée.
Voici la bonne nouvelle, et elle est bonne. Depuis le 18 mars 2014, chaque JDK embarque une boîte à outils Base64 complète dans la bibliothèque standard : java.util.Base64. Pas de téléchargement, pas de coordonnée Maven, pas de bibliothèque native. Un import, sept méthodes de fabrique, trois alphabets, et le même comportement de Java 8 jusqu'au Java 26 d'aujourd'hui. Tout cet article est construit sur cette unique classe.
Une limite honnête avant de commencer : c'est ici le côté décodeur de l'histoire. Vous allez apprendre à choisir le bon décodeur pour l'alphabet que vous rencontrez, à lire les messages d'erreur du JDK comme un médecin lit une radio, à transformer des octets en texte sans charabia, à déballer l'armure PEM, à faire défiler des payloads de plusieurs gigaoctets, et à repérer les pièges de sécurité que le format laisse discrètement sur la route. L'autre direction, enfiler des octets dans une chaîne, a son propre guide, et il est lié à la fin de celui-ci.
Ce que vous possédez déjà
Installer Base64 en Java, c'est la réponse en une ligne que vous donnez au tableau blanc : « C'est dans le JDK. » La classe java.util.Base64 fait partie du module java.base depuis la 1.8, et sa javadoc dit toujours Since: 1.8 en 2026. La seule chose que vous installez est un JDK, n'importe quel Java 8 ou plus récent de n'importe quel éditeur (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) fonctionne, et sur une machine basée sur Debian c'est une seule commande :
sudo apt install openjdk-17-jdk-headless
L'API est une fabrique : vous ne construisez jamais de décodeur ; vous demandez à la classe de vous en donner un. Les sept méthodes de fabrique distribuent trois personnalités dans chaque direction, et le côté décodeur ressemble à ceci :
| Méthode de fabrique | Alphabet | Tempérament | Quand y recourir |
|---|---|---|---|
getDecoder() |
A-Z a-z 0-9 + / |
Strict : refuse tout caractère hors alphabet | Les données que vous produisez ou contrôlez |
getUrlDecoder() |
A-Z a-z 0-9 - _ |
Strict, alphabet URL-safe | JWT, jetons, identifiants, tout ce qui naît dans une URL |
getMimeDecoder() |
A-Z a-z 0-9 + / |
Permissif : saute tout caractère hors alphabet | Emails, entrées réellement enveloppées, corps PEM |
getEncoder(), getUrlEncoder(), getMimeEncoder() |
comme ci-dessus | Encodage, territoire du guide sœur | Chaque fois que vous produisez du Base64 plutôt que de le lire |
Trois propriétés des instances renvoyées méritent d'être mémorisées. D'abord, elles sont thread-safe : la javadoc dit que les instances sont « sûres pour une utilisation par plusieurs threads concurrents », et le code source montre que les méthodes de fabrique renvoient la même instance partagée à chaque appel, si bien que Base64.getDecoder() == Base64.getDecoder() est vrai. Construisez un décodeur dans un champ statique et partagez-le dans tout votre service ; vous ne copiez même rien. Deuxièmement, elles sont sans état entre les appels, donc il n'y a rien à réinitialiser et rien à synchroniser. Troisièmement, passer null là où un tableau d'octets ou une chaîne est attendu n'est pas un no-op bienveillant : c'est un NullPointerException, exactement comme le promet la javadoc de la classe.
Vous croiserez encore de vieilles bibliothèques dans les bases de code, alors voici une carte rapide du paysage. Apache Commons Codec (actuellement 1.22.1) porte sa propre org.apache.commons.codec.binary.Base64 depuis la 1.0, avec une API Builder qui expose la politique strict ou permissif, la longueur de ligne et le séparateur comme des molettes ; c'est le bon outil seulement si vous devez soutenir des JVM antérieures à Java 8 ou si vous voulez ses aides de vérification de forme. Guava livre com.google.common.io.BaseEncoding, un vétéran aux capacités comparables, toujours populaire dans les piles big data. Pour tout ce qui tourne sur une JVM moderne, java.util.Base64 est le choix par défaut : zéro dépendance, et les benchmarks de la communauté le trouvent toujours le plus rapide du lot (on y revient dans la section performance).
Décoder votre première chaîne
Quatre-vingt-dix pour cent de la vie du décodage tiennent dans une poignée de lignes. Voici toute la cérémonie, en utilisant le plus petit exemple que la RFC elle-même emploie pour expliquer l'alphabet :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstDecode {
public static void main(String[] args) {
byte[] bytes = Base64.getDecoder().decode("TWFu");
String text = new String(bytes, StandardCharsets.UTF_8);
System.out.println(text); // Man
}
}
Quatre phrases sur ce qui vient de se passer. D'abord, le point d'entrée est une instance, pas la classe : decode() vit sur l'objet Base64.Decoder que la fabrique vous a donné. Deuxièmement, et c'est la décision de conception la plus importante de toute l'API, le résultat est un tableau d'octets, jamais une String. Le payload peut être une phrase, un JPEG ou un hash, et aucun de ces trois ne doit être traité pareil avant de savoir ce que vous avez, alors le JDK s'arrête volontairement aux octets. Troisièmement, le saut des octets vers le texte est une étape séparée et délibérée, avec un charset explicite, et c'est dans cette étape que « café » tourne au charabia si vous n'êtes pas attentif ; la section charset plus bas lui est dédiée. Quatrièmement, la chaîne vide est une valeur de plein droit : Base64.getDecoder().decode("") vous donne un tableau de longueur nulle, sans exception, sans histoire.
Comme données de test dans votre tête, retenez que TWFu est le test de fumée de la norme elle-même : si votre code de décodage en fait Man, la machine est honnête. L'aller-retour dans l'autre sens fait deux lignes de la même API et reçoit le traitement complet dans le guide d'encodage lié à la fin.
La gamme de décodeurs
Java ne vous donne pas un décodeur ; il vous en donne trois, et la différence entre eux est une décision de politique sur l'alphabet à accepter et la quantité de saleté à tolérer. Les trois sont des instances de la même classe imbriquée Base64.Decoder. La javadoc de la classe énonce la séparation en une phrase par tempérament. Pour les décodeurs de base et URL-safe : le décodeur « refuse les données qui contiennent des caractères hors de l'alphabet base64 ». Pour le décodeur MIME : « tous les séparateurs de lignes et autres caractères non trouvés dans la table de l'alphabet base64 sont ignorés lors de l'opération de décodage ». Cette deuxième phrase est toute l'histoire du MIME en une ligne, et elle a des crocs, parce que « ignorés » signifie tout ce qui n'est pas un caractère de l'alphabet, pas seulement les retours à la ligne.
La règle de choix est courte. Partez sur getDecoder() par défaut. Si la valeur vient d'une URL, d'un jeton ou d'une API qui a promis « URL-safe », passez à getUrlDecoder(). Ce n'est que si vous attendez vraiment une entrée de forme MIME (retours à la ligne tous les 76 caractères, direct d'un système de mail) que vous saisissez getMimeDecoder(). En cas de doute, choisissez strict : le travail d'un décodeur strict est de faire échouer les surprises, et c'est exactement ce que vous voulez à une frontière de confiance. Un décodeur permissif, en revanche, est une loupe pour la corruption : une chaîne avec des caractères égarés dedans se décodera en quelque chose de plausible et de faux, sans aucune erreur.
Lire les plaintes du décodeur
Les décodeurs stricts échouent fort, et ils échouent précisément. Chaque mauvaise entrée lève une IllegalArgumentException dont le message vous dit exactement ce qui a mal tourné, alors la première fois qu'une chaîne de production explose, c'est cette table que vous lisez. Les messages ci-dessous sont le texte exact du JDK actuel :
| Entrée (vers getDecoder sauf mention contraire) | Ce qui ne va pas | Message exact |
|---|---|---|
"SGVs bG8s" |
une espace s'est glissée | Illegal base64 character 20 |
"SGVs\nbG8s" |
un retour à la ligne s'est glissé | Illegal base64 character a |
"SGVs$bG8s" |
un signe dollar n'est pas dans l'alphabet | Illegal base64 character 24 |
"SGVsbG8-" |
un tiret URL-safe dans le décodeur standard | Illegal base64 character 2d |
"ab+c" vers getUrlDecoder() |
un signe plus dans le décodeur URL-safe | Illegal base64 character 2b |
"S" |
un symbole ne peut pas former un octet | Input byte[] should at least have 2 bytes for base64 bytes |
"SG=VsbG8s" |
du padding au milieu des données | Input byte array has wrong 4-byte ending unit |
"Zm8==" |
deux pads là où un seul a sa place | Input byte array has incorrect ending byte at 4 |
"Z=" |
un caractère suivi d'un pad | Last unit does not have enough valid bits |
"SGVsbG8sIHdvcmxkIQ==xx" |
de la saleté après les pads | Input byte array has incorrect ending byte at 20 |
Ce nombre hexadécimal dans le message est la valeur en octet du caractère fautif, imprimée avec Integer.toString(byte, 16) : 20 est une espace, a est un retour à la ligne, d est un retour chariot, 24 est un signe dollar, 2d est le tiret URL-safe, 2b le plus, 2f la barre oblique, 5f le tiret bas. Deux bizarreries à garder en poche. D'abord, le message peut passer négatif : donnez au décodeur une chaîne contenant é et il se plaint Illegal base64 character -17, parce que le caractère est d'abord mappé sur l'octet Latin-1 0xE9, qui en octet Java signé fait moins 23, et moins 23 en hexa fait moins 17. Votre log d'erreurs fait, le temps d'un instant, de l'arithmétique signée. Deuxièmement, la position : dans la famille incorrect ending byte at N, N est l'index de base zéro du premier octet que le décodeur n'a pas réussi à comprendre, ce qui est un cadeau quand vous bissectez un payload corrompu.
Un changement de costume à connaître : quand le décodage passe par le flux enveloppé (la variante wrap(InputStream), couverte plus bas), les mêmes problèmes se manifestent comme une IOException avec un préfixe 0x au lieu : Illegal base64 character 0x20 (JDK actuels ; le décodeur en flux du JDK 8 imprime la valeur de consultation, -1, au lieu de l'octet). Même problème, exception différente, orthographe légèrement différente. Et le décodeur MIME permissif, bien sûr, ne se plaint de rien de tout cela : il saute simplement. C'est le prix du tempérament permissif.
Les règles du padding
Chaque chaîne Base64 dans la nature fait une promesse silencieuse sur le padding, et la promesse de Java est inhabituellement amicale. La javadoc du décodeur le dit exactement : le caractère de padding = « est accepté et interprété comme la fin des données d'octets encodées, mais n'est pas requis ». Une unité finale de deux ou trois caractères se décode comme si elle était munie du padding, et lorsque des pads sont présents, ils doivent être présents dans la quantité exacte. Le comportement du JDK actuel autour des exemples classiques :
| Entrée | Résultat |
|---|---|
"" |
tableau d'octets vide, aucune erreur |
"Zm8" |
"fo", le padding est simplement absent |
"Zm8=" |
"fo", l'orthographe canonique |
"Zm8==" |
IllegalArgumentException : incorrect ending byte at 4 |
"Zm9v=" |
IllegalArgumentException : wrong 4-byte ending unit |
"Zg==" |
"f", un octet |
"Z=" |
IllegalArgumentException : last unit does not have enough valid bits |
"AA==" |
exactement un octet, l'octet NUL 0x00 |
"AAAA" |
trois octets NUL |
Lisez cette table deux fois. La chaîne vide se décode en rien, alors que AA== se décode en un seul octet NUL : en Base64, « rien » et « un zéro » sont deux créatures différentes, et les deux sont des entrées parfaitement valides. Et le padding, quand il est présent, doit être exact : Zm8= est juste, Zm8== est faux, Zm9v= est faux, et un pad au milieu de la chaîne est faux. Conséquence pratique pour vos propres protocoles : choisissez une orthographe (avec ou sans padding) et imposez-la aux deux extrémités, parce qu'une valeur qui peut arriver en deux orthographes est une valeur qui peut casser un contrôle d'égalité naïf quelque part en aval.
base64url : l'alphabet fait pour les URL
Le Base64 standard termine son alphabet par + et /, et ce sont exactement les deux caractères qui se mal comportent dans les URL : un + dans une chaîne de requête est déjà une espace avant même que Java ne le voie, un / est un séparateur de chemin, et un = en traîne veut être encodé en pourcentage, en un monstre à trois caractères. La RFC 4648, section 5, dessine le correctif : l'alphabet sûr pour URL et noms de fichiers, où + devient -, / devient _, et le padding final = est typiquement omis quand la longueur est connue implicitement. La RFC est catégorique sur le nom : cet encodage « ne doit pas être considéré comme le même que l'encodage base64 ». Vous le croisez sous le nom base64url, et c'est là que vivent les JSON Web Tokens, les paramètres de state OAuth, les identifiants de session API et les identifiants vidéo de onze caractères.
Le payload base64url le plus célèbre du web est le JWT, et jeter un coup d'œil dedans est une affaire de trois lignes. Les parties d'un jeton sont conventionnellement sans padding, et le décodeur URL s'en accommode, parce que le padding est accepté mais pas requis, souvenez-vous :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtPeek {
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+ ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
+ ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
String[] parts = token.split("\\.");
byte[] header = Base64.getUrlDecoder().decode(parts[0]);
byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
System.out.println(new String(header, StandardCharsets.UTF_8));
// {"alg":"HS256","typ":"JWT"}
System.out.println(new String(payload, StandardCharsets.UTF_8));
// {"sub":"1234567890","name":"John Doe","iat":1516239022}
}
}
Deux avertissements honnêtes vivent ici. D'abord, décoder un JWT, c'est jeter un coup d'œil, pas faire confiance : la troisième partie est une signature, et les deux parties que vous venez de lire ne sont ni secrètes ni authentifiées. Faire confiance à un payload avant de vérifier sa signature, c'est le bug JWT classique, et le correctif est de confier la vérification à une bibliothèque JOSE comme JJWT (0.13.0) ou nimbus-jose-jwt (10.9.1) plutôt que de bricoler votre propre crypto. Deuxièmement, les erreurs identifient la direction : donnez une chaîne à alphabet standard à getUrlDecoder() et vous obtenez Illegal base64 character 2b ou 2f, et l'inverse vous vaut 2d ou 5f. L'inadéquation d'alphabet est l'échec de décodage Base64 le plus courant dans la nature, et le message d'erreur pointe dessus en un battement de cœur. Si un jeton dans une chaîne de requête était censé être du Base64 standard, ses + et / ont probablement été massacrés par le transport avant même de vous arriver, et l'erreur de décodage vous parle d'un bug en amont, pas dans votre décodeur.
Des octets aux mots
Chaque appel de décodage de cet article s'arrête aux octets exprès, parce que Base64 est un format d'octets, point final. La question « quel texte était-ce ? » vous revient de répondre, et la réponse par défaut moderne est UTF-8. Il y a pourtant un détail de charset du côté décodage de l'API qui surprend, alors le voici. La surcharge decode(String) n'interprète pas votre chaîne en UTF-8. La javadoc le dit exactement : un appel « a exactement le même effet que d'invoquer decode(src.getBytes(StandardCharsets.ISO_8859_1)) ». Ce n'est pas un bug - c'est une astuce : l'alphabet Base64 est du pur ASCII, donc mapper la chaîne via Latin-1 remet au décodeur exactement les mêmes octets avec un coût de conversion nul, et tout caractère non ASCII dans l'entrée devient simplement un symbole invalide que le décodeur strict refuse (c'est d'où viennent les nombres hexadécimaux négatifs des messages d'erreur).
Le charset du payload est une décision complètement séparée, celle de l'étape new String(bytes, charset). Voici le cas classique : « café » en UTF-8, c'est les cinq octets 63 61 66 C3 A9, qui s'encodent en Y2Fmw6k= :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetDecode {
public static void main(String[] args) {
byte[] packed = Base64.getDecoder().decode("Y2Fmw6k=");
System.out.println(new String(packed, StandardCharsets.UTF_8));
// café, l'accent survit
System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
// caf suivi de charabia, les octets UTF-8 mal lus en Latin-1
}
}
Cette deuxième ligne est le mode d'échec à reconnaître instantanément : un payload UTF-8 lu à travers Latin-1, qui produit une chaîne exactement un caractère trop longue et un octet de travers. Le remède est toujours de convenir d'un charset avec le producteur et de le passer explicitement. Et de le passer explicitement dans le code, pas seulement dans votre tête : le constructeur sans argument new String(bytes) utilise le charset par défaut de la plateforme, qui sur un serveur Windows peut être Cp1252 et sur un Linux plus ancien peut être ce que la machine a envie d'être. Depuis le JDK 18 (JEP 400, « UTF-8 by Default »), la valeur par défaut est UTF-8 sur toutes les plateformes, donc sur une JVM moderne la forme sans argument est juste par hasard, mais votre code devrait encore le dire, parce que la personne suivante qui le lira ne devrait pas avoir à savoir ce qu'est la valeur par défaut. Et quand le payload n'est pas du texte du tout, le même code se contente d'avoir une fin différente : des octets entrent, des octets sortent, jusqu'à la toute dernière étape.
Quand le payload est un fichier
Le travail le plus courant sur les fichiers est l'inverse de certaine routine d'export : un fichier texte .b64 arrive, et il vous faut le fichier d'origine. Avec un décodage strict, c'est déjà de forme production :
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DecodeFile {
public static void main(String[] args) throws Exception {
byte[] packed = Files.readAllBytes(Paths.get("payload.bin.b64"));
byte[] raw = Base64.getDecoder().decode(packed);
Files.write(Paths.get("payload.bin"), raw);
}
}
Rien dans ce chemin ne se soucie de savoir si le payload est un fichier texte, une archive ZIP ou une vidéo : byte[], ce sont juste des octets. Le calcul des tailles joue aussi en votre faveur : la sortie décodée fait trois quarts de la longueur de l'entrée encodée, donc le décodage ne rend jamais la mémoire pire, et un fichier encodé de plusieurs centaines de mégaoctets est le plus petit des deux. Une bonne habitude est de laisser les octets se présenter eux-mêmes avant de faire confiance à n'importe quelle étiquette. Les huit premiers octets d'un PNG sont toujours le nombre magique 89 50 4E 47 0D 0A 1A 0A, ce qui veut dire que chaque PNG encodé en Base64 que vous croiserez commence par le même préfixe, iVBORw0K : si un payload « prétend » être une image et ne commence pas ainsi, quelque chose est déjà faux.
Si vous possédez déjà le tampon de destination, la surcharge à deux tableaux écrit directement dedans et renvoie exactement combien d'octets sont arrivés, sans allocation intermédiaire :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DecodeInto {
public static void main(String[] args) {
byte[] src = "SGVsbG8sIHdvcmxkIQ==".getBytes(StandardCharsets.ISO_8859_1);
byte[] dst = new byte[16];
int written = Base64.getDecoder().decode(src, dst);
System.out.println(written); // 13
System.out.println(new String(dst, 0, written, StandardCharsets.UTF_8));
// Hello, world!
}
}
Un coin vif sur cette surcharge, documenté dans la javadoc : si la destination est trop petite, aucun octet du tout n'est écrit et vous obtenez IllegalArgumentException: Output byte array is too small for decoding all input bytes. Dimensionnez le tampon depuis le simple calcul, environ 3 * n / 4 moins le padding, et l'exception ne montre jamais sa face. Il existe aussi une surcharge ByteBuffer qui renvoie un tampon neuf dont la limite est réglée sur la longueur décodée, pratique quand votre pipeline vit en NIO.
Depuis le câble : en-têtes, JSON et data URIs
Base64 rencontre Java le plus souvent au bord du réseau. Trois formes méritent chacune un exemple commenté.
Forme un : l'en-tête d'authentification HTTP Basic. L'en-tête d'authentification le plus ancien du web roule encore sur du Base64. Selon la RFC 7617, une requête Basic envoie Authorization: Basic suivi de l'encodage Base64 de username:password, et la RFC est explicite : c'est de l'encodage, pas de la protection : n'importe qui avec une capture de paquets peut lire les deux moitiés d'une touche. L'exemple de la RFC elle-même, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, se décode en Aladdin:open sesame. Analyser l'en-tête côté serveur est une affaire de quelques lignes :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuth {
public static String[] credentials(String header) {
if (header == null || !header.startsWith("Basic ")) {
return null;
}
byte[] packed = header.substring(6).getBytes(StandardCharsets.ISO_8859_1);
byte[] raw = Base64.getDecoder().decode(packed);
String userPass = new String(raw, StandardCharsets.UTF_8);
int colon = userPass.indexOf(':');
if (colon < 0) {
return null;
}
return new String[] {userPass.substring(0, colon), userPass.substring(colon + 1)};
}
}
Deux détails gardent cela en sécurité. Le découpage sur le premier deux-points compte, parce qu'un mot de passe peut légitimement contenir des deux-points à lui-même. Et la comparaison du mot de passe décodé avec votre valeur stockée doit être en temps constant : hachez les deux valeurs avec SHA-256 et comparez les empreintes avec MessageDigest.isEqual, jamais un equals plain qu'un attaquant peut chronométrer pour reconstruire votre liste d'utilisateurs. N'utilisez cela que sur HTTPS ; sur une connexion plain, la couche Base64 est un habillage de vitrine.
Forme deux : du binaire dans le JSON. Une grande part des API modernes imbrique du binaire sous forme de texte Base64 dans le JSON : points de terminaison d'upload de fichiers, API de contenu, coffres à secrets et webhooks le font tous, parce que les octets bruts casseraient autrement les règles d'échappement des chaînes JSON. Le motif est toujours le même : le champ arrive comme une chaîne plain, et vous le décodez à la frontière, pas à l'intérieur de vos objets de domaine :
import java.util.Base64;
public class ApiField {
public static void main(String[] args) {
// Le JSON analysé portait : "content" : "iVBORw0KGgoAAA..."
String field = "iVBORw0KGgo=";
byte[] image = Base64.getUrlDecoder().decode(field);
// Certaines API parlent plutôt le Base64 standard. Lisez la spec,
// puis choisissez getDecoder() ou getUrlDecoder() en conséquence.
System.out.println(image.length); // 8
}
}
Le piège ici, ce n'est pas le décodage ; c'est la lecture de la spec. Certaines API veulent du Base64 standard avec padding, d'autres du base64url sans, et quelques-unes sont indulgentes avec les deux. Quand la spec se tait, le correctif le moins cher est de regarder une valeur d'exemple venue de l'autre côté : un - ou un _ n'importe où dans la valeur tranche la question de l'alphabet, et des = en fin de chaîne tranchent celle du padding.
Forme trois : la data URI. Quelqu'un colle une image dans un formulaire et le front end vous remet la data URI complète : data:image/png;base64,iVBORw0KGgo.... La RFC 2397 définit la forme : data:, un type média facultatif, un drapeau ;base64 facultatif, une virgule, puis les données. Quand le drapeau est présent, le payload est du Base64 ; quand il est absent, le payload est du texte plain encodé en pourcentage, plus rare mais légal. Si le type média est omis, la valeur par défaut est text/plain;charset=US-ASCII. En découper une est simple :
import java.util.Base64;
public class DataUri {
public static void main(String[] args) {
String uri = "data:image/png;base64,iVBORw0KGgo=";
int comma = uri.indexOf(',');
String meta = uri.substring(5, comma);
String payload = uri.substring(comma + 1);
boolean isBase64 = meta.endsWith(";base64");
String mime = isBase64 ? meta.substring(0, meta.length() - 7) : meta;
byte[] raw = Base64.getDecoder().decode(payload);
System.out.println(mime + " -> " + raw.length + " bytes");
// image/png -> 8 bytes
}
}
Deux pièges vivent dans ce format. Le drapeau ;base64 manquant est le premier : une data URI légale sans le drapeau porte un payload encodé en pourcentage, et le faire passer dans Base64.getDecoder() lève une exception. Le second est le type média réclamé : c'est un indice de l'expéditeur, pas un fait, donc vérifiez les octets magiques de ce que vous avez décodé avant de classer le fichier sous « png ». Et souvenez-vous du conseil de la RFC elle-même : les data URIs sont faites pour des valeurs courtes ; une image de plusieurs mégaoctets dans une URL est une odeur, pas un motif.
Email, MIME et armure PEM
Base64 est né pour le courrier, et le Base64 de forme courrier arrive encore sans cesse dans les programmes Java. La norme MIME (RFC 2045) a fait de Base64 l'un des encodages de transfert binaire et a ajouté deux règles de la maison : les lignes encodées ne doivent pas dépasser 76 caractères, et les décodeurs doivent ignorer tout caractère hors alphabet, retours à la ligne compris. Les décodeurs stricts rejettent le tout premier retour à la ligne ; getMimeDecoder() a été construit pour exactement cette entrée :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class MimeDecode {
public static void main(String[] args) {
String wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
byte[] bytes = Base64.getMimeDecoder().decode(wrapped);
System.out.println(new String(bytes, StandardCharsets.UTF_8));
// Hello, standard
}
}
Ça marche, et vous devriez quand même connaître le hic, parce que le hic a des crocs. Le décodeur permissif ne fait pas qu'« ignorer les retours à la ligne » ; il ignore tout ce qui n'est pas dans son alphabet. Si une chaîne Base64 standard est corrompue avec des caractères égarés, la saleté disparaît et le reste se décode en quelque chose de plausible, alors n'allez vers getMimeDecoder() que lorsque vous attendez vraiment une entrée de forme MIME.
Le cousin de MIME dans la nature est l'armure PEM, l'histoire de -----BEGIN CERTIFICATE----- qui enveloppe certificats et clés. Voici le piège : les lignes d'armure sont pleines de caractères d'alphabet tout à fait ordinaires. Les lettres de « BEGIN CERTIFICATE » sont juste des lettres Base64, alors si vous faites ingérer un bloc PEM entier, armure comprise, l'armure se décode comme si c'était des données. Retirez l'armure vous-même, puis remettez le corps nu à un décodeur :
import java.util.Base64;
public class PemDecode {
public static void main(String[] args) {
String pem = "-----BEGIN CERTIFICATE-----\n"
+ "TUlJQm96Q0NBVWlnQXdJQkFnSUpBSXBhVDJUaVFvZU1BMEdDU3FHU0liM0RRRUE9\n"
+ "-----END CERTIFICATE-----\n";
String body = pem.replaceAll("(?m)^-----.*$", "").replaceAll("\\s", "");
byte[] der = Base64.getDecoder().decode(body);
System.out.println(der.length); // le corps DER, armure exclue
}
}
Le PEM enveloppe conventionnellement à 64 caractères par ligne (le MIME à 76), et une fois les espaces disparus, le décodeur strict et le décodeur MIME tombent d'accord sur le résultat. Utilisez le strict : une surprise a au moins la décence de lever une exception. Pour le cas sale mais standard, la recette classique est de retirer les espaces connus et de décoder avec l'instance stricte, et de laisser toute saleté résiduelle se payer d'une IllegalArgumentException au lieu d'un certificat corrompu.
Configuration, environnement et valeurs de base de données
Base64 est un conteneur de texte, voilà pourquoi il apparaît dans des endroits où vous ne l'attendez pas. Dans les bases de données, un blob binaire (un fichier, une icône, une structure sérialisée) peut vivre dans une colonne TEXT sous forme de Base64, survivant à tous les outils qui supposent du texte ; attendez-vous à une valeur stockée environ un tiers plus grosse que l'origine, et dimensionnez la colonne en conséquence. Dans les fichiers de configuration et les variables d'environnement, Base64 est l'astuce pour faire passer des valeurs qui casseraient autrement le format : un DSN avec des points-virgules, un mot de passe avec des guillemets, un certificat multi-lignes. Décoder au démarrage, c'est tout le travail :
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigDecode {
public static void main(String[] args) {
String value = System.getenv("DB_DSN_B64");
if (value == null) {
return;
}
byte[] raw = Base64.getDecoder().decode(value);
String dsn = new String(raw, StandardCharsets.UTF_8);
// dsn peut être : pg:host=db;password=qu"ote
}
}
La même prudence s'applique deux fois ici. D'abord, c'est de la sécurité de format, pas du secret : le moment où un développeur lit le fichier de configuration, il peut décoder la valeur en un appel, alors ne stockez jamais un secret en Base64 et en l'appelant chiffré ; la section sécurité plus bas approfondit. Deuxièmement, validez au démarrage : une valeur d'env corrompue ou collée à moitié est une IllegalArgumentException venue de l'appel strict, et un contrôle de deux lignes transforme une erreur d'exécution cryptique en message de démarrage actionnable. Une note spécifique Java pour les gens de base de données : gardez le binaire décodé en byte[] (un paramètre byte[] dans votre code JDBC), et ne faites jamais aller-retour du binaire à travers une String, parce que les constructeurs de chaînes sont l'endroit où les payloads binaires vont mourir.
Le streaming du gros
Pour des payloads grands mais qui tiennent encore dans un tampon que vous gérez, les API à tableaux vont bien. Pour des payloads qui ne devraient tout simplement pas tenir en mémoire, l'adaptateur de flux est le bon coup : wrap(InputStream) renvoie un flux d'entrée qui décode au fil de vos lectures, alors un fichier encodé de plusieurs gigaoctets n'a jamais à se poser dans un tableau d'octets :
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamDecode {
public static void main(String[] args) throws Exception {
InputStream packed = Base64.getDecoder().wrap(Files.newInputStream(Paths.get("bigfile.b64")));
OutputStream raw = Files.newOutputStream(Paths.get("bigfile.bin"));
byte[] buf = new byte[8192];
int n;
while ((n = packed.read(buf)) != -1) {
raw.write(buf, 0, n);
}
raw.close();
packed.close();
}
}
Deux détails qui méritent d'être connus. Les méthodes de lecture du flux enveloppé lèvent une IOException quand elles rencontrent des octets qu'elles ne peuvent pas décoder, alors un fichier corrompu échoue avec une exception de flux au lieu d'une IllegalArgumentException. Et fermer le flux enveloppé ferme le flux sous-jacent, si bien que l'exemple ferme packed en dernier, après la boucle de copie, et en production vous mettriez les deux dans un bloc try-with-resources. (Le tampon 8192 est juste un généreux tampon de lecture ; le flux enveloppé décode en interne, donc la taille avec laquelle vous lisez est un choix de performance, pas une exigence de protocole.)
Maintenant un avertissement héritage, parce que voici le seul vrai bug de toute l'histoire, et il a un numéro de bug. Sur chaque JDK antérieur à 16 (le rapport de bug le reproduit sur 8, 10 et 11), lire un décodeur enveloppé avec certaines tailles de tampon ajoute deux octets zéro égarés à la fin des données décodées : le JDK 8222187, dont la reproduction classique associe un tampon de lecture de sept octets à une entrée plain de huit octets, et qui est corrigé dans le JDK 16. Si vous devez streamer sur un JDK 8 héritage, revérifiez la longueur décodée après la copie, parce que le bug se déclenche pour certaines combinaisons d'entrée et de tampon, et qu'un tampon de 4096 octets a aussi été rapporté dans la nature, ou mieux, montez de version le JDK, ce qui corrigerait une centaine d'autres choses de toute façon.
Du ruban d'emballage, pas un cadenas
Voici maintenant la section qui sépare les soigneux des brûlés. Base64 n'est pas du chiffrement, et la norme elle-même le dit deux fois. RFC 4648, section 12 : l'encodage Base « masque visuellement des informations autrement facilement reconnaissables, comme des mots de passe, mais ne fournit aucune confidentialité calculatoire », et elle poursuit en notant que cela « a été connu pour causer des incidents de sécurité » quand quelqu'un colle un échange de protocole dans un ticket et révèle accidentellement le mot de passe. Le conseil de la RFC pour les implémenteurs mérite lui aussi un cadre : « un décodeur ne doit pas casser sur une entrée invalide, y compris, par exemple, des caractères NUL embarqués ».
Le piège plus subtil est la malléabilité. Retenez que chaque symbole porte six bits, et qu'une unité finale courte laisse des bits libres qui doivent être nuls dans un encodage bien formé. Un encodeur bâclé ou hostile peut y mettre de la saleté, et le résultat a toujours l'air complètement valide : MQ== et MT== se décodent tous les deux en l'unique octet du chiffre 1. Java prend le côté indulgent de cette histoire : Base64.getDecoder().decode("MT==") ne vérifie pas les bits non significatifs et vous remet gaiement le même octet. Pourquoi s'en soucier ? Parce que deux chaînes différentes qui se décodent en les mêmes données cassent l'hypothèse d'« orthographe unique » sur laquelle reposent discrètement les contrôles de hash, la déduplication et les comparaisons de signatures, et un attaquant qui peut altérer une valeur encodée en transit peut échanger une orthographe contre l'autre. L'article de 2022 « La malléabilité Base64 en pratique » de Chatzigiannis et Chalkias (ACM ASIA CCS 2022) passe en revue exactement ces incohérences à travers des implémentations réelles. Les mots de la RFC elle-même sur les bits libres : ils « peuvent être abusés pour fuiter de l'information ou utilisés pour contourner des comparaisons d'égalité de chaînes ou pour déclencher des problèmes d'implémentation ». La règle pratique n'est pas « ne jamais décoder » ; c'est « connaître votre frontière » : pour des données entre vos propres systèmes, la générosité du JDK va bien, mais pour des données qui traversent une frontière de confiance, imposez la forme canonique (bonne longueur, bits libres nuls, une seule orthographe de padding) avant de faire confiance à quoi que ce soit que vous ayez décodé.
Notes de performance
Voici la bonne nouvelle en une phrase : sur une JVM moderne, le décodeur intégré est assez rapide pour que Base64 soit presque jamais votre goulot d'étranglement, et c'est la référence contre laquelle le reste de l'écosystème se benchmark. Un cas à l'appui : en 2025, le projet gRPC-java a benchmarké publiquement sa gestion Base64 basée sur Guava face à java.util.Base64 (issue 11857), et l'implémentation JDK est sortie environ 2,5 à 3,8 fois plus rapide à l'encodage et 1,3 à 2,1 fois plus rapide au décodage sur les JDK 17 et 21, avec les plus gros écarts sur x86. C'est un indice fort sur l'endroit où l'effort d'implémentation du JDK s'est porté, et c'est la même conclusion que vous continuerez de trouver dans les benchmarks Base64 : la version de la bibliothèque standard est désormais la rapide, pas la version héritage.
Deux notes pratiques. D'abord, pour les fichiers énormes, c'est le profil mémoire, pas la vitesse, que vous gérez, voilà pourquoi la section streaming existe : wrap(InputStream) tient l'ensemble de travail à votre tampon de lecture. Deuxièmement, si vous finissez sur un chemin critique qui décode des millions de petites valeurs, partagez une instance de décodeur (la fabrique renvoie déjà la même instance partagée, comme noté en haut), évitez la surcharge decode(String) quand vous avez déjà des octets (elle copie d'abord la chaîne via Latin-1), et laissez la surcharge decode(byte[], byte[]) écrire dans un tableau de destination pré-dimensionné pour éviter la valse d'allocations.
Pièges à l'accent Java
Les pièges réunis en un seul endroit, tous spécifiques Java :
- Mauvais décodeur pour l'alphabet. Une chaîne base64url dans
getDecoder()(ou l'inverse), c'est le plantage classiqueIllegal base64 character, généralement avec un2d,5f,2bou2fdans le message. Alignez le décodeur sur le protocole, à chaque fois. - Espaces de fin venus de la nature. Les valeurs copiées depuis un terminal, une variable d'env ou un fichier de configuration arrivent souvent avec un retour à la ligne, et le décodeur strict en fait
Illegal base64 character a. Faitesstrip()sur l'entrée, ou n'utilisez le décodeur MIME que quand les données sont réellement enveloppées. - Le piège de l'armure.
getMimeDecoder()ne comprend pas les en-têtes PEM, et les lettres de BEGIN et CERTIFICATE se décodent comme des données. Retirez les lignes d'armure vous-même, toujours. - L'indulgence MIME comme raccourci. Décoder avec le décodeur MIME juste « pour être sûr » saute en silence tout caractère égaré hors alphabet, alors un payload corrompu peut ressortir plausible et faux. Utilisez-le pour de la vraie entrée MIME seulement.
- Charset laissé au hasard. Le
new String(bytes)sans argument utilise la valeur par défaut de la plateforme. C'est UTF-8 sur JDK 18+, mais votre code devrait passerStandardCharsets.UTF_8explicitement, ou appréciez le charabia après la prochaine migration de serveurs. - Stringifier du binaire.
new String(decodedPng)et le retour en arrière, c'est de la destruction de données : toute séquence d'octets non valide dans votre charset devient le caractère de remplacement, et l'aller-retour est à sens unique. Des octets entrent, des octets sortent, jusqu'à la toute dernière étape. - Confiance dans les bits libres.
MT==se décode exactement commeMQ==, alors un payload avec de la saleté cachée dans les bits non significatifs passe tous les contrôles que le JDK exécute. Si le protocole compte, imposez la forme canonique. - Flux des JDK 8, 11 et 12. Le décodeur enveloppé sur ces versions peut ajouter deux octets zéro égarés pour certaines tailles de tampon (JDK 8222187, corrigé dans 16). Sur 16 et plus, ce n'est pas un problème ; sur les plus anciens, si.
- null n'est pas vide. Passer
nullàdecode(), c'est unNullPointerException, pas un tableau vide. Si une variable peut être null, coalescez-la avant l'appel. - Android est une autre ménagerie. Sur Android,
java.util.Base64n'existe qu'à partir du niveau API 26 ; en dessous, la classe du framework estandroid.util.Base64avec ses propres constantes de drapeaux. Un code qui durcit l'un ou l'autre sans vérification casse sur exactement les appareils que vous n'avez jamais testés. - Oublier que ce n'est pas de la sécurité. Base64 cache un mot de passe à un coup d'œil et à personne d'autre. Si les données sont secrètes, chiffrez-les d'abord, et seulement ensuite emballez-les si le canal exige du texte.
Le long chemin vers java.util.Base64
L'histoire du format est plus vieille que Java. Dans les années 1980, l'infrastructure de courrier d'Internet ne pouvait porter que de l'ASCII 7 bits, et ceux qui voulaient faire voyager des binaires ont inventé des dialectes locaux : uuencode pour UNIX (son alphabet parcourt des codes ASCII consécutifs, alors l'encodage était un simple ajout de 32 sans table de consultation) et BinHex pour les machines Apple (qui a affiné son alphabet pour éliminer les caractères visuellement confondables comme 7, O, g et o). En 1987, le protocole Privacy-Enhanced Mail (RFC 989) a standardisé le schéma à 64 caractères avec des lignes de 64 caractères pour porter des certificats, et la RFC 1421 en 1993 a gardé l'alphabet et les règles de padding. En 1996, le MIME (RFC 2045, mettant à jour la RFC 1521 de 1993) a porté le schéma déjà nommé « base64 » d'après son alphabet de 64 caractères, a fixé la longueur de ligne de 76 caractères qui enveloppe encore vos pièces jointes, et a écrit la règle du décodeur permissif que getMimeDecoder() implémente encore aujourd'hui. En 2003, la RFC 3548 a essayé de remettre de l'ordre dans toute la famille et a déclaré que les décodeurs devaient refuser les caractères hors alphabet, et en 2006 la RFC 4648 est devenue la norme que tout le monde cite, avec les tables d'alphabet, la variante base64url en section 5, et la section sécurité qui garde la dernière des sections de cet article honnête.
Le chapitre propre à Java est un peu plus dramatique. Pendant des années, le seul Base64 dans le JDK était le duo interne sun.misc.BASE64Encoder et sun.misc.BASE64Decoder, ce genre d'API qui compile aujourd'hui et disparaît sans avertissement de dépréciation, et si vous aviez besoin de Base64 dans un monde XML, il y avait aussi javax.xml.bind.DatatypeConverter de JAXB. Tout le monde d'autre utilisait Apache Commons Codec ou Guava. Puis le 18 mars 2014 : Java 8 a livré java.util.Base64, implémentant la RFC 4648 et la RFC 2045 dans une seule classe avec le patron de fabrique que vous utilisez depuis le début. Trois ans et demi plus tard, Java 9 (21 septembre 2017) a supprimé définitivement le duo sun.misc, et le guide officiel de migration est lapidaire : « Notons que sun.misc.BASE64Encoder et sun.misc.BASE64Decoder ont été supprimés. À la place, utilisez la classe prise en charge java.util.Base64, ajoutée dans le JDK 8 ». Lancez jdeps sur du vieux code qui les référence encore et l'outil signale la dépendance comme « JDK removed internal API ». Java 11 a suivi en supprimant le module JAXB et son DatatypeConverter avec lui (JEP 320). Depuis la 1.8, l'API publique n'a changé aucune méthode, et la javadoc porte encore son étiquette Since: 1.8 d'origine. Ce qui a bougé, c'est le moteur en dessous : corrections de bugs (le bug de flux JDK 8222187, corrigé dans le JDK 16) et travail de performance, voilà pourquoi les benchmarks de la communauté atterrissent toujours sur la même conclusion. Douze ans, une API, et c'est toujours le Base64 le plus rapide que vous n'aurez pas à payer.
Faits amusants, édition Java
Parce qu'un guide complet devrait se terminer sur un sourire, voici quelques faits spécifiques Java qui sont simplement amusants :
- La javadoc Oracle de
decode(byte[] src, byte[] dst)promet que « certains octets ont peut-être été écrits dans le tableau d'octets de sortie avant que IllegalargumentException ne soit levée ». Pas IllegalArgumentException, IllegalargumentException, avec un a minuscule. La faute de frappe est dans le code source JDK réel, et elle est là depuis 2014. Une documentation aussi engagée dans une faute de frappe est plus rare qu'elle ne devrait l'être. - Décodez une chaîne contenant
éet le message d'erreur estIllegal base64 character -17: un nombre hexadécimal négatif, parce que le caractère devient l'octet Latin-10xE9, qui en octet Java signé fait moins 23, et le JDK l'imprime en base 16. Votre log d'erreurs fait, le temps d'un instant, de l'arithmétique signée. Base64.getDecoder() == Base64.getDecoder()est vrai. Le code source renvoie une instance statique partagée à chaque appel, alors l'API « obtenez-en un neuf » est un déguisement pour un singleton, et la promesse de thread-safety n'est qu'une description de ce que la JVM fait déjà.- Donnez au décodeur URL une chaîne de quatre tirets bas,
"____", et il renvoie trois octets de pur0xFF. Le tiret bas a la valeur alphabet 63, quatre font 24 bits, et 24 bits de uns font le triplet d'octetsFF FF FF. Rien d'illégal là-dedans, et c'est la partie la plus drôle. AA==se décode en un seul octet NUL alors que la chaîne vide se décode en rien. En Base64, « rien » et « un zéro » sont deux créatures différentes, et les deux sont des entrées parfaitement valides.- La petite chaîne
TWFuqui se décode enManest devenue le test de fumée préféré de l'écosystème : elle apparaît dans la RFC, dans Wikipédia, dans les manuels de référence et dans la plupart des tutoriels Base64 de la terre, alors chaque décodeur écrit depuis lui paie le même petit tribut. - Chaque PNG encodé en Base64 que vous avez jamais décodé commence par
iVBORw0K. C'est le nombre magique du PNG déguisé, et c'est l'un des préfixes de huit caractères les plus reconnaissables d'Internet. - La section URL-safe de la RFC 4648 est l'endroit où naît le nom « base64url » : la spec dit que l'encodage « peut être appelé base64url » et met en garde qu'il « ne doit pas être considéré comme le même que l'encodage base64 ». L'origine de l'alphabet URL-safe est mise en note d'après un message de 2001 sur une liste de diffusion P2P-hackers, alors le nom que vous collez dans chaque URL a un pedigree de liste de diffusion.
- Les identifiants vidéo YouTube sont du base64url sans padding, la familière chaîne de onze caractères que vous pouvez coller n'importe où dans une URL. Le format conçu pour les pièces jointes de courrier fait maintenant tourner une plateforme vidéo, et
getUrlDecoder()est la partie de votre JDK qui le rend possible. - Décodez la chaîne
YmFzZTY0et vous obtenez le motbase64en retour, sans padding nécessaire, parce que six est un multiple de trois. Un format qui se décrit lui-même est l'équivalent technique d'un miroir qui parle en morse.
L'autre direction
C'est le côté décodeur de l'histoire, et c'est là que vit la plupart de la douleur, parce que le décodage est l'endroit où vous rencontrez les données des autres : leurs choix de padding, leurs retours à la ligne, leurs charsets, leurs jetons, leur armure. L'autre direction, transformer des octets en chaîne Base64 avec les encodeurs de java.util.Base64, est une bête plus calme : il ne lève jamais sur une entrée invalide (il n'y a pas d'entrée invalide à encoder), il a une facture de taille à payer au lieu d'un message d'erreur à lire, et sa propre série de pièges (l'étape charset, les molettes MIME, la décision de padding pour les jetons) a un guide à elle. L'encodage Base64 en Java, lié depuis cette page, couvre l'encodeur dans la même profondeur, et les deux se lisent confortablement en duo.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Java : un guide complet