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-codering in C++ (Cpp): een complete gids

Het omgekeerde probleem is dat met het grotere kopje: je hebt bytes - een certificaat, een afbeelding, een willekeurige blob, een signatuur - en ze moeten reizen door iets dat alleen tekst spreekt: een JSON-veld, een e-mailheader, een URL, een omgevingsvariabele. De startpagina van deze site behandelt het formaat uitgebreid, dus dit is alleen de korte versie: drie bytes worden vier alfabettekens, een korte staart krijgt één of twee =-tekens, en de geëncodeerde vorm loopt ongeveer 33 procent groter dan het origineel. Encoderen is de groeiende richting, dus elke buffer in dit artikel is daarop afgekast, en de rekensom is één regel - 4 * ((n + 2) / 3) - die niet verandert welke encoder je ook kiest.

Net als de decoderingskant zal C++ zelf geen enkele byte voor je encoderen. De standaardlibrary had dertig jaar de tijd om een base64-functie te kweken en heeft ze allemaal aan andere dingen besteed, dus elk C++-programma pikt zijn encoder uit een rij van vier heel verschillende persoonlijkheden, plus de optie om zelf zo'n veertig regels te schrijven. De ene is een werkpaard dat TLS sinds de jaren 90 draagt en dat padt, NUL-afsluit en regels wikkelt zonder erom te vragen. De andere is een snelle header-only-codec die zich verborgen houdt in een namespace die de auteurs "detail" noemden. De derde is een iterator uit 2002 die blijkbaar een padkarakter nog nooit heeft ontmoet. De vierde is een functie die het besturingssysteem al decennia levert en die CRLF achter je token plakt. En de vijfde optie is die van jou. Zodra je weet wat elke encoder toevoegt, weigert of in stilte eraan plakt, stopt encoderen met een bron van off-by-one-bugs zijn. Laten we aan het inpakken gaan.

De standaard heeft nooit een inpakker geleverd

Elke standaard sinds C++98 - en die zijn er acht, door C++26 heen - keek naar het alfabet van 64 tekens en ging verder. Er is geen <base64>, geen std::base64, en niets in <string> of <vector> dat je bytes kan inpakken. De technische inhoud van C++26 werd voltooid en met 114-12-3 aangenomen op de ISO C++-bijeenkomst van maart 2026 in Croydon, VK, en die voegt inderdaad een <text_encoding>-header toe voor werk aan tekstcoders. De volgende bijeenkomsten van het comité - in juni 2026 (Brno) en november 2026 (Búzios, Brazilië) - openen in plaats daarvan de C++29-working draft, in plaats van C++26 opnieuw te bekijken. Base64 zit niet in de standaard, en je kunt het comité weinig verwijten maken: tekstcodering gaat over karaktersets, en base64 gaat over bytes, dus de nieuwe header was nooit de juiste thuisbasis. In de praktijk deed het ecosysteem het werk. De EVP-base64-routines van OpenSSL zitten in elke OpenSSL-release, de Boost-library's dragen twee onafhankelijke encoders, Windows levert een CryptoAPI-functie met een flagtabel voor dit werk, en een snippet van veertig regels is sinds 2008 over de hele taal gekopieerd en geplakt. Als je project CMake-gebaseerd is, is de hele afhankelijkheidsset drie regels:

find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)

De Boost-release om te onthouden is 1.92.0 uit augustus 2026, van een project opgericht in 1998 dat sinds zijn eerste release in 1999 libraries levert. Beide Boost-encoders hieronder zijn header-only - er is helemaal niets te linken - terwijl OpenSSL -lcrypto wil, maar de meeste C++-programma's die met TLS te maken hebben, hebben die al in de binary.

Eerst de rekensom: elke buffer in dit artikel afmeten

Base64 groepeert bytes in drietallen, dus de outputlengte heeft een vorm die je niet meer verast als je die kent: per 3 inputbytes komen er 4 karakters uit, en een korte staart wordt gepad tot een volle groep. Het exacte aantal voor n inputbytes is:

4 * ((n + 2) / 3)

De +2 is de plafondtruc: geheeltallige deling rondt naar beneden af, dus als je eerst 2 optelt, rondt het naar boven af naar het volgende veelvoud van drie. Vanaf daar is elke buffergrootte in dit artikel een invulling. De one-shot functie van OpenSSL wil een buffer die de geëncodeerde data kan bevatten, plus het NUL dat hij aan het einde plaatst - de man page illustreert de overeenkomst met 16 inputbytes die 24 geëncodeerde bytes plus 1 NUL worden, 25 bytes in totaal, en de functie geeft de lengte zonder het NUL terug. Het streamende pad verwerkt input in blokken van 48 bytes, en de man page meet de output in op 65 bytes per blok (64 tekens plus de nieuwe regel die elk blok altijd produceert) plus nog een byte voor het NUL. De Boost.Beast-header reikt je de exacte formule aan als constexpr-functie. En je eigen code reserveert (n + 2) / 3 * 4 en noemt het klaar. Hier zijn de aantallen die je daadwerkelijk zult tegenkomen:

Input Output (gepad) Let op dit detail
1 byte 4 tekens De kleinste gepadde vorm: QQ==
2 bytes 4 tekens Drie datakarakters en één pad
3 bytes 4 tekens Één volle groep, helemaal geen padding
48 bytes 64 tekens Precies één OpenSSL-streamend blok
500 bytes 668 tekens Wikkel op 64 en het zijn 11 regels, 679 tekens met nieuwe regels
1 GB ongeveer 1,33 GB Reken de kolom, het bestand en de verbinding bij voor de belasting

Als de ontvangende kant een kolom met vaste grootte is, een buffer, of een regel in een tekstbestand, dan is deze formule het hele ontwerpdokument. De ene richting waarin het je kan beetpakken, is de andere: de decoderingskant heeft 3n/4 minus pads nodig, en een decodebuffer dat met de encoderingsformule is afgemeten, is een klassieke overallocatie die uitgroeit tot een memory-bug-ticket. De verkleinende richting afmeten is het probleem van de zusterhandleiding; hier groei je alleen maar.

Hier is het landschap, omdat de verschillen allemaal in de extra's zitten - de padding, de nieuwe regels, de NUL's - en niet in de kern van het inpakken, die elke rij identiek implementeert:

Encoder Waar het vandaan komt Padding Extra bytes om te budgetteren De quirk om te onthouden
EVP_EncodeBlock <openssl/evp.h>, link -lcrypto Altijd 1 (een NUL in de buffer) Het voorbeeld van 16 bytes in de man page is de overeenkomst
EVP_EncodeUpdate + Final idem Altijd 65 per blok van 48 bytes Wikkelt hard op 64 tekens, elk blok eindigt in een nieuwe regel
Boost.Beast encode boost/beast/core/detail/base64.hpp, header-only Altijd 0 Zit in een namespace met de naam detail
Boost.Serialization-iterators boost/archive/iterators/base64_from_binary.hpp, header-only Nooit 0 - je voegt de 1 of 2 pads zelf toe Oudste encoder in de gereedschapskist, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib Altijd 2 (een CRLF) tenzij NOCRLF Heeft een URL-safe flag die de rest van de gereedschapskist mist
Je eigen veertig regels Nergens: die zijn van jou Jouw keuze Jouw keuze Jij bezit elke edge case voor altijd

Het kernalgoritme is in elke rij identiek - dat is het troostende deel van een formaat uit 1987. Wat verschilt, is wat elke implementatie rond de payload toevoegt, en vrijwel elke val in dit artikel is een van die toevoegingen die een verbruiker tegenkomt die het niet verwachtte.

OpenSSL: de encoder die je TLS-stack al linkt

Als je programma OpenSSL al linkt voor TLS, hoef je niets toe te voegen. De one-shot functie is één enkele aanroep:

int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);

Geef hem de bronbytes en de lengte, en hij schrijft de gepadde, éénregelige codering. De overeenkomst is de moeite waard om te leren, want de man page stelt hem met een voorbeeld: per 3 inputbytes komen er 4 outputbytes; een staart die niet deelbaar is door 3 wordt gepad, zodat de output altijd deelbaar is door 4; en er komt bovenop een NUL-eindteken bij. Het gedocumenteerde voorbeeld is 16 bytes in, 24 geëncodeerde bytes plus 1 NUL, 25 bytes in totaal in de buffer, waarbij de functie 24 teruggeeft - de lengte zonder het NUL. Meet de buffer daarop af en de wrapper is een paar regels:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>

std::string openssl_encode(const std::string &in) {
  std::string out;
  out.resize(4 * ((in.size() + 2) / 3) + 1);
  int n = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(out.data()),
                          reinterpret_cast<const unsigned char *>(in.data()),
                          static_cast<int>(in.size()));
  if (n < 0) return {};
  out.resize(static_cast<size_t>(n));
  return out;
}

int main() {
  std::printf("%s\n", openssl_encode("Mane").c_str());
  std::printf("%s\n", openssl_encode("M").c_str());
  std::printf("%s\n", openssl_encode("").c_str());
}

Merk op wat de std::string doet dat C je zou hebben opgelegd: hij groeit tot exact de teruggegeven lengte, dus het NUL dat OpenSSL heeft toegevoegd, ligt simpelweg voorbij de bijgehouden lengte en wordt nooit deel van de payload. Encodeer "Mane" en je krijgt TWFuZQ==, de klassieke vierkarakter-staart met zijn enkele pad; encodeer één byte en je krijgt een datakoppel van twee karakters dat een padkostuum van twee karakters draagt; encodeer niets en je krijgt de lege string, het ene geval waarin een base64-encoder zich exact als de identiteitsfunctie gedraagt. De ene regel aan echte logica in de hele functie is de resize: hij zet "bytes geschreven plus een NUL" om in "exact de payload".

Voor data die in stukken aankomt - een bestand, een socket, een stream die je niet wilt buffern - heeft OpenSSL een context die je voedt en afrondt, en is de blokrekensom van de man page ongewoon expliciet. Alleen volledige blokken van 48 bytes worden onmiddellijk verwerkt; de rest wordt binnen de context vastgehouden en vrijgegeven door een latere aanroep of door de definitieve. Elk verwerkt blok schrijft 64 tekens plus een nieuwe regel - 65 bytes - en de definitieve aanroep handelt het onvolledige blok af, waardoor het gedocumenteerde plafond van die aanroep 65 bytes plus het NUL is. Het gevolg om te weten vóórdat je aanroept: deze API wikkelt op 64 tekens. Dat is niet instelbaar. Dat is wat de streamende encoder is.

#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_encode_wrapped(const std::string &in) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_EncodeInit(ctx);
  std::string out;
  out.reserve(4 * ((in.size() + 2) / 3) + in.size() / 48 + 2);
  std::vector<unsigned char> buf(128);
  int outl = 0;
  for (size_t pos = 0; pos < in.size();) {
    size_t take = std::min<size_t>(48, in.size() - pos);
    EVP_EncodeUpdate(ctx, buf.data(), &outl,
                     reinterpret_cast<const unsigned char *>(in.data()) + pos,
                     static_cast<int>(take));
    out.append(reinterpret_cast<const char *>(buf.data()), outl);
    pos += take;
  }
  EVP_EncodeFinal(ctx, buf.data(), &outl);
  out.append(reinterpret_cast<const char *>(buf.data()), outl);
  EVP_ENCODE_CTX_free(ctx);
  return out;
}

int main() {
  std::string s = openssl_encode_wrapped(std::string(500, 'A'));
  std::printf("500 bytes -> %zu chars\n", s.size());
  int lines = 0;
  size_t longest = 0, run = 0;
  for (char c : s) {
    if (c == '\n') { lines++; run = 0; }
    else run++;
    longest = std::max(longest, run);
  }
  std::printf("lines=%d longest=%zu lastchar=%c\n", lines, longest, s.back());
}

Geef hem 500 bytes van de letter A en de berekening komt precies uit zoals de man page beloofde: 668 geëncodeerde tekens, en omdat de output in regels van 64 tekens is gesneden, krijg je 11 regels, 679 tekens in totaal, en het allerlaatste teken is een nieuwe regel. Die afsluitende nieuwe regel is degene die verbruikers kapotmaakt: plak het resultaat in een JSON-string en je hebt een controlekarakter waar een aanhalingsteken had moeten zitten; gebruik het als een segment van een token en je hebt een nieuw segment uitgevonden. De vuistregel: de blok-API voor éénregelige payloads (tokens, headers, configwaarden), de streamende API wanneer de verbruiker MIME-vormige omwikkelde output wil, en bij twijfel de afsluitende nieuwe regel strippen met een while (out.back() == '\n')-lus voordat de payload een grens oversteekt die die niet verwacht.

Boost.Beast: een snelle inpakker in een detail::namespace

De HTTP-library van Boost levert een base64-codec mee op het onwaarschijnlijke adres boost/beast/core/detail/base64.hpp. De detail::-namespace is de manier van Boost om te zeggen "dit is onze interne aangelegenheid", en de maintainers hebben geweigerd de codec te promoten naar een publieke API. Iedereen gebruikt het toch: het is klein, het is snel, het is header-only (definieer BOOST_BEAST_HEADER_ONLY vóór de include en er is niets te linken), en het is dezelfde codec die de eigen WebSocket-handshake van Boost.Beast gebruikt voor de Sec-WebSocket-Accept-berekening, wat betekent dat het al jaren echt verkeer kauwt.

Op de encoderingskant is de API bijna beledigend rustig. Een constexpr-helper geeft je de exacte outputgrootte - 4 * ((n + 2) / 3), dezelfde formule als de rekensomsectie, nu met een compiler om hem te checken - en de encode-functie schrijft het gepadde resultaat in je buffer en vertelt je hoeveel tekens hij gebruikte. Er is geen foutkanaal, want encoderen kan niet falen: elke byte is geldige input, en de outputlengte is een pure functie van de inputlengte. De wrapper:

#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace b64 = boost::beast::detail::base64;

std::string beast_encode(const std::string &in) {
  std::string out(b64::encoded_size(in.size()), '\0');
  std::size_t n = b64::encode(out.data(), in.data(), in.size());
  out.resize(n);
  return out;
}

int main() {
  std::printf("%s\n", beast_encode("Mane").c_str());
  std::printf("%s\n", beast_encode("M").c_str());
}

Encodeer "Mane" en je krijgt TWFuZQ==; encodeer de enkele byte M en je krijgt TQ== - dezelfde bytes als de OpenSSL-wrapper, zonder NUL waar je je zorgen over maakt en zonder regels die je moet strippen. Twee dingen om op te slaan. Eerst de oorsprong: de bron is copyright 2016-2019 van Vinnie Falco, met een voetnoot die delen toekent aan een snippet van Rene Nyffenegger uit 2004-2008 - hetzelfde volkslied waarmee het C++-base64-verhaal begon, nu meegeleverd in Boost, in je binary, en doet WebSocket-handshakes voor het hele web. Ten tweede de praktische: omdat de codec padt en nooit wikkelt, is het het juiste gereedschap voor alles wat op één regel moet - tokens, headers, API-payloads - en de encoded_size-formule geeft je een buffer die exact goed is, nooit een benadering.

Boost.Serialization: de iterator die vergeten was dat padding bestaat

De oudste base64 in het C++-ecosysteem is geen functie maar een reeks samenstelbare iterator-adapters, geschreven door Robert Ramey in 2002 voor de serialisatiebibliotheek van Boost. De encoderingsrichting is een keten van twee adapters: een breedtetransformer die je rauwe bytes hergroept van acht naar zes, en een iterator die elke hergegroepte waarde omzet in een alfabetkarakter:

#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace it = boost::archive::iterators;

std::string boost_iter_encode(const std::string &in) {
  using enc =
      it::base64_from_binary<it::transform_width<const char *, 6, 8>>;
  std::string out(enc(in.data()), enc(in.data() + in.size()));
  switch (in.size() % 3) {
    case 1: out += "=="; break;
    case 2: out += '=';  break;
    default: break;
  }
  return out;
}

int main() {
  std::printf("%s\n", boost_iter_encode("Mane").c_str());
  std::printf("%s\n", boost_iter_encode("M").c_str());
}

De iterator doet de kern van het inpakken en niets anders - geen padding, geen NUL, geen nieuwe regels en geen foutkanaal, want de kern van het inpakken kan niet falen. Encodeer "Mane" en de iterator geeft je zes karakters, TWFuZQ, met een serieuze blik: een echte codering van vier bytes is acht karakters, en een iterator uit 2002 viel het nooit in zich daar zorgen over te maken. Daarom is de switch-statement onmisbaar, niet decoratief: één byte kort van een groep krijgt twee pads, twee bytes kort krijgt er één. Dezelfde keten min de switch is wat je krijgt als je die stap vergeet, en het resultaat is een string die prima decodeert onder een soepele decoder, faalt onder een strikte, en de foutmelding van je API-verbruiker in een raadsel verandert. (De decoderingskant van deze zelfde iteratorfamilie is degene die een uitzondering gooit bij een enkele verdwaalde spatie - meer daarover in de zusterhandleiding.)

Veertig regels, nul afhankelijkheden

Base64 is klein genoeg dat een correcte encoder een respectabel ding is om zelf te bezitten, en in C++ is de opbrengst beter dan in enige andere taal: std::string maakt het bufferbeheer prettig, de formule geeft je de exacte grootte vooraf, en een handgemaakte encoder is degene die helemaal geen mening heeft - geen NUL, geen nieuwe regels, geen platformgewoontes - en dat is precies wat je wilt onder een configbestand of over een API-grens heen. Deze versie pakt in groepen van 3 bytes af tegen een tabel van 64 tekens:

#include <cstddef>
#include <cstdio>
#include <string>

std::string base64_encode(const std::string &in) {
  static const char *table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((in.size() + 2) / 3 * 4);
  const unsigned char *p =
      reinterpret_cast<const unsigned char *>(in.data());
  size_t n = in.size();
  for (size_t i = 0; i < n; i += 3) {
    unsigned v = p[i] << 16;
    if (i + 1 < n) v |= p[i + 1] << 8;
    if (i + 2 < n) v |= p[i + 2];
    out.push_back(table[(v >> 18) & 63]);
    out.push_back(table[(v >> 12) & 63]);
    out.push_back(i + 1 < n ? table[(v >> 6) & 63] : '=');
    out.push_back(i + 2 < n ? table[v & 63] : '=');
  }
  return out;
}

int main() {
  std::printf("%s\n", base64_encode("Mane").c_str());
  std::printf("%s\n", base64_encode("M").c_str());
  std::printf("%s\n", base64_encode("M\312\277").c_str());
}

Loop mee door de onderdelen. De reserve-regel is de rekensomsectie: (n + 2) / 3 * 4 karakters, exact, dus geen herallocatie halverwege de lus. De reinterpret_cast naar const unsigned char * is niet voor de show - op platforms waar char gesigneerd is, zou een byte boven 127 anders een negatief getal zijn, en het moment dat die een tabelindex raakt heb je ongedefinieerd gedrag in een laboratoriumjas. Elke iteratie haalt tot drie bytes naar een 24-bitswaarde, duwt de vier 6-bits-slices door de tabel, en geeft op de staart = terug ter plaatse van de byte die er niet was - de i + 1 < n en i + 2 < n-wachten zijn de hele paddinglogica. Geef hem "Mane" en je krijgt TWFuZQ==. Geef hem een enkele M en je krijgt TQ==. Geef hem bytes boven 127 - het 0xCA 0xBF-paar op de derde regel van het voorbeeld - en de output blijft zuiver ASCII (Tcq/), want een byte boven 127 is gewoon een byte, en de tabel geeft niet om wat het betekent. Veertig regels, geen afhankelijkheden, en elke edge case is een regel die je zelf schreef, en dat is het hele punt.

Windows CryptoAPI: de inpakker die in het OS zit

Op Windows zit er een base64-encoder in het besturingssysteem zelf, ouder dan de meeste frameworks in dit artikel: CryptBinaryToStringA uit wincrypt.h, in crypt32.lib, deel van de CryptoAPI die al decennia met Windows wordt meegeleverd. Hij zet een bytearray om naar een opgeformatteerde string, en zijn flagtabel leest als een menu van de hele geschiedenis van het formaat:

Flag Waarde Wat je krijgt
CRYPT_STRING_BASE64HEADER 0x0 Base64 gewikkeld in BEGIN/END-headerregels van een certificaat
CRYPT_STRING_BASE64 0x1 Gewone base64, geen headers
CRYPT_STRING_BASE64URI 0xD Het URL-safe alfabet: + wordt -, / wordt _, conform sectie 5 van RFC 4648
CRYPT_STRING_NOCRLF 0x40000000 Geen nieuwe regel toegevoegd aan het einde
CRYPT_STRING_NOCR 0x80000000 Een kale LF in plaats van de standaard CRLF

Het eerste om te weten is het standaardgedrag: tenzij je CRYPT_STRING_NOCRLF doorgeeft, voegt de functie een retour/nieuwe-regel-paar toe aan het einde van je string - het gedocumenteerde gedrag is dat elk niet-binair formaat een regeleinde-afsluiting krijgt - dus een base64-token dat op één regel moet passen, wil BASE64 | NOCRLF, en die combinatie is de idiomatische aanroep. Het tweede is de aanroepconventie, de klassieke Windows-tweestap: roep aan met een NULL-buffer om te vragen hoeveel ruimte er nodig is (het antwoord bevat het afsluitende NUL), alloceren, nog een keer aanroepen, en lees de lengte zonder het NUL terug:

#include <windows.h>
#include <wincrypt.h>
#include <cstddef>
#include <string>

std::string win32_encode(const std::string &in,
                         DWORD flags = CRYPT_STRING_BASE64) {
  DWORD need = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            nullptr, &need))
    return {};
  std::string out(need, '\0');
  DWORD got = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            out.data(), &got))
    return {};
  out.resize(got);
  return out;
}

Nog twee notities. De URI-flag is het enige native base64url in dit hele artikel - op Windows kun je het tokenalfabet direct encoderen, en de transcode-aanpak in de sectie hieronder is strikt voor de andere platforms. En het CRYPT_STRING_BASE64HEADER-item, met een waarde van 0, is ook de flag die je krijgt als je nul doorgeeft, dus een aanroep die "geen" flags bedoelde, wikkelt de payload in stilte in de headerregels van een certificaat - de framinggewoonte uit het PEM-tijdperk, handig om .pem-bestanden te genereren en een verrassing voor alles andere. Link tegen crypt32.lib en de functie is voor de rest van het leven van het programma van jou.

Base64url: het alfabet voor tokens en URLs

Het standaardalfabet heeft twee karakters die een URL niet overleven: + betekent spatie in een query string, en / betekent map in een path. Sectie 5 van RFC 4648 lost dit op met twee karakterwissels - + wordt - en / wordt _ - en zegt er geen woorden omheen: deze codering is "niet als hetzelfde te beschouwen als de base64-encodering". Het is het alfabet van JWTs, OAuth PKCE code challenges, YouTube-video-identifiers en de meeste API-tokens, en het laat ook routinematig de =-padding vallen, omdat de lengte in een token impliciet bekend is en de pads slechts percent-escapes zouden zijn die zich nog mochten aandienen.

Van de encoders in dit artikel geeft alleen de Windows-flag het alfabet natief uit - OpenSSL heeft geen URL-safe modus, en geen van de Boost-varianten ook - dus op de meeste platforms is het recept: standaard encoderen, de twee karakters wisselen, de pads weggelaten. Het zijn een dozijn regels:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string base64url_encode(const std::string &in, bool pad = false) {
  std::string out = base64_encode(in);
  for (char &c : out) {
    if (c == '+') c = '-';
    else if (c == '/') c = '_';
  }
  if (!pad)
    while (!out.empty() && out.back() == '=')
      out.pop_back();
  return out;
}

int main() {
  std::printf("%s\n", base64url_encode("M\312\277").c_str());
  std::printf("%s\n", base64url_encode("M").c_str());
  std::printf("%s\n", base64url_encode("M", true).c_str());
}

De eerste outputregel is Tcq_, waar het standaardalfabet / zou hebben geschreven; de twee daaropvolgende regels laten de pad-schakelaar in actie zien - TQ zonder padding als standaard, TQ== als de verbruiker ze terug wil. Dat pad-argument is degene waar je over na moet denken, want de verbruikers zijn het niet eens: JWT-segmenten willen geen pads, PKCE-challenges willen geen pads, maar een base64url-waarde die in een veld belandt waar de decoder streng is over de lengte, kan ze terug willen, en de schakelaar is een bool, geen herschrijving. En het falingspatroon om te onthouden in de omgekeerde richting: een - binnen een payload in het standaardalfabet is simpelweg ongeldig, dus de twee alfabetten zijn niet op byte-niveau verwisselbaar - een token dat met het verkeerde alfabet is geëncodeerd, decodeert niet, het faalt, en dat is het falen dat je wilt op een beveiligingsgrens.

Regelafbreking: 64, 76, of nooit

Omwikkelde base64 heeft in het wild drie regellengtes, elk met een eigen geschiedenis. De OpenSSL-streamende encoder is strak op 64 tekens - de PEM-gewoonte, waarbij de standaard uit 1987 voor Privacy-Enhanced Mail op 64 wikkelde. MIME, dat de codering voor e-mail in 1993 standaardiseerde, ging over naar 76 tekens, en dat getal is de standaard van het coreutils-base64-commando (zijn -w-flag stelt de breedte in, en -w 0 schakelt het wikkelen helemaal uit), en van de meeste tools in het ecosysteem. RFC 4648 zelf kiest geen kant: het noemt 76 als de MIME-limiet en zegt implementaties niet te wikkelen tenzij de verwijzende specificatie ze dat opdraagt. Welke je uitstuur, hangt af van wie het verbruikt, en de verbruiker - niet het formaat - is de ontwerpregel.

Wikkelwerk is een nabewerkingsstap op de geëncodeerde string, nooit een invoerstap: groepen van 4 tekens zijn de eenheid van betekenis, dus de string snijden op elk veelvoud van de breedte is een veilige snede - elke regeleinde landt tussen groepen. De C++-versie is een lus:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string wrap_lines(std::string s, size_t width = 76) {
  std::string out;
  for (size_t i = 0; i < s.size(); i += width)
    out += s.substr(i, width) + "\r\n";
  return out;
}

int main() {
  std::string mime = wrap_lines(base64_encode(std::string(200, 'x')));
  int lines = 0;
  for (char c : mime)
    if (c == '\n') lines++;
  std::printf("mime: %d lines, %zu chars\n", lines, mime.size());
}

De berekening: 200 bytes encodeert naar 268 tekens, en gewikkeld op 76 met CRLF-afsluitingen is dat 4 regels - drie volledige regels en een staart van 40 tekens - 276 tekens op de verbinding. De CRLF-keuze in de snippet is de keuze van e-mail; voor alles anders is LF de moderne standaard, en de ene niet-onderhandelbare regel is consistentie - een decoder die CRLF verwacht, leest een eenzame LF als datakarakter als hij streng is. (De regel van MIME is dat decoders regeleinden moeten negeren, waarom e-mail nooit heeft geleden onder het verschil.) De derde gewoonte om te kennen: het openssl base64-commando - het enc-programma in een trenchcoat, dat zijn eigen naam checkt in argv[0] - wikkelt op 64 zonder -A en geeft met -A één regel, en het is het ene command-line-tool waarvan je het gedrag per run checkt in plaats van het uit je geheugen te vertrouwen.

Binair in JSON en config

Een JSON-string heeft een klein lijstje karakters dat hij niet onbewerkt kan bevatten: het aanhalingsteken, de backslash, en de controlekarakters onder 0x20. Een certificaat, een willekeurige sleutel, een signatuur - ze zitten allemaal vol bytes die, als ze zouden proberen mee te reizen in een rauw stringveld, een cascade van escapes zouden worden, en de controlekarakters zouden sommige parsers gewoon laten stikken. Base64 is de oplossing, en het is het standaardantwoord dat elke configindeling die binair moet vervoeren je geeft: de waarde wordt opgeslagen als één regel van pure alfabetkarakters, en de quotingsregels van de JSON-library hebben niets meer te doen.

Het C++-patroon is de hele implementatie: de bytes lezen (in binair, uiteraard), encoderen, de string opslaan. De verbruiker decodeert aan de andere kant. De ene JSON-specifieke val is de omwikkelde string: een certificaat dat op 76 tekens is omwikkeld en rechtstreeks in een JSON-bestand wordt geplakt, is een string vol letterlijke controlekarakters, en dat is of een parsefout of stille corruptie, afhankelijk van de stemming van de parser. Als de waarde voor de mensenoege omwikkeld moet zijn, moet die geëscapt zijn of één regel - en voor machine-op-machine-config is één regel het antwoord. De andere val is de niet-benoemde waarde: een configkolom die in een man page uit 2014 base64 zegt, is meestal gepad standaardalfabet, maar tokens uit het API-tijdperk zijn URL-safe zonder padding, en de vierkarakter-test uit de decoderingshandleiding - bevat het + of /, - of _, een = aan het einde? - is de hele diagnose.

Data-URIs: bestanden die zich in pagina's plakken

Een data-URI is een URL waarvan de payload er rechtstreeks in het adres zit: data:, een optionele mediatype, een optionele ;base64-marker, een komma, en de data zelf - het hele schema van RFC 2397. Browsers gebruiken ze om afbeeldingen, lettertypen en kleine scripts rechtstreeks in HTML en CSS in te bedden zonder extra request, en als een pagina doorgaat met het netwerk uitgeschakeld, dan is een data-URI een sterke verdachte. Aan de C++-kant is de encoderingsklus het assembleren van de string, en dat is stringconcatenatie met één constante:

#include <cstdio>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string make_data_uri(const std::string &mime_type,
                          const std::string &binary) {
  return "data:" + mime_type + ";base64," + base64_encode(binary);
}

int main() {
  std::printf("%s\n", make_data_uri("text/plain", "hi").c_str());
}

Alle valkuilen zitten in de details. De ;base64-marker is exact zeven karakters, wat de lengte is waar off-by-one-bugs op jagen: een parser die zes checkt, is een parser die data:text/plain;base4,... accepteert en met een serieuze blik rommel decodeert. En de payload van een base64-data-URI is één regel - nieuwe regels zijn geen deel van de URI-grammatica, dus als je encoder de afbeelding op 76 wikkelt (en MIME-vormige encoders doen dat standaard), is de URI kapot vóórdat hij de browser bereikt. De regel voor deze verbruiker: encoderen, niet wikkelen, en de mediatype accuraat houden - een verkeerde image/png op een JPEG is het soort leugen dat zich alleen manifesteert als een kapotte miniatuur om twee uur 's nachts.

Tokens: JWTs, PKCE en API-sleutels

De base64 met de hoogste inzet op het internet zit in een token. Een JSON Web Token is drie base64url-segmenten die met punten zijn gelijmd: een header-JSON, een claims-JSON, en een signatuur die over de string header.claims is berekend. C++ heeft geen ingebouwd JWT-type, maar er één bouwen is de base64url-encoder van hierboven plus één HMAC-aanroep, want het hele token is base64url tot het het niet meer is - tot het een signatuur is:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
#include <openssl/hmac.h>

/* base64_encode en base64url_encode uit eerdere secties */

std::string jwt_hmac256(const std::string &signing_input,
                        const std::string &secret) {
  unsigned char digest[EVP_MAX_MD_SIZE];
  unsigned int len = 0;
  HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()),
       reinterpret_cast<const unsigned char *>(signing_input.data()),
       signing_input.size(), digest, &len);
  return std::string(reinterpret_cast<const char *>(digest), len);
}

int main() {
  const std::string header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const std::string claims_json =
      "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
  std::string head = base64url_encode(header_json);
  std::string claims = base64url_encode(claims_json);
  std::string signing_input = head + "." + claims;
  std::string sig = base64url_encode(jwt_hmac256(signing_input, "secret"));
  std::printf("token: %s\n", (signing_input + "." + sig).c_str());
}

Voer het voorbeeld uit en het token dat eruit komt is een leerboekvoorbeeld HS256-token: de header decodeert naar {"alg":"HS256","typ":"JWT"}, de claims naar een onderwerp, een naam en een issued-at-tijdstempel, en de signatuur is de base64url van een HMAC-SHA256 over de twee geëncodeerde segmenten. Drie details dragen het hele ontwerp. De tekeninput zijn de geëncodeerde segmenten, niet de rauwe JSON - teken de JSON en je hebt de verkeerde bytes getekend. De segmenten zijn base64url zonder padding - de pads zouden midden in een URL zitten, en het hele punt van het alfabet was om het token één schone string te houden. En HS256 betekent een gedeeld geheim, en dat is een server-naar-server-algoritme: een geheim dat in de code van een client woont, is geen geheim, en het token dat het tekent, is geen credentieel. (De PKCE-flow van OAuth gebruikt hetzelfde alfabet een slag verderop: een willekeurige verifier, gehasht met SHA-256, zonder pads in base64url gezet tot een code challenge - de encoder uit de base64url-sectie is de hele clientkantige implementatie.)

HTTP Basic Auth

De oudste base64 in HTTP is de inloggegevens-header: Authorization: Basic gevolgd door de base64 van user:password, een schema zo oud dat het ouder is dan JSON. Het bouwen is concatenatie:

#include <cstdio>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string basic_auth_header(const std::string &user,
                              const std::string &pass) {
  return "Basic " + base64_encode(user + ":" + pass);
}

int main() {
  std::printf("%s\n", basic_auth_header("user", "password").c_str());
}

De output is de string die je vast al eens hebt gezien in een vastgelegde request: Basic dXNlcjpwYXNzd29yZA==. Twee C++-notities. De concatenatie user + ":" + pass is waar een wachtwoord met een dubbele punt een naïeve parser aan de andere kant zou verwarren - de parseregel is "splitsen op de eerste dubbele punt", en daarom mag de bouwkant in beide velden alles kwijt. En als de inloggegevens geen ASCII zijn, is de veilige lectuur van het schema om gebruikers-id en wachtwoord als UTF-8 te behandelen vóór de base64, wat in C++ betekent dat je std::string het werk al doet - zolang je hem met UTF-8-bytes hebt gevuld en niet met wat de locale besloot. De beveiligingsnoot hoort bij elke noeming van dit schema: Basic auth is obfuscatie, geen bescherming. De header rijdt in het open voor iedereen die het netwerk kan lezen, dus is het alleen aanvaardbaar achter TLS, en zelfs dan is het de keuze voor machine-op-machine-aanroepen, niet voor mensen. (De codec van Boost.Beast - die uit de detail::-namespace - doet dezelfde headerklus binnen de WebSocket-implementatie van Boost, door de SHA-1-digest die de Sec-WebSocket-Accept-sleutel wordt in base64 te zetten, wat het stille bewijs is dat het patroon dit al sinds 2017 doet.)

E-mail: zevenbits-regels, base64-antwoord

E-mail is waar base64 zijn gewoontes heeft geleerd, en de gewoontes zijn nog steeds draagkrachtig. SMTP was in zijn oorspronkelijke vorm gebouwd om zevenbits-ASCII te vervoeren, dus alles binair moest worden geschreven als afdrukbare tekst voordat het kon reizen. Privacy-Enhanced Mail deed het in 1987 met regels van 64 tekens en een RSA-MD2/MD5-berichtintegriteitscheck die eraan was gelijmd, en MIME, dat de codering voor e-mail in 1993 standaardiseerde, versoepelde de limiet naar 76 tekens en voegde de regel toe dat een conforme decoder regeleinden gewoon moet negeren. Een e-mailbijlage is vandaag nog steeds base64, gewikkeld op 76, en de exacte rekensom komt uit op 4/3 keer 78/76 - ongeveer 137 procent van de oorspronkelijke grootte, plus zo'n 814 bytes aan headers.

De C++-kant is de encoder plus de wrapfunctie van hierboven - encoderen naar één regel, wikkelen op 76 met CRLF, klaar. De twee e-mailspecifieke details: de laatste regel mag wel of niet een afsluitende nieuwe regel dragen (decoders moeten die negeren, dus beide zijn legaal en beide zijn gangbaar), en de omwikkelde waarde is geen JSON-waarde, geen omgevingsvariabele en geen token - het is een blob die thuishoort in een MIME-lichaam, en hem ergens anders naartoe verplaatsen is waar het wikkelen stopt met een gewoonte te zijn en begint een bug te zijn. De omgekeerde richting - een bijlage die omwikkeld op 76 aankomt - is het terrein van de zusterhandleiding, waar de vier C++-decoders het over regeleinden op vier verschillende manieren niet eens zijn.

Bestanden, streams en het plafond van twee gigabyte

Encoderen van een bestand is het spiegelbeeld van het bestandwerk in de decoderingshandleiding: openen in binair (op Windows zou een read in tekstmodus CRLF-paren vertalen naar enkele nieuwe regels en je data veranderen vóórdat de encoder ze zag), de bytes lezen, encoderen, binair schrijven. De kleine-bestandenversie is een werkje van één functie:

#include <cstdio>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string encode_file(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  if (!in) return {};
  std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(in),
                                   std::istreambuf_iterator<char>()};
  return base64_encode(
      std::string(reinterpret_cast<const char *>(bytes.data()), bytes.size()));
}

int main() {
  std::string b64 = encode_file("/etc/hostname");
  std::printf("file -> %zu chars\n", b64.size());
}

Het plafond is het C++-specifieke feit in de sectietitel: elke lengteparameter in de EVP-API is een int. Een enkele EVP_EncodeBlock-aanroep kan dus hooguit ongeveer 2 GB aan input encoderen, en de outputbuffer voor die aanroep - 1,33 keer groter - past niet eens in een int. Onder het plafond is de blok-API prima voor bestanden die in het geheugen passen. Daarboven, of voor een bestand dat je niet in het geheugen wilt, chunk je - en de chunkregel is de ene base64-specifieke beperking op de lus: chunks moeten veelvouden van 3 bytes zijn, want de groepering is in drietallen, en een chunkgrens in het midden van een groep verandert de output. 3072 - drie chunks van 1024 bytes - is een comfortabele chunkgrootte, en de lus wordt:

#include <algorithm>
#include <cstddef>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

std::string encode_streamed(const std::string &data) {
  std::string out;
  for (size_t pos = 0; pos < data.size();) {
    size_t take = std::min<size_t>(3072, data.size() - pos);
    out += base64_encode(data.substr(pos, take));
    pos += take;
  }
  return out;
}

Elke chunk encodeert onafhankelijk, en de concatenatie is identiek aan het one-shot-resultaat - dat is de eigenschap die chunking überhaupt veilig maakt, en die rechtstreeks uit de 3-byte-groepering volgt. (De OpenSSL-streamende context uit de encodersectie doet hetzelfde werk met 64-karakter-regelafbreking er gratis bij, wat het juiste gereedschap is wanneer de verbruiker MIME-vorm wil.) En de outputkant heeft hetzelfde budget als de inputkant: een bestand van 10 GB wordt een string van 13,3 GB, dus de buffer - of het bestand dat je schrijft - wordt afgemeten met de formule uit de rekensomsectie, en het int-plafond zegt dat het chunkpad boven 2 GB geen gemak is - het is de enige weg.

Omgevingsvariabelen en de command line

Omgevingsvariabelen hebben hetzelfde probleem als JSON-strings, en een slechter antwoord: ze kunnen helemaal geen NUL-bytes bevatten, en controlekarakters zijn ook geen vriend van hen. De standaardtruc is de payload in base64 te zetten zodat hij een shell overleeft, en in C++ is de encoderingsrichting één regel:

#include <cstdio>
#include <cstdlib>
#include <string>

/* base64_encode uit de sectie "Veertig regels, nul afhankelijkheden" */

int main() {
  setenv("MY_PAYLOAD", base64_encode("hello, env").c_str(), 1);
  std::printf("env: %s\n", getenv("MY_PAYLOAD"));
}

De waarde die in de omgeving belandt, is aGVsbG8sIGVudg==: puur alfabet, shellveilig, .env-bestandsveilig, CI-dashboardveilig, en decodeerbaar op elke machine die een base64-decoder heeft. De command line zelf heeft hetzelfde twee-toolsverhaal als de decoderingskant, maar met de encoderingsrichtings-flags: base64 van coreutils (of de uutils-herimplementatie die nieuwere distributies leveren; check met base64 --version) wikkelt standaard op 76, en -w 0 geeft je één regel; openssl base64 - het enc-programma dat zijn eigen naam checkt in argv[0] en naar base64-modus schakelt - wikkelt op 64 en pakt -A voor één regel:

# één regel, voor tokens en config
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# omgebroken, voor e-mail en tekstbestanden
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

Geen van beide spreekt base64url natief, dus een token dat je in een shell munt, krijgt de transcode-behandeling voordat het in een URL gaat. En de command line is waar de gewoonte van het stille falen aan de encoderingskant het gevaarlijkst is: een encoder die wikkelt wanneer je verbruiker één regel verwachtte, geeft geen fout - hij produceert gewoon een string met nieuwe regels erin - en dat is precies het falen dat je nu in productie opspourt. Voor alles wat ertoe doet, encodeer in je programma, waar de buffer wordt afgemeten met de formule en de regelvorm een variabele is die jij controleert.

Vallen: de C++-editie

  • Het NUL dat je niet bestelde. EVP_EncodeBlock voegt een NUL-eindteken toe na de payload. Het voorbeeld in de man page: 16 bytes in, 24 geëncodeerd plus het NUL, 25 in de buffer, 24 teruggegeven. Reserveer de extra byte en resize naar de retourwaarde, of je token eindigt met een nulbyte.
  • De strakke 64. De OpenSSL-streamende API wikkelt op 64 tekens, elk blok eindigt in een nieuwe regel, en er is geen flag om dat te veranderen. Gewikkelde encoder-output in een eengeregels verbruiker is een controlekarakter-bug.
  • Het blok van 48 bytes. EVP_EncodeUpdate geeft alleen output voor volledige inputblokken van 48 bytes. De rest zit in de context totdat EVP_EncodeFinal komt. Reken 65 outputbytes per blok plus het NUL bij, en lees *outl niet als "de bytes van mijn payload" - het is het aantal bytes dat deze aanroep schreef, en voor een kleine eerste aanroep is dat nul.
  • De ontbrekende pads van de iterator. De Boost.Serialization-keten geeft nooit =. Een iterator uit 2002 die "Mane" encodeert, geeft je zes karakters. Voeg de pads zelf toe, of je strikte verbruiker weigert de string.
  • De CRLF die je niet vroeg. CryptBinaryToStringA voegt een CR/LF-paar toe tenzij je CRYPT_STRING_NOCRLF doorgeeft. Een base64-token dat met de standaardflags is gebouwd, is twee karakters langer dan het zou moeten zijn, en het voorlaatste karakter is een retour.
  • De NULL-aanroep telt het NUL mee. De Windows-groottepeiling geeft de vereiste lengte terug inclusief het afsluitende null. De echte aanroep geeft de lengte zonder terug. Die twee verwisselen is de klassieke off-by-one, en het schrijft een byte voorbij de buffer of verliest het laatste karakter.
  • Wikkel achteraf, niet onderweg. Regelafbreking is een nabewerkingsstap op de geëncodeerde string. Snijd op veelvouden van de breedte - altijd veilig, want elke groep van 4 tekens is zelfstandig - en wikkel nooit de rauwe bytes, want daar horen regeleinden niet.
  • Chunks van drie. Als je een grote payload in stukken encodeert, moeten de stukgrenzen op groepen van 3 bytes vallen, anders verandert de groepering - en de output. 3072 is een vriendelijke chunk, 3071 is een bug.
  • int, geen size_t. Elke EVP-lengteparameter is een int. Het plafond voor een enkele aanroep is ongeveer 2 GB aan input, en de output voor die input past niet eens in een int. Boven het plafond is het chunkpad of de streamende weg geen voorkeur meer.
  • Gesigneerde char. Als je inpakt vanaf een char * zonder de unsigned-cast, is een byte boven 127 op platforms waar char gesigneerd is een negatief getal, en een tabel ermee indexeren is ongedefinieerd gedrag. const unsigned char * is geen ritueel.
  • De omwikkelde JSON-string. Een op 76 tekens omwikkelde waarde die in een JSON-bestand wordt geplakt, is een string van letterlijke controlekarakters. Of het is één regel, of het is geëscapt, of het zit niet in JSON.
  • Pads zijn een contract. Sommige verbruikers willen padding (MIME, de meeste decoders), sommige niet (JWT, PKCE, tokens in URLs), en een paar strenge weigeren ontbrekende of niet-kanonieke pads ronduit. De pad is geen decoratie. Hij is deel van de formaatafspraak.
  • De twee alfabetten. Een - of _ binnen een payload in het standaardalfabet is ongeldig, en een + of / binnen een URL-safe payload is ongeldig. De alfabetten zijn niet op byte-niveau verwisselbaar - encodeer met de juiste voor de bestemming, en transcodeer met opzet.
  • std::string en strlen. std::string houdt nulbytes gelukkig, maar het moment dat je een C-string overhandigt aan een legacy API, stopt strlen bij het eerste NUL. Geef pointer en lengte, nooit een kale pointer.
  • Het budget. De output is 4/3 van de input: als de input 1,5 GB is, dan is de output 2 GB - dat is ook het int-plafond. Meet de ontvangende buffer, de kolom en de verbinding af met de formule, niet met een gok.

Hoe C++ haar Base64 kreeg

De geschiedenis van het formaat is ouder dan het moderne tijdperk van de taal, en het C++-verhaal is het verhaal van een taal die het keer op keer niet leverde. Het eerste gestandaardiseerde gebruik van de codering die nu MIME base64 heet, was het Privacy-Enhanced Mail-protocol, voorgesteld in 1987 met regels van 64 tekens en een RSA-MD2/MD5-berichtintegriteitscheck die eraan was gelijmd. De naam "base64" zelf kwam pas in 1993, toen de MIME-standaarden hem die naam gaven. C++ verscheen in 1998 als C++98 - vijf jaar na MIME - en de eerste base64-code waarnaar de ontwikkelaars van de taal grepen, was het C-paar van Rene Nyffenegger uit 2004-2008, dat een Stack Overflow-vraag van 4 december 2008 over het web verspreidde. Het mooiste deel van dat verhaal: een antwoord linkte naar de eigen pagina van Nyffenegger en nam de implementatie over, licentieheader en alles, en een topgestemd antwoord benchmarkde zijn oplossing tegen de rest van het veld. Het volkslied heeft een licentieheader - de componist zelf verscheen nooit in het commentaar.

Toen deed het ecosysteem wat ecosystemen doen. In 2002 bracht de Boost.Serialization van Robert Ramey de iterator-adapters uit - de oudste base64 in de C++-gereedschapskist, strikt in de decoderingsrichting en beroemd ongepad in de encoderingsrichting, een jaar vóórdat RFC 3548 de alfabetregels codificeerde die hij al afdwong. In 2017 bracht Boost 1.66 Beast, en samen met haar de header-only-codec die vandaag nog wordt meegeleverd, met de Nyffenegger-toeschrijving in de voetnoot. De EVP_EncodeBlock en vrienden van OpenSSL zitten in elke OpenSSL-release, dus het werkpaard zit in de gereedschapskist zo lang als de taal betoogt over of het in de standaard hoort. Op Windows is het verhaal simpelweg dat het besturingssysteem het leverde: één functie, één flagtabel, helemaal geen standaard erbij. Ondertussen ging de standaard zelf C++11, C++14, C++17, C++20 en C++23 (gepubliceerd in 2024), en nu C++26, en ze keken allemaal even naar het alfabet van 64 tekens en gingen daarna gewoon verder. De technische inhoud van C++26 was voltooid en met 114-12-3 aangenomen op de ISO C++-bijeenkomst van maart 2026 in Croydon, VK, en die voegt inderdaad een nieuwe <text_encoding>-header toe voor werk aan tekstcoders. De volgende bijeenkomsten van het comité, in juni 2026 (Brno) en november 2026 (Búzios, Brazilië), openen de C++29-working draft in plaats van C++26 opnieuw te bekijken. Base64 zit niet in de standaard. Acht standaarden, drie decennia, één header voor tekstcodering - en het comité heeft nu elke denkbare reden gehad om base64 toe te voegen, en heeft ze allemaal afgeslagen. De praktische geschiedenis van base64 in C++ is, en blijft, de geschiedenis van zijn libraries: het EVP-paar, twee Boost-varianten, een Windows-flag, en een snippet van veertig regels die van jou is.

Vreemde dingetjes die het weten waard zijn

  • Het blok van 48 bytes van de OpenSSL-streamende encoder is een getal dat in geen RFC voorkomt. Het is 16 base64-groepen, gekozen zodat de outputregel precies 64 tekens wordt - de PEM-gewoonte - en het is een van de laatste plekken waar 1987 in 2026 nog onmisbaar werk doet.
  • encoded_size van Boost.Beast is de rekensomsectie als constexpr-functie: 4 * ((n + 2) / 3), op het compileertijd geëvalueerd als je een constante geeft. De standaardlibrary kreeg deze eenregelaar nooit; Boost leverde hem in een detail::-namespace.
  • De kleinste mogelijke gepadde base64 is vier karakters, QQ==: één byte in een kostuum van twee karakters. De kleinste zonder padding is twee karakters, QQ. Het aantal pads is ook een bericht: twee pads betekent dat de laatste groep één byte had, één pad betekent dat het twee waren, en geen pads betekent dat het drie waren - de ontvanger kan de inputlengte alleen al uit de staart herstellen.
  • De overheadrekensom van MIME is exact: 4/3 keer 78/76, daarom komt een e-mailbijlage aan op ongeveer 137 procent van de oorspronkelijke grootte, plus zo'n 814 bytes aan headers. Elke encoder in dit artikel betaalt dezelfde belasting. De wikkelbreedte verandert alleen hoe ze in rekening wordt gebracht.
  • Op een gangbare libstdc++ of MSVC vervoert std::string kleine payloads in een stack-buffer via small-string optimization in plaats van te alloceren. Een input van 9 bytes encodeert naar 12 tekens en raakt de heap nooit aan. De base64-vorm van je token kan letterlijk in een stackframe wonen, en dat is het soort gratis lunch dat de standaardlibrary niet reclameert.
  • Het openssl base64-commando waarnaar je in een shell mag grijpen, is helemaal geen commando. Het is het enc-programma dat zijn eigen naam checkt in argv[0] en van persoonlijkheid wisselt. Een alias via stringvergelijking, wat de C++-wijze is om dingen te doen, in C.
  • YouTube-video-identifiers zijn base64url: elf karakters, geen padding, geen + of / in de buurt van een URL. Het meest bekeken coderingsformaat ter planeet draait op de "URL- en bestandsnaam-veilige"-variant die RFC 4648 toevoegde in een sectie die op één pagina past.
  • Vier A's - AAAA - encodeert drie nulbytes, want A is de nul van het alfabet. Als je ooit een base64-blob hebt gezien die geheel uit één karakter bestaat, weet je nu wat die zei: niets.
  • Hetzelfde paar functies verschijnt in de antwoorden op een Stack Overflow-vraag uit 2008, in de bron van Boost.Beast met een toeschrijvingsvoetnoot, en in de headerbestanden van oneindig veel privé-codebases. Vraag een C++-ontwikkelaar waar hun base64 vandaan komt, en het eerlijkste antwoord is "ik weet het niet, en het internet ook niet".

De andere richting

Alles wat je zojuist hebt ingepakt, wordt aan de andere kant uitgepakt door dezelfde gereedschapskist, en de uitpakkant heeft haar eigen set gewoontes: de one-shot OpenSSL-functie die zijn staart opvult met nullen, de bugfix uit 2025 die verandert wat de streamende decoder teruggeeft voor gepadde input, de Boost.Beast-decode die stopt bij een verdwaald karakter en er nooit een woord over zegt, de iterator die bij een enkele spatie een uitzondering gooit, en de strikte decoder van veertig regels die wijst naar de exacte byte die pijn deed. Het complete uitpakverhaal - de temperamenten van de vier decoders, base64url-transcoderen, bestanden, de 76-karakters-gewoonte van MIME, en de twee command-line-tools die in stilte falen - staat in de C++-decoderingshandleiding op de zustersite. Ga die lezen, en kom dan terug om iets groots in te pakken. Dat is het hele spel: geen standaardlibrary, vier aanbieders met vier verschillende meningen over nieuwe regels en NUL's, een formule die elke buffer in het artikel afmet, en één 33-procent-belasting die elke ontvanger terug mag krijgen. Fijn inpakwerk.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-decodering in C++ (Cpp): een complete gids