Décodage Base64 en Kotlin : un guide complet
Vous fixez du regard une valeur qui refuse de se laisser lire : SGVsbG8sIFdvcmxkIQ==. Une suite de lettres et de chiffres, un + ou un / de-ci de-là, et généralement un ou deux = en queue de chaîne. C'est du Base64, et ce guide est là pour le ramener à ce qu'il était avant - une phrase, une image, un certificat, un bloc binaire - à la façon Kotlin. Un rappel en une phrase avant de plonger : Base64 emballe chaque trio d'octets en quatre caractères tirés d'un alphabet de 64 symboles, et une petite queue de padding = marque là où se sont terminées les vraies données. La visite complète du format se trouve sur la page d'accueil, alors nous n'y consacrons qu'une phrase ici, et une de plus : comme quatre caractères portent ce que portaient trois octets, la forme texte est environ un tiers plus longue que les données d'origine.
La bonne nouvelle avec Kotlin : vous n'avez besoin d'aucun paquet. La bibliothèque standard embarque sa propre implémentation Base64 depuis des années ; elle est entièrement stable depuis Kotlin 2.2, et elle tourne sur toutes les plateformes où Kotlin tourne, de la JVM de votre portable au téléphone Android, en passant par Node.js et une fonction edge WASI. Tout ce qui suit fonctionne avec le Kotlin livré avec votre projet.
Les bonnes nouvelles d'abord : ce qu'il faut vraiment
Il n'y a aucun artefact base64 à ajouter à Gradle, aucun paquet à la NuGet, aucun module npm. La classe qu'il vous faut est kotlin.io.encoding.Base64, qui fait partie de la bibliothèque standard Kotlin elle-même. Si vous savez écrire println, vous savez décoder du Base64. Trois API peuvent faire du travail Base64 dans un projet Kotlin, et choisir la bonne est la première vraie décision :
| API | Où elle tourne | Quand y recourir |
|---|---|---|
kotlin.io.encoding.Base64 |
Toutes les plateformes Kotlin : JVM, Android, JS, Native, Wasm | Le choix par défaut. Stable depuis Kotlin 2.2, multiplateforme, API moderne |
java.util.Base64 |
JVM uniquement (Java 8+ ; sur Android, API 26+) | Les bases de code JVM-only qui vivent déjà du côté de l'interop Java |
android.util.Base64 |
Android uniquement (API 8+) | Le code Android hérité, ou quand vous avez besoin de ses constantes de drapeaux |
Deux notes de version à connaître. D'abord, la classe de la bibliothèque standard est apparue pour la première fois dans Kotlin 1.8.20 (avril 2023), derrière une porte @ExperimentalEncodingApi ; Kotlin 2.0.20 a apporté la molette withPadding et la règle stricte du padding, et Kotlin 2.2.0 (juin 2025) a stabilisé l'API et ajouté l'instance PEM. Donc avec Kotlin 2.2 ou plus récent - y compris la ligne stable actuelle, 2.4.x - vous pouvez utiliser tout ce guide sans aucune annotation. Ensuite, si votre projet fixe une version de Kotlin entre 1.8 et 2.1, la même classe existe mais est marquée expérimentale, et le compilateur ne vous laissera pas l'utiliser sans une annotation @OptIn sur la fonction.
Un piège d'installation qui a coûté plus d'un après-midi : le paquet kotlin des dépôts Debian et Ubuntu est en version 1.3.31, qui précède entièrement l'API Base64 de la bibliothèque standard, il ne peut donc pas compiler un seul exemple de cet article. Récupérez plutôt le compilateur depuis les releases Kotlin sur GitHub ou depuis SDKMAN, et dans les projets Gradle, fixez explicitement le plugin :
plugins {
kotlin("jvm") version "2.4.10"
}
Votre premier décodage : deux lignes et un résultat en octets
Toute la cérémonie tient en deux déclarations, et la chaîne classique TWFu est un bon point de départ :
import kotlin.io.encoding.Base64
fun main() {
val packed = "TWFu"
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // Man
}
Lisez ce code lentement, parce que trois décisions de conception se cachent dedans. D'abord, Base64.decode(...) sans aucun .Default n'est pas une coquille : Default est l'objet compagnon de la classe, donc appeler la fonction sur la classe elle-même est un raccourci pour l'appeler sur Base64.Default. Vous verrez aussi Base64.Default.decode(...) dans les tutoriels plus anciens, et ça veut dire exactement la même chose. Deuxièmement, et c'est plus important qu'il n'y paraît : decode vous remet un ByteArray, jamais une String. Le payload pourrait être un JPEG, un certificat X.509 ou une phrase, et l'API refuse de deviner lequel, donc le saut des octets vers le texte est une étape séparée et délibérée. Troisièmement, c'est dans cette étape que vit la décision de charset, et c'est là que naissent la plupart des bugs du type « mon Base64 est revenu en charabia ». On y arrive dans un instant ; d'abord, un aller-retour pour prouver que le décodage est fidèle :
import kotlin.io.encoding.Base64
fun main() {
val original = "Hello, World!".encodeToByteArray()
val packed = Base64.encode(original)
val back = Base64.decode(packed)
println(packed) // SGVsbG8sIFdvcmxkIQ==
println(back.contentEquals(original)) // true
}
Quatre schémas, quatre tempéraments
La classe n'est jamais instanciée ; vous choisissez parmi quatre instances prêtes à l'emploi, et chacune décode avec un tempérament différent :
import kotlin.io.encoding.Base64
fun main() {
val data = "Hello?".encodeToByteArray()
println(Base64.Default.encode(data)) // SGVsbG8/
println(Base64.UrlSafe.encode(data)) // SGVsbG8_
println(Base64.Mime.encode(data)) // SGVsbG8/
println(Base64.Pem.encode(data)) // SGVsbG8/
}
| Instance | Alphabet | Comment elle décode |
|---|---|---|
Base64.Default |
A-Z a-z 0-9 + / |
Strict : tout caractère hors de l'alphabet lève une exception ; le padding est obligatoire |
Base64.UrlSafe |
A-Z a-z 0-9 - _ |
Strict, mais contre l'alphabet URL ; un + ou un / dans l'entrée lève une exception |
Base64.Mime |
A-Z a-z 0-9 + / |
Permissif : ignore les séparateurs de lignes et les autres caractères hors alphabet, mais rien ne doit suivre le padding = ; le padding est obligatoire |
Base64.Pem |
A-Z a-z 0-9 + / |
Permissif, mêmes règles que Mime ; c'est la variante PEM/PKI du même alphabet |
La distinction permissif/strict est la chose la plus utile à ancrer. Default et UrlSafe traitent tout caractère étranger comme une scène de crime et lèvent l'exception immédiatement. Mime et Pem haussent les épaules devant les retours à la ligne, les espaces et la ponctuation égarée - parce que c'est exactement ce que contiennent les vrais fichiers d'email et de certificat - mais ils ne sont pas sans limites : le moment où un caractère de données apparaît après le padding, même eux lèvent l'exception. Vous verrez les messages d'erreur exacts dans le guide des échecs plus loin dans cet article.
Une autre conséquence des tempéraments : un schéma ne peut pas lire la sortie d'un autre. Donnez un jeton base64url à Base64.Default et le caractère - n'est pas dans son alphabet, donc vous obtenez IllegalArgumentException: Invalid symbol '-'(55) at index .... En cas de doute sur l'origine d'une chaîne, choisissez le schéma qui correspond à celui qui a produit la donnée, pas celui qui correspond à votre humeur.
Base64 URL-safe et JWT
Deux caractères de l'alphabet standard posent problème dès que la donnée doit voyager dans une URL. Un + dans une chaîne de requête est régulièrement réinterprété comme une espace au moment où quelque chose la lit, et / est un séparateur de chemin, donc il ne peut tout simplement pas apparaître dans un segment d'URL. La RFC 4648, section 5, résout le problème en échangeant les deux derniers symboles de l'alphabet : + devient - et / devient _. Le nom que vous entendrez le plus est base64url, et en Kotlin c'est Base64.UrlSafe.
Le plus gros consommateur de base64url est le JSON Web Token. Un JWT sous sa forme compacte est trois parties base64url jointes par des points : header.payload.signature. La RFC 7515 spécifie ces parties en base64url sans padding, ce qui est une deuxième différence par rapport à l'alphabet classique, et pas seulement dans les caractères. Voici un jeton qu'on dépaquete pour l'inspection :
import kotlin.io.encoding.Base64
fun main() {
val token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
val (header, payload, signature) = token.split(".")
val lenient = Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
println(lenient.decode(header).decodeToString())
// {"alg":"HS256"}
println(lenient.decode(payload).decodeToString())
// {"sub":"1234567890","name":"John Doe"}
println(signature.length) // 43
}
Deux choses à remarquer. Les parties du jeton ne portent aucun padding, mais Base64.UrlSafe réclame du padding tel quel, donc la ligne withPadding(PRESENT_OPTIONAL) fait du vrai travail : elle accepte aussi bien l'entrée avec padding que celle sans. Et le split(".") plus la déstructuration, c'est du pur Kotlin qui fait ce que le format lui demande. Un avertissement sérieux : dépaqueter un JWT, c'est pour regarder un jeton, pas pour lui faire confiance. L'en-tête et le payload sont des données plaines après décodage ; seule une signature vérifiée dit que le jeton est authentique, et pour ça vous voulez une vraie bibliothèque JWT, pas un découpage de chaînes fait main.
Les modes de padding et la molette de rigueur
Le padding n'est pas un fait figé du Base64 en Kotlin ; c'est un réglage. Chaque instance porte un PaddingOption, les quatre instances preset démarrent sur PRESENT, et withPadding vous remet une nouvelle instance avec un réglage différent en laissant l'originelle intacte. Voici la molette, option par option :
| Option | Entrée sans padding | Entrée avec le bon padding |
|---|---|---|
PRESENT (défaut partout) |
Lève une exception | Décode |
ABSENT |
Décode | Lève une exception |
PRESENT_OPTIONAL |
Décode | Décode |
ABSENT_OPTIONAL |
Décode | Décode |
import kotlin.io.encoding.Base64
fun main() {
val data = "Hello".encodeToByteArray()
println(Base64.Default.withPadding(Base64.PaddingOption.ABSENT).encode(data))
// SGVsbG8
println(Base64.Default.encode(data))
// SGVsbG8=
val eitherWay = Base64.Default.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL)
println(eitherWay.decode("SGVsbG8").decodeToString()) // Hello
println(eitherWay.decode("SGVsbG8=").decodeToString()) // Hello
}
Côté décodage, PRESENT_OPTIONAL est votre filet de sécurité : c'est l'option qui dit « je ne sais pas si l'expéditeur a mis du padding, et j'ai l'intention de continuer à fonctionner ». Les messages d'erreur des autres combinaisons sont d'une rare utilité, vous les reconnaîtrez donc instantanément quand un décodeur strict rencontre la mauvaise entrée : padding manquant sous PRESENT produit The padding option is set to PRESENT, but the input is not properly padded, et padding présent sous ABSENT produit The padding option is set to ABSENT, but the input has a pad character at index 7. Un comportement mérite une mise en garde parce qu'il surprend : un double padding comme SGVsbG8== n'est pas « un plus, mais sans gravité ». Le premier = termine les données, et le deuxième est un caractère là où des données étaient attendues, donc même les décodeurs les plus permissifs le refusent.
Il y a aussi une histoire de versions cachée ici. Si vous avez hérité de code écrit contre l'API expérimentale 1.8.x, souvenez-vous que l'ancien decode acceptait l'entrée avec ou sans padding. Dans Kotlin 2.0.20, Default est passé à la règle stricte PRESENT, donc une entrée sans padding qui fonctionnait avant lève désormais une exception dès qu'on passe au-delà de cette version. Le correctif tient en une ligne : withPadding(Base64.PaddingOption.PRESENT_OPTIONAL), ou bien normalisez vos entrées avant de décoder.
Des octets au texte : charsets et Unicode
Une fois votre ByteArray en main, la question est ce qu'il veut dire. Si le payload est du texte, la réponse par défaut est decodeToString(), qui interprète les octets en UTF-8 et fonctionne sur toutes les plateformes. Pour le cas courant des API modernes, des emails et des données web, c'est tout ce dont vous aurez jamais besoin, emoji compris :
import kotlin.io.encoding.Base64
fun main() {
val original = "héllo 😀"
val packed = Base64.encode(original.encodeToByteArray())
println(packed) // aMOpbGxvIPCfmIA=
println(Base64.decode(packed).decodeToString()) // héllo 😀
}
Dès le moment où l'expéditeur a utilisé autre chose que UTF-8, en revanche, la décision de charset vous incombe. Les conversions de texte intégrées de Kotlin sont volontairement UTF-8 uniquement : decodeToString() n'a pas de paramètre charset, et il n'existe pas non plus de fonction de chaînes vers octets qui en ait un. Sur la JVM, vous descendez à l'API charset de la plateforme, qui est honnête et explicite :
import kotlin.io.encoding.Base64
import java.nio.charset.Charset
fun main() {
val latinOne = "héllo".toByteArray(Charsets.ISO_8859_1)
val packed = Base64.encode(latinOne)
println(packed) // aOlsbG8=
val asUtf8 = Base64.decode(packed).decodeToString()
val asLatin = String(Base64.decode(packed), Charsets.ISO_8859_1)
println(asUtf8) // h?llo (l'octet é n'est pas du UTF-8 valide)
println(asLatin) // héllo
val byName = String(Base64.decode(packed), Charset.forName("ISO-8859-1"))
println(byName) // héllo
}
Ce ? de la ligne du milieu n'est pas un problème de police ; c'est U+FFFD, le caractère de remplacement Unicode, qui se met à la place d'un octet qui ne forme pas de UTF-8 valide. Si vous en voyez une suite après décodage, votre payload est sain - c'est votre hypothèse sur le charset qui ne l'est pas. Notez aussi l'asymétrie qui mord : côté encodage, l'extension JVM toByteArray(charset) existe ; côté décodage, le constructeur correspondant est String(octets, charset). Aucun des deux ne prend un nom de charset ; pour ça il faut Charset.forName("..."), qui lève UnsupportedCharsetException pour un nom inventé, donc une faute de frappe dans une valeur de config échoue vite plutôt que de choisir silencieusement un autre encodage.
Puisque nous sommes dans le pays des octets, un piège propre à Kotlin : un Char est une valeur sur 16 bits, et toByte() dessus ne garde que les huit bits de poids faible, en silence. Si vous fabriquez des octets à la main à partir de caractères, "中".first().code.toByte() vous donne 45, un nombre qui n'a rien à voir avec le caractère. Le bon chemin est toujours encodeToByteArray(), qui fait le vrai travail d'encodage - le même caractère fait trois octets UTF-8, et sa forme Base64 est 5Lit. Laissez la bibliothèque standard encoder ; ne jamais emballer des caractères dans des octets à la main.
Fichiers, sous-chaînes et grandes entrées
Les données Base64 ne sont pas toujours une chaîne bien rangée en mémoire. Parfois c'est un fichier, une tranche d'une plus grande réponse, ou quelque chose de trop gros pour être tenu d'un coup. Kotlin vous ouvre les trois portes.
Les fichiers sont un cas ennuyeux dans le meilleur sens du terme : lire les octets, décoder, terminé. Les deux API fichiers standards fonctionnent, à vous de prendre celle que votre projet utilise déjà :
import java.io.File
import kotlin.io.encoding.Base64
import kotlin.io.path.Path
import kotlin.io.path.readBytes
fun main() {
val fromFile = File("payload.b64").readBytes()
println(Base64.decode(fromFile.decodeToString()).size) // nombre d'octets décodés
val fromPath = Path("payload.b64").readBytes()
println(Base64.decode(fromPath.decodeToString()).size) // le même nombre
}
Les sous-chaînes, c'est là que les surcharges CharSequence montrent leur utilité. decode accepte n'importe quelle séquence de caractères avec un index de début et de fin, donc vous pouvez lui passer une tranche d'un long corps de réponse sans d'abord faire une copie de la tranche :
import kotlin.io.encoding.Base64
fun main() {
val body = "prefix junk SGVsbG8= trailing junk"
val bytes = Base64.decode(body, 12, 20)
println(bytes.decodeToString()) // Hello
}
Si vous connaissez déjà la taille de la sortie et que vous voulez réutiliser un tampon, decodeIntoByteArray écrit dans un tableau de destination de votre choix et vous dit combien d'octets il a écrit. Donnez-lui un tampon trop petit et il lève IndexOutOfBoundsException avec la capacité requise dans le message, donc l'erreur fait doublé de conseil de dimensionnement.
Pour des flux vraiment gros sur la JVM, il y a une troisième porte : les décodeurs en streaming. Ils sont encore marqués expérimentaux - d'où l'annotation d'opt-in - et n'existent que pour la JVM, mais ils décodent au fil de l'eau au lieu de tout garder en mémoire :
import java.io.ByteArrayInputStream
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.io.encoding.decodingWith
@OptIn(ExperimentalEncodingApi::class)
fun main() {
val stream = ByteArrayInputStream("SGVsbG8gV29ybGQh".toByteArray())
stream.decodingWith(Base64.Default).use {
println(it.readBytes().decodeToString()) // Hello World!
}
}
Deux détails pratiques. Les fonctions d'extension vivent au niveau supérieur du paquet, donc vous les importez par leur nom (un import par étoile marche aussi, mais les noms sont plus clairs). Et le décodeur traite le padding comme un arrêt définitif : si le flux sous-jacent continue après la section Base64, la lecture du flux décodé s'arrête au = et les octets restants restent disponibles dans le flux d'origine. Ce qui est parfait pour les formats qui greffent du Base64 devant autre chose.
Sur le terrain : API HTTP et corps JSON
Le JSON ne peut pas porter d'octets bruts - c'est un protocole de texte - donc les API qui doivent transporter du binaire (images, certificats, blobs arbitraires) l'enveloppent presque toujours en Base64 dans un champ chaîne. Le schéma est : analyser le JSON, prendre le champ, décoder. Avec la bibliothèque de sérialisation officielle, la partie JSON n'est qu'à deux annotations :
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
@Serializable
data class ImageResponse(val name: String, val data: String)
fun main() {
val body = """{"name":"icon.png","data":"iVBORw0KGgo="}"""
val response = Json.decodeFromString<ImageResponse>(body)
val bytes = Base64.decode(response.data)
println("${response.name}: ${bytes.size} bytes") // icon.png: 8 bytes
}
Cet exemple a besoin du plugin et de la bibliothèque de sérialisation, à ajouter une fois au build :
plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}
Sans la bibliothèque, la même idée marche sur la chaîne brute, ce qui est pratique pour des scripts rapides : sortez le champ avec substringBetween et décodez-le. Les pièges sont les classiques des API : le champ peut être en réalité une data URL complète (avec le préfixe data:image/png;base64,, traité plus loin dans cet article), le payload peut être enveloppé en MIME avec des retours à la ligne, et le payload encodé peut être environ un tiers plus gros que le binaire d'origine, donc surveillez votre budget mémoire sur les grandes réponses.
Sur le terrain : email et entrées enveloppées en MIME
L'email est un monde de texte 7 bits, et la réponse de la RFC 2045 aux pièces jointes binaires est du Base64 avec une particularité : la sortie encodée doit être enveloppée pour qu'aucune ligne ne dépasse 76 caractères. Si vous avez déjà reçu une pièce jointe comme du texte, c'est pourquoi elle ressemble à une colonne de Base64 indentée. Pour exactement cette entrée, Base64.Mime est le bon décodeur, parce qu'il ignore les séparateurs de lignes et les autres caractères hors alphabet au fil de l'eau :
import kotlin.io.encoding.Base64
fun main() {
val wrapped = "SGVs\nbG8=\r\n"
println(Base64.Mime.decode(wrapped).decodeToString()) // Hello
val withJunk = "Y@{mFz!Z!TY}0"
println(Base64.Mime.decode(withJunk).decodeToString()) // base64
}
La permissivité est réelle, mais bornée. Enveloppez l'entrée, saupoudrez d'un ou deux espaces, aucun problème. Ajoutez un caractère de données après le = final, en revanche, et même Mime lève l'exception : Symbol 'e'(145) at index 7 is prohibited after the pad character. Et rappelez-vous que Mime exige que le padding soit présent et correct. Un décodeur MIME qui avalerait aussi le padding manquant serait une invitation aux ennuis. La recette pratique pour un payload d'email entrant capricieux : décodez d'abord en Mime, et si ça lève l'exception, lisez le message - il vous dit exactement quel symbole, à quel index, a rompu les règles.
Sur le terrain : images et data URLs
Une data URL est la façon du web d'intégrer un fichier directement dans un document : un type média, un marqueur base64, et le payload, le tout dans une seule chaîne. Les navigateurs, le CSS et les interfaces embarquées les adorent pour les petits assets - icônes, avatars, graphiques de remplacement - parce qu'il n'y a pas de deuxième requête à faire. Le format ressemble à ça :
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==
En décoder une en Kotlin, c'est une opération sur chaîne suivie d'un décodage Base64. Le préfixe ne porte aucun secret ; tout ce qui suit la virgule est le payload :
import kotlin.io.encoding.Base64
fun main() {
val dataUrl = "data:image/png;base64,iVBORw0KGgo="
val mediaType = dataUrl.substringBefore(";")
val packed = dataUrl.substringAfter("base64,")
val bytes = Base64.decode(packed)
println(mediaType) // data:image/png
println(bytes.size) // 8
println(bytes.contentToString()) // [-119, 80, 78, 71, ...]
}
Ce premier octet, -119 (soit 0x89), suivi des lettres PNG, est le nombre magique qui identifie un fichier PNG. Vérifier les quatre ou huit premiers octets après décodage est une façon bon marché de confirmer qu'une data URL contient vraiment ce que son préfixe prétend. Deux mises en garde honnêtes : le Base64 ajoute environ un tiers à la taille, donc une data URL est un échange de taille contre un aller-retour réseau, et pour tout ce qui est grand, vous êtes généralement mieux de servir le fichier depuis une vraie URL et de laisser le cache faire son travail.
Sur le terrain : configuration, variables d'environnement et bases de données
Le Base64 apparaît dans les fichiers de configuration et les variables d'environnement chaque fois qu'une valeur binaire doit voyager par un canal texte uniquement : une petite icône intégrée dans un fichier de propriétés, un jeton rangé dans une variable d'env d'un conteneur, un blob d'octets garé dans une colonne texte parce que le schéma précède un vrai type binaire. Le côté décodage est le même enchaînement de deux étapes partout - lire le texte, le décoder :
import kotlin.io.encoding.Base64
fun main() {
val line = "icon: UE5HREFUQQ=="
val packed = line.substringAfter("icon: ").trim()
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // PNGDATA
val fromEnv: String? = System.getenv("MY_ICON_B64")
if (fromEnv != null) {
println(Base64.decode(fromEnv).size)
}
}
Le piège de tout ce quartier tient en une ligne : le Base64 n'est pas un chiffrement. C'est un artifice de transport, pas un cadenas. Personne ne devrait regarder une valeur Base64 et croire que la donnée dedans est cachée ; elle est à un seul appel de fonction de la visibilité, et elle est visible dans chaque ligne de log que vous écrivez. Si une valeur est sensible, gardez-la sensible de bout en bout - un coffre à secrets, une colonne chiffrée, tout ce que votre stack fournit - et utilisez le Base64 seulement pour faire voyager les octets dans du texte, pas pour les protéger.
Sur le terrain : la ligne de commande
Le cas d'usage le plus ancien de tous : transformer un blob Base64 sur la ligne de commande en fichier. Un outil complet tient en huit lignes de Kotlin, parce que la bibliothèque standard fait le gros du travail. Compilez-le une fois avec le compilateur Kotlin, et il est à vous pour toujours :
import java.io.File
import kotlin.io.encoding.Base64
fun main(args: Array<String>) {
val packed = if (args.isNotEmpty()) args[0] else readlnOrNull().orEmpty()
val bytes = Base64.decode(packed.trim())
File("decoded.bin").writeBytes(bytes)
println("Wrote ${bytes.size} bytes to decoded.bin")
}
Lancez-le avec un argument pour une valeur ponctuelle, ou pipez-lui un fichier pour du travail en lot : le programme lit le premier argument s'il y en a un, et bascule sur l'entrée standard sinon. Le trim() fait du travail discret ici, parce que les arguments shell et les valeurs collées arrivent souvent avec des espaces parasites que le décodeur strict rejetterait. Et si vos payloads sont en base64url, remplacez Base64.decode par Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL).decode, et l'outil sera prêt pour les jetons aussi.
Guide de terrain des échecs de décodage
Chaque décodeur de ce guide échoue avec l'un de deux types d'exception, et chaque message est assez précis pour vous dire exactement ce qui a mal tourné. Voici la carte complète, avec les messages exacts produits par la bibliothèque standard :
| Situation | Exception | Message (tel que produit) |
|---|---|---|
| Caractère hors de l'alphabet (espace, retour à la ligne, symbole du mauvais schéma) | IllegalArgumentException |
Invalid symbol ' '(40) at index 5 |
| Caractère de données après le padding | IllegalArgumentException |
Symbol 'e'(145) at index 7 is prohibited after the pad character |
Padding manquant alors que l'option est PRESENT |
IllegalArgumentException |
The padding option is set to PRESENT, but the input is not properly padded |
Padding présent alors que l'option est ABSENT |
IllegalArgumentException |
The padding option is set to ABSENT, but the input has a pad character at index 7 |
| Index hors des bornes de la source | IndexOutOfBoundsException |
startIndex: 0, endIndex: 100, size: 8 |
startIndex supérieur à endIndex |
IllegalArgumentException |
startIndex: 3 > endIndex: 2 |
Tampon de destination trop petit pour decodeIntoByteArray |
IndexOutOfBoundsException |
The destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8 |
Remarquez le motif des deux premières lignes : le message nomme le symbole fautif, son code numérique entre parenthèses, et son index. C'est un cadeau pour le débogage. Quand un décodage lève l'exception en production, loggez les premières dizaines de caractères de l'entrée et l'index du message, et vous retrouverez presque toujours le coupable en quelques secondes, qu'il s'agisse d'un retour à la ligne collé, d'un payload tronqué ou d'une chaîne base64url qui s'est égarée dans un décodeur standard.
Des pièges qui piquent particulièrement les développeurs Kotlin
- Partir du principe que le payload est du texte.
decoderenvoie unByteArrayexprès. AppelerdecodeToString()sur un JPEG parce que « c'est probablement du texte » vous donne un mur de caractères de remplacement. Décidez ce que sont les octets avant de les convertir. - Les espaces du copier-coller. Le décodeur par défaut est strict, et une valeur arrachée à un message de chat ou à un log arrive presque toujours avec un retour à la ligne en fin ou un espace en début. Faites un trim avant de décoder, ou décodez à travers
Mime, ou acceptez l'IllegalArgumentExceptionet gérez-la. - La montée de version depuis l'ère expérimentale. Le code écrit pour l'API expérimentale 1.8.x portait des annotations
@OptIn(ExperimentalEncodingApi::class)et comptait sur un padding facultatif. À partir de 2.0.20, la même entrée peut lever l'exception. Le correctif estPRESENT_OPTIONAL, ou nettoyer les entrées avant qu'elles n'arrivent au décodeur. - Associer le schéma au mauvais producteur. Une partie de JWT décodée avec
Base64.Defaultéchoue sur les caractères-et_; un payload de l'alphabet standard décodé avecUrlSafeéchoue sur+et/. L'exception nomme le symbole exact, mais le correctif, c'est de savoir d'où vient la chaîne. - Le trou du charset.
decodeToString()est UTF-8 uniquement, sans surcharge pour les autres encodages. Si l'expéditeur a utilisé Latin-1 ou Windows-1252, prévoyezString(octets, charset)sur la JVM, et attendez-vous aux caractères de remplacement U+FFFD comme symptôme quand vous oubliez. - Le compilateur du paquet de distribution.
apt install kotlinsur Debian et Ubuntu sert la 1.3.31, d'avant l'existence de cette API. Si vos exemples refusent soudain de compiler avec « unresolved reference », vérifiez quel compilateur est vraiment sur le PATH.
Bonnes pratiques du décodage
- Décodez d'abord en octets, interprétez ensuite. Gardez
Base64.decodeet la conversion en texte comme deux étapes séparées. Ça rend le charset explicite, ça garde les payloads binaires binaires, et ça rend les tests triviaux : comparez des tableaux d'octets, pas des chaînes. - Choisissez l'instance qui correspond au producteur. JWT et données destinées aux URL, c'est
UrlSafe; email et fichiers PEM, c'estMimeouPem; tout le reste part deDefault. Les décodeurs permissifs sont faits pour une entrée connue pour être sale, pas pour servir de filet de sécurité général. - Normalisez l'entrée non fiable une seule fois, à bon marché. Un
trim()et, quand le format est connu pour être propre, un nettoyage des espaces, avant un décodage strict, attrapent plus d'échecs du monde réel que toute quantité de try-catch. Un petit helper avec un repliPRESENT_OPTIONALest un bon motif pour les valeurs de sources inconnues. - Estimez la taille avant d'allouer. La sortie décodée fait au plus trois quarts de la longueur de l'entrée (quatre symboles portent trois octets), donc un simple contrôle de longueur vous donne la taille de destination avant de décoder, ce qui est exactement ce qu'il faut savoir avant de remplir un tampon pré-alloué ou d'accepter une chaîne de plusieurs mégaoctets.
- Faites confiance au message d'erreur. La bibliothèque standard rapporte le symbole, son code et son index. Loggez le voisinage de cet index pour une entrée non fiable, et arrêtez de deviner.
- N'utilisez pas le décodage pour cacher des choses, et n'utilisez pas le décodage pour prouver des choses. Le Base64 est un encodage de transport. Il n'apporte ni secret ni intégrité ; si vous en avez besoin, c'est le travail de la cryptographie, pas celui du décodeur.
Comment Base64 est entré dans Kotlin
Le Base64 est plusieurs décennies plus vieux que Kotlin - la spécification MIME qui a donné la règle des lignes de 76 caractères date de 1993, et l'alphabet lui-même des RFC du milieu des années 1990 - mais l'histoire propre à Kotlin est courte et récente. Le paquet kotlin.io.encoding est arrivé dans Kotlin 1.8.20 en avril 2023, apportant Base64 avec trois instances - Default, UrlSafe et Mime - derrière l'annotation @ExperimentalEncodingApi, ainsi que les extensions de streaming exclusives à la JVM qui sont encore expérimentales aujourd'hui. Pendant deux ans, l'utiliser, c'était une ligne d'opt-in dans chaque fonction et un petit risque que l'API bouge.
Kotlin 2.2.0, sorti en juin 2025, a changé le contrat. Toute l'API est devenue stable en une seule release, et l'instance Pem a rejoint la famille (la variante à lignes de 64 caractères de la RFC 1421, utilisée autour de la PKI). La rigueur qui oblige le code de l'ère 1.8 reposant sur un padding facultatif à être révisé après une montée de version est arrivée un peu avant : dans 2.0.20, quand withPadding et ses quatre valeurs PaddingOption ont remplacé le comportement fixe d'avant et que le décodeur a commencé à exiger du padding. La release 2.2 a aussi stabilisé la classe sœur HexFormat dans kotlin.text, l'API de formatage hexadécimal expérimentale depuis Kotlin 1.9, de sorte que les encodages textuels au niveau octet ont maintenant une maison fixée dans la bibliothèque standard. Et en note de maintenance : depuis Kotlin 2.4.0, la bibliothèque standard JVM est livrée avec une fenêtre de support de 18 mois par ligne de release, ce qui est une raison de plus pour un projet sur la ligne 2.4.x actuelle de traiter cette API comme un point fixe plutôt que comme un point mobile.
Le coin des faits amusants
- L'objet compagnon fait du travail. Puisque
Defaultest le compagnon deBase64, le nom de la classe fait doublé d'instance par défaut :Base64.decode(x)etBase64.Default.decode(x)sont le même appel. C'est pour ça que l'exemple en deux lignes en tête de cet article reste en deux lignes. - Le décodage de chaînes a un hack de vitesse sur la JVM. La boucle de décodage courante travaille sur des octets, mais les chaînes Kotlin sont des séquences de caractères. L'implémentation JVM évite la conversion en réinterprétant les caractères d'une
Stringcomme des valeurs ISO-8859-1 sur un octet avant que la boucle partagée ne tourne - un artifice que les commentaires du code source affirment jusqu'à dix fois plus rapide que le chemin courant, et c'est pourquoidecode(String)paraît instantané même sur de longs payloads. - Le nom du paquet est une piste. Vous trouverez cette API dans
kotlin.io.encoding, pas danskotlin.text, parce que tout l'enjeu, c'est que la donnée est des octets - l'entrée et la sortie ont la forme de l'entrée/sortie, et le texte n'est que ce qui arrive au résultat ensuite. - Les messages d'erreur incluent le code du caractère.
Symbol 'e'(145)rapporte la valeur du symbole fautif en octal, pas seulement son glyphe. Pratique quand le coupable est un espace blanc :' '(40)vous dit que c'était un espace bien avant que vous ne l'imaginiez. - PEM est arrivé en retard.
Base64.Pemne faisait pas partie de l'API d'origine 1.8.20 ; elle est apparue avec la stabilisation 2.2. Si un article de blog de 2023 ou 2024 ne liste que trois instances, il n'a pas tort - il a juste deux releases de retard. - Elle est écrite par plateforme, pas déléguée. La bibliothèque standard implémente le codec séparément pour chaque cible, avec des fonctions expect/actual. Sur la JVM il y a même une optimisation commentée qui confierait le travail à
java.util.Base64, désactivée derrière un ticket de compilateur toujours ouvert, et c'est pourquoi le comportement de l'implémentation Kotlin est le comportement de référence sur toutes les plateformes.
En résumé
Décoder du Base64 en Kotlin, c'est une courte liste de choix délibérés : l'instance qui correspond à l'endroit d'où la donnée vient, le mode de padding qui correspond à la façon dont elle a été envoyée, le tampon ou le flux qui correspond à sa taille, et le charset qui correspond à ce qu'elle veut dire. La bibliothèque standard vous remet les quatre comme des fonctions plaines sans dépendance, et ses messages d'erreur sont assez précis pour qu'un échec soit un diagnostic, pas un mystère. La direction opposée - choisir le bon schéma, le bon padding et le bon enveloppement des lignes quand c'est vous qui produisez le Base64 - a ses propres décisions et ses propres pièges, et l'article lié sur le site sœur couvre l'encodage Base64 en Kotlin en profondeur.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Kotlin : un guide complet