Base64-Dekodierung in Kotlin: Ein vollständiger Leitfaden
Sie starren auf einen Wert, der sich partout nicht lesen lassen will: SGVsbG8sIFdvcmxkIQ==. Eine Kette aus Buchstaben und Ziffern, hier und da ein + oder / dazwischen, und meist ein oder zwei =-Zeichen, die am Ende baumeln. Das ist Base64, und dieser Leitfaden dreht sich darum, daraus wieder das zu machen, was es einmal war - einen Satz, ein Bild, ein Zertifikat, einen binären Blob - auf die Kotlin-Art. Ein Refresher in einer Zeile, bevor wir eintauchen: Base64 packt jeweils drei Bytes in vier Zeichen aus einem Alphabet mit 64 Symbolen, und ein kurzer Schwanz aus =-Padding markiert, wo die echten Daten aufhörten. Die komplette Format-Tour gibt es auf der Startseite, also widmen wir ihr hier nur einen Satz, und einen zweiten: Weil vier Zeichen das tragen, was drei Bytes trugen, ist die Textform etwa ein Drittel länger als die Originaldaten.
Die gute Nachricht bei Kotlin: Sie brauchen gar kein Paket. Die Standardbibliothek liefert seit Jahren eine eigene Base64-Implementierung mit; seit Kotlin 2.2 ist sie vollständig stabil, und sie läuft auf jeder Plattform, auf der Kotlin läuft, von der JVM Ihres Laptops über ein Android-Handy und Node.js bis zu einer WASI-Edge-Funktion. Alles, was jetzt kommt, funktioniert mit dem Kotlin, das in Ihrem Projekt mitgeliefert wird.
Die guten Nachrichten zuerst: Was Sie wirklich brauchen
Es gibt kein base64-Artefakt, das Sie zu Gradle hinzufügen müssten, kein NuGet-ähnliches Paket, kein npm-Modul. Die Klasse, die Sie suchen, ist kotlin.io.encoding.Base64, ein Teil der Kotlin-Standardbibliothek selbst. Wenn Sie println schreiben können, können Sie Base64 dekodieren. Drei APIs können in einem Kotlin-Projekt Base64-Arbeit erledigen, und die richtige zu wählen ist die erste echte Entscheidung:
| API | Wo sie läuft | Wann Sie nach ihr greifen |
|---|---|---|
kotlin.io.encoding.Base64 |
Jede Kotlin-Plattform: JVM, Android, JS, Native, Wasm | Standardwahl. Stabil seit Kotlin 2.2, Multiplatform, moderne API |
java.util.Base64 |
Nur JVM (Java 8+; auf Android API 26+) | JVM-only-Codebasen, die ohnehin im Java-Interop-Land leben |
android.util.Base64 |
Nur Android (API 8+) | Legacy-Android-Code, oder wenn Sie gezielt ihre Flaggen-Konstanten brauchen |
Zwei Versionsnotizen, die sich zu kennen lohnen. Erstens: Die Standardbibliotheks-Klasse tauchte zum ersten Mal in Kotlin 1.8.20 (April 2023) hinter einem @ExperimentalEncodingApi-Tor auf; Kotlin 2.0.20 brachte den withPadding-Regler und die strenge Padding-Regel, und Kotlin 2.2.0 (Juni 2025) machte die API stabil und fügte die PEM-Instanz hinzu. Somit: Auf Kotlin 2.2 oder neuer - einschließlich der aktuellen stabilen Linie, 2.4.x - können Sie alles aus diesem Leitfaden mit null Annotationen verwenden. Zweitens: Wenn Ihr Projekt eine Kotlin-Version zwischen 1.8 und 2.1 pinnt, existiert dieselbe Klasse, ist aber als experimentell markiert, und der Compiler lässt sie Ihnen nicht ohne eine @OptIn-Annotation auf der Funktion durch.
Eine Installationsfalle, die manchem schon den einen oder anderen Nachmittag gekostet hat: Das kotlin-Paket in den Debian- und Ubuntu-Repositories ist Version 1.3.31, was komplett vor der Base64-API der Standardbibliothek liegt, sodass es nicht ein einziges Beispiel aus diesem Artikel kompilieren kann. Holen Sie den Compiler stattdessen von den Kotlin-Releases auf GitHub oder von SDKMAN, und pinnen Sie in Gradle-Projekten das Plugin ausdrücklich:
plugins {
kotlin("jvm") version "2.4.10"
}
Ihr erstes Dekodieren: Zwei Zeilen und ein Bytes-Ergebnis
Die gesamte Zeremonie passt in zwei Aussagen, und der klassische TWFu-String ist ein guter Ausgangspunkt:
import kotlin.io.encoding.Base64
fun main() {
val packed = "TWFu"
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // Man
}
Lesen Sie das langsam, denn darin verstecken sich drei Design-Entscheidungen. Erstens: Base64.decode(...) ohne jedes .Default ist kein Tippfehler: Default ist das Companion-Objekt der Klasse, also ist der Aufruf der Funktion auf der Klasse selbst die Kurzform für den Aufruf auf Base64.Default. In älteren Tutorials sehen Sie auch Base64.Default.decode(...), und es bedeutet genau dasselbe. Zweitens, und das ist wichtiger, als es aussieht: decode übergibt Ihnen ein ByteArray, nie einen String. Das Payload könnte ein JPEG, ein X.509-Zertifikat oder ein Satz sein, und die API weigert sich zu raten, welches es ist, also ist der Sprung von Bytes zu Text ein eigener, bewusster Schritt. Drittens: Genau in diesem Schritt wohnt die Zeichensatz-Entscheidung, und hier entstehen die meisten "Mein Base64 kam als Müll zurück"-Bugs. Wir kommen in einem Moment dazu; zuerst ein Roundtrip, der beweist, dass das Dekodieren treu ist:
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
}
Vier Schemata, vier Persönlichkeiten
Die Klasse wird nie instanziiert; Sie wählen eine von vier fertigen Instanzen, und jede dekodiert mit einem anderen Temperament:
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/
}
| Instanz | Alphabet | Wie sie dekodiert |
|---|---|---|
Base64.Default |
A-Z a-z 0-9 + / |
Strikt: wirft bei jedem Zeichen außerhalb des Alphabets; Padding ist erforderlich |
Base64.UrlSafe |
A-Z a-z 0-9 - _ |
Strikt, aber gegen das URL-Alphabet; ein + oder / in der Eingabe wirft |
Base64.Mime |
A-Z a-z 0-9 + / |
Nachsichtig: ignoriert Zeilentrenner und andere Nicht-Alphabet-Zeichen, aber nach dem =-Padding darf nichts folgen; Padding ist erforderlich |
Base64.Pem |
A-Z a-z 0-9 + / |
Nachsichtig, dieselben Regeln wie Mime; das ist die PEM/PKI-Variante desselben Alphabets |
Diesen Strikt/Nachsichtig-Split sollten Sie sich mehr als alles andere einprägen. Default und UrlSafe behandeln jedes fremde Zeichen wie einen Tatort und werfen sofort. Mime und Pem zucken nur mit den Schultern über Zeilenumbrüche, Leerzeichen und verirrte Interpunktion - denn genau das enthält echte E-Mail- und Zertifikatsdateien - aber sie sind auch nicht grenzenlos: Sobald ein Datenzeichen nach dem Padding auftaucht, werfen sogar sie. Die exakten Fehlermeldungen sehen Sie im Feldführer für Dekodierfehler später in diesem Artikel.
Eine weitere Folge dieser Persönlichkeiten: Ein Schema kann die Ausgabe eines anderen nicht lesen. Füttern Sie Base64.Default einen base64url-Token, und das --Zeichen ist nicht in dessen Alphabet, also bekommen Sie IllegalArgumentException: Invalid symbol '-'(55) at index .... Im Zweifel über die Herkunft eines Strings wählen Sie das Schema, das zum Erzeuger passt, nicht das, das zu Ihrer Stimmung passt.
URL-sicheres Base64 und JWTs
Zwei Zeichen im Standardalphabet machen sofort Ärger, wenn Daten durch eine URL reisen müssen. Ein + in einem Query-String wird, bis irgendetwas sie liest, routinemäßig als Leerzeichen neu interpretiert, und / ist ein Pfadtrenner, also darf es in einem URL-Segment gar nicht vorkommen. RFC 4648, Abschnitt 5, löst das, indem er die letzten zwei Alphabet-Symbole tauscht: + wird - und / wird _. Der Name, den Sie am häufigsten hören werden, ist base64url, und in Kotlin heißt es Base64.UrlSafe.
Der größte Verbraucher von base64url ist das JSON Web Token. Ein JWT in seiner kompakten Form ist drei base64url-Teile, verbunden durch Punkte: header.payload.signature. RFC 7515 spezifiziert diese Teile als base64url ohne Padding, was ein zweiter Unterschied zum einfachen Alphabet ist, nicht nur die Zeichen. Hier wird ein Token zum Hinsehen auseinandergenommen:
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
}
Zwei Dinge zu beachten. Die Token-Teile tragen kein Padding, aber Base64.UrlSafe verlangt standardmäßig Padding, also leistet die withPadding(PRESENT_OPTIONAL)-Zeile echte Arbeit: Sie akzeptiert gepaddete und ungepaddete Eingaben gleichermaßen. Und das split(".") plus Destrukturierung ist einfach reines Kotlin, das tut, was das Format verlangt. Eine ernste Warnung: Das Aufmachen eines JWTs dient dazu, ein Token anzuschauen, nicht dazu, ihm zu vertrauen. Header und Payload sind nach dem Dekodieren einfache Daten; nur eine verifizierte Signatur bestätigt, dass das Token echt ist, und dafür brauchen Sie eine echte JWT-Bibliothek, kein von Hand gerolltes String-Splitting.
Padding-Modi und der Strenge-Regler
Padding ist in Kotlin keine feste Tatsache über Base64; es ist eine Einstellung. Jede Instanz trägt eine PaddingOption, alle vier Preset-Instanzen starten auf PRESENT, und withPadding gibt Ihnen eine neue Instanz mit einer anderen Einstellung, während die ursprüngliche unangetastet bleibt. Hier ist der Regler, Option für Option:
| Option | Eingabe ohne Padding | Eingabe mit korrektem Padding |
|---|---|---|
PRESENT (überall Standard) |
Wirft | Dekodiert |
ABSENT |
Dekodiert | Wirft |
PRESENT_OPTIONAL |
Dekodiert | Dekodiert |
ABSENT_OPTIONAL |
Dekodiert | Dekodiert |
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
}
Auf der Dekodierungsseite ist PRESENT_OPTIONAL Ihr Sicherheitsnetz: Es ist die Option, die sagt: "Ich weiß nicht, ob der Sender gepadded hat, und ich gedenke weiterzuarbeiten." Die Fehlermeldungen der anderen Kombinationen sind ungewöhnlich hilfreich, also erkennen Sie sie sofort, wenn ein strikter Decoder auf die falsche Eingabe trifft: Fehlendes Padding unter PRESENT erzeugt The padding option is set to PRESENT, but the input is not properly padded, und Padding unter ABSENT erzeugt The padding option is set to ABSENT, but the input has a pad character at index 7. Ein Verhalten verdient einen Hinweis, weil es die Leute überrascht: Ein doppeltes Padding wie SGVsbG8== ist nicht "zusätzlich, aber in Ordnung". Das erste = beendet die Daten, und das zweite ist ein Zeichen an der Stelle, an der Daten erwartet wurden, also lehnen selbst die nachsichtigsten Decoder es ab.
Hier steckt auch eine Versionsgeschichte. Wenn Sie Code geerbt haben, der gegen die experimentelle 1.8.x-API geschrieben wurde, denken Sie daran, dass das alte decode Eingaben mit oder ohne Padding akzeptierte. In Kotlin 2.0.20 ist Default zur strengen PRESENT-Regel gewechselt, also wirft eine einmal funktionierende ungepaddete Eingabe beim Upgrade über diese Punkt-Version hinaus aus. Die Korrektur ist eine Zeile: withPadding(Base64.PaddingOption.PRESENT_OPTIONAL), oder normalisieren Sie Ihre Eingaben vor dem Dekodieren.
Von Bytes zu Text: Zeichensätze und Unicode
Sobald Sie das ByteArray haben, ist die Frage, was es bedeutet. Wenn das Payload Text ist, lautet die Standardantwort decodeToString(), das die Bytes als UTF-8 interpretiert und auf jeder Plattform funktioniert. Für den üblichen Fall von modernen APIs, E-Mails und Webdaten ist das alles, was Sie je brauchen werden, und Emojis sind auch dabei:
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 😀
}
Im Moment allerdings, in dem der Sender irgendetwas anderes als UTF-8 verwendet hat, liegt die Zeichensatz-Entscheidung bei Ihnen. Kotlins eingebaute Textumwandlungen sind absichtlich nur UTF-8: decodeToString() hat keinen Zeichensatz-Parameter, und es gibt auch keine Zeichen-zu-Bytes-Funktion mit einem. Auf der JVM steigen Sie auf die Plattform-Zeichensatz-API ab, die ehrlich und explizit ist:
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 (das é-Byte ist kein gültiges UTF-8)
println(asLatin) // héllo
val byName = String(Base64.decode(packed), Charset.forName("ISO-8859-1"))
println(byName) // héllo
}
Das ? in der mittleren Zeile ist kein Schriftart-Problem; es ist U+FFFD, das Unicode-Ersetzungszeichen, das für ein Byte einsteht, das kein gültiges UTF-8 bildet. Wenn Sie nach dem Dekodieren eine Kette davon sehen, ist Ihr Payload in Ordnung - Ihre Zeichensatz-Annahme nicht. Beachten Sie auch die Asymmetrie, die Leute beißt: Auf der Kodierungsseite existiert die JVM-Extension toByteArray(charset), auf der Dekodierungsseite ist der passende Konstruktor String(bytes, charset). Keiner nimmt einen Zeichensatz-namen; dafür brauchen Sie Charset.forName("..."), das für erfundene Namen UnsupportedCharsetException wirft, also schlägt ein Tippfehler in einem Konfigurationswert schnell fehl, statt still eine andere Kodierung zu wählen.
Da wir sowieso im Byte-Land sind, eine Kotlin-spezifische Falle: Ein Char ist ein 16-Bit-Wert, und toByte() darauf behält stillschweigend nur die unteren acht Bits bei. Wenn Sie Bytes von Hand aus Zeichen basteln, liefert "中".first().code.toByte() 45, eine Zahl, die mit dem Zeichen nichts zu tun hat. Der richtige Weg ist immer encodeToByteArray(), das die echte Kodierungsarbeit erledigt - dasselbe Zeichen ist drei UTF-8-Bytes, und seine Base64-Form ist 5Lit. Lassen Sie die Standardbibliothek kodieren; packen Sie niemals von Hand Zeichen in Bytes.
Dateien, Teilstrings und große Eingaben
Base64-Daten sind nicht immer ein sauberer String im Speicher. Manchmal ist es eine Datei, ein Ausschnitt aus einer größeren Antwort oder zu groß, um alles auf einmal zu halten. Kotlin gibt Ihnen alle drei Türen.
Dateien sind der langweilige Fall auf die beste Art: Bytes lesen, dekodieren, fertig. Beide Standard-Datei-APIs funktionieren, welche auch immer Ihr Projekt schon nutzt:
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) // Anzahl der dekodierten Bytes
val fromPath = Path("payload.b64").readBytes()
println(Base64.decode(fromPath.decodeToString()).size) // dieselbe Zahl
}
Teilstrings sind der Ort, an dem die CharSequence-Overloads sich bezahlt machen. decode akzeptiert jede Zeichenfolge mit Start- und Endindex, also können Sie ihm einen Ausschnitt aus einem langen Antwortkörper übergeben, ohne zuerst eine Kopie des Ausschnitts anzulegen:
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
}
Wenn Sie die Ausgabegröße schon kennen und einen Buffer wiederverwenden wollen, schreibt decodeIntoByteArray in ein Ziel-Array Ihrer Wahl und sagt Ihnen, wie viele Bytes es geschrieben hat. Geben Sie ihm einen zu kleinen Buffer, und es wirft IndexOutOfBoundsException mit der nötigen Kapazität in der Meldung, sodass der Fehler gleich als Größen-Hinweis dient.
Für wirklich große Streams auf der JVM gibt es eine dritte Tür: die Streaming-Dekoder. Sie sind immer noch als experimentell markiert - daher die Opt-in-Annotation - und existieren nur für die JVM, aber sie dekodieren auf dem Laufenden, statt alles im Speicher zu halten:
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!
}
}
Zwei praktische Details. Die Extension-Funktionen leben auf der obersten Ebene des Pakets, also importieren Sie sie beim Namen (ein Stern-Import funktioniert auch, aber Namen sind freundlicher). Und der Dekoder behandelt das Padding als harten Stopp: Wenn der zugrunde liegende Stream nach dem Base64-Abschnitt weitergeht, endet das Lesen aus dem dekodierten Stream beim =, und die übrig gebliebenen Bytes bleiben im Original-Stream verfügbar. Das macht es sauber für Formate, die Base64 vor irgendetwas anderes tackern.
Im Schützengraben: HTTP-APIs und JSON-Bodies
JSON kann keine rohen Bytes tragen - es ist ein Textprotokoll - also wickeln APIs, die Binärdaten bewegen müssen (Bilder, Zertifikate, beliebige Blobs), sie fast immer in Base64 ein und legen sie in ein String-Feld. Das Muster ist: JSON parsen, das Feld nehmen, dekodieren. Mit der offiziellen Serialisierungsbibliothek ist der JSON-Teil nur zwei Annotationen entfernt:
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
}
Dieses Beispiel braucht das Serialisierungs-Plugin und die Bibliothek, einmal zum Build hinzugefügt:
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")
}
Ohne die Bibliothek funktioniert dieselbe Idee auf dem rohen String, was für schnelle Skripte praktisch ist: Ziehen Sie das Feld mit substringBetween heraus und dekodieren Sie es. Die Fallen sind die vertrauten API-Fallen: Das Feld könnte tatsächlich eine vollständige Data-URL sein (mit dem data:image/png;base64,-Präfix, das später in diesem Artikel behandelt wird), das Payload könnte MIME-mäßig mit Zeilenumbrüchen umwickelt sein, und das kodierte Payload kann etwa ein Drittel größer sein als die ursprünglichen Binärdaten, also achten Sie bei großen Antworten auf Ihr Speicher-Budget.
Im Schützengraben: E-Mail und MIME-umwickelte Eingaben
E-Mail ist eine 7-Bit-Textwelt, und die Antwort von RFC 2045 auf binäre Anhänge ist Base64 mit einem Kniff: die kodierte Ausgabe muss umgebrochen werden, damit keine Zeile länger als 76 Zeichen wird. Wenn Sie schon einmal einen Anhang als Text erhalten haben, wissen Sie, warum er wie eine eingerückte Spalte aus Base64 aussieht. Für genau diese Eingabe ist Base64.Mime der richtige Dekoder, denn es ignoriert auf dem Weg Zeilentrenner und andere Nicht-Alphabet-Zeichen:
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
}
Die Nachsichtigkeit ist real, aber begrenzt. Wickeln Sie die Eingabe um, streuen Sie ein, zwei Leerzeichen ein, kein Problem. Hängen Sie aber ein Datenzeichen nach dem letzten = an, und sogar Mime wirft: Symbol 'e'(145) at index 7 is prohibited after the pad character. Und denken Sie daran, dass Mime nach wie vor verlangt, dass das Padding vorhanden und korrekt ist. Ein MIME-Dekoder, der auch fehlendes Padding verschluckt, würde sich Ärger einhandeln. Das praktische Rezept für unordentliche eingehende E-Mail-Payloads lautet: erst mit Mime dekodieren, und wenn das wirft, die Meldung lesen - sie sagt Ihnen genau, welches Symbol an welchem Index die Regeln gebrochen hat.
Im Schützengraben: Bilder und Data-URLs
Eine Data-URL ist die Art und Weise des Webs, eine Datei direkt in ein Dokument einzubetten: ein Medientyp, eine base64,-Markierung und das Payload, alles in einem String. Browser, CSS und eingebettete UIs lieben sie für kleine Assets - Icons, Avatare, Platzhalter-Grafiken - denn es ist keine zweite Anfrage nötig. Das Format sieht so aus:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==
Das Dekodieren einer davon in Kotlin ist eine Zeichenkettenoperation, gefolgt von einem Base64-Dekodieren. Das Präfix trägt kein Geheimnis; alles nach dem Komma ist das 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, ...]
}
Das erste Byte, -119 (das ist 0x89), gefolgt von den Buchstaben PNG, ist die magische Zahl, die eine PNG-Datei identifiziert. Die ersten vier oder acht Bytes nach dem Dekodieren zu prüfen, ist ein billiger Weg, zu bestätigen, dass eine Data-URL wirklich das enthält, was ihr Präfix behauptet. Zwei ehrliche Einschränkungen: Base64 fügt etwa ein Drittel zur Größe hinzu, also ist eine Data-URL ein Größen-Kompromiss, den Sie gegen einen Netzwerk-Roundtrip eintauschen, und für alles Große lohnt es sich meist, die Datei von einer echten URL auszuliefern und den Cache seine Arbeit tun zu lassen.
Im Schützengraben: Konfiguration, Umgebungsvariablen und Datenbanken
Base64 taucht in Konfigurationsdateien und Umgebungsvariablen auf, wann immer ein binärer Wert durch einen reinen Textkanal reisen muss: ein kleines eingebettetes Icon in einer Properties-Datei, ein Token, das in einer Umgebungsvariablen auf einem Container gespeichert ist, ein Byte-Blob, der in einer Textspalte parkt, weil das Schema vor der Einführung eines echten binären Typs lag. Die Dekodierseite ist überall dieselben zwei Schritte - den Text lesen, ihn dekodieren:
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)
}
}
Die Falle in dieser ganzen Gegend passt in eine Zeile: Base64 ist keine Verschlüsselung. Es ist ein Transport-Trick, kein Schloss. Niemand sollte einen Base64-Wert lesen und denken, die Daten darin seien verborgen; er ist einen Funktionsaufruf von sichtbar entfernt, und er ist in jeder Zeile sichtbar, die Sie in ein Log schreiben. Wenn ein Wert sensibel ist, halten Sie ihn von Anfang bis Ende sensibel - ein Secret-Store, eine verschlüsselte Spalte, was auch immer Ihr Stack bietet - und nutzen Sie Base64 nur, damit die Bytes durch Text reisen, nicht um sie zu schützen.
Im Schützengraben: Die Kommandozeile
Der älteste Anwendungsfall von allen: einen Base64-Blob auf der Kommandozeile in eine Datei zu verwandeln. Ein komplettes Werkzeug sind acht Zeilen Kotlin, weil die Standardbibliothek die schwere Arbeit macht. Kompilieren Sie es einmal mit dem Kotlin-Compiler, und es ist für immer Ihres:
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")
}
Führen Sie es mit einem Argument für einen Einmal-Wert aus, oder pipen Sie eine Datei hinein für Stapelarbeit: Das Programm liest das erste Argument, wenn vorhanden, und fällt sonst auf die Standardeingabe zurück. Das trim() leistet hier stillen Dienst, weil Shell-Argumente und eingefügte Werte gern mit ungewolltem Leerraum ankommen, den der strikte Dekoder ablehnen würde. Und wenn Ihre Payloads base64url sind, tauschen Sie Base64.decode gegen Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL).decode aus, und das Werkzeug ist auch für Tokens bereit.
Ein Feldführer für Dekodierfehler
Jeder Dekoder in diesem Leitfaden schlägt mit einem von zwei Ausnahmetypen fehl, und jede Meldung ist spezifisch genug, um Ihnen genau zu sagen, was schiefgegangen ist. Hier ist die komplette Landkarte, mit den exakten Meldungen, die die Standardbibliothek produziert:
| Situation | Ausnahme | Meldung (so wie sie erzeugt wird) |
|---|---|---|
| Zeichen außerhalb des Alphabets (Leerzeichen, Zeilenumbruch, Symbol des falschen Schemas) | IllegalArgumentException |
Invalid symbol ' '(40) at index 5 |
| Datenzeichen nach dem Padding | IllegalArgumentException |
Symbol 'e'(145) at index 7 is prohibited after the pad character |
Fehlendes Padding, während die Option PRESENT ist |
IllegalArgumentException |
The padding option is set to PRESENT, but the input is not properly padded |
Padding vorhanden, während die Option ABSENT ist |
IllegalArgumentException |
The padding option is set to ABSENT, but the input has a pad character at index 7 |
| Index außerhalb der Grenzen der Quelle | IndexOutOfBoundsException |
startIndex: 0, endIndex: 100, size: 8 |
startIndex größer als endIndex |
IllegalArgumentException |
startIndex: 3 > endIndex: 2 |
Ziel-Buffer zu klein für decodeIntoByteArray |
IndexOutOfBoundsException |
The destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8 |
Beachten Sie das Muster in den ersten beiden Zeilen: Die Meldung benennt das übertretende Symbol, seinen numerischen Code in Klammern und seinen Index. Das ist ein Debugging-Geschenk. Wenn ein Dekodieren in Produktion wirft, loggen Sie die ersten paar Dutzend Zeichen der Eingabe und den Index aus der Meldung, und Sie finden den Übeltäter fast immer in Sekunden, egal ob es ein eingefügter Zeilenumbruch ist, ein abgeschnittenes Payload oder ein base64url-String, der in einen Standard-Dekoder verirrt ist.
Fallen, die Kotlin-Entwickler besonders beißen
- Das Payload für Text halten.
decodegibt absichtlich einByteArrayzurück.decodeToString()auf ein JPEG aufzurufen, weil es "wohl Text sein muss", gibt Ihnen eine Wand aus Ersetzungszeichen. Entscheiden Sie, was die Bytes sind, bevor Sie sie umwandeln. - Kopierter Leerraum. Der Standard-Dekoder ist strikt, und ein Wert, der aus einer Chat-Nachricht oder einem Log gerettet wurde, kommt fast immer mit einem Zeilenumbruch am Ende oder einem Leerzeichen am Anfang an. Trimmen Sie vor dem Dekodieren, oder dekodieren Sie über
Mime, oder akzeptieren Sie dieIllegalArgumentExceptionund behandeln Sie sie. - Upgrade aus der experimentellen Ära. Code, der für die experimentelle 1.8.x-API geschrieben wurde, trug
@OptIn(ExperimentalEncodingApi::class)-Annotationen und verließ sich darauf, dass Padding optional war. Ab 2.0.20 kann dieselbe Eingabe werfen. Die Korrektur istPRESENT_OPTIONAL, oder die Eingaben zu bereinigen, bevor sie den Dekoder erreichen. - Das Schema auf den falschen Erzeuger ausrichten. Ein JWT-Teil, dekodiert mit
Base64.Default, scheitert an seinen-- und_-Zeichen; ein Standard-Alphabet-Payload, dekodiert mitUrlSafe, scheitert an+und/. Die Ausnahme benennt das exakte Symbol, aber die Korrektur ist zu wissen, woher der String kommt. - Die Zeichensatz-Lücke.
decodeToString()ist nur UTF-8, ohne Overload für andere Kodierungen. Wenn der Sender Latin-1 oder Windows-1252 verwendet hat, planen SieString(bytes, charset)auf der JVM ein, und erwarten Sie U+FFFD-Ersetzungszeichen als Symptom, wenn Sie es vergessen. - Der Distribution-Paket-Compiler.
apt install kotlinauf Debian und Ubuntu serviert 1.3.31, aus einer Zeit vor dieser API. Wenn Ihre Beispiele plötzlich mit "unresolved reference" die Kompilierung verweigern, prüfen Sie, welcher Compiler tatsächlich auf dem PATH ist.
Best Practices fürs Dekodieren
- Erst zu Bytes dekodieren, zweitens interpretieren. Halten Sie
Base64.decodeund die Textumwandlung als separate Schritte. Das macht den Zeichensatz explizit, hält binäre Payloads binär und macht Tests trivial: vergleichen Sie Byte-Arrays, keine Strings. - Wählen Sie die Instanz, die zum Erzeuger passt. JWT und URL-gebundene Daten bedeuten
UrlSafe; E-Mail und PEM-Dateien bedeutenMimeoderPem; alles andere startet beiDefault. Die nachsichtigen Dekoder sind für bekannte unordentliche Eingaben da, nicht als allgemeines Sicherheitsnetz. - Unvertraute Eingabe einmal normalisieren, günstig. Ein
trim()und, wo das Format bekanntermaßen sauber ist, ein Entfernen des Leerraums vor einem strikten Dekodieren fängt mehr reale Fehler ab als jede Menge try-catch. Ein kleiner Helfer mitPRESENT_OPTIONAL-Fallback ist ein gutes Muster für Werte aus unbekannten Quellen. - Budgetieren Sie die Größe, bevor Sie allozieren. Dekodierte Ausgabe ist höchstens drei Viertel der Eingabelänge (vier Symbole tragen drei Bytes), also sagt ein schneller Längencheck Ihnen die Zielgröße, bevor Sie dekodieren, was genau das ist, was Sie vor dem Befüllen eines vorallozierten Buffers oder dem Akzeptieren eines mehr-Megabyte-Strings wollen.
- Vertrauen Sie der Fehlermeldung. Die Standardbibliothek meldet das Symbol, seinen Code und seinen Index. Loggen Sie die Umgebung dieses Indexes für unvertraute Eingabe und hören Sie auf zu raten.
- Dekodieren Sie nicht zum Verstecken, und dekodieren Sie nicht zum Beweisen. Base64 ist eine Transport-Kodierung. Sie fügt weder Geheimhaltung noch Integrität hinzu; wenn Sie eines davon brauchen, ist das die Aufgabe der Kryptographie, nicht die des Dekoders.
Wie Base64 in Kotlin kam
Base64 ist um einige Jahrzehnte älter als Kotlin - die MIME-Spezifikation, die die 76-Zeichen-Regel lieferte, stammt aus 1993, und das Alphabet selbst aus RFCs von Mitte der 1990er - aber die Kotlin-spezifische Geschichte ist kurz und jung. Das kotlin.io.encoding-Paket kam in Kotlin 1.8.20 im April 2023, brachte Base64 mit drei Instanzen - Default, UrlSafe und Mime - hinter der @ExperimentalEncodingApi-Annotation, zusammen mit den nur für die JVM vorhandenen Streaming-Extensions, die auch heute noch experimentell sind. Zwei Jahre lang bedeutete die Nutzung eine Opt-in-Zeile in jeder Funktion und eine kleine Chance, dass sich die API noch bewegt.
Kotlin 2.2.0, veröffentlicht im Juni 2025, änderte den Vertrag. Die ganze API wurde in einem Release stabil, und die Pem-Instanz trat zur Familie hinzu (die RFC-1421-Variante mit 64-Zeichen-Zeilen, die um PKI herum verwendet wird). Die Strenge, die Code aus der 1.8-Ära, in der Padding optional war, nach einem Upgrade Aufmerksamkeit kostet, kam einen Schritt früher: in 2.0.20, als withPadding mit seinen vier PaddingOption-Werten das alte feste Verhalten ersetzte und der Dekoder begann, Padding zu verlangen. Das 2.2-Release stabilisierte auch die Schwesterklasse HexFormat in kotlin.text, die Hex-Formatierungs-API, die seit Kotlin 1.9 experimentell war, sodass textuelle Kodierungen auf Byte-Ebene jetzt einen festen Wohnsitz in der Standardbibliothek haben. Und ein Pflege-Hinweis: Seit Kotlin 2.4.0 liefert die JVM-Standardbibliothek ein 18-Monate-Unterstützungsfenster pro Release-Linie aus, was ein weiterer Grund ist, warum ein Projekt auf der aktuellen 2.4.x-Linie diese API als festen Punkt behandeln kann, nicht als einen sich bewegenden.
Fun Facts
- Das Companion-Objekt hat eine Aufgabe. Da
Defaultdas Companion vonBase64ist, dient der Klassenname auch als Standard-Instanz:Base64.decode(x)undBase64.Default.decode(x)sind derselbe Aufruf. Das ist der Grund, warum das Zwei-Zeilen-Beispiel oben in diesem Artikel bei zwei Zeilen bleibt. - String-Dekodierung bekommt auf der JVM einen Geschwindigkeits-Trick. Die übliche Dekodier-Schleife arbeitet auf Bytes, aber Kotlin-Strings sind Zeichenfolgen. Die JVM-Implementation umgeht die Umwandlung, indem sie die Zeichen eines
Strings, bevor die gemeinsame Schleife läuft, als einbytegroße ISO-8859-1-Werte neu interpretiert - laut Quellcode-Kommentaren bis zu zehnmal schneller als der gewöhnliche Pfad, und genau deshalb fühlt sichdecode(String)auch auf langen Payloads sofort an. - Der Paketname ist ein Hinweis. Diese API finden Sie in
kotlin.io.encoding, nicht inkotlin.text, denn der ganze Punkt ist, dass die Daten Bytes sind - Eingabe und Ausgabe haben I/O-Form, und der Text ist nur das, was danach mit dem Ergebnis passiert. - Die Fehlermeldungen enthalten den Code des Zeichens.
Symbol 'e'(145)meldet den Wert des übertretenden Symbols in Oktal, nicht nur seine Glyphe. Praktisch, wenn der Übeltäter Leerraum ist:' '(40)sagt Ihnen, dass es ein Leerzeichen war, lange bevor Sie eines vermuten. - PEM kam spät.
Base64.Pemwar nicht Teil der ursprünglichen 1.8.20-API; es erschien mit der 2.2-Stabilisierung. Wenn ein Blog-Post aus 2023 oder 2024 nur drei Instanzen auflistet, ist er nicht falsch - er ist nur zwei Releases veraltet. - Es ist pro Plattform geschrieben, nicht delegiert. Die Standardbibliothek implementiert den Codec mit expect/actual-Funktionen separat für jedes Ziel. Auf der JVM gibt es sogar eine auskommentierte Optimierung, die die Arbeit an
java.util.Base64übergeben würde, deaktiviert hinter einem offenen Compiler-Problem, deshalb ist das Verhalten der Kotlin-Implementation das Referenzverhalten auf jeder Plattform.
Zum Abschluss
Base64 in Kotlin zu dekodieren kommt auf eine kurze Liste bewusster Entscheidungen an: die Instanz, die dazu passt, woher die Daten kamen, der Padding-Modus, der dazu passt, wie sie gesendet wurden, der Buffer oder Stream, der dazu passt, wie groß sie sind, und der Zeichensatz, der dazu passt, was sie bedeuten. Die Standardbibliothek gibt Ihnen alle vier als gewöhnliche Funktionen ohne Abhängigkeiten, und ihre Fehlermeldungen sind spezifisch genug, dass ein Fehler eine Diagnose ist, nicht ein Rätsel. Die entgegengesetzte Richtung - das richtige Schema, das passende Padding und den passenden Zeilenumbruch zu wählen, wenn Sie selbst das Base64 produzieren - hat ihre eigenen Entscheidungen und ihre eigenen Fallen, und der verwandte Artikel auf der Schwesternsite behandelt die Base64-Kodierung in Kotlin ausführlich.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Kotlin: Ein vollständiger Leitfaden