Werk je met Base64-indeling? Dan is deze site perfect voor jou! Gebruik onze handige online tool om je gegevens te coderen of te decoderen.

Base64-decodering in Java: een complete gids

Het verschijnt in een supportticket, in een API-antwoord, in een Kubernetes-secret, of diep verstopt in het midden van een URL: een lange opeenvolging van letters en cijfers, met hier en daar een +, /, - of _, en misschien nog een of twee =-tekens die aan het einde ophangen. Iemand zegt dat het Base64 is en dat het iets bevat wat je nodig hebt: een wachtwoord, een JSON-payload, een certificaat, een foto. Deze gids is het Java-recept om het terug te krijgen. Even snel oriënteren, want de startpagina doorloopt het formaat in detail: Base64 schrijft elke drie bytes data om in vier tekens uit een alfabet van 64 letters, en hakt er een of twee =-pads achteraan als het laatste blok te kort is. Decoderen is de krimpende richting van die ruil: vier tekens gaan erin, drie bytes komen eruit, dus het resultaat heeft altijd ongeveer een kwart minder ruimte nodig dan de invoer.

Hier is de kop, en het is een goede. Sinds 18 maart 2014 levert elke JDK een compleet Base64-werkpakket in de standaardbibliotheek mee: java.util.Base64. Geen download, geen Maven-coördinaat, geen native bibliotheek. Eén import, zeven factory-methoden, drie alfabetten, en hetzelfde gedrag van Java 8 tot het huidige Java 26. Alles in dit artikel is gebouwd op die ene class.

Eén eerlijke grens vóór we beginnen: dit is de decoderkant van het verhaal. Je leert de juiste decoder kiezen voor het alfabet dat je tegenkomt, de foutmeldingen van de JDK lezen alsof een arts een scan bekijkt, bytes omzetten naar tekst zonder mojibake, de PEM-omhulling afstrippen, payloads van meerdere gigabytes streamen, en de beveiligingsvalkuilen herkennen die het formaat stilletjes in de weg legt. De andere richting, bytes in een tekenreeks verpakken, krijgt zijn eigen gids, en die is aan het einde van deze verlinkt.

Wat je al in huis hebt

Base64 installeren in Java is het antwoord in één regel dat je aan het whiteboard geeft: "Het zit in de JDK." De class java.util.Base64 maakt sinds 1.8 deel uit van de module java.base, en de javadoc zegt in 2026 nog steeds Since: 1.8. Het enige dat je installeert is een JDK; elke Java 8 of nieuwer van welke vendor dan ook (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) doet het, en op een Debian-gebaseerde machine is dat één commando:

sudo apt install openjdk-17-jdk-headless

De API is een factory: je construeert nooit zelf een decoder, je vraagt de class er om. De zeven factory-methoden reiken in beide richtingen drie karakters aan, en de decoderkant ziet er zo uit:

Factory-methode Alfabet Stellingname Grijp ernaar wanneer
getDecoder() A-Z a-z 0-9 + / Strikt: wijst elk teken buiten het alfabet af Data die je zelf produceert of controleert
getUrlDecoder() A-Z a-z 0-9 - _ Strikt, URL-safe alfabet JWT's, tokens, IDs, alles wat in een URL is geboren
getMimeDecoder() A-Z a-z 0-9 + / Liberaal: slaat elk teken buiten het alfabet over E-mail, echt omwikkelde invoer, PEM-inhoud
getEncoder(), getUrlEncoder(), getMimeEncoder() zoals hierboven Codering, het domein van de zuster-gids Altijd wanneer je Base64 produceert in plaats van het te lezen

Drie eigenschappen van de teruggegeven instanties zijn het memoriseren waard. Ten eerste zijn ze thread-safe: de javadoc zegt dat instanties "veilig zijn voor gebruik door meerdere gelijktijdige threads", en de broncode toont aan dat de factory-methoden bij elke aanroep dezelfde gedeelde instantie teruggeven, dus is Base64.getDecoder() == Base64.getDecoder() waar. Bouw één decoder in een statisch veld en deel hem over je hele service; je kopieert letterlijk niets. Ten tweede zijn ze tussen de aanroepen zonder state, dus is er niets om te resetten en niets om te synchroniseren. Ten derde is null doorgeven waar een byte-array of tekenreeks verwacht wordt geen vriendelijke no-op: het is een NullPointerException, precies zoals de class-javadoc belooft.

Oude bibliotheken kom je in codebases nog tegen, dus even een snelle landkaart. Apache Commons Codec (momenteel 1.22.1) draagt sinds 1.0 haar eigen org.apache.commons.codec.binary.Base64 mee, met een Builder-API die het strikt-of-liberaal-beleid, de regellengte en de scheiding als regelaars blootlegt; die is het juiste gereedschap alleen als je JVM's vóór Java 8 moet ondersteunen of de vormcheck-helpers ervan wilt. Guava levert com.google.common.io.BaseEncoding, een even capabele veteraan, nog steeds populair in big data-stacks. Voor alles dat op een moderne JVM draait is java.util.Base64 de standaardkeuze: nul afhankelijkheden, en community-benchmarks stellen steeds opnieuw vast dat het het snelst is van de hele ploeg (meer daarover in de sectie over snelheid).

Jouw eerste tekenreeks decoderen

Negentig procent van het decoderen past in een handvol regels. Hier is de volledige ceremonie, met het kleinste voorbeeld dat de RFC zelf gebruikt om het alfabet uit te leggen:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstDecode {
  public static void main(String[] args) {
    byte[] bytes = Base64.getDecoder().decode("TWFu");
    String text = new String(bytes, StandardCharsets.UTF_8);
    System.out.println(text); // Man
  }
}

Vier zinnen over wat er net gebeurde. Ten eerste is het instappunt een instantie, niet de class: decode() zit op het Base64.Decoder-object dat je uit de factory kreeg. Ten tweede, en dat is de belangrijkste designbeslissing in de hele API, is het resultaat een byte-array, nooit een String. De payload kan een zin, een JPEG of een hash zijn, en geens van de drie mag hetzelfde behandeld worden vóór je weet wat je hebt, dus stopt de JDK bewust bij de bytes. Ten derde is de sprong van bytes naar tekst een aparte, bewuste stap met een expliciete tekenset, en precies in die stap wordt "café" mojibake als je onzorgvuldig bent; de sectie over tekensets hieronder is eraan gewijd. Ten vierde is de lege tekenreeks een waarde van de eerste rang: Base64.getDecoder().decode("") geeft je een array van lengte nul, geen uitzondering, geen gedoe.

Voor testdata in je hoofd: onthoud dat TWFu de smoke test van de standaard zelf is: als je decode-code het in Man verandert, is de machine eerlijk. De roundtrip in de andere richting is twee regels van dezelfde API en krijgt de volledige behandeling in de coderingsgids die aan het einde is verlinkt.

De decoderselectie

Java geeft je niet één decoder, maar drie, en het verschil tussen hen is een beleidsbeslissing over welk alfabet je accepteert en hoeveel rommel je tolereert. Alle drie zijn instanties van dezelfde geneste class Base64.Decoder. De class-javadoc beschrijft de indeling in één zin per stemming. Voor de basis- en URL-safe decoders: de decoder "wijst data af die tekens bevat buiten het base64-alfabet". Voor de MIME-decoder: "alle regeleindes of andere tekens die niet in de base64-alfabet-tabel voorkomen, worden bij de decodeeroperatie genegeerd". Die tweede zin is het hele MIME-verhaal in één regel, en hij heeft tanden, want "genegeerd" betekent alles wat geen alfabetteken is, niet alleen regeleindes.

De keusregel is kort. Standaard gebruik je getDecoder(). Als de waarde uit een URL, een token of een API kwam die "URL-safe" belooft, schakel dan over op getUrlDecoder(). Alleen wanneer je écht MIME-vormige invoer verwacht (regeleindes om de 76 tekens, rechtstreeks uit een mailsysteem) grijp je naar getMimeDecoder(). Bij twijfel kies je strikt: het werk van een strikte decoder is om verrassingen mislukken te laten, en dat wil je precies op een vertrouwensgrens. Een liberale decoder daarentegen is een vergrootglas voor corruptie: een tekenreeks met ongevraagde tekens erin decodeert naar iets plausibel maar fouts, zonder enige fout.

De klachten van de decoder lezen

De strikte decoders falen luid, en ze falen precies. Elke slechte invoer gooit een IllegalArgumentException wiens bericht exact vertelt wat er misging, dus de eerste keer dat een productie-tekenreeks ontploft, is deze tabel wat je leest. De berichten hieronder zijn de exacte formulering van de huidige JDK:

Invoer (naar getDecoder tenzij anders vermeld) Wat er mis is Exact bericht
"SGVs bG8s" er is een spatie ingeslopen Illegal base64 character 20
"SGVs\nbG8s" er is een regeleinde ingeslopen Illegal base64 character a
"SGVs$bG8s" een dollarteken zit niet in het alfabet Illegal base64 character 24
"SGVsbG8-" een URL-safe koppelteken in de standaarddecoder Illegal base64 character 2d
"ab+c" naar getUrlDecoder() een plusteken in de URL-safe decoder Illegal base64 character 2b
"S" één symbool kan geen byte vormen Input byte[] should at least have 2 bytes for base64 bytes
"SG=VsbG8s" padding in het midden van de data Input byte array has wrong 4-byte ending unit
"Zm8==" twee pads waar er één thuishoort Input byte array has incorrect ending byte at 4
"Z=" één teken gevolgd door een pad Last unit does not have enough valid bits
"SGVsbG8sIHdvcmxkIQ==xx" afval achter de pads Input byte array has incorrect ending byte at 20

Dat hex-getal in het bericht is de byte-waarde van het overtredende teken, gedrukt met Integer.toString(byte, 16): 20 is een spatie, a een line feed, d een carriage return, 24 een dollarteken, 2d het URL-safe koppelteken, 2b het plusteken, 2f de slash, 5f de underscore. Twee rare weetjes die in je binnenzak mogen. Ten eerste kan het bericht negatief worden: geef de decoder een tekenreeks met é en hij klaagt over Illegal base64 character -17, want het teken wordt eerst afgebeeld op de Latin-1 byte 0xE9, die als gesigne Java-byte min 23 is, en min 23 in hex is min 17. Je foutlogger doet kortom gesigne rekenwerk. Ten tweede de positie: in de incorrect ending byte at N-familie is N de nul-gebaseerde index van de eerste byte waar de decoder geen zin van kon maken, en dat is een geschenk als je een gecorrupte payload aan het bisecten bent.

Eén kostuumwisseling om te kennen: wanneer het decoderen gebeurt via de omwikkelde stream (de wrap(InputStream)-variant, hieronder behandeld), duiken dezelfde problemen op als IOException met in plaats daarvan een 0x-prefix: Illegal base64 character 0x20 (huidige JDK's; de streamdecoder van JDK 8 toont de opzoekwaarde, -1, in plaats van de byte). Zelfde probleem, andere uitzondering, iets andere spelling. En de liberale MIME-decoder klaagt uiteraard over geen van dit alles: hij slaat gewoon over. Dat is de prijs van de liberale stemming.

De paddingregels

Elke Base64-tekenreeks die je in het wild tegenkomt doet een stille belofte over padding, en de belofte van Java is ongebruikelijk vriendelijk. De javadoc van de decoder zegt het exact: het padteken = "wordt geaccepteerd en geïnterpreteerd als het einde van de gecodeerde bytedata, maar is niet verplicht". Een laatste eenheid van twee of drie tekens decodeert alsof deze gepad was, en als pads aanwezig zijn, moeten ze in het exacte juiste aantal aanwezig zijn. Het gedrag van de huidige JDK rond de klassieke voorbeelden:

Invoer Resultaat
"" leeg byte-array, geen fout
"Zm8" "fo", padding gewoon afwezig
"Zm8=" "fo", de canonieke spelling
"Zm8==" IllegalArgumentException: incorrect ending byte at 4
"Zm9v=" IllegalArgumentException: wrong 4-byte ending unit
"Zg==" "f", één byte
"Z=" IllegalArgumentException: last unit does not have enough valid bits
"AA==" exact één byte, de NUL-byte 0x00
"AAAA" drie NUL-bytes

Lees die tabel twee keer. De lege tekenreeks decodeert naar niets, terwijl AA== decodeert naar één enkele NUL-byte: in Base64 zijn "niets" en "een nul" verschillende wezens, en beide zijn perfect geldige invoer. En padding moet, wanneer aanwezig, exact zijn: Zm8= is goed, Zm8== is fout, Zm9v= is fout, en een pad in het midden van de tekenreeks is fout. Praktisch gevolg voor je eigen protocollen: kies één spelling (met of zonder padding) en handhaaf deze aan beide uiteinden, want een waarde die in twee spellingsvormen kan aankomen, is een waarde die ergens later een naïeve gelijkheidscheck kan breken.

base64url: het alfabet gebouwd voor URLs

Standaard Base64 eindigt zijn alfabet met + en /, en dat zijn precies de twee tekens die zich in URLs niet goed gedragen: een + in een querystring is al een spatie vóór Java hem ooit ziet, een / is een scheidingsteken in het pad, en een ophangende = wil percent-gecodeerd worden tot een monster van drie tekens. RFC 4648 sectie 5 tekent de oplossing: het URL- en bestandsnaam-veilige alfabet, waar + wordt -, / wordt _, en de afsluitende =-padding doorgaans wordt weggelaten wanneer de lengte impliciet bekend is. De RFC is nadrukkelijk over de naam: deze codering "mag niet als dezelfde worden beschouwd als de base64-codering". Je zult het tegenkomen als base64url, en daar wonen JSON Web Tokens, OAuth-stateparameters, API-sessie-IDs en video-IDs van elf tekens.

De beroemdste base64url-payload op het web is de JWT, en naar binnen kijken is drie regels werk. Token-onderdelen zijn conventioneel ongepad, en de URL-decoder staat daar gelukkig mee, want padding wordt geaccepteerd maar is niet verplicht, onthoud:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtPeek {
  public static void main(String[] args) {
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
      + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
      + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
    String[] parts = token.split("\\.");
    byte[] header = Base64.getUrlDecoder().decode(parts[0]);
    byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
    System.out.println(new String(header, StandardCharsets.UTF_8));
    // {"alg":"HS256","typ":"JWT"}
    System.out.println(new String(payload, StandardCharsets.UTF_8));
    // {"sub":"1234567890","name":"John Doe","iat":1516239022}
  }
}

Twee eerlijke disclaimers wonen hier. Ten eerste is decoderen van een JWT kijken, niet vertrouwen: het derde deel is een handtekening, en de twee delen die je zojuist las zijn niet geheim en niet geverifieerd. Een payload vertrouwen vóór je de handtekening hebt geverifieerd is de klassieke JWT-bug, en de oplossing is de verificatie over te laten aan een JOSE-bibliotheek zoals JJWT (0.13.0) of nimbus-jose-jwt (10.9.1), in plaats van zelf crypto uit te vinden. Ten tweede identificeren de fouten de richting: geef een tekenreeks uit het standaardalfabet aan getUrlDecoder() en je krijgt Illegal base64 character 2b of 2f, en de omgekeerde richting levert 2d of 5f op. Een alfabetmismatch is de met veruit de meest voorkomende Base64-decode-fout in het wild, en de foutmelding wijst er binnen een hartslag naar. Als een token in een querystring moest standaard Base64 zijn, dan zijn diens + en / waarschijnlijk door de transport al verstoord vóór ze bij je aankwamen, en vertelt de decode-fout je over een bug stroomopwaarts, niet in je decoder.

Van bytes naar woorden

Elke decode-aanroep in dit artikel stopt bewust bij de bytes, want Base64 is een byte-formaat, punt. De vraag "wat was die tekst?" is aan jou om te beantwoorden, en het moderne standaardantwoord is UTF-8. Er is wel één tekenset-detail aan de decode-kant van de API dat mensen verbaast, dus hier is het. De decode(String)-overload interpreteert je tekenreeks niet als UTF-8. De javadoc zegt het exact: een aanroep "heeft exact hetzelfde effect als decode(src.getBytes(StandardCharsets.ISO_8859_1)) aanroepen". Dat is geen bug - het is een truc: het Base64-alfabet is zuiver ASCII, dus door de tekenreeks via Latin-1 af te beelden geef je de decoder precies dezelfde bytes met nul omzettingskosten, en elk niet-ASCII-teken in de invoer wordt simpelweg een ongeldig symbool dat de strikte decoder afwijst (vandaar de negatieve hex-getallen in de foutmeldingen).

De tekenset van de payload is een volledig aparte beslissing, de bij de new String(bytes, charset)-stap. Hier is het klassieke geval: "café" in UTF-8 is de vijf bytes 63 61 66 C3 A9, die encoderen naar Y2Fmw6k=:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetDecode {
  public static void main(String[] args) {
    byte[] packed = Base64.getDecoder().decode("Y2Fmw6k=");
    System.out.println(new String(packed, StandardCharsets.UTF_8));
    // café, het accent overleeft
    System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
    // caf gevolgd door mojibake, de UTF-8-bytes verkeerd gelezen als Latin-1
  }
}

Die tweede regel is de faalmodus om onmiddellijk te herkennen: een UTF-8-payload gelezen via Latin-1, wat een tekenreeks oplevert die exact één teken te lang is en één byte afwijkt. De remedie is altijd om een tekenset af te spreken met de maker en deze expliciet door te geven. En geef deze expliciet door in de code, niet alleen in je hoofd: de new String(bytes)-constructor zonder argumenten gebruikt de standaardtekenset van het platform, die op een Windows-server Cp1252 kan zijn en op een oudere Linux wat de machine maar voelt. Sinds JDK 18 (JEP 400, "UTF-8 by Default") is de standaard op elk platform UTF-8, dus op een moderne JVM is de vorm zonder argumenten toevallig juist, maar je code zou het toch moeten zeggen, want de volgende persoon die het leest hoeft niet te weten wat de standaard is. En als de payload helemaal geen tekst is, krijgt dezelfde code simpelweg een ander eind: bytes in, bytes uit, tot aan de allerlaatste stap.

Wanneer de payload een bestand is

Het meest voorkomende bestandswerk is de omgekeerde van een of andere exportroutine: er komt een .b64-tekstbestand aan, en je hebt het oorspronkelijke bestand terug nodig. Met strikt decoderen is dit al productieklaar:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DecodeFile {
  public static void main(String[] args) throws Exception {
    byte[] packed = Files.readAllBytes(Paths.get("payload.bin.b64"));
    byte[] raw = Base64.getDecoder().decode(packed);
    Files.write(Paths.get("payload.bin"), raw);
  }
}

Niets in dit traject geeft om of de payload een tekstbestand, een ZIP-archief of een video is: byte[] is gewoon bytes. De groottemat werkt ook in je voordeel: de gedecodeerde output is drie kwart van de lengte van de gecodeerde invoer, dus decoderen maakt geheugen nooit slechter, en een gecodeerd bestand van honderden megabytes is de kleinere van de twee. Een goede gewoonte is om de bytes zichzelf te laten aankondigen vóór je een label vertrouwt. De eerste acht bytes van een PNG zijn altijd het magische getal 89 50 4E 47 0D 0A 1A 0A, wat betekent dat elke Base64-gecodeerde PNG die je ooit zult tegenkomen begint met dezelfde prefix, iVBORw0K: als een payload "beweert" een afbeelding te zijn en zo niet begint, is er al iets mis.

Als je de bestemmingsbuffer al in huis hebt, schrijft de twee-array overload er rechtstreeks in en geeft exact terug hoeveel bytes er zijn beland, zonder tussentijdse allocatie:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DecodeInto {
  public static void main(String[] args) {
    byte[] src = "SGVsbG8sIHdvcmxkIQ==".getBytes(StandardCharsets.ISO_8859_1);
    byte[] dst = new byte[16];
    int written = Base64.getDecoder().decode(src, dst);
    System.out.println(written); // 13
    System.out.println(new String(dst, 0, written, StandardCharsets.UTF_8));
    // Hello, world!
  }
}

Eén scherpe hoek aan die overload, gedocumenteerd in de javadoc: als de bestemming te klein is, wordt geen enkele byte geschreven en krijg je IllegalArgumentException: Output byte array is too small for decoding all input bytes. Bepaal de buffergrootte uit de eenvoudige rekensom, ruwweg 3 * n / 4 minus de padding, en de uitzondering toont nooit haar gezicht. Er is ook een ByteBuffer-overload die een verse buffer teruggeeft met de limit ingesteld op de gedecodeerde lengte, handig wanneer je pipeline in NIO woont.

Vanaf de draad: headers, JSON en data URIs

Base64 ontmoet Java het vaakst aan de rand van het netwerk. Drie vormen verdienen elk een werkvoorbeeld.

Vorm één: de HTTP Basic auth header. De oudste authenticatieheader op het web is nog steeds gebaseerd op Base64. Volgens RFC 7617 stuurt een Basic-verzoek Authorization: Basic gevolgd door de Base64-codering van username:password, en de RFC stelt nadrukkelijk dat dit codering is, geen bescherming: wie een packetcapture heeft, kan beide helften in één toetsaanslag lezen. Het eigen voorbeeld van de RFC, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, decodeert naar Aladdin:open sesame. De header parsen aan de serverkant is een paar regels werk:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuth {
  public static String[] credentials(String header) {
    if (header == null || !header.startsWith("Basic ")) {
      return null;
    }
    byte[] packed = header.substring(6).getBytes(StandardCharsets.ISO_8859_1);
    byte[] raw = Base64.getDecoder().decode(packed);
    String userPass = new String(raw, StandardCharsets.UTF_8);
    int colon = userPass.indexOf(':');
    if (colon < 0) {
      return null;
    }
    return new String[] {userPass.substring(0, colon), userPass.substring(colon + 1)};
  }
}

Twee details houden dit veilig. De split op het eerste dubbelepunt is belangrijk, want een wachtwoord mag legaal z'n eigen dubbele punten bevatten. En de vergelijking van het gedecodeerde wachtwoord met je opgeslagen waarde moet in vaste tijd verlopen: hash beide waarden met SHA-256 en vergelijk de digests met MessageDigest.isEqual, nooit een gewone equals die een aanvaller kan timen tot een gebruikerslijst. Lever dit alleen via HTTPS; op een gewone verbinding is de Base64-laag alleen maar raamdecoratie.

Vorm twee: binair in JSON. Een groot deel van moderne APIs embedt binair als Base64-tekst in JSON: upload-endpoints, content-API's, secret-stores en webhooks doen het allemaal, want ruwe bytes zouden anders de escaping-regels van de JSON-string breken. Het patroon is altijd hetzelfde: het veld komt aan als een gewone tekenreeks, en je decodeert het aan de grens, niet in je domeinobjecten:

import java.util.Base64;
public class ApiField {
  public static void main(String[] args) {
    // Het geparste JSON droeg:  "content" : "iVBORw0KGgoAAA..."
    String field = "iVBORw0KGgo=";
    byte[] image = Base64.getUrlDecoder().decode(field);
    // Sommige APIs spreken in plaats daarvan standaard Base64. Lees de spec,
    // en kies daarna bijgevolg getDecoder() of getUrlDecoder().
    System.out.println(image.length); // 8
  }
}

De val hier is niet decoderen; het is de spec lezen. Sommige APIs willen standaard Base64 met padding, sommige willen base64url zonder, en een paar zijn liberaal over beide. Als de spec zwijgt, is de goedkoopste fix om naar een voorbeeldwaarde van de andere kant te kijken: een - of _ ergens in de waarde beslist het alfabet, en afsluitende = beslist de padding.

Vorm drie: de data URI. Iemand plakt een afbeelding in een formulier en de frontend reikt je de volledige data URI aan: data:image/png;base64,iVBORw0KGgo.... RFC 2397 definieert de vorm: data:, een optionele media type, een optionele ;base64-vlag, een komma, en dan de data. Als de vlag aanwezig is, is de payload Base64; als ze ontbreekt, is de payload percent-gecodeerde gewone tekst, zeldzamer maar legaal. Als de media type wordt weggelaten, is de standaard text/plain;charset=US-ASCII. Een data URI opsplitsen is eenvoudig:

import java.util.Base64;
public class DataUri {
  public static void main(String[] args) {
    String uri = "data:image/png;base64,iVBORw0KGgo=";
    int comma = uri.indexOf(',');
    String meta = uri.substring(5, comma);
    String payload = uri.substring(comma + 1);
    boolean isBase64 = meta.endsWith(";base64");
    String mime = isBase64 ? meta.substring(0, meta.length() - 7) : meta;
    byte[] raw = Base64.getDecoder().decode(payload);
    System.out.println(mime + " -> " + raw.length + " bytes");
    // image/png -> 8 bytes
  }
}

Twee vallen wonen in dit formaat. De ontbrekende ;base64-vlag is de eerste: een legaal data URI zonder de vlag draagt een percent-gecodeerde payload, en hem door Base64.getDecoder() halen gooit een uitzondering. De tweede is de beweerde media type: dat is een hint van de afzender, geen feit, dus check de magic bytes van wat je gedecodeerd hebt vóór je het onder "png" archiveert. En onthoud het eigen advies van de RFC dat data URIs zijn voor korte waarden; een afbeelding van meerdere megabytes in een URL is een stank, geen patroon.

E-mail, MIME en PEM-omhulling

Base64 is voor de mail geboren, en mail-vormig Base64 komt nog steeds continu in Java-programma's binnen. De MIME-standaard (RFC 2045) maakte Base64 een van de binaire transfer-coderingen en voegde twee huishoudelijke regels toe: gecodeerde regels mogen niet langer zijn dan 76 tekens, en decoders moeten elk teken buiten het alfabet negeren, regeleindes meegeteld. De strikte decoders wijzen het allereerste regeleinde al af; getMimeDecoder() is precies voor deze invoer gebouwd:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class MimeDecode {
  public static void main(String[] args) {
    String wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
    byte[] bytes = Base64.getMimeDecoder().decode(wrapped);
    System.out.println(new String(bytes, StandardCharsets.UTF_8));
    // Hello, standard
  }
}

Dat werkt, maar je moet de vangst toch kennen, want de vangst heeft tanden. De liberale decoder negeert niet "regeleindes"; hij negeert alles wat niet in zijn alfabet zit. Als een standaard Base64-tekenreeks gecorrupt raakt met ongevraagde tekens, dan verdwijnt de afval en decodeert de rest naar iets plausibels, dus grijp alleen naar getMimeDecoder() wanneer je écht MIME-vormige invoer verwacht.

De broer van MIME in het wild is de PEM-omhulling, de -----BEGIN CERTIFICATE------boel die certificaten en sleutels omwikkelt. Hier is de val: de omhullingsregels zitten vol gewone alfabettekens. De letters in "BEGIN CERTIFICATE" zijn gewoon Base64-letters, dus als je een volledig PEM-blok voert, omhulling en al, decodeert de omhulling alsof het data was. Strip de omhulling zelf, en geef dan het naakte lichaam aan een decoder:

import java.util.Base64;
public class PemDecode {
  public static void main(String[] args) {
    String pem = "-----BEGIN CERTIFICATE-----\n"
      + "TUlJQm96Q0NBVWlnQXdJQkFnSUpBSXBhVDJUaVFvZU1BMEdDU3FHU0liM0RRRUE9\n"
      + "-----END CERTIFICATE-----\n";
    String body = pem.replaceAll("(?m)^-----.*$", "").replaceAll("\\s", "");
    byte[] der = Base64.getDecoder().decode(body);
    System.out.println(der.length); // het DER-lichaam, omhulling uitgesloten
  }
}

PEM omwikkelde conventioneel op 64 tekens per regel (MIME op 76), en zodra de witruimte weg is, komen de strikte decoder en de MIME-decoder overeen over het resultaat. Gebruik de strikte: een verrassing heeft minstens de beleefdheid om een uitzondering te gooien. Voor het vies maar standaard geval is het klassieke recept om de bekende witruimte te strippen en met de strikte instantie te decoderen, en eventuele achtergebleven afval een IllegalArgumentException te laten opleveren in plaats van een gecorrupt certificaat.

Config, environment en databasewaarden

Base64 is een tekstcontainer, en daarom duikt het op in plekken waar je het niet zou verwachten. In databases kan een binair blob (een bestand, een icoon, een geserialiseerde structuur) in een TEXT-kolom leven als Base64, en overleeft elk hulpmiddel dat tekst veronderstelt; verwacht dat de opgeslagen waarde ongeveer een derde groter is dan het origineel en dimensioneer de kolom daarop. In config-bestanden en omgevingsvariabelen is Base64 de truc om waarden te smokkelen die anders het formaat zouden breken: een DSN met puntkommas, een wachtwoord met aanhalingstekens, een meerregelig certificaat. Decoderen bij het opstarten is het hele werk:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigDecode {
  public static void main(String[] args) {
    String value = System.getenv("DB_DSN_B64");
    if (value == null) {
      return;
    }
    byte[] raw = Base64.getDecoder().decode(value);
    String dsn = new String(raw, StandardCharsets.UTF_8);
    // dsn kan zijn: pg:host=db;password=qu"ote
  }
}

Dezelfde voorzichtigheid geldt hier tweevoudig. Ten eerste is dit formatveiligheid, niet geheimhouding: het moment dat een developer het config-bestand leest, kan hij de waarde in één aanroep decoderen, dus sla nooit een secret op als Base64 en noem het versleuteld; de beveiligingssectie hieronder gaat dieper in. Ten tweede, valideer bij het opstarten: een gecorrupte of half geplakte env-waarde is een IllegalArgumentException van de strikte aanroep, en een check van twee regels verandert een cryptische runtime-fout in een bruikbaar opstartbericht. Eén Java-specifieke opmerking voor de database-clan: houd het gedecodeerde binair vast als byte[] (een byte[]-parameter in je JDBC-code), en laat binair nooit een roundtrip maken via een String, want de string-constructors zijn waar binair payloads naartoe gaan sterven.

De grote dingen streamen

Voor payloads die groot zijn maar nog passen in een buffer die je beheert, zijn de array-API's prima. Voor payloads die helemaal niet in het geheugen zouden mogen passen, is de stream-adapter de zet: wrap(InputStream) geeft een input stream terug die decodeert terwijl je leest, zodat een gecodeerd bestand van meerdere gigabytes nooit in een byte-array hoeft te zitten:

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamDecode {
  public static void main(String[] args) throws Exception {
    InputStream packed = Base64.getDecoder().wrap(Files.newInputStream(Paths.get("bigfile.b64")));
    OutputStream raw = Files.newOutputStream(Paths.get("bigfile.bin"));
    byte[] buf = new byte[8192];
    int n;
    while ((n = packed.read(buf)) != -1) {
      raw.write(buf, 0, n);
    }
    raw.close();
    packed.close();
  }
}

Twee details om te kennen. De read-methoden van de omwikkelde stream gooien IOException wanneer ze bytes tegenkomen die niet gedecodeerd kunnen worden, dus een gecorrupt bestand faalt met een stream-uitzondering in plaats van een IllegalArgumentException. En sluit je de omwikkelde stream, dan sluit je ook de onderliggende stream, dus het voorbeeld sluit packed als laatste, na de koppelus, en in productie zou je beide in een try-with-resources-blok plaatsen. (De 8192-buffer is gewoon een ruime leesbuffer; de omwikkelde stream decodeert intern, dus de grootte waarmee je leest is een prestatiekeus, geen protocolvereiste.)

Nu een legacy-waarschuwing, want dit is de échte bug in het hele verhaal en hij heeft een bugnummer. Op elke JDK vóór 16 (het bugrapport reproduceert het op 8, 10 en 11) voegt het lezen van een omwikkelde decoder met bepaalde buffergroottes twee afgedwaalde nullen toe aan het einde van de gedecodeerde data: JDK 8222187, wiens klassieke repro een zeven-byte leesbuffer combineert met een simpele acht-byte invoer, en het is gefixt in JDK 16. Als je op een legacy JDK 8 moet streamen, hercontroleer de gedecodeerde lengte na de kopie, want de bug brandt af voor bepaalde invoer-en-buffer-combinaties, en zelfs een 4096-byte buffer is in het wild gerapporteerd, of beter, upgrade de JDK, die ongeveer honderd andere zaken toch zou fixen.

Pakkband, geen slot

Nu de sectie die de zorgvuldigen van de verbranden scheidt. Base64 is geen versleuteling, en de standaard zelf zegt het er twee keer over. RFC 4648 sectie 12: Base-codering "verbergt visueel anders makkelijk herkenbare informatie, zoals wachtwoorden, maar biedt geen enkele computationele vertrouwelijkheid", en het voegt eraan toe dat dit "bekend staat veiligheidsincidenten te veroorzaken" wanneer iemand een protocolruil in een ticket plakt en per ongeluk het wachtwoord onthult. Het advies van de RFC voor implementatoren verdient ook een raam: "een decoder mag niet breken op ongeldige invoer, inclusief bijv. ingebedde NUL-tekens".

De subtielere val is vervormbaarheid. Onthoud dat elk symbool zes bits draagt, en dat een korte laatste eenheid overtollige bits achterlaat die in een goed gevormde codering nul moeten zijn. Een slordige of vijandige encoder kan afval in die overtollige bits stoppen, en het resultaat lijkt nog steeds volledig geldig: MQ== en MT== decoderen beide naar de ene byte van het cijfer 1. Java kiest hier de vergevingsgezinde kant: Base64.getDecoder().decode("MT==") verifieert de niet-bepalende bits niet en reikt je zonder meer dezelfde byte aan. Waarom zou je je er zorgen over maken? Omdat twee verschillende tekenreeksen die decoderen naar dezelfde data de aanname van "één enkele spelling" breken waar hashchecks, deduplicatie en handtekeningenvergelijkingen stilletjes op leunen, en een aanvaller die een gecodeerde waarde onderweg kan wijzigen, kan de ene spelling door de andere zetten. Het paper van 2022 "Base64-vervormbaarheid in de praktijk" van Chatzigiannis en Chalkias (ACM ASIA CCS 2022) doorloopt precies deze inconsistenties over werkelijke implementaties heen. De eigen woorden van de RFC over de overtollige bits: ze "kunnen worden misbruikt om informatie te leken, of gebruikt worden om tekenreeksvergelijkingen te omzeilen of implementatieproblemen uit te lokken". De praktische regel is niet "nooit decoderen"; het is "ken je grens": voor data tussen je eigen systemen is de genereusheid van de JDK prima, maar voor data die een vertrouwensgrens oversteekt, handhaaf dan de canonieke vorm (juiste lengte, nul overtollige bits, één spelling van padding) vóór je iets vertrouwt dat je gedecodeerd hebt.

Opmerkingen over snelheid

Hier is het goede nieuws in één zin: op een moderne JVM is de ingebouwde decoder snel genoeg dat Base64 bijna nooit je bottleneck is, en het is het referentiepunt waartegen de rest van het ecosysteem zichzelf benchmarkt. Een voorbeeld: in 2025 heeft het gRPC-java-project openlijk zijn op Guava gebaseerde Base64-verwerking gebenchmarkt tegen java.util.Base64 (issue 11857), en de JDK-implementatie kwam naar voren als ruwweg 2,5 tot 3,8 keer sneller bij het encoderen en 1,3 tot 2,1 keer sneller bij het decoderen op JDK 17 en 21, met de grootste afstanden op x86. Dat is een sterke aanwijzing waar de implementatie-inspanning van de JDK naartoe is gegaan, en het is dezelfde conclusie die je blijft terugvinden in Base64-benchmarks: de standaardbibliotheekversie is nu de snelle, niet de legacy.

Twee praktische opmerkingen. Ten eerste is het bij enorme bestanden het geheugengebruik, niet de snelheid, wat je aan het beheren bent, en daarom bestaat de streamingsectie: wrap(InputStream) houdt de werkverzameling beperkt tot je leesbuffer. Ten tweede, als je toch op een heet pad eindigt dat miljoenen kleine waarden decodeert, deel dan één decoder-instantie (de factory geeft al dezelfde gedeelde terug, zoals hierboven genoteerd), sla de decode(String)-overload over als je al bytes hebt (hij kopieert de tekenreeks eerst via Latin-1), en laat de decode(byte[], byte[])-overload schrijven in een vooraf gedimensioneerde bestemmingsarray om de allocatiedans over te slaan.

Valkuilen met een Java-accent

De vallen, verzameld op één plek, allemaal Java-specifiek:

  • Fout decoder voor het alfabet. Een base64url-tekenreeks naar getDecoder() (of omgekeerd) is de klassieke Illegal base64 character-crash, meestal met een 2d, 5f, 2b of 2f in het bericht. Pas de decoder elke keer aan het protocol aan.
  • Afsluitende witruimte uit het wild. Waarden die uit een terminal, een env-var of een config-bestand zijn gekopieerd, komen vaak met een nieuwe regel binnen, en de strikte decoder maakt daar Illegal base64 character a van. strip() de invoer, of gebruik de MIME-decoder alleen als de data écht omwikkelde is.
  • De omhullingsval. getMimeDecoder() begrijpt geen PEM-headers, en de letters in BEGIN en CERTIFICATE decoderen als data. Strip de omhullingsregels zelf, altijd.
  • MIME-liberaliteit als snelweg. Decoderen met de MIME-decoder alleen om "veilig te zijn" slaat stilletjes elk afgedwaald niet-alfabetteken over, dus een gecorrupte payload kan er plausibel maar fout uitkomen. Gebruik het alleen voor écht MIME-invoer.
  • Tekenset overgelaten aan het geluk. De argumentloze new String(bytes) gebruikt de platformstandaard. Dat is UTF-8 op JDK 18+, maar je code zou StandardCharsets.UTF_8 expliciet moeten doorgeven, anders geniet je van mojibake na de volgende servermigratie.
  • Binair verstringen. new String(decodedPng) en terug is datavernietiging: elke bytevolgorde die niet geldig is in je tekenset wordt het vervangingsteken, en de roundtrip is eenwegverkeer. Bytes in, bytes uit, tot aan de allerlaatste stap.
  • Vertrouwen in de overtollige bits. MT== decodeert net als MQ==, dus een payload met afval verborgen in de niet-bepalende bits slaat elke check die de JDK uitvoert. Als het protocol iets telt, handhaaf dan de canonieke vorm.
  • JDK 8, 11 en 12 streams. De omwikkelde decoder op die versies kan twee afgedwaalde nullen toevoegen voor bepaalde buffergroottes (JDK 8222187, gefixt in 16). Op 16 en later is dit geen probleem; op de oudere wel.
  • null is niet leeg. null doorgeven aan decode() is een NullPointerException, geen lege array. Als een variabele null kan zijn, coalesceer hem dan vóór de aanroep.
  • Android is een andere dierentuin. Op Android bestaat java.util.Base64 pas vanaf API-niveau 26; daaronder is de framework-class android.util.Base64 met zijn eigen vlagconstanten. Code die er één van de twee hardcodeert zonder check, breekt precies op de apparaten die je nooit getest hebt.
  • Vergeten dat het geen beveiliging is. Base64 verbergt een wachtwoord voor een blik en voor niemand anders. Als de data geheim is, versleutel het eerst en pak het dan pas in als het kanaal tekst vereist.

De lange weg naar java.util.Base64

Het verhaal van het formaat is ouder dan Java. In de jaren tachtig kon de mailinfrastructuur van het internet alleen 7-bit ASCII vervoeren, en mensen die binair wilden verplaatsen, verzonnen lokale dialecten: uuencode voor UNIX (diens alfabet loopt door opeenvolgende ASCII-codes heen, dus encoderen was één optelling met 32 zonder opzoektabel) en BinHex voor Apple-machines (die haar alfabet samen sneed om visueel verwisselbare tekens als 7, O, g en o te laten vallen). In 1987 standaardiseerde het Privacy-Enhanced Mail-protocol (RFC 989) het 64-teken-systeem met regels van 64 tekens voor het vervoeren van certificaten, en RFC 1421 in 1993 behield het alfabet en de paddingregels. In 1996 voerde MIME (RFC 2045, de opvolger van RFC 1521 van 1993) het systeem door dat al naar zijn alfabet van 64 tekens "base64" heette, stelde de regellengte van 76 tekens in die nog steeds je e-mailbijlagen omwikkelt, en schreef de liberale decoderregel die getMimeDecoder() tot op de dag van vandaag implementeert. In 2003 probeerde RFC 3548 de hele familie op te schonen en verklaarde dat decoders tekens buiten het alfabet af moesten wijzen, en in 2006 werd RFC 4648 de standaard die iedereen aanhaalt, met de alfabettabellen, de base64url-variant in sectie 5, en de beveiligingssectie die de laatste sectie van dit artikel eerlijk houdt.

Het eigen hoofdstuk van Java is wat dramatischer. Jarenlang was het enige Base64 in de JDK het interne paar sun.misc.BASE64Encoder en sun.misc.BASE64Decoder, het soort API dat vandaag compileert en zonder deprecation-waarschuwing verdwijnt, en als je Base64 nodig had in een XML-wereld was er ook javax.xml.bind.DatatypeConverter van JAXB. Iedereen anders gebruikte Apache Commons Codec of Guava. Toen, 18 maart 2014: Java 8 bracht java.util.Base64, die RFC 4648 en RFC 2045 in één class implementeert met het factory-methodepatroon dat je het hele tijd al gebruikt. Drie en een half jaar later, Java 9 (21 september 2017), verwijderde het sun.misc-paar voorgoed, en de officiële migratiegids spreekt er rechtuit over: "Opvallend is dat sun.misc.BASE64Encoder en sun.misc.BASE64Decoder verwijderd zijn. Gebruik in plaats daarvan de ondersteunde java.util.Base64-class, die in JDK 8 is toegevoegd". Voer jdeps uit op oude code die ze nog aanroept, en het tool markeert de afhankelijkheid als "JDK removed internal API". Java 11 volgde door de JAXB-module en diens DatatypeConverter er ook te verwijderen (JEP 320). Sinds 1.8 is de publieke API om geen enkele methode veranderd, en de javadoc draagt nog steeds zijn oorspronkelijke Since: 1.8-tag. Wat zich heeft verplaatst, is de motor eronder: bugfixes (de JDK 8222187-streambug, gefixt in JDK 16) en prestatiewerk, en daarom belanden community-benchmarks steeds weer bij dezelfde conclusie. Twaalf jaar, één API, en het is nog steeds het snelste Base64 waarvoor je niet hoeft te betalen.

Leuke feitjes, Java-editie

Want een complete gids moet eindigen met een glimlach, hier zijn een paar Java-specifieke feiten die gewoon leuk zijn:

  • De Oracle-javadoc voor decode(byte[] src, byte[] dst) belooft dat "enkele bytes kunnen zijn geschreven naar het output byte-array vóórdat IllegalargumentException wordt gegooid". Niet IllegalArgumentException, maar IllegalargumentException, met een kleine a. De typefout zit in de feitelijke JDK-broncode, en hij is er sinds 2014. Documentatie die zo vasthoudt aan een typefout, is zeldzamer dan dat zou moeten.
  • Decodeer een tekenreeks met é en de foutmelding is Illegal base64 character -17: een negatief hex-getal, want het teken wordt de Latin-1 byte 0xE9, die als gesigne Java-byte min 23 is, en de JDK toont het in basis 16. Je foutlogger doet kortom gesigne rekenwerk.
  • Base64.getDecoder() == Base64.getDecoder() is waar. De broncode geeft bij elke aanroep een gedeelde statische instantie terug, dus de "haal een nieuwe"-API is een kostuum voor een singleton, en de belofte van thread-safety is gewoon een beschrijving van wat de JVM al doet.
  • Geef de URL-decoder een tekenreeks van vier underscores, "____", en hij geeft drie bytes van puur 0xFF terug. De underscore is alfabetwaarde 63, vier daarvan maken 24 bits, en 24 bits van enen zijn het byte-drietal FF FF FF. Er is niets illegaals aan, en dat is het grappigste deel.
  • AA== decodeert naar één enkele NUL-byte terwijl de lege tekenreeks naar niets decodeert. In Base64 zijn "niets" en "een nul" verschillende wezens, en beide zijn perfect geldige invoer.
  • De kleine tekenreeks TWFu die decodeert naar Man is de favoriete smoke test geworden van het ecosysteem: die verschijnt in de RFC, op Wikipedia, in naslagwerken en in de meeste Base64-tutorials op aarde, dus elke decoder die sindsdien is geschreven, brengt dezelfde kleine hommage.
  • Elke Base64-gecodeerde PNG die je ooit hebt gedecodeerd, begint met iVBORw0K. Dat is het PNG-magische getal in vermomming, en het is een van de meest herkenbare prefixes van acht tekens op het internet.
  • De URL-safe-sectie van RFC 4648 is waar de naam "base64url" wordt geboren: de spec zegt dat de codering "base64url mag worden genoemd" en waarschuwt dat hij "niet als dezelfde mag worden beschouwd als de base64-codering". De oorsprong van het URL-safe alfabet krijgt een voetnoot naar een post uit 2001 op een P2P-hackers mailinglist, dus de naam die je in elke URL plakt, heeft een mailinglist-geboortecertificaat.
  • YouTube-video-IDs zijn base64url zonder padding, de bekende tekenreeks van elf tekens die je overal in een URL kunt plakken. Het formaat dat was ontworpen voor e-mailbijlagen, runt nu een videoplatform, en getUrlDecoder() is het deel van je JDK dat het mogelijk maakt.
  • Decodeer de tekenreeks YmFzZTY0 en je krijgt het woord base64 terug, geen padding nodig, want zes is een veelvoud van drie. Een formaat dat zichzelf beschrijft, is de technische tegenhanger van een spiegel die in Morse spreekt.

De andere richting

Dat is de decoderkant van het verhaal, en daar woont het meeste gedoe, want decoderen is waar je de data van anderen tegenkomt: hun paddingkeuzes, hun regeleindes, hun tekensets, hun tokens, hun omhulling. De andere richting, bytes omzetten naar een Base64-tekenreeks met de encoders van java.util.Base64, is een kalmer dier: het gooit nooit op ongeldige invoer (er is geen ongeldige invoer om te coderen), het heeft in plaats van een foutmelding om te lezen een grootte-inkasso die betaald moet worden, en het eigen stel vallen (de tekensetstap, de MIME-regelaars, de paddingbeslissing voor tokens) krijgt een gids van zichzelf. Base64-codering in Java, van deze pagina gelinkt, dekt de encoder in dezelfde diepte, en de twee lezen prettig als een paar.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-codering in Java: een complete gids