Base64-Dekodierung in C++ (Cpp): Ein vollständiger Leitfaden
Sie haben den String. Ein langes Band aus Buchstaben und Ziffern, hin und wieder ein + oder /, vielleicht ein = oder zwei am Ende, und irgendwo in Ihrem Ticket, Vertrag oder Ihrer Datenbankspalte das Versprechen, dass es Base64 ist. Jetzt brauchen Sie die originalen Bytes zurück, in C++, und Sie brauchen sie korrekt. Die Startseite dieser Site erklärt das Format im Detail, also hier nur die Kurzversion: Vier Alphabetzeichen tragen drei Bytes, ein =-Schweif aus einem oder zwei Zeichen markiert, wo die echten Daten aufgehört haben, und die kodierte Form ist etwa 33 Prozent größer als das Original. Dekodieren ist die schrumpfende Richtung, also braucht ein Dekodierer nie mehr Speicher als der Payload, den er bereits hält. Das ist eine wahrhaft angenehme Eigenschaft und einer der stillen Freuden der Arbeit in diese Richtung.
Die größere Schlagzeile ist, dass C++ selbst nicht ein einziges Zeichen für Sie dekodiert. Die Standardbibliothek hatte dreißig Jahre, um eine base64-Funktion zu züchten, und hat sie alle für andere Dinge verwendet, also bringt jedes C++-Programm seinen eigenen Dekodierer mit - ein Aufgebot aus drei sehr verschiedenen Persönlichkeiten, plus die Option, etwa vierzig eigene Zeilen zu schreiben. Einer ist ein Arbeitstier, das das Internet seit den 1990ern trägt, einer ist eine stille Sorte, die mitten im Satz stehenbleibt und nie ein Wort darüber sagt, und einer ist ein Perfektionist, der bei einem einzigen verirrten Leerzeichen eine Exception wirft. Wenn Sie wissen, was jeder verzeiht, was er ablehnt und was er still und leise hinter Ihrem Rücken tut, ist das Dekodieren keine Quelle für Mysterien-Bugs mehr. Öffnen wir ein paar Pakete.
Die Werkzeugkiste: Vier Dekodierer, vier Temperamente
Hier ist das Bild auf einen Blick. Alle vier können das Standard-Alphabet; die Unterschiede liegen an den Rändern, und an den Rändern wohnen die Bugs.
| Dekodierer | Herkunft | Fehlermodell | Die Eigenheit, die Sie im Kopf behalten |
|---|---|---|---|
| OpenSSL EVP | <openssl/evp.h>, Link -lcrypto |
Gibt bei kaputter Eingabe -1 zurück |
Die One-Shot-Version füllt den Tail mit Nullen auf |
| Boost.Beast | <boost/beast/core/detail/base64.hpp>, header-only |
Keine: es hört einfach auf | Kein Fehlerkanal in irgendeiner Form |
| Boost.Serialization-Iteratoren | <boost/archive/iterators/binary_from_base64.hpp>, header-only |
Wirft dataflow_exception |
Behandelt = als echten Nullwert |
| Ihre eigenen vierzig Zeilen | Nirgendwo: Es gehört Ihnen | Ihre Wahl, bis auf die Byte-Position | Sie besitzen jeden Randfall für immer |
Die Installation ist ein Paketname pro Distro. Für OpenSSL: libssl-dev auf Debian und Ubuntu, openssl-devel auf Fedora und RHEL, openssl auf Arch und brew install openssl auf macOS. Für Boost, dessen aktuelle Version 1.92.0 von August 2026 stammt, von einem Projekt, das seit 1998 Bibliotheken baut: libboost-dev oder boost-devel. Beide Boost-Dekodierer unten sind header-only, also gibt es gar nichts zum Linken. Wenn Ihr Projekt CMake-basiert ist, besteht die komplette Einrichtung aus drei Zeilen:
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)
Ein Versionshinweis vor dem Code, denn er ändert, was Ihr Dekodierer zurückgibt. OpenSSL 3.5, im April 2025 als Long-Term-Support-Zweig veröffentlicht, hat einen echten Bug im Streaming-Dekodierer gefixt (mehr in einer Minute), und das neuere 4.0-Feature-Release von April 2026 hat den Fix geerbt. Wenn Ihr Build ein altes 3.0 oder 3.3 festpinnt, lesen Sie den Versionsabsatz im OpenSSL-Abschnitt unten, bevor Sie einer Tail-Länge vertrauen.
OpenSSL: Der Dekodierer, den Sie wahrscheinlich schon linked haben
Wenn Ihr C++-Programm TLS, Hashing oder Zertifikate berührt, ist OpenSSL schon im Binär, und seine EVP-Base64-Routinen sind die am härtesten erprobten Dekodierer im Geschäft. Die One-Shot-Funktion ist ein einziger Aufruf:
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
Geben Sie ihm einen Puffer mit Base64-Zeichen und seiner Länge, und er schreibt die dekodierten Bytes nach t. Er schneidet vorangestellten Weißraum, nachgestellten Weißraum, Zeilenumbrüche und Wagenrücksetzer weg, und dann setzt er Regeln ohne Kompromiss um: kein interner Weißraum, und die abgeschnittene Länge muss ein Vielfaches von 4 sein. Jedes vier Eingabezeichen erzeugen exakt drei Ausgabe-Bytes, und hier ist der Teil, der Menschen überrascht: Padding-Zeichen werden zu sechs Null-Bits dekodiert, und die Man-Seite bemerkt ruhig, dass der Aufrufer dafür verantwortlich ist, das nachgestellte Padding zu berücksichtigen. Mit anderen Worten: Die Funktion macht die Arithmetik für Sie und hängt dann still und leise bis zu zwei Bonus-Null-Bytes an das Ende an. Der idiomatische C++-Wrapper versteckt die Puffer-Mathematik hinter einem std::string:
#include <cstddef>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode(const std::string &b64) {
std::vector<unsigned char> out(b64.size() * 3 / 4 + 4);
int n = EVP_DecodeBlock(out.data(),
reinterpret_cast<const unsigned char *>(b64.data()),
static_cast<int>(b64.size()));
if (n < 0) return {};
size_t pads = 0;
for (size_t i = b64.size(); i > 0 && b64[i - 1] == '='; --i) ++pads;
return std::string(reinterpret_cast<const char *>(out.data()),
static_cast<size_t>(n) - pads);
}
int main() {
std::string one = openssl_decode("TQ==");
std::printf("%zu bytes: %02x\n", one.size(),
static_cast<unsigned char>(one[0]));
std::string word = openssl_decode("TWFuZQ==");
std::printf("%zu bytes: %s\n", word.size(), word.c_str());
}
Führen Sie es aus, und das Null-Fill zeigt sich genau dort, wo es die Dokumentation versprochen hat: Das Dekodieren des vier Zeichen langen Strings TQ== ergibt drei Bytes, den Buchstaben M plus zwei Nullen, bevor der Wrapper sie wegschneidet. Füttern Sie ihm TWFuZQ==, und Sie bekommen die sauberen vier Bytes von "Mane". Füttern Sie ihm ein Zeichen außerhalb des Alphabets, und Sie bekommen einen leeren String zurück, denn die Funktion hat mit -1 geantwortet. Beachten Sie, was std::string in diesem Wrapper still und leise leistet: Es verfolgt seine eigene Länge und enthält gerne Null-Bytes, also kann ein dekodiertes JPEG in Ihrem Text-Typ leben und verglichen, gehasht und herumgereicht werden. In C wären Sie froh um eine Längen-Variable; hier funktioniert der String einfach.
Für Daten, die in Stücken ankommen, gibt Ihnen OpenSSL einen Kontext, in den Sie Chunks einfüllen und aus dem Sie Ergebnisse herausholen, und die drei Funktionen haben ein kompaktes Rückgabewert-Vokabular:
| Aufruf | Rückgabe | Was es bedeutet |
|---|---|---|
EVP_DecodeUpdate |
-1 |
Ungültiges Zeichen oder ein Pad-Zeichen mitten in den Daten |
EVP_DecodeUpdate |
1 |
Es wird weitere Eingabe erwartet |
EVP_DecodeUpdate |
0 |
Ende der Daten: Die letzte Gruppe trug Padding, oder der weiche Eingabe-Endmarker ist aufgetaucht |
EVP_DecodeFinal |
1 / -1 |
Stream endete sauber / die verbliebenen Zeichen waren kein Vielfaches von 4 |
Zwei Verhaltensweisen machen den Streaming-Dekodierer zum nachsichtigsten in der Werkzeugkiste. Er überspringt Leerzeichen, Tabs, Wagenrücksetzer und Zeilenumbrüche überall im Stream, also fließt ein MIME-Mail-Block mit 76 Zeichen langen CRLF-Zeilen genau so durch wie ein einzeiliger String, und er meldet die echte Byte-Zahl: Das selbe TQ==, das die One-Shot-Funktion getäuscht hat, gibt Ihnen hier exakt ein Byte, ohne dass Sie rechnen müssen. Er kaut Eingabe in Chunks von bis zu 64 Base64-Zeichen, arbeitet von einem 80-Byte-internen Puffer und puffert alles, was nicht in eine Gruppe von vier passt, weshalb Sie ihm Chunks in beliebigen Stückgrößen füttern können. Der Wrapper:
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode_stream(const std::string &b64) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string out;
std::vector<unsigned char> chunk(1024);
int outl = 0;
for (size_t pos = 0; pos < b64.size(); pos += chunk.size()) {
size_t take = std::min(chunk.size(), b64.size() - pos);
int ret = EVP_DecodeUpdate(ctx, chunk.data(), &outl,
reinterpret_cast<const unsigned char *>(b64.data()) + pos,
static_cast<int>(take));
if (ret < 0) {
EVP_ENCODE_CTX_free(ctx);
return {};
}
out.append(reinterpret_cast<const char *>(chunk.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
int fin = EVP_DecodeFinal(ctx, tail, &tail_l);
EVP_ENCODE_CTX_free(ctx);
if (fin != 1) return {};
out.append(reinterpret_cast<const char *>(tail), tail_l);
return out;
}
Und jetzt die Fußnote zur Version aus der Werkzeugkiste-Tabelle, denn das ist genau die Sorte Ding, die ein "gelöstes" Ticket wieder öffnet: In jedem OpenSSL-Release vor 3.5 hatte der Streaming-Pfad dieselbe Null-Fill-Gewohnheit wie der Block-Dekodierer. Es wurde im Februar 2025 als Issue 26677 gemeldet; der Fix-Commit landete am 27. Februar 2025 in master, und der zugehörige Pull-Request wurde am selben Tag geschlossen. Die offizielle Man-Seite verzeichnet es jetzt in ihrem Geschichts-Abschnitt: Ab OpenSSL 3.5 erzeugt EVP_DecodeUpdate die Anzahl an Bytes, die die Dokumentation immer behauptet hat, und dekodiert Padding nicht mehr zu Null-Bits. Wenn Ihr Codebase ein altes OpenSSL festpinnt und Ihre Tail-Längen ein oder zwei Bytes danebenliegen, ist das das Erste, was Sie prüfen sollten. Und es gibt eine Exzentrizität, die von der PEM-Ära geerbt wurde: Der Bindestrich - ist überhaupt kein Alphabetzeichen - er ist ein weicher Eingabe-Endmarker. Wenn Ihr Stream nach einem Vielfachen von 4 gültigen Zeichen einen enthält, gibt der Dekodierer 0 zurück und bittet Sie, aufzuhören, deshalb lässt sich ein base64url-String, der seine --Zeichen wirklich braucht, einfach nicht dekodieren - transkodieren Sie zuerst, im Abschnitt unten, und der Zeitreisende verschwindet.
Boost.Beast: Der schnelle Dekodierer, der nie meckert
Wenn Boost schon im Projekt ist, liefert seine HTTP-Bibliothek einen Base64-Codec an der unwahrscheinlichen Adresse boost/beast/core/detail/base64.hpp. Der detail::-Namespace ist Boosts Art zu sagen "das ist unsere interne Angelegenheit", und die Maintainer haben es abgelehnt, ihn zu einer öffentlichen API zu machen. Trotzdem nutzt ihn jeder, denn er ist klein, schnell und header-only: Definieren Sie BOOST_BEAST_HEADER_ONLY vor dem Include, und es gibt nichts zum Linken. Er ist auch nebenbei der Codec, den der eigene WebSocket-Handshake von Boost.Beast für die Sec-WebSocket-Accept-Berechnung verwendet, also kaut er seit Jahren echten Verkehr.
Die Persönlichkeit der Dekodier-Funktion ist die Überraschung. Sie nimmt Ihren Ausgabe-Puffer, die Eingabe und deren Länge entgegen und gibt ein Paar zurück: die Anzahl der geschriebenen Oktette und die Anzahl der gelesenen Zeichen. Sie hält beim ersten = an und beim ersten ungültigen Zeichen - und in beiden Fällen tut sie es, ohne Sie zu informieren. Es gibt keinen Fehlercode, keine Exception, kein Status-Flag. Ein beschädigter Payload, ein zeilenumgebrochener Payload und ein abgeschnittener Tail erzeugen alle ein erfolgreiches Teilergebnis:
decode("TWFuZQ==") -> 4 Bytes "Mane", 6 Zeichen gelesen
decode("TWF!ZQ==") -> 2 Bytes "Ma", 3 Zeichen gelesen (stoppt bei '!')
decode("TWFu\nZQ==") -> 3 Bytes "Man", 4 Zeichen gelesen (stoppt bei '\n')
decode("TWFuZQ") -> 4 Bytes "Mane", 6 Zeichen gelesen (Tail war abgeschnitten)
decode("TQ==") -> 1 Byte 'M', 2 Zeichen gelesen (Padding sauber behandelt)
Lesen Sie diese Liste noch einmal, denn sie ist das komplette Bedrohungsmodell eines Dekodierers, der nie meckert: Er hat sein Bestes gegeben, er ist dort stehen geblieben, wo er stehen geblieben ist, und es liegt an Ihnen, es zu bemerken. Die Prüfung hat einen Haken: Bei gepadderter Eingabe hält die "gelesene Zeichen"-Zählung beim ersten = an, also fügen Sie die Pads vor dem Vergleichen wieder hinzu, und verlangen Sie auch, dass die Gesamtzahl ein Vielfaches von vier ist, denn das ist die einzige Form, die ein echter Payload hat:
#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <string>
namespace b64 = boost::beast::detail::base64;
std::string beast_decode(const std::string &in) {
std::string out;
out.resize(in.size() / 4 * 3 + 3); /* Puffer für ungerade Längen */
auto result = b64::decode(out.data(), in.data(), in.size());
out.resize(result.first);
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
if (result.second + pads != in.size() || in.size() % 4 != 0)
return {}; /* es hat früh aufgehört, oder der Tail war unmöglich */
return out;
}
Zwei Details zum Abheften. Erstens: Der decoded_size(n)-Helfer, auf den der Header Sie verweist, ist nur eine gültige Obergrenze, wenn n ein Vielfaches von 4 ist - der eigene Kommentar der Funktion sagt genau das -, deshalb fügt der Wrapper oben ein paar Bytes Puffer hinzu, anstatt ihm für beliebige Längen zu trauen. Zweitens: Die Herkunft: Die Quelldateien tragen ein Copyright von Vinnie Falco aus 2016-2019, mit einer Fußzeile, die Teile einem Snippet von Rene Nyffenegger aus 2004-2008 zuschreibt. Dieses Snippet ist das Base64-Paar, das seit zwei Jahrzehnten über das englischsprachige Internet kopiert und eingefügt wird, und es wird jetzt in Boost ausgeliefert, in Ihrem Binär, und macht HTTP-Basic-Auth für das ganze Web.
Boost.Serialization: Der Dekodierer, der bei einem verirrten Leerzeichen wirft
Boosts Serialisierung-Bibliothek trägt das älteste Base64 im C++-Ökosystem: eine Reihe von kombinierbaren Iterator-Adaptern von 2002, verfasst von Robert Ramey, die "sei großzügig darin, was du akzeptierst" als persönliche Beleidigung auffassen. Die Dekodier-Richtung lebt in binary_from_base64.hpp (ja, der Name ist aus Sicht der Ausgabe) und wird mit einem Breiten-Transformer gepaart, der 6-Bit-Werte in 8-Bit-Bytes verpackt:
#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <string>
namespace it = boost::archive::iterators;
std::string boost_decode(const std::string &in) {
using dec =
it::transform_width<it::binary_from_base64<const char *>, 8, 6>;
std::string out(dec(in.data()), dec(in.data() + in.size()));
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
out.resize(out.size() - pads);
return out;
}
Der innere Iterator wandelt jedes Base64-Zeichen in seinen 6-Bit-Wert um, und der äußere gruppiert diese Werte zu Bytes. Seine Strenge ist total: Jedes Zeichen außerhalb des Alphabets - einschließlich eines einzelnen Leerzeichens - lässt den Iterator boost::archive::iterators::dataflow_exception mit der Nachricht "attempt to decode a value not in base64 char set" werfen. Das ist das "ablehnen, es sei denn, man sagt etwas anderes"-Verhalten, das die RFCs später explizit gemacht haben - implementiert 2002, ein volles Jahr, bevor RFC 3548 dieselbe Regel kodifizierte. Die praktische Konsequenz: MIME-umgebrochene Eingabe muss ihre Zeilenumbrüche verlieren, bevor sie diesen Iterator berührt. Die zweite Eigenheit ist subtiler: In der Nachschlage-Tabelle wird das Padding-Zeichen = nicht übersprungen - es wird als der Wert null dargestellt. Das Dekodieren von TWFuZQ== erzeugt also sechs Bytes - 4d 61 6e 65 00 00 -, denn beide Pad-Zeichen haben echte - wenn auch null - Daten beigetragen, und die resize(size - pads)-Zeile im Snippet ist tragend, nicht dekorativ. Dekodieren Sie TQ==, und Sie bekommen drei Bytes, die sich auf den einzelnen Buchstaben M zusammenschneiden, genau dort, wo Sie landen wollen.
Vierzig Zeilen, die Sie besitzen
Base64 ist klein genug, dass ein korrekter Dekodierer etwas Ehrenwertes ist, das man besitzt, und in C++ ist die Belohnung besser als in jeder anderen Sprache: std::string macht die Puffer-Verwaltung angenehm, und ein handgebauter Dekodierer kann etwas, das keine der Bibliotheks-Versionen oben kann, nämlich auf das exakte Byte zeigen, das geschadet hat. Diese Version folgt der strengen Lesart von RFC 4648 - die Enden abschneiden, internen Weißraum ablehnen, Padding in der Mitte ablehnen, die Längen-Regeln durchsetzen und sogar die Pad-Bits prüfen, die der RFC sagt, dass ein konformer Encoder zu nullen hat:
#include <cstddef>
#include <cstring>
#include <string>
std::string strict_decode(const std::string &in, size_t *error_pos = nullptr) {
static const char *table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
auto fail = [&](size_t pos) {
if (error_pos) *error_pos = pos;
return std::string();
};
size_t start = 0;
size_t end = in.size();
while (start < end && (in[start] == ' ' || in[start] == '\t' ||
in[start] == '\r' || in[start] == '\n'))
++start;
while (end > start && (in[end - 1] == ' ' || in[end - 1] == '\t' ||
in[end - 1] == '\r' || in[end - 1] == '\n'))
--end;
size_t pads = 0;
while (end > start && in[end - 1] == '=') {
--end;
++pads;
}
size_t body = end - start;
if (pads > 2 || (pads == 1 && body % 4 != 3) ||
(pads == 2 && body % 4 != 2) || (pads == 0 && body % 4 == 1))
return fail(in.size());
if (body >= 2 && body % 4 != 0) {
int leftover = static_cast<int>((body % 4) * 6 % 8);
int last = static_cast<int>(std::strchr(table, in[end - 1]) - table);
if ((last & ((1 << leftover) - 1)) != 0)
return fail(end - 1); /* nicht-kanonische Pad-Bits */
}
int value = 0;
int bits = -8;
std::string out;
out.reserve(body / 4 * 3);
for (size_t i = start; i < end; ++i) {
const char *p = std::strchr(table, in[i]);
if (!p)
return fail(i);
value = (value << 6) + static_cast<int>(p - table);
bits += 6;
if (bits >= 0) {
out.push_back(static_cast<char>((value >> bits) & 0xFF));
bits -= 8;
}
}
return out;
}
Gehen Sie durch, was es durchsetzt. Vorangestellter und nachgestellter Weißraum wird abgeschnitten, denn ein Payload, der aus einem Mail-Header kopiert wurde, kommt sehr wahrscheinlich damit gekleidet. Interner Weißraum wird abgelehnt, denn RFC 4648 sagt, dass ein Dekodierer MUST Nicht-Alphabetzeichen ablehnen muss, es sei denn, die umgebende Spezifikation sagt etwas anderes, und an einer Sicherheits-Grenze wollen Sie die strenge Lesart. Ein Pad-Zeichen mitten in den Daten wird abgelehnt, ebenso jede Länge, die keinem echten Payload entsprechen kann: ein Zeichen weniger als eine Gruppe ist unmöglich, und ein einzelnes Pad ist nur hinter drei Körper-Zeichen legal. Die Prüfung auf Kanonizität am Ende ist die, die die meisten Implementierungen überspringen: Wenn die letzte Gruppe ein oder zwei Pad-Zeichen hatte, müssen die ungenutzten niedrigen Bits des letzten Alphabetzeichens null sein, sonst könnten dieselben Bytes als zwei sichtbar verschiedene Strings geschrieben werden. Das ist die Formbarkeit, die den Grund für die Prüfung gibt, und sie kostet vier Zeilen. Schließlich akzeptiert der Dekodierer Eingabe ohne Pads, was genau das ist, was JWT-Segmente sind. Und bei einem Fehlschlag gibt er Ihnen die Position: Füttern Sie ihm TWF!ZQ==, und der Fehler sitzt an Index 3, auf dem Ausrufezeichen, was der Unterschied zwischen einem Bug-Report und einem Fix ist.
Base64url: Das Alphabet, das Tokens sprechen
Das Standard-Alphabet hat zwei Zeichen, die eine URL nicht überleben: + bedeutet Leerzeichen in einem Query-String, und / bedeutet Verzeichnis in einem Pfad. RFC 4648 Abschnitt 5 behebt das mit zwei Zeichen-Tauschen - + wird - und / wird _ - und ist unmissverständlich über das Ergebnis: Diese Kodierung "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". Es ist das Alphabet von JWTs, OAuth-PKCE-Code-Challenges, YouTube-Video-Identifikatoren und den meisten API-Tokens, und es lässt routinemäßig das =-Padding weg, denn in einem Token ist die Länge implizit bekannt, und das Padding wäre nur ein Prozent-Escape, das darauf wartet, zu passieren. Keiner der C++-Dekodierer oben spricht es nativ - OpenSSL behandelt sogar ein - als diesen weichen Eingabe-Endmarker - also ist der Fix ein kleines Transkodieren, bevor Sie dekodieren. Es ist so kurz, dass man es leicht im Kopf behalten kann:
#include <string>
std::string url_to_standard(std::string in) {
for (char &c : in) {
if (c == '-') c = '+';
else if (c == '_') c = '/';
}
switch (in.size() % 4) {
case 2: in += "=="; break; /* das weggelassene Padding wiederherstellen */
case 3: in += '='; break;
default: break;
}
return in;
}
Wenden Sie es auf ein echtes JWT an, und die ersten zwei Segmente sind gewöhnlicher JSON, der darauf wartet, zu passieren. Ein klassisches Beispiel-Token dekodiert zu einem Header von {"alg":"HS256","typ":"JWT"} und einem Satz von Claims, die einen Subject, einen Namen und einen issued-at-Zeitstempel enthalten. Das dritte Segment dekodiert auf dieselbe Weise und gibt Ihnen die rohen Signatur-Bytes - nicht Text, und nicht der Beweis für irgendetwas. Das Dekodieren eines Tokens sagt Ihnen, was es behauptet; das Verifizieren der Signatur sagt Ihnen, ob Sie es glauben sollen, und das ist ein Kryptografie-Job, den keine Base64-Bibliothek für Sie tut. Auf Windows ist die Situation einseitig in dieselbe Richtung: CryptBinaryToStringA hat ein CRYPT_STRING_BASE64URI-Flag für die Kodierungs-Seite, aber die Dekodier-Richtung hat überhaupt kein URL-sicheres Flag, also verdient das Transkodieren oben seinen Platz in Ihrem Muskelgedächtnis auf jeder Plattform.
Von Bytes zurück zu Text
Fragen Sie einen C++-Dekodierer "Welches Charset habe ich gerade dekodiert?", und Sie bekommen die ehrlichste Antwort, die die Sprache hat: keins. Dekodierer sind von Anfang bis Ende byte-orientiert. Sie sehen keinen Text; sie sehen Bytes, und sie geben Ihnen genau die Bytes zurück, die verpackt waren. Wenn das Original UTF-8 war, halten Sie jetzt UTF-8, und es wird nichts weiter gebraucht. Der nette C++-Kniff ist, dass std::string selbst ein Byte-Container mit einem Längen-Mitglied ist, also verdampft der klassische C-Fehlermodus - eine String-Funktion, die beim ersten NUL anhält - größtenteils. Eine dekodierte Datei, ein dekodiertes Zertifikat, ein dekodiertes Bild: Alle können in einem string leben, mit == verglichen, gehasht und per Wert übergeben werden, und die Null-Bytes darin sind einfach nur Bytes. Nur nicht in eine C-String umwandeln und dann mit strlen messen; verwenden Sie size().
Für die Legacy-Kodierungen, die immer noch in alten Datenbanken, Exports und handgeschriebenen Tools lauern - ISO-8859-1, Windows-1252 und ihre Verwandten - ist das Standard-Tool POSIX iconv, das mit glibc ausgeliefert wird. Dekodieren Sie zuerst in Bytes, und wandeln Sie diese Bytes dann mit dem Codec in UTF-8 um, der zur Quelle passt:
#include <iconv.h>
#include <cstddef>
#include <string>
#include <vector>
std::string to_utf8(const std::vector<unsigned char> &raw,
const char *source_charset) {
iconv_t cd = iconv_open("UTF-8", source_charset);
if (cd == (iconv_t)-1) return {};
char *inptr = reinterpret_cast<char *>(const_cast<unsigned char *>(raw.data()));
size_t inleft = raw.size();
std::vector<char> utf8buf(raw.size() * 4 + 8);
char *outptr = utf8buf.data();
size_t outleft = utf8buf.size();
if (iconv(cd, &inptr, &inleft, &outptr, &outleft) == (size_t)-1) {
iconv_close(cd);
return {};
}
iconv_close(cd);
return std::string(utf8buf.data(),
static_cast<size_t>(outptr - utf8buf.data()));
}
Die Rundreise ist in beide Richtungen verlustfrei: Packen Sie einen String als ISO-8859-1, kodieren Sie ihn als Base64, versenden Sie ihn, dekodieren Sie ihn, wandeln Sie ihn um, und Sie bekommen genau das, womit Sie angefangen haben, mit den Akzent-Zeichen intakt. Und binäre Daten haben überhaupt kein Charset - ein PNG ist ein PNG, ob Sie es mögen oder nicht, was die befreiendste Antwort im ganzen Artikel ist.
Dateien dekodieren
Kleine Dateien sind ein vier-Schritte-Tanz: im Binären öffnen, in einen Vector einlesen, dekodieren, das Ergebnis im Binären zurückschreiben. Binärmodus, jedes Mal, auf jeder Plattform - auf Windows würde ein Textmodus-Lesevorgang CRLF-Paare in einzelne Zeilenumbrüche übersetzen und Ihre Daten still und leise ändern, bevor der Dekodierer sie überhaupt sieht:
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
std::vector<unsigned char> read_file(const std::string &path) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
}
Beachten Sie die geschweiften Klammern im Code oben. Mit einfachen Klammern ist eine Zeile der Form std::vector<unsigned char> bytes(istreambuf_iterator<char>(file), istreambuf_iterator<char>()) der berüchtigte "most vexing parse": Der Compiler liest sie als Deklaration einer Funktion, die einen Vector zurückgibt, und er ist dabei völlig im Recht. Die Form mit geschweiften Klammern oben umgeht die Grammatik komplett. Sobald die Bytes in der Hand sind, führen Sie sie durch jeden Dekodierer aus diesem Artikel und schreiben Sie das Ergebnis mit std::ofstream in std::ios::binary, indem Sie write(data.data(), data.size()) statt des Stream-Operators verwenden, damit eingebettete Null-Bytes die Reise auf die Platte überstehen. Eine .b64-Datei und ihr dekodierter Zwilling unterscheiden sich dann genau um die 33-Prozent-Steuer, die Sie auf dem Hinweg gezahlt haben, was einen befriedigenden Checksummen-Moment macht.
Große Dateien: In beide Richtungen streamen
Für Dateien, die zu groß sind, um sie im Speicher zu halten, macht der Streaming-Dekodierer aus dem OpenSSL-Abschnitt den ganzen Job: einen Chunk lesen, durch den Kontext schieben, schreiben, was herauskommt, wiederholen. Zu jedem Zeitpunkt lebt nur ein kleiner Puffer im RAM, also dekodiert eine 10-GB-Base64-Datei mit demselben Code wie eine 10-KB-Datei, und MIME-artiges Zeilenumbrechen braucht auf dem Weg durch keine Vorverarbeitung, denn der Streaming-Dekodierer zuckt bei Zeilenumbrüchen nur die Schultern:
#include <fstream>
#include <string>
#include <vector>
#include <openssl/evp.h>
bool decode_stream_to_file(const std::string &in_path,
const std::string &out_path) {
std::ifstream in(in_path, std::ios::binary);
std::ofstream out(out_path, std::ios::binary);
if (!in || !out) return false;
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string chunk(65536, '\0');
std::vector<unsigned char> decoded(49152);
bool ok = true;
for (;;) {
std::streamsize got = in.read(chunk.data(), chunk.size()).gcount();
if (got < 0) { ok = false; break; }
if (got == 0) break;
int outl = 0;
int ret = EVP_DecodeUpdate(ctx, decoded.data(), &outl,
reinterpret_cast<const unsigned char *>(chunk.data()),
static_cast<int>(got));
if (ret < 0) { ok = false; break; }
out.write(reinterpret_cast<const char *>(decoded.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
if (ok && EVP_DecodeFinal(ctx, tail, &tail_l) != 1)
ok = false;
if (ok)
out.write(reinterpret_cast<const char *>(tail), tail_l);
EVP_ENCODE_CTX_free(ctx);
return ok;
}
Die Puffer-Größen sind nicht willkürlich: Die Längen-Parameter der EVP-Funktionen sind int, also ist ein einzelner Aufruf bis 2 GB sicher, und die Zahlen oben halten jeden Chunk bei 64 KB Eingabe mit einem 48-KB-Ausgabe-Puffer, was exakt 3 von 4 ist. Diese int-Decke ist der ganze Grund, warum der Streaming-Pfad existiert, und es lohnt sich, sie als harte Tatsache zu kennen, statt sie als Plattform-Bug zu entdecken. Wenn sich die Eingabe als korrupt herausstellt, gibt die Funktion false beim ersten Chunk zurück, der nicht dekodiert werden kann, und die Ausgabedatei hält alles, was davor gültig war - was, je nach Pipeline, genau das Teilergebnis sein kann, das Sie wollten.
HTTP, APIs und die JSON-Felder, die Bytes verstecken
Base64 taucht in HTTP in zwei Formen auf. Die erste ist Daten: Eine JSON-Antwort mit einem "certificate"- oder "avatar"-Feld voller Base64, ein Upload-Endpunkt, der Bytes in einer text-sicheren Spalte annimmt, ein Download-Endpunkt, der Ihnen eine .b64-Datei reicht. Das Muster ist immer dasselbe - das JSON parsen, den String herausziehen, ihn dekodieren, das Ergebnis als Bytes behandeln - und die Dekodier-Seite dieses Artikels ist die komplette Implementierung. Die zweite Form sind Credentials: Der Authorization: Basic-Header ist das Base64 von user:password, und er ist seit dreißig Jahren der eine Base64-Fall des Standards. Das Parsen ist zwei Schritte, und der erste ist der, in dem Leute nach einer NUL-terminierten C-String mitten in binär-nahen Daten greifen und sich wundern, warum:
#include <optional>
#include <string>
/* strict_decode aus dem Abschnitt "Vierzig Zeilen, die Sie besitzen" */
std::optional<std::pair<std::string, std::string>> parse_basic_auth(
const std::string &b64) {
std::string raw = strict_decode(b64);
size_t colon = raw.find(':');
if (colon == std::string::npos)
return std::nullopt;
return std::make_pair(raw.substr(0, colon), raw.substr(colon + 1));
}
Geben Sie ihm den Header-Wert nach dem Basic -Präfix, und er gibt Ihnen den Benutzer und das Passwort als ordentliche Strings mit verwalteter Länge, oder gar nichts, wenn der Payload kein user:pass-Paar ist. Der Sicherheits-Hinweis gehört hierher, auch wenn es kein C++-Thema ist: Basic-Auth ist Verschleierung, nicht Schutz. Der Header reist im Klartext für jeden, der das Netzwerk lesen kann, also ist er nur hinter TLS akzeptabel, und selbst dann ist er die Wahl für Maschine-zu-Maschine-Aufrufe, nicht für Menschen.
JWTs: Lesen, was ein Token behauptet
Ein JSON Web Token ist drei base64url-Segmente, die mit Punkten verklebt sind: Header, Claims, Signatur. Die ersten zwei sind JSON-Objekte; das dritte ist eine kryptografische Signatur über den String header.claims, berechnet mit dem im Header genannten Algorithmus. C++ hat keinen eingebauten JWT-Typ, aber das Lesen eines Tokens braucht nichts weiter als das Transkodieren aus dem base64url-Abschnitt und einen Dekodierer, denn der interessante Teil ist das Lesen:
#include <cstddef>
#include <cstdio>
#include <string>
/* url_to_standard und strict_decode aus früheren Abschnitten */
int main() {
const std::string token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
size_t dot1 = token.find('.');
size_t dot2 = token.find('.', dot1 + 1);
std::string header = strict_decode(
url_to_standard(token.substr(0, dot1)));
std::string claims = strict_decode(
url_to_standard(token.substr(dot1 + 1, dot2 - dot1 - 1)));
std::printf("header: %s\n", header.c_str());
std::printf("claims: %s\n", claims.c_str());
}
Der Header kommt als {"alg":"HS256","typ":"JWT"} zurück und die Claims als {"sub":"1234567890","name":"John Doe","iat":1516239022} - der Subject, der Name und ein issued-at-Zeitstempel. Das ist die gesamte Lese-Seite, und sie ist wahrhaft nützlich: aufzuzeichnen, was ein Token behauptet, einen 401 zu debuggen, indem man das Expiry-Feld ansieht, oder zu entscheiden, welchen Claims man vertraut, ist alles nur ein Dekodieren entfernt. Was es nicht ist, ist Verifikation. Das Signatur-Segment ist ebenfalls base64url, und das Dekodieren ergibt 32 oder 64 rohe Bytes, die für sich genommen nichts beweisen; die Signatur hat nur Bedeutung, wenn Sie den Hash von header.claims mit dem geteilten Secret oder öffentlichen Schlüssel neu berechnen und vergleichen. Behandeln Sie ein dekodiertes JWT so wie einen Brief: Es sagt, was es sagt, und das Verifizieren des Siegels ist ein eigener, kryptografischer Job.
Data-URIs: Dateien, die sich selbst in eine Seite kopiert haben
Eine data URI ist eine URL, deren Payload direkt in der Adresse steht: data: gefolgt von einem optionalen Media-Type, einem optionalen ;base64-Marker, einem Komma und dann den Daten selbst - das ganze Schema von RFC 2397. Browser nutzen sie, um Bilder, Schriften und kleine Skripte direkt in HTML und CSS einzubetten, ohne zusätzlichen Request, und wenn Sie jemals eine Seite sehen, die mit deaktiviertem Netzwerk weiterarbeitet, ist eine data URI eine starke Verdächtige. Auf der C++-Seite ist der Dekodier-Job, die URI aufzuteilen und dann den Payload durch Ihren üblichen Dekodierer zu jagen, denn wenn der ;base64-Marker vorhanden ist, ist der Payload gewöhnliches Standard-Base64 - meist gepaddet, meist auf einer Zeile:
#include <cstddef>
#include <string>
std::string data_uri_payload(const std::string &uri, bool *is_base64) {
const std::string prefix = "data:";
if (uri.rfind(prefix, 0) != 0)
return {};
size_t comma = uri.find(',');
if (comma == std::string::npos)
return {};
std::string meta = uri.substr(prefix.size(), comma - prefix.size());
*is_base64 = meta.size() >= 7 &&
meta.compare(meta.size() - 7, 7, ";base64") == 0;
return uri.substr(comma + 1);
}
Rufen Sie es mit data:image/png;base64,iVBORw0KGgo=... auf, und es gibt Ihnen den Payload plus das Flag, das Ihnen sagt, welchen Dekodier-Pfad Sie nehmen sollen. Zwei Fallen. Erstens: Wenn der Marker fehlt, ist der Payload URL-kodierter Text, kein Base64, also ist das Flag keine Formalität - eine URI, die wie Base64 aussieht, aber als Prozent-kodierter Text generiert wurde, dekodiert zu Müll. Zweitens: Einige Generatoren brechen lange data-URIs mit Zeilenumbrüchen um, so wie MIME es tut; Ihr strenger Dekodierer wird diese ablehnen, also streichen Sie Zeilenumbrüche vor dem Dekodieren, wenn die Quelle nicht unter Ihrer Kontrolle ist. Das Dekodieren des Payloads einer data:image/png-URI ergibt die exakten PNG-Bytes, Header inklusive, was die stille Zufriedenheit der ganzen Übung ist.
E-Mail, MIME und die 76-Zeichen-Gewohnheit
E-Mail ist der Grund, warum Base64 gelernt hat, seine Zeilen umzubrechen. SMTP, in seiner ursprünglichen Form, wurde gebaut, um 7-Bit-ASCII zu tragen, also musste alles binäre als druckbarer Text umgeschrieben werden, bevor es reisen konnte. Privacy-Enhanced Mail tat es 1987 mit 64-Zeichen-Zeilen, und MIME, als es 1993 die Kodierung für E-Mail standardisierte, lockerte die Grenze auf 76 Zeichen und fügte die Regel hinzu, dass ein konformer Dekodierer Zeilenumbrüche einfach ignorieren muss. Die Gewohnheit überlebte: Ein E-Mail-Anhang ist heute noch Base64, umgebrochen bei 76, und die exakte Arithmetik ergibt 4/3 mal 78/76 - etwa 137 Prozent der Originalgröße, plus ein paar hundert Bytes Header. Ihr C++-Dekodierer schrumpft alles wieder auf 100 Prozent, was der Punkt des ganzen Formats ist.
Der Haken in C++ ist, dass die Dekodierer in diesem Artikel sich bei Zeilenumbrüchen nicht einig sind, und jeder hat einen Grund. Der OpenSSL-Streaming-Dekodierer überspringt sie überall im Stream, was genau die MIME-Regel ist. Die OpenSSL-One-Shot-Funktion lehnt jeden Weißraum im Payload ab. Boost.Beast hält beim ersten Zeilenumbruch an, ohne es zu sagen. Die Boost-Iteratoren werfen bei einem einzelnen Leerzeichen. Wenn also ein Payload aus E-Mail kommt, ist Ihre erste Entscheidung, welchen Dekodierer Sie verwenden, oder Sie streichen die Zeilenumbrüche selbst - ein einzeiliger erase-remove-Durchgang über \r und \n - und lassen jeden Dekodierer Ihrer Wahl die eigentliche Arbeit tun. Vorne streichen ist die langweilige, zuverlässige Wahl, und sie ist die, die die Wahl Ihres Dekodierers unabhängig von der Geschichte Ihres Payloads hält.
Datenbanken, Konfigurationsdateien und Umgebungsvariablen
Die dritte Heimat von Base64 ist die Speicher-Ebene: eine Spalte in einer Legacy-Datenbank, deren Dokumentation "base64" sagt und nichts mehr, ein Config-Blob in einer JSON-Datei, ein Payload in einer Umgebungsvariablen, den ein Dienst base64-kodiert hat, damit er eine Shell übersteht. Das Dekodier-Muster ist dasselbe wie überall sonst - den String lesen, dekodieren, als Bytes behandeln - aber die unbezeichnete Eingabe verdient einen eigenen Absatz, denn manchmal wissen Sie wirklich nicht, welches Alphabet verwendet wurde. Sie können es nicht wissen, aber Sie können es testen, denn vier Zeichen machen den größten Teil der Arbeit:
- Enthält
+oder/- nur das Standard-Alphabet kann richtig sein. - Enthält
-oder_- nur das URL-sichere Alphabet kann richtig sein. - Weder noch, aber endet in
=- gepaddetes Standard, oder ein gepaddeter URL-sicherer String, dessen Payload nie zufällig die beiden getauschten Zeichen brauchte. - Weder noch, kein Padding - es könnte beides sein; die rohe URL-sichere Form ist die übliche im Web, also für Daten, die in einer URL oder einem Token geboren wurden, versuchen Sie die zuerst.
Strings, die keines der vier unterscheidenden Zeichen verwenden, dekodieren unter beiden Alphabeten identisch, also ist bei ihnen die Reihenfolge, in der Sie sie versuchen, eine Frage davon, woher die Daten kamen: Dinge, die in E-Mail geboren wurden, wollen das Standard-Alphabet, Dinge, die in einer URL geboren wurden, wollen das URL-Alphabet. Und denken Sie daran, die gepaddete und ungepaddete Lesart desselben Strings zu versuchen - ein fehlendes = ist der Unterschied zwischen "abgelehnt" und "gelöst", deshalb akzeptiert der strenge Dekodierer oben beides.
Die Kommandozeile hat zwei Base64s
Für Einmal-Jobs hat eine Linux-Kiste normalerweise zwei Base64-Dekodierer, und sie verhalten sich genau so unterschiedlich, dass es Menschen beißt. Der erste ist base64 aus GNU coreutils (einige neuere Distributionen liefern stattdessen den uutils-Nachbau mit, und beide sprechen dieselben Flags - prüfen mit base64 --version). Es hält sich an RFC 4648, bricht beim Kodieren bei 76 Zeichen um (mit -w 0 schaltet man das aus), und beim Dekodieren akzeptiert es gerne Zeilenumbrüche überall; sein -i-Flag macht die Müll-Toleranz explizit statt zufällig. Der zweite ist der von OpenSSL, und hier ist der Kniff: openssl base64 ist überhaupt keine eigene App. Seit der 1.1.0-Serie (2016) prüft das enc-Programm seinen eigenen Aufrufnamen, und wenn es als "base64" aufgerufen wurde, schaltet es sich selbst in den Base64-Modus - ein String-Vergleich auf argv[0], was der C-Weg ist, einen Alias auszuliefern. Ohne -A erwartet es einen Zeilenumbruch irgendwo in den ersten 1024 Bytes der Eingabe, also kommt ein langer einzeiliger String zurück leer, mit Exit-Code 0. Mit -A liest es eine Zeile, und die dokumentierte Bug-Liste für den enc-Befehl ist ein Museum mit zwei Exponaten: Die -A-Option funktioniert nicht richtig mit großen Dateien, und ohne -A, wenn die ersten 1024 Bytes keinen Zeilenumbruch halten, werden die ersten zwei Zeilen der Eingabe ignoriert. In einer Pipeline sieht eine stille leere Datei genau aus wie ein erfolgreicher Dekodier-Vorgang eines leeren Payloads.
# die ehrlichen Einzeiler
base64 -d < payload.b64 > payload.bin
openssl base64 -d -A < payload.b64 > payload.bin
Keiner spricht base64url nativ, was ein weiterer Grund ist, warum das Transkodieren-Snippet in Ihrem Muskelgedächtnis seinen Platz verdient. Für alles, was zählt, dekodieren Sie in Ihrem Programm, wo Fehler als Zahlen zurückkommen, die Sie testen können, und der Exit-Code eines stillen Tools nicht Ihr einziges Signal ist.
Fallen, die speziell C++ beißen
- Das Null-Fill.
EVP_DecodeBlockgibt drei Bytes fürTQ==zurück: den Buchstaben M plus zwei Nullen. Erholen Sie die echte Länge aus dem Padding, oder verwenden Sie die Streaming-API, die bei der Zählung ehrlich ist. - Die Streaming-Eigenheit vor 3.5. In OpenSSL-Releases vor 3.5.0 (April 2025) hatte
EVP_DecodeUpdatedieselbe Null-Fill-Gewohnheit. Code, der gegen ein 3.0- oder 3.3-Pin geschrieben wurde, kann Sie über Tail-Längen anlügen; der Fix ist im Geschichts-Abschnitt der Man-Seite verzeichnet. - Der stille Stopp. Boost.Beasts
decodehat keinen Fehlerkanal: Es hält bei jedem ungültigen Zeichen, jedem Zeilenumbruch und jeder unmöglichen Tail-Länge an und gibt ein Teilergebnis mit geradem Gesicht zurück. Prüfen Sie, dassconsumed + pads == input.size()und dass die Gesamtzahl ein Vielfaches von 4 ist, sonst dekodieren Sie, was es entschieden hat, zu dekodieren. - Die decoded_size-Falle.
b64::decoded_size(n)nimmt an, dassndurch 4 teilbar ist. Zwei Eingabezeichen können ein Byte ergeben, währenddecoded_size(2)null sagt - fügen Sie Puffer für ungerade Längen hinzu. - Die Null-Byte-Pads. Die Boost-Iteratoren dekodieren
=als den Wert null, also wirdTWFuZQ==zu sechs Bytes einschließlich zwei nachgestellter Nullen. Subtrahieren Sie die Pad-Zahl, oder genießen Sie Ihre Gespenster. - Weißraum, auf vier Arten. OpenSSL-Streaming überspringt ihn, OpenSSL-One-Shot lehnt ihn intern ab, die Archive-Iteratoren werfen darauf, und Beast hält an ihm an. Hingekopierte Strings lieben es, Weißraum mit sich zu tragen, und jeder Dekodierer hat dazu seine eigene Meinung.
- Signierter char-Index. Wenn Sie Ihre eigene Dekodier-Tabelle nach Zeichen indexiert bauen, indexieren Sie mit
unsigned char. Auf Plattformen, wocharsigniert ist, wird ein Byte über 127 zu einem negativen Index, was undefiniertes Verhalten ist, das sich einen Laborkittel überstreift. - Das Minus-Zeichen ist ein Zeitreisender. In OpenSSL ist
-ein weicher Eingabe-Endmarker aus der PEM-Ära, kein Alphabetzeichen. Transkodieren Sie base64url, bevor Sie dekodieren. - int, nicht size_t. Die EVP-Längen-Parameter sind
int. Über 2 GB ist nur der chunk-basierte Streaming-Pfad sicher, deshalb existiert er. - Textmodus auf Windows. Eine Datei im Textmodus zu öffnen, übersetzt CRLF zu LF und beschädigt Ihre Eingabe vor dem Dekodieren.
std::ios::binary, jedes Mal, auf jeder Plattform. - Der most vexing parse.
std::vector<char> v(istreambuf_iterator<char>(f), istreambuf_iterator<char>())ist eine Funktions-Deklaration. Verwenden Sie Initialisierung mit geschweiften Klammern oder ein Zeiger-Paar. - Nicht-kanonische Pad-Bits. Ein nachsichtiger Dekodierer kann Strings akzeptieren, deren ungenutzte Pad-Bits nicht null sind, also dekodieren zwei sichtbar verschiedene Strings zu denselben Bytes (Base64-Formbarkeit). An Sicherheits-Grenzen lehnen Sie ab, was Sie nicht brauchen - RFC 4648 sagt, dass Dekodierer genau das MAY tun.
- Die Kommandozeile scheitert still.
openssl base64 -dohne-Averschlingt einzeilige Eingabe (leere Ausgabe, Exit 0); die dokumentierten Bugs decken große Dateien und Eingabe ohne Zeilenumbrüche in beide Richtungen ab. Prüfen Sie Ihre Ausgabe in Pipelines. - strlen auf Binär.
std::stringhält Null-Bytes glücklich, aber im Moment, in dem Sie eine C-String an eine Legacy-API geben, hältstrlenbeim ersten NUL an. Geben Sie Länge und Zeiger weiter, nie einen nackten Zeiger.
Eine kurze Geschichte von Base64 in C++
Das Format ist älter als die moderne Ära der Sprache. Die erste standardisierte Verwendung der heute MIME-Base64 genannten Kodierung war das Privacy-Enhanced-Mail-Protokoll, vorgeschlagen 1987 mit 64-Zeichen-Zeilen und einer RSA-MD2/MD5-Nachrichten-Integritätsprüfung, die am Ende angeklebt war; der Name "base64" selbst kam erst 1993, als die MIME-Standards ihn benannten. C++ kam als C++98 1998 auf die Bühne - fünf Jahre nach MIME - und der erste Base64-Code, nach dem die Entwickler der Sprache griffen, war das C-Paar von Rene Nyffenegger (2004-2008), das eine Stack-Overflow-Frage vom 4. Dezember 2008 über das Web verbreitete. Der schönste Teil dieser Geschichte ist, wer nicht aufgetaucht ist: Der Autor postete nie selbst eine Antwort, aber sein Snippet wurde das Volkslied, das jeder kopierte. Eine Antwort im Thread druckte seine komplette Implementierung von seiner eigenen Website ab, für den Fall, dass die Site ausfällt.
Dann tat das Ökosystem, was Ökosysteme tun. 2002 lieferte Robert Rameys Boost.Serialization die Iterator-Adapter - das älteste Base64 in der C++-Werkzeugkiste, so streng, dass es bei einem einzelnen Leerzeichen eine Exception wirft, ein Jahr, bevor RFC 3548 die Regel kodifizierte, die es schon durchsetzte. 2017 brachte Boost 1.66 Beast, und mit ihm den header-only Codec, der heute noch ausgeliefert wird, mit der Nyffenegger-Zuschreibung in der Fußzeile. Inzwischen ging der Standard selbst C++11, C++14, C++17, C++20 und C++23 (veröffentlicht 2024), und jedes einzelne von ihnen schaute sich das 64-Zeichen-Alphabet an und ging weiter. C++26 fügt einen neuen <text_encoding>-Header für Text-Codec-Arbeit hinzu, bereits 2022 genehmigt; sein technischer Inhalt wurde auf der ISO-Tagung im März 2026 in London fertiggestellt, wo der Ausschuss mit 114-12-3 darüber abstimmte, ihn zur Veröffentlichung zu schicken, und die späteren 2026-Tagungen des Ausschusses - einschließlich der in Búzios, Brasilien am 16.-21. November - verbringen ihre Zeit damit, den nächsten Arbeits-Entwurf, C++29, zu öffnen, statt über diesen abzustimmen. Base64 war nie im Entwurf. Sieben Standards, drei Jahrzehnte, ein Header für Text-Kodierung - und der Ausschuss hatte jetzt jeden möglichen Grund, Base64 hinzuzufügen, und hat auf alle verzichtet. Die praktische Geschichte von Base64 in C++ ist und bleibt die Geschichte seiner Bibliotheken: OpenSSLs EVP-Routinen, zwei Boost-Varianten, ein Windows-API-Aufruf und ein vierzigzeiliges Snippet, das Sie besitzen.
Fun-Fakten, C++-Ausgabe
- Dieses Paar Funktionen taucht in den Antworten auf eine Stack-Overflow-Frage von 2008 auf, im Quellcode von Boost.Beast mit einer Zuschreibungs-Fußzeile und in den Header-Dateien unzähliger privater Codebases. Fragen Sie einen C++-Entwickler, woher sein Base64 kommt, und die ehrlichste Antwort ist "Ich weiß es nicht, und das Internet auch nicht".
- Boosts Archive-Iteratoren sind das älteste Base64 in diesem Artikel, Copyright 2002 - dasselbe Jahr, in dem das .NET-Framework-1.0-SDK ausgeliefert wurde. Sie werfen bei einem einzelnen Leerzeichen eine Exception, was heißt, dass sie die "Nicht-Alphabetzeichen ablehnen"-Regel durchsetzten, bevor die RFCs nachkamen: RFC 3548 kodifizierte sie 2003, und RFC 4648 wiederholte sie 2006.
- OpenSSLs Streaming-Dekodierer arbeitet von einem 80-Byte-internen Puffer, aber er leert alle 64 Base64-Zeichen, dieselbe Zeilenbreite, die die PEM-Panzerung seit 1987 verwendet. Das stille 64 ist einer der letzten Orte, an denen das alte Format 2026 noch tragende Arbeit leistet.
- Das kleinstmögliche gepaddete Base64 sind vier Zeichen,
TQ==: ein Byte in einem zwei-Zeichen-Kostüm. Das kleinste ungepaddete sind zwei Zeichen,TQ. Welches Sie zu dekodieren bekommen, hängt vollständig davon ab, wer es kodiert hat, und diese Person hat nicht an Sie gedacht. - MIMEs Mathematik ist exakt: 4/3 mal 78/76, deshalb kommt ein E-Mail-Anhang bei etwa 137 Prozent seiner Originalgröße an (plus ein paar hundert Bytes Header obendrauf). Ihr C++-Dekodierer schrumpft ihn wieder auf 100 Prozent, was die stille Freude der ganzen Übung ist.
- Auf einem typischen libstdc++ oder MSVC trägt
std::stringkleine Payloads in einem Stapel-Puffer durch Small-String-Optimierung, statt zu allozieren. Eine 9-Byte-Eingabe dekodiert zu 6 Bytes und berührt nie den Heap. Die Base64-Form Ihres winzigen Config-Blobs kann wörtlich in einem Stack-Frame leben, was die Art von Gratis-Mittagessen ist, die die Standardbibliothek nicht bewirbt. - Der
openssl base64-Befehl, nach dem Sie in einer Shell greifen könnten, ist überhaupt kein Befehl. Es ist dasenc-Programm, das seinen eigenen Namen inargv[0]prüft und die Persönlichkeit wechselt. Ein Alias durch String-Vergleich, was der C++-Weg ist, Dinge zu tun, in C. - YouTube-Video-Identifikatoren sind base64url: elf Zeichen, kein Padding, kein
+oder/in der Nähe einer URL. Das meistgesehene Kodier-Format des Planeten läuft auf der "URL and Filename Safe"-Variante, die RFC 4648 in einem Abschnitt hinzugefügt hat, der auf eine Seite passt.
Wenn Sie stattdessen verpacken müssen
Alles, was Sie gerade dekodiert haben, wurde von derselben Werkzeugkiste auf der anderen Seite verpackt: EVP_EncodeBlock für One-Shots, EVP_EncodeUpdate plus EVP_EncodeFinal für Streams (und dorther kommen diese 64-Zeichen-Zeilen), dieselbe Puffer-Arithmetik in umgekehrter Reihenfolge und dieselbe 33-Prozent-Steuer, die das Dekodieren still und leise erstattet. Die komplette Verpackungs-Geschichte - die Größen-Mathematik postenweise, die Encoder, die ihre Ausgabe mit null terminieren, der Boost-Iterator, der ein Pad-Zeichen nie getroffen hat, base64url, MIME-Umbruch, Dateien und die Windows-API mit ihrer CRLF-Gewohnheit - lebt im C++-Kodierungs-Guide auf der Schwestersite. Gehen Sie ihn lesen, dann kommen Sie zurück und öffnen Sie etwas Großes. Das ist das ganze Spiel: keine Standardbibliothek, drei vertrauenswürdige Anbieter mit drei verschiedenen Temperamenten, ein Dekodierer, der auf das exakte Byte zeigt, das geschadet hat, ein 2025-Bugfix, der den Streaming-Tail änderte, und ein null-aufgefülltes Triplett, das man für immer im Kopf behalten sollte. Viel Freude beim Entpacken.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in C++ (Cpp): Ein vollständiger Leitfaden