Base64-decodering in Kotlin: een complete gids
Je staar naar een waarde die weigert gelezen te worden: SGVsbG8sIFdvcmxkIQ==. Een reeks letters en cijfers, hier en daar een + of / erbij, en meestal nog één of twee =-tekens die aan het einde ophangen. Dit is Base64, en deze gids gaat over het terugbrengen van die waarde naar wat hij oorspronkelijk was - een zin, een afbeelding, een certificaat, een binair blok - op de Kotlin-manier. Voor we ingaan, even de herhaling in één regel: Base64 pakt elke drie bytes samen in vier tekens uit een alfabet van 64 symbolen, en een korte staart van =-padding markeert waar de echte data ophield. De volledige toer door het formaat staat op de startpagina, dus besteden we hier alleen één zin eraan, plus nog één: omdat vier tekens dragen wat drie bytes droegen, is de tekstvorm ongeveer een derde langer dan de oorspronkelijke data.
Goed nieuws over Kotlin: je hebt geen enkel pakket nodig. De standaardbibliotheek levert al jaren haar eigen Base64-implementatie mee; die is sinds Kotlin 2.2 volledig stabiel, en draait op elk platform waarop Kotlin draait, van de JVM op je laptop tot een Android-telefoon, Node.js en een WASI-edgefunctie. Alles hieronder werkt met de Kotlin die bij je project geleverd wordt.
Eerst het goede nieuws: wat je écht nodig hebt
Er is geen base64-artefact dat je aan Gradle toevoegt, geen NuGet-achtig pakket, geen npm-module. De class die je zoekt is kotlin.io.encoding.Base64, een onderdeel van de Kotlin-standaardbibliotheek zelf. Als je println kunt schrijven, kun je Base64 decoderen. Drie API's kunnen Base64-werk doen in een Kotlin-project, en de juiste kiezen is het eerste echte besluit:
| API | Waar het draait | Wanneer je ernaartoe grijpt |
|---|---|---|
kotlin.io.encoding.Base64 |
Elk Kotlin-platform: JVM, Android, JS, Native, Wasm | Standaardkeuze. Stabiel sinds Kotlin 2.2, multiplatform, moderne API |
java.util.Base64 |
Alleen JVM (Java 8+; op Android API 26+) | Alleen-JVM-codebases die al in het interop-gebied van Java leven |
android.util.Base64 |
Alleen Android (API 8+) | Oude Android-code, of wanneer je specifiek zijn vlagconstanten nodig hebt |
Twee versienota's die het weten waard zijn. Ten eerste verscheen de class uit de standaardbibliotheek voor het eerst in Kotlin 1.8.20 (april 2023) achter een @ExperimentalEncodingApi-poort; Kotlin 2.0.20 bracht de withPadding-regelaar en de strikte paddingregel, en Kotlin 2.2.0 (juni 2025) maakte de API stabiel en voegde de PEM-instantie toe. Dus op Kotlin 2.2 of nieuwer - waaronder de huidige stabiele lijn, 2.4.x - kun je alles uit deze gids gebruiken zonder enige annotatie. Ten tweede: als je project een Kotlin-versie tussen 1.8 en 2.1 vastzet, bestaat dezelfde class maar is hij experimenteel gemarkeerd, en laat de compiler hem zonder een @OptIn-annotatie op de functie niet gebruiken.
Eén installatieval die al meer dan één middag heeft gekost: het kotlin-pakket in de Debian- en Ubuntu-repositories is versie 1.3.31, wat voorafgaat aan de Base64-API van de standaardbibliotheek als geheel, en kan dus geen enkel voorbeeld uit dit artikel compileren. Pak de compiler in plaats daarvan in van de Kotlin-releases op GitHub of van SDKMAN, en zet de plug-in in Gradle-projecten expliciet vast:
plugins {
kotlin("jvm") version "2.4.10"
}
Je eerste decode: twee regels en een bytes-resultaat
Heel het ceremonieel past in twee instructies, en de klassieke TWFu-string is een prima plek om te beginnen:
import kotlin.io.encoding.Base64
fun main() {
val packed = "TWFu"
val bytes = Base64.decode(packed)
println(bytes.decodeToString()) // Man
}
Lees dat langzaam, want er zitten drie ontwerpbeslissingen in verstopt. Eerst: Base64.decode(...) zonder .Default is geen typefout: Default is het companion-object van de class, dus de functie op de class zelf aanroepen is een afkorting voor het aanroepen ervan op Base64.Default. Je zult ook Base64.Default.decode(...) in oudere tutorials zien, en dat betekent exact hetzelfde. Ten tweede, en dit telt meer dan het lijkt: decode reikt je een ByteArray aan, nooit een String. Het payload kan een JPEG zijn, een X.509-certificaat of een zin, en de API weigert te raden welk, dus de sprong van bytes naar tekst is een afzonderlijke, doelbewuste stap. Ten derde: die stap is waar het charset-besluit woont, en waar de meeste "mijn Base64 kwam terug als rommel"-bugs geboren worden. We komen daar zo; eerst een heen-en-weertrip om te bewijzen dat de decode trouw is:
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 schema's, vier temperamenten
De class wordt nooit geïstantieerd; je kiest een van de vier kant-en-klare instanties, en elke instantie decodeert met een ander 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/
}
| Instantie | Alfabet | Hoe het decodeert |
|---|---|---|
Base64.Default |
A-Z a-z 0-9 + / |
Strikt: elk teken buiten het alfabet gooit een uitzondering; padding is vereist |
Base64.UrlSafe |
A-Z a-z 0-9 - _ |
Strikt, maar tegen het URL-alfabet; een + of / in de invoer gooit een uitzondering |
Base64.Mime |
A-Z a-z 0-9 + / |
Tolerant: negeert regeleinden en andere niet-alfabettekens, maar er mag niets na de =-padding volgen; padding is vereist |
Base64.Pem |
A-Z a-z 0-9 + / |
Tolerant, dezelfde regels als Mime; dit is de PEM/PKI-flavor van hetzelfde alfabet |
De opdeling tolerant/strikt is het meest nuttige punt om te binnenslijpen. Default en UrlSafe behandelen elk vreemd teken als een misdadlocatie en gooien direct een uitzondering. Mime en Pem schouderen bij regeleinden, spaties en verweesde leestekens - want juist dat bevatten echte e-mails en certificaatbestanden - maar ze zijn niet onbegrensd: het moment dat een datateken verschijnt na de padding, gooien óók zij een uitzondering. De exacte foutmeldingen zie je in de mislukkingengids verderop in dit artikel.
Nog een gevolg van temperamenten: een schema kan de uitvoer van een ander schema niet lezen. Geef een base64url-token aan Base64.Default en het --teken zit niet in zijn alfabet, dus krijg je IllegalArgumentException: Invalid symbol '-'(55) at index .... Twijfel je over waar een string vandaan komt, kies dan het schema dat bij de producent past, niet het een dat bij je stemming past.
URL-safe Base64 en JWTs
Twee tekens in het standaardalfabet veroorzaken problemen zodra data door een URL moet reizen. Een + in een query string wordt doorgaans herlezen als een spatie voordat er iets van wordt gelezen, en / is een padseparator, dus mag hij helemaal niet in een URL-segment voorkomen. RFC 4648, sectie 5, lost dit op door de twee laatste alfabettekens te verwisselen: + wordt - en / wordt _. De naam die je het meest hoort is base64url, en in Kotlin is dat Base64.UrlSafe.
De grootste afnemer van base64url is de JSON Web Token. Een JWT in zijn compacte vorm is drie base64url-onderdelen, door punten aan elkaar gekoppeld: header.payload.signature. RFC 7515 specificeert deze onderdelen als base64url zonder padding, wat een tweede verschil is met het simpele alfabet, naast de tekens zelf. Hier is een token dat wordt uit elkaar gehaald om te bekijken:
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
}
Twee dingen om op te letten. De tokenonderdelen hebben geen padding, maar Base64.UrlSafe vraagt er standaard om, dus doet de withPadding(PRESENT_OPTIONAL)-regel echt werk: die accepteert gepadden en ongepadden invoer allebei. En split(".") plus destructurering is gewoon Kotlin dat doet wat het formaat van hem vraagt. Eén ernstige waarschuwing: een JWT uit elkaar halen is om er naar te kijken, niet om hem te vertrouwen. De header en het payload zijn na het decoderen simpele data; alleen een geverifieerde handtekening zegt dat het token echt is, en daarvoor wil je een echte JWT-bibliotheek, geen zelfgeschreven string-splitsing.
Paddingmodi en de regelaar voor striktheid
Padding is geen vast gegeven van Base64 in Kotlin; het is een instelling. Elke instantie heeft een PaddingOption, alle vier de voorgedefinieerde instanties starten op PRESENT, en withPadding reikt je een nieuwe instantie aan met een andere instelling, terwijl het origineel onaangetast blijft. Hier is de regelaar, optie voor optie:
| Optie | Invoer zonder padding | Invoer met correcte padding |
|---|---|---|
PRESENT (overal de standaard) |
Gooit een uitzondering | Decodeert |
ABSENT |
Decodeert | Gooit een uitzondering |
PRESENT_OPTIONAL |
Decodeert | Decodeert |
ABSENT_OPTIONAL |
Decodeert | Decodeert |
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
}
Op de decoderingskant is PRESENT_OPTIONAL je vangnet: het is de optie die zegt "ik weet niet of de afzender padding gebruikte, en ik wil gewoon doorgaan". De foutmeldingen voor de andere combinaties zijn opvallend behulpzaam, dus herken je ze direct als een strikte decoder de verkeerde invoer tegenkomt: ontbrekende padding bij PRESENT levert The padding option is set to PRESENT, but the input is not properly padded, en padding bij ABSENT levert The padding option is set to ABSENT, but the input has a pad character at index 7. Eén gedrag verdient een aparte noot, want het verbaast mensen: dubbele padding zoals SGVsbG8== is niet "extra, maar oké". De eerste = beëindigt de data, en de tweede staat op een plek waar data verwacht werd, dus weigeren zelfs de tolerantste decoders hem.
Er zit ook een versiegeschiedenis verborgen in dit hoekje. Als je code hebt overgenomen die geschreven is tegen de experimentele 1.8.x-API, houd dan rekening met het feit dat de oude decode invoer accepteerde met of zonder padding. In Kotlin 2.0.20 ging Default over op de strikte PRESENT-regel, dus een ooit-werkende niet-ingevulde invoer gooit nu een uitzondering bij een upgrade voorbij die puntrelease. De oplossing is één regel: withPadding(Base64.PaddingOption.PRESENT_OPTIONAL), of normaliseer je invoer voordat je decodeert.
Van bytes naar tekst: charsets en Unicode
Als je je ByteArray hebt, is de vraag wat die betekent. Als het payload tekst is, dan is het standaardantwoord decodeToString(), dat de bytes als UTF-8 interpreteert en op elk platform werkt. Voor het algemene geval van moderne API's, e-mails en webdata is dat alles wat je ooit nodig hebt, en emoji inclusief:
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 😀
}
Het moment dat de afzender iets anders dan UTF-8 gebruikte, is het charset-besluit aan jou. De ingebouwde tekstconversies van Kotlin zijn bewust alleen UTF-8: decodeToString() heeft geen charset-parameter, en er is ook geen functie van string naar bytes die er een heeft. Op de JVM zak je terug naar het platform-charset-API, dat eerlijk en expliciet is:
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 (de é-byte is geen geldige UTF-8)
println(asLatin) // héllo
val byName = String(Base64.decode(packed), Charset.forName("ISO-8859-1"))
println(byName) // héllo
}
Die ? in het midden is geen lettertypeprobleem; het is U+FFFD, het Unicode-vervangingsteken, dat opkomt voor een byte die geen geldige UTF-8 vormt. Zie je een reeks van die tekens na het decoderen, dan is je payload in orde - je charset-aanneming niet. Merk ook de asymmetrie op waar mensen tegenaan lopen: aan de encodeerkant bestaat de JVM-extensie toByteArray(charset); aan de decodeerkant is de overeenkomstige constructor String(bytes, charset). Geen van beide neemt een charset-naam; daarvoor heb je Charset.forName("...") nodig, die voor een verzonnen naam een UnsupportedCharsetException gooit, zodat een typefout in een config-waarde snel faalt in plaats van stil een andere encoding te kiezen.
Terwijl we in het rijk van de bytes zijn, één Kotlin-specifieke val: een Char is een 16-bits-waarde, en toByte() daarop behoudt zonder waarschuwing alleen de laagste acht bits. Stel je zelf bytes samen uit tekens, dan geeft "中".first().code.toByte() je 45, een getal dat niets met het teken te maken heeft. De juiste weg is altijd encodeToByteArray(), dat het echte encodeerwerk doet - hetzelfde teken is drie UTF-8-bytes, en de Base64-vorm daarvan is 5Lit. Laat de standaardbibliotheek encoderen; pak tekens nooit handmatig in bytes.
Bestanden, substrings en grote invoer
Base64-data is niet altijd een netjes opgeruimde string in het geheugen. Soms is het een bestand, een stuk van een groter antwoord, of te groot om het allemaal tegelijk vast te houden. Kotlin geeft je alle drie de deuren.
Bestanden zijn het saaie geval, op de beste manier: lees de bytes, decodeer, klaar. Beide standaard-bestands-API's werken, welke je project ook al gebruikt:
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) // aantal gedecodeerde bytes
val fromPath = Path("payload.b64").readBytes()
println(Base64.decode(fromPath.decodeToString()).size) // hetzelfde getal
}
Substrings zijn waar de CharSequence-overloads zich bewijzen. decode accepteert elke tekenreeks met een begin- en eindindex, dus je kunt hem een stuk van een lang antwoordlichaam geven zonder eerst een kopie van dat stuk te maken:
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
}
Als je de grootte van de uitvoer al weet en een buffer wilt hergebruiken, schrijft decodeIntoByteArray naar een doelarray naar keuze en zegt je hoeveel bytes het schreef. Geef hem een buffer die te klein is en hij gooit een IndexOutOfBoundsException met de benodigde capaciteit in de melding, zodat de fout dubbel als groottehint dient.
Voor echt grote streams op de JVM is er een derde deur: de streaming-decoders. Die zijn nog steeds experimenteel gemarkeerd - vandaar de opt-in-annotatie - en bestaan alleen voor de JVM, maar ze decoderen tijdens het verstroomen in plaats van alles in het geheugen te houden:
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!
}
}
Twee praktische details. De extensiefuncties zitten op het topniveau van het package, dus importeer je ze met naam (een star-import werkt ook, maar namen zijn vriendelijker). En de decoder behandelt de padding als een harde stop: als de onderliggende stream door gaat na de Base64-sectie, eindigt het lezen van de gedecodeerde stream bij de = en blijven de resterende bytes beschikbaar in de oorspronkelijke stream. Dat maakt het netjes voor formaten die Base64 vooraan plakt bij iets anders.
Veldpraktijk: HTTP-API's en JSON-lichamen
JSON kan geen ruwe bytes dragen - het is een tekstprotocol - dus API's die binaire data moeten verplaatsen (afbeeldingen, certificaten, willekeurige blobs) wikkelen die bijna altijd in Base64 in een string-veld. Het patroon is: parseer de JSON, pak het veld, decodeer. Met de officiële serialisatiebibliotheek zit het JSON-gedeelte op twee annotaties vandaan:
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
}
Dit voorbeeld heeft de serialisatieplug-in en de bibliotheek nodig, een keer toegevoegd aan de 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")
}
Zonder de bibliotheek werkt hetzelfde idee op de ruwe string, wat handig is voor snelle scripts: trek het veld eruit met substringBetween en decodeer het. De valkuilen zijn de bekende API-valkuilen: het veld kan in werkelijkheid een volledige data-URL zijn (met de data:image/png;base64,-vooraandeling, later in dit artikel behandeld), het payload kan MIME-omgewikkeld zijn met regeleinden, en de gecodeerde lading kan ongeveer een derde groter zijn dan de oorspronkelijke binaire data, dus houd je geheugenbudget in de gaten bij grote antwoorden.
Veldpraktijk: e-mail en MIME-omgewikkelde invoer
E-mail is een 7-bits tekstwereld, en het antwoord van RFC 2045 op binaire bijlagen is Base64 met een draai: de gecodeerde uitvoer moet omgewikkeld worden, zodat geen enkele regel langer dan 76 tekens wordt. Als je ooit een bijlage als tekst hebt ontvangen, dan is dat de reden waarom het eruitziet als een ingezette kolom Base64. Voor precies deze invoer is Base64.Mime de juiste decoder, want hij negeert regeleinden en andere niet-alfabettekens terwijl hij doorgaat:
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
}
De tolerantie is echt, maar begrensd. Wikkel de invoer in, strooi er een spatie of twee in, geen probleem. Voeg echter een datateken toe na de laatste =, en zelfs Mime gooit dan een uitzondering: Symbol 'e'(145) at index 7 is prohibited after the pad character. En vergeet niet: Mime vereist dat de padding aanwezig en correct is; een MIME-decoder die ook ontbrekende padding zou slikken, zou de boel maar in de war brengen. Het praktische recept voor rommelige binnenkomende e-mailpayloads is: eerst Mime-decoderen, en als dat faalt, bekijk dan de melding - die zegt je exact welk symbool, op welke index, de regels bracht.
Veldpraktijk: afbeeldingen en data-URLs
Een data-URL is de manier van het web om een bestand rechtstreeks in een document te inlijnen: een mediatype, een base64,-markeerder en het payload, alles in één string. Browsers, CSS en ingebedde UI's houden van ze voor kleine assets - iconen, avatars, plaatshouders - want dan is er geen tweede request nodig. Het formaat ziet er zo uit:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg==
Een data-URL decoderen in Kotlin is een stringoperatie gevolgd door een Base64-decode. De vooraandeling draagt geen geheim; alles na de komma is het 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, ...]
}
Die eerste byte, -119 (wat 0x89 is), gevolgd door de letters PNG, is het magische getal dat een PNG-bestand identificeert. De eerste vier of acht bytes na het decoderen controleren is een goedkope manier om te bevestigen dat een data-URL echt bevat wat zijn vooraandeling claimt. Twee eerlijke kanttekeningen: Base64 voegt ongeveer een derde aan de grootte toe, dus een data-URL is een ruil qua grootte die je maakt tegen een netwerkronde, en voor iets groots ben je meestal beter af met het bestand serveren vanaf een echte URL en de cache zijn werk laten doen.
Veldpraktijk: configuratie, omgevingsvariabelen en databases
Base64 duikt op in configuratiebestanden en omgevingsvariabelen telkens als een binaire waarde door een puur tekstkanaal moet reizen: een klein ingebed icoon in een properties-bestand, een token opgeslagen in een env-var op een container, een blob bytes geparkeerd in een tekstkolom omdat het schema ouder is dan een fatsoenlijk binair type. De decoderingskant is overal dezelfde twee stappen - lees de tekst, decodeer het:
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)
}
}
De valkuil in deze hele hoek past in één regel: Base64 is geen versleuteling. Het is een transporteergreep, geen slot. Niemand zou een Base64-waarde moeten lezen en denken dat de data erin verborgen is; ze staat op één functieaanroep van zichtbaar, en ze is zichtbaar in elke logregel die je schrijft. Als een waarde gevoelig is, houdt je hem gevoelig van begin tot eind - een geheimenkluis, een versleutelde kolom, wat je stack ook aanbiedt - en gebruik Base64 alleen om de bytes door tekst te laten reizen, niet om ze te beschermen.
Veldpraktijk: de command line
Het oudste gebruik van allemaal: een Base64-blob op de command line omzetten naar een bestand. Een compleet tool is acht regels Kotlin, want de standaardbibliotheek doet het zware werk. Compileer het een keer met de Kotlin-compiler en het is voor altijd van jou:
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")
}
Voer het uit met een argument voor een eenmalige waarde, of pijp er een bestand in voor bulkwerk: het programma leest het eerste argument als dat er is, en valt anders terug op de standaardinvoer. De trim() doet hier stille dienst, want shell-argumenten en geplakte waarden komen graag aan met afgedwaald witruimte, dat de strikte decoder zou afwijzen. En als je payloads base64url zijn, wissel dan Base64.decode om voor Base64.UrlSafe.withPadding(Base64.PaddingOption.PRESENT_OPTIONAL).decode en is het tool ook klaar voor tokens.
Een veldgids voor decoderingen die mislopen
Elke decoder in deze gids faalt met één van twee uitzonderingstypen, en elke melding is specifiek genoeg om je exact te zeggen wat er mis is. Hier is de volledige kaart, met de exacte meldingen die de standaardbibliotheek produceert:
| Situatie | Uitzondering | Melding (zoals geproduceerd) |
|---|---|---|
| Teken buiten het alfabet (spatie, regeleinde, symbool van het verkeerde schema) | IllegalArgumentException |
Invalid symbol ' '(40) at index 5 |
| Datateken na de padding | IllegalArgumentException |
Symbol 'e'(145) at index 7 is prohibited after the pad character |
Ontbrekende padding terwijl de optie PRESENT is |
IllegalArgumentException |
The padding option is set to PRESENT, but the input is not properly padded |
Padding aanwezig terwijl de optie ABSENT is |
IllegalArgumentException |
The padding option is set to ABSENT, but the input has a pad character at index 7 |
| Index buiten de grenzen van de bron | IndexOutOfBoundsException |
startIndex: 0, endIndex: 100, size: 8 |
startIndex groter dan endIndex |
IllegalArgumentException |
startIndex: 3 > endIndex: 2 |
Doelarray te klein voor decodeIntoByteArray |
IndexOutOfBoundsException |
The destination array does not have enough capacity, destination offset: 0, destination size: 2, capacity needed: 8 |
Merk het patroon op in de eerste twee rijen: de melding noemt het veroorzakende symbool, de numerieke code in haakjes en de index. Dat is een cadeautje voor het debuggen. Als een decode in productie een uitzondering gooit, log dan de eerste tientallen tekens van de invoer en de index uit de melding, en je vindt de veroorzaker bijna altijd in seconden, of het nu een geplakt regeleinde is, een afgekapt payload of een base64url-string die in een standaarddecoder is beland.
Valkuilen met een Kotlin-accent
- Aanvaarden dat het payload tekst is.
decoderetourneert bewust eenByteArray.decodeToString()aanroepen op een JPEG omdat "het waarschijnlijk tekst is" levert een muur van vervangingstekens op. Bepaal eerst wat de bytes zijn voordat je ze convertert. - Kopieer-plak-witruimte. De standaarddecoder is strikt, en een waarde uit een chatbericht of een log komt bijna altijd met een afsluitend regeleinde of een voorloopspatie aan. Trim voordat je decodeert, of decodeer via
Mime, of accepteer deIllegalArgumentExceptionen behandel die. - Upgraden uit de experimentele tijd. Code geschreven voor de 1.8.x experimentele API had
@OptIn(ExperimentalEncodingApi::class)-annotaties en ging ervan uit dat padding optioneel was. Vanaf 2.0.20 kan dezelfde invoer een uitzondering gooien. De oplossing isPRESENT_OPTIONAL, of je invoer opruimen voordat het de decoder bereikt. - Het schema aan de verkeerde bron koppelen. Een JWT-onderdeel gedecodeerd met
Base64.Defaultfaalt op zijn-en_-tekens; een payload uit het standaardalfabet gedecodeerd metUrlSafefaalt op+en/. De uitzondering noemt het exacte symbool, maar de oplossing is weten waar de string vandaan kwam. - Het charset-gat.
decodeToString()is alleen UTF-8, zonder overload voor andere encodings. Als de afzender Latin-1 of Windows-1252 gebruikte, reken dan opString(bytes, charset)op de JVM, en verwacht U+FFFD-vervangingstekens als symptoom wanneer je het vergeet. - De compiler uit het distributiepakket.
apt install kotlinop Debian en Ubuntu levert 1.3.31, van vóórdat deze API bestond. Als je voorbeelden plots weigeren te compileren met "unresolved reference", check dan welke compiler er daadwerkelijk op de PATH staat.
Beste praktijken voor decoderen
- Decodeer eerst naar bytes, interpreteer dan. Houd
Base64.decodeen de tekstconversie gescheiden. Dat maakt het charset expliciet, houdt binaire payloads binair en maakt tests kinderlijk simpel: vergelijk arrays van bytes, niet strings. - Kies de instantie die bij de bron past. JWTs en URL-gebonden data betekent
UrlSafe; e-mail en PEM-bestanden betekentMimeofPem; alles anders begint bijDefault. De tolerante decoders zijn voor bekende rommelige invoer, niet voor een algemeen vangnet. - Normaliseer niet-getroouwde invoer één keer, goedkoop. Een
trim()en, waar het formaat bekend is als schoon, een witruimte-stripping vóór een strikte decode vangt meer echte mislukkingen uit de echte wereld dan hoeveel try-catch dan ook. Een kleine helper met eenPRESENT_OPTIONAL-fallback is een goed patroon voor waarden van onbekende bron. - Bepaal de grootte voordat je toewijst. Gedecodeerde uitvoer is hooguit drie kwarten van de invoerlengte (vier symbolen dragen drie bytes), dus een snelle lengtecheck vertelt je de doelgrootte voordat je decodeert, en precies dat wil je vóórdat je een vooraf-toegewezen buffer vult of een string van meerdere megabytes accepteert.
- Vertrouw de foutmelding. De standaardbibliotheek rapporteert het symbool, zijn code en zijn index. Log de buurt van die index voor niet-getroouwde invoer en stop met raden.
- Decodeer niet om dingen te verbergen, en decodeer niet om dingen te bewijzen. Base64 is een transportencoding. Het voegt geen geheimhouding en geen integriteit toe; heb je die nodig, dan is dat het werk van de cryptografie, niet van de decoder.
Hoe Base64 bij Kotlin terechtkwam
Base64 is een aantal decennia ouder dan Kotlin - de MIME-specificatie die de regel van 76 tekens per regel gaf dateert van 1993, en het alfabet zelf van RFC's uit de midden jaren negentig - maar het Kotlin-specifieke verhaal is kort en recent. Het kotlin.io.encoding-package arriveerde in Kotlin 1.8.20 in april 2023, met Base64 en drie instanties - Default, UrlSafe en Mime - achter de @ExperimentalEncodingApi-annotatie, plus de alleen-op-de-JVM streaming-extensies die vandaag nog steeds experimenteel zijn. Twee jaar lang betekende het gebruik ervan een opt-in-regel in elke functie en een kleine kans dat de API zou verschuiven.
Kotlin 2.2.0, uitgebracht in juni 2025, veranderde de afspraak. De hele API werd in één release stabiel, en de Pem-instantie voegde zich bij het gezin (de RFC 1421-variant met regels van 64 tekens, gebruikt in de PKI-omgeving). De striktheid die code met optionele padding uit de 1.8-tijd aandacht vraagt na een upgrade, kwam een stapje eerder: in 2.0.20, toen withPadding met zijn vier PaddingOption-waarden het oude vaste gedrag verving en de decoder begon padding te vereisen. De 2.2-release maakte ook de zusterclass HexFormat in kotlin.text stabiel, de hex-formatering-API die sinds Kotlin 1.9 experimenteel was, zodat tekstuele encodings op bytesniveau nu een vaste plek hebben in de standaardbibliotheek. En een onderhoudsnotitie: sinds Kotlin 2.4.0 komt de JVM-standaardbibliotheek uit met een ondersteuningsvenster van 18 maanden per release-lijn, en dat is nog een reden waarom een project op de huidige 2.4.x-lijn deze API als een vast punt kan behandelen in plaats van een bewegend.
Leuke feiten
- Het companion-object doet werk. Omdat
Defaulthet companion-object vanBase64is, dient de classnaam dubbel als de standaardinstantie:Base64.decode(x)enBase64.Default.decode(x)zijn dezelfde aanroep. Daarom blijft het tweeregelsvoorbeeld bovenaan dit artikel op twee regels. - String-decoding krijgt een snelheidstruc op de JVM. De gangbare decoderingslus werkt op bytes, maar Kotlin-strings zijn tekenreeksen. De JVM-implementatie gaat de conversie uit de weg door de tekens van een
Stringte herinterpreteren als single-byte ISO-8859-1-waarden voordat de gedeelde lus draait - een truc die volgens de broncodecommentaren tot tien keer sneller is dan de gangbare weg, en daarom voeltdecode(String)direct, ook bij lange payloads. - De package-naam is een hint. Je vindt deze API in
kotlin.io.encoding, niet inkotlin.text, want het hele punt is dat de data bytes zijn - invoer en uitvoer hebben een I/O-vorm, en de tekst is alleen wat er daarna met het resultaat gebeurt. - De foutmeldingen bevatten de code van het teken.
Symbol 'e'(145)rapporteert de waarde van het veroorzakende symbool in octaal, niet alleen het glif. Handig als de veroorzaker witruimte is:' '(40)vertelt je dat het een spatie was, lang voordat je er een vermoedens van hebt. - PEM kwam laat.
Base64.Pemmaakte geen deel uit van de originele 1.8.20-API; hij verscheen met de 2.2-stabilisatie. Als een blogpost uit 2023 of 2024 alleen drie instanties noemt, is die niet onjuist - hij is gewoon twee releases achterlopend. - Het is per platform geschreven, niet gedelegeerd. De standaardbibliotheek implementeert de codec apart voor elk platform met expect/actual-functies. Op de JVM is er zelfs een in een comment geplaatste optimalisatie die het werk zou overlaten aan
java.util.Base64, uitgeschakeld achter een open compiler-issue, en daarom is het gedrag van de Kotlin-implementatie het referentiegedrag op elk platform.
Afronding
Base64 decoderen in Kotlin komt neer op een korte lijst van doelbewuste keuzes: de instantie die past bij waar de data vandaan kwam, de paddingmodus die past bij hoe ze werd verstuurd, de buffer of stream die past bij hoe groot ze is, en het charset dat past bij wat ze betekent. De standaardbibliotheek reikt je alle vier aan als simpele functies zonder afhankelijkheden, en zijn foutmeldingen zijn specifiek genoeg dat een mislukking een diagnose is, geen raadsel. De omgekeerde richting - het juiste schema, de juiste padding en het juiste regelombraken kiezen wanneer jij de Base64 produceert - heeft zijn eigen keuzes en zijn eigen valkuilen, en het gerelateerde artikel op de zustersite behandelt Base64-encoderen in Kotlin in diepte.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Kotlin: een complete gids