Haben Sie mit dem Base64-Format zu tun? Dann ist diese Website genau das Richtige für Sie! Nutzen Sie unser superpraktisches Online-Tool, um Ihre Daten zu kodieren oder zu dekodieren.

Base64-Kodierung in C++ (Cpp): Ein vollständiger Leitfaden

Das umgekehrte Problem ist das mit der größeren Schlagzeile: Sie haben Bytes - ein Zertifikat, ein Bild, einen zufälligen Blob, eine Signatur - und sie müssen durch etwas reisen, das nur Text spricht: ein JSON-Feld, ein E-Mail-Header, eine URL, eine Umgebungsvariable. Die Startseite dieser Site geht im Detail durch das Format, also ist das hier nur die kurze Version: Drei Bytes werden zu vier Alphabetzeichen, ein kurzer Tail bekommt ein oder zwei =-Marken, und die kodierte Form wird etwa 33 Prozent größer als das Original. Kodieren ist die wachsende Richtung, also ist jeder Buffer in diesem Artikel dafür dimensioniert, und die Arithmetik ist ein Einzeiler - 4 * ((n + 2) / 3) - der sich nicht ändert, welchen Encoder Sie auch wählen.

Wie auf der Dekodier-Seite wird C++ selbst Ihnen nicht ein einziges Byte kodieren. Die Standardbibliothek hatte dreißig Jahre Zeit, eine base64-Funktion wachsen zu lassen, und hat sie alle auf andere Dinge verwendet, also bringt jedes C++-Programm seinen eigenen Encoder aus einer Bank mit vier sehr unterschiedlichen Persönlichkeiten mit, plus der Option, etwa vierzig Zeilen davon selbst zu schreiben. Das eine ist ein Arbeitstier, das TLS seit den 1990ern trägt und das paddet, null-terminiert und Zeilen umbricht, ohne um Erlaubnis zu fragen. Das andere ist ein schneller header-only-Codec, der in einem Namespace versteckt ist, den seine Autoren "detail" nannten. Das dritte ist ein 2002er-Iterator, der offensichtlich noch nie ein Padding-Zeichen getroffen hat. Das vierte ist eine Funktion, die das Betriebssystem seit Jahrzehnten ausliefert und die CRLF ans Ende Ihres Tokens hängt. Und die fünfte Option ist Ihre. Sobald Sie wissen, was jeder von ihnen hinzufügt, ablehnt oder still anhängt, hört Kodieren auf, eine Quelle für Off-by-one-Bugs zu sein. Kommen wir zum Verpacken.

Der Standard hat nie einen Packer ausgeliefert

Jeder Standard seit C++98 - und es gab acht davon, bis C++26 - hat auf das 64-Zeichen-Alphabet geschaut und ist weitergegangen. Es gibt kein <base64>, kein std::base64, nichts in <string> oder <vector>, das Ihre Bytes verpackt. Die technische Arbeit an C++26 wurde fertiggestellt und auf dem ISO-C++-Meeting im März 2026 in Croydon, UK, mit 114-12-3 angenommen, und es fügt tatsächlich einen <text_encoding>-Header für Text-Codec-Arbeit hinzu; die nächsten Meetings des Ausschusses, im Juni 2026 (Brno) und November 2026 (Búzios, Brasilien), eröffnen den C++29-Arbeitsentwurf, statt C++26 noch einmal anzuschauen. Base64 ist nicht im Standard, und es fällt schwer, den Ausschuss dafür zu beschimpfen: Textkodierung geht um Zeichensätze, und Base64 geht um Bytes, also war der neue Header nie das richtige Zuhause. In der Praxis hat das Ökosystem die Arbeit gemacht. OpenSSLs EVP-Base64-Routinen sind in jeder OpenSSL-Version dabei, die Boost-Bibliotheken tragen zwei unabhängige Encoder, Windows liefert eine CryptoAPI-Funktion mit einer Flag-Tabelle für den Job aus, und ein vierzigzeiliges Snippet wird seit 2008 kopiert und eingefügt quer durch die Sprache. Wenn Ihr Projekt auf CMake basiert, ist das komplette Abhängigkeits-Setup drei Zeilen:

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

Die Boost-Version, die man kennen sollte, ist 1.92.0 von August 2026, aus einem Projekt, das 1998 gegründet wurde und seit seiner ersten Release 1999 Bibliotheken ausliefert. Beide Boost-Encoder unten sind header-only - es gibt überhaupt nichts zu linken - während OpenSSL -lcrypto will, das die meisten C++-Programme, die TLS anfassen, ohnehin schon im Binary haben.

Erst die Mathematik: Jeder Buffer in diesem Artikel wird dimensioniert

Base64 gruppiert Bytes zu dritt, also hat die Ausgabelänge eine Form, die nie überrascht, sobald man sie kennt: Für jede 3 Bytes Eingabe kommen 4 Zeichen heraus, und ein kurzer Tail wird zu einer vollen Gruppe aufgefüllt. Die exakte Anzahl für n Bytes Eingabe ist:

4 * ((n + 2) / 3)

Das +2 ist der Runden-auf-Trick: Ganzzahl-Division rundet ab, also führt das Hinzuzählen von 2 dazu, dass auf das nächste Vielfache von drei aufgerundet wird. Von dort aus ist jede Buffer-Größe in diesem Artikel eine Einsetzung. Die One-Shot-Funktion von OpenSSL will einen Buffer, der die kodierte Datenmenge plus das NUL halten kann, das sie ans Ende hängt - die Manpage illustriert den Vertrag mit 16 Eingabe-Bytes, die 24 kodierte Bytes plus 1 NUL ergeben, insgesamt 25 Bytes, und die Funktion gibt die Länge ohne das NUL zurück. Ihr Streaming-Pfad verarbeitet die Eingabe in 48-Byte-Blöcken, und die Manpage dimensioniert die Ausgabe auf 65 Bytes pro Block (64 Zeichen plus der Zeilenumbruch, den jeder Block immer erzeugt) plus ein weiteres Byte für das NUL. Der Boost.Beast-Header gibt Ihnen die exakte Formel als constexpr-Funktion an die Hand. Und Ihr eigener Code reserviert (n + 2) / 3 * 4 und betrachtet den Tag damit als geschafft. Hier sind die Zahlen, auf die Sie tatsächlich stoßen werden:

Eingabe Ausgabe (gepaddet) Was man beachten sollte
1 Byte 4 Zeichen Die kleinste gepaddete Form: QQ==
2 Bytes 4 Zeichen Drei Daten-Zeichen und ein Pad
3 Bytes 4 Zeichen Eine volle Gruppe, überhaupt kein Padding
48 Bytes 64 Zeichen Genau ein OpenSSL-Streaming-Block
500 Bytes 668 Zeichen Bei 64 umbrechen, und es sind 11 Zeilen, 679 Zeichen mit Umbrüchen
1 GB etwa 1,33 GB Die Spalte, die Datei und die Leitung für die Steuer einplanen

Wenn die Empfangsseite eine fest dimensionierte Spalte, ein Buffer oder eine Zeile in einer Textdatei ist, ist diese Formel das komplette Design-Dokument. Die eine Richtung, in der sie Sie beißen kann, ist die andere: Die Dekodier-Seite braucht 3n/4 minus Pads, und ein Decode-Buffer, der mit der Encode-Formel dimensioniert wurde, ist eine klassische Über-Allokation, die zu einem Memory-Bug-Ticket heranwächst. Die schrumpfende Richtung zu dimensionieren ist das Problem des Schwester-Leitfadens; hier wachsen Sie nur.

Hier ist die Lage, weil die Unterschiede alle in den Extras liegen - dem Padding, den Zeilenumbrüchen, den NULs - und nicht im Kernpacken, das jede Zeile identisch implementiert:

Encoder Woher er kommt Padding Zusätzliche Bytes, die eingeplant werden müssen Kuriosität zum Merken
EVP_EncodeBlock <openssl/evp.h>, link -lcrypto Immer 1 (ein NUL im Buffer) Das 16-Byte-Beispiel der Manpage ist der Vertrag
EVP_EncodeUpdate + Final dasselbe Immer 65 pro 48-Byte-Block Harter Umbruch bei 64 Zeichen, jeder Block endet in einem Zeilenumbruch
Boost.Beast encode boost/beast/core/detail/base64.hpp, header-only Immer 0 Sitzt in einem Namespace namens detail
Boost.Serialization-Iteratoren boost/archive/iterators/base64_from_binary.hpp, header-only Nie 0 - Sie fügen die 1 oder 2 Pads selbst hinzu Ältester Encoder in der Werkzeugkiste, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib Immer 2 (ein CRLF), außer mit NOCRLF Hat ein URL-safe-Flag, das der Rest der Werkzeugkiste nicht hat
Ihre eigenen vierzig Zeilen Nirgendwo: Sie sind Ihre Ihre Wahl Ihre Wahl Sie besitzen jeden Edge Case für immer

Der Kernalgorithmus ist in jeder Zeile identisch - das ist der beruhigende Teil eines 1987er-Formats. Was sich unterscheidet, ist, was jede Implementierung um das Payload herum hinzufügt, und fast jede Falle in diesem Artikel ist einer dieser Zusätze, der auf einen Konsumenten trifft, der es nicht erwartet hat.

OpenSSL: Der Encoder, den Ihr TLS-Stack schon linkt

Wenn Ihr Programm OpenSSL ohnehin schon für TLS linkt, müssen Sie nichts hinzufügen. Die One-Shot-Funktion ist ein einzelner Aufruf:

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

Reichen Sie ihm die Quell-Bytes und die Länge, und er schreibt die gepaddete, einzeilige Kodierung. Der Vertrag lohnt sich zu merken, weil die Manpage ihn mit einem Beispiel ausspricht: Für jede 3 Bytes Eingabe, 4 Bytes Ausgabe; ein Rest, der nicht durch 3 teilbar ist, wird gepaddet, damit die Ausgabe immer durch 4 teilbar ist; und oben drauf kommt ein NUL-Terminator. Das dokumentierte Beispiel ist 16 Bytes rein, 24 kodierte Bytes plus 1 NUL, 25 Bytes insgesamt im Buffer, und die Funktion gibt 24 zurück - die Länge ohne das NUL. Dimensionieren Sie den Buffer entsprechend, und der Wrapper ist ein paar Zeilen:

#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());
}

Beachten Sie, was der std::string tut, was C Ihnen aufoktroyieren würde: Er wächst auf exakt die zurückgegebene Länge, also ist das NUL, das OpenSSL angehängt hat, einfach außerhalb der geführten Länge und wird nie Teil des Payloads. Kodieren Sie "Mane", und Sie bekommen TWFuZQ==, der klassische vier Zeichen lange Tail mit seinem einzelnen Pad; kodieren Sie ein Byte, und Sie bekommen ein zwei Zeichen langes Daten-Paar im zwei-Zeichen-Pad-Kostüm; kodieren Sie nichts, und Sie bekommen den leeren String, den einen Fall, in dem ein base64-Encoder sich exakt wie die Identitätsfunktion verhält. Die einzige Zeile echter Logik in der ganzen Funktion ist das resize: Es verwandelt "geschriebene Bytes plus ein NUL" in "genau das Payload".

Für Daten, die in Stücken ankommen - eine Datei, ein Socket, ein Stream, den Sie nicht puffern wollen - hat OpenSSL einen Kontext, den Sie füttern und finalisieren, und die Blockarithmetik der Manpage ist ungewöhnlich explizit. Nur volle 48-Byte-Blöcke werden sofort verarbeitet; ein Rest wird im Kontext gehalten und von einem späteren Aufruf oder dem finalen freigegeben. Jeder verarbeitete Block schreibt 64 Zeichen plus einen Zeilenumbruch - 65 Bytes - und der finale Aufruf erledigt den Teilblock, weshalb seine dokumentierte Obergrenze 65 Bytes plus das NUL ist. Die Konsequenz, die Sie kennen sollten, bevor Sie aufrufen: Diese API bricht bei 64 Zeichen um. Sie ist nicht konfigurierbar. So ist der Streaming-Encoder nun mal.

#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());
}

Füttern Sie ihm 500 Bytes des Buchstabens A, und die Rechnung kommt exakt so raus, wie die Manpage versprochen hat: 668 kodierte Zeichen, und weil die Ausgabe in 64-Zeichen-Zeilen geschnitten wird, bekommen Sie 11 Zeilen, 679 Zeichen insgesamt, und das allerletzte Zeichen ist ein Zeilenumbruch. Genau dieser abschließende Zeilenumbruch ist der, der Konsumenten kaputt macht: Fügen Sie das Ergebnis in einen JSON-String ein, und Sie haben ein Steuerzeichen dort, wo ein Anführungszeichen hin sollte; verwenden Sie es als Token-Segment, und Sie haben ein neues Segment erfunden. Die Faustregel: Die Block-API für einzeilige Payloads (Tokens, Header, Config-Werte), die Streaming-API, wenn der Konsument MIME-geformte, umgebrochene Ausgabe will, und im Zweifel den abschließenden Zeilenumbruch mit einer while (out.back() == '\n')-Schleife streichen, bevor das Payload eine Grenze überschreitet, die ihn nicht erwartet.

Boost.Beast: Ein schneller Packer in einem detail::-Namespace

Boosts HTTP-Bibliothek liefert einen Base64-Codec an der unwahrscheinlichen Adresse boost/beast/core/detail/base64.hpp. Der detail::-Namespace ist Boosts Art zu sagen "das ist unser internes Geschäft", und die Maintainer haben es abgelehnt, den Codec zu einer öffentlichen API zu erklären. Alle benutzen ihn trotzdem: Er ist klein, er ist schnell, er ist header-only (definieren Sie BOOST_BEAST_HEADER_ONLY vor dem include, und es gibt nichts zu linken), und er ist derselbe Codec, den Boost.Beasts eigener WebSocket-Handshake für die Sec-WebSocket-Accept-Berechnung verwendet, was bedeutet, dass er seit Jahren echten Verkehr kaut.

Auf der Kodierungsseite ist die API beinahe beleidigend ruhig. Ein constexpr-Helper gibt Ihnen die exakte Ausgabegröße - 4 * ((n + 2) / 3), dieselbe Formel wie im Mathematik-Abschnitt, jetzt mit einem Compiler, der sie prüft - und die encode-Funktion schreibt das gepaddete Ergebnis in Ihren Buffer und sagt Ihnen, wie viele Zeichen sie verwendet hat. Es gibt keinen Fehlerkanal, weil Kodieren nicht scheitern kann: Jedes Byte ist gültige Eingabe, und die Ausgabelänge ist eine reine Funktion der Eingabelänge. Der 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());
}

Kodieren Sie "Mane", und Sie bekommen TWFuZQ==; kodieren Sie das einzelne Byte M, und Sie bekommen TQ== - dieselben Bytes, die der OpenSSL-Wrapper produziert hat, ohne NUL, über das Sie sich Sorgen machen müssten, und ohne Zeilen, die Sie streichen müssten. Zwei Dinge zum Abheften. Erstens die Herkunft: Die Quelle ist urheberrechtlich 2016-2019 von Vinnie Falco geschützt, mit einem Footer, der Teile einem Snippet von Rene Nyffenegger aus 2004-2008 zuschreibt - dasselbe Volkslied, das die C++-Base64-Geschichte begann, jetzt ausgeliefert in Boost, in Ihrem Binary, und macht WebSocket-Handshakes für das ganze Web. Zweitens das praktische: Weil der Codec paddet und nie umbreicht, ist er das richtige Werkzeug für alles, was eine Zeile sein muss - Tokens, Header, API-Payloads - und die encoded_size-Formel gibt Ihnen einen Buffer, der exakt richtig ist, nie eine Näherung.

Boost.Serialization: Der Iterator, der vergessen hat, dass Padding existiert

Das älteste Base64 im C++-Ökosystem ist keine Funktion, sondern eine Sammlung kombinierbarer Iterator-Adapter, geschrieben von Robert Ramey 2002 für Boosts Serialisierungsbibliothek. Die Kodier-Richtung ist eine Zwei-Adapter-Kette: ein Width-Transformer, der Ihre rohen Bytes acht-zu-sechs neu gruppiert, und ein Iterator, der jeden neu gruppierten Wert in ein Alphabetzeichen verwandelt:

#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());
}

Der Iterator macht das Kernpacken und nichts anderes - kein Padding, kein NUL, keine Zeilenumbrüche und kein Fehlerkanal, weil das Kernpacken nicht scheitern kann. Kodieren Sie "Mane", und der Iterator händigt Ihnen sechs Zeichen, TWFuZQ, mit geradem Gesicht aus: Eine echte Kodierung von vier Bytes ist acht Zeichen, und es ist einem 2002er-Iterator nie in den Sinn gekommen, sich dafür zu interessieren. Deshalb ist die switch-Anweisung tragend, nicht dekorativ: Ein Byte zu wenig in einer Gruppe bekommt zwei Pads, zwei Bytes zu wenig bekommt eines. Dieselbe Kette ohne die switch ist das, was Sie bekommen, wenn Sie diesen Schritt vergessen, und das Ergebnis ist ein String, der unter einem nachsichtigen Decoder fein dekodiert, unter einem strengen scheitert und die Fehlermeldung Ihres API-Konsumenten zu einem Rätsel macht. (Die Dekodier-Seite dieser selben Iterator-Familie ist die, die bei einem einzelnen versehentlichen Leerzeichen eine Ausnahme wirft - mehr dazu im Schwester-Leitfaden.)

Vierzig Zeilen, Null Abhängigkeiten

Base64 ist klein genug, dass ein korrekter Encoder etwas Respektables ist, das man besitzen kann, und in C++ ist der Ertrag besser als in jeder anderen Sprache: std::string macht das Buffer-Management angenehm, die Formel gibt Ihnen die exakte Größe von vornherein, und ein selbstgebauter Encoder ist der, der überhaupt keine Meinungen hat - kein NUL, keine Zeilenumbrüche, keine Plattform-Gewohnheiten - was genau das ist, was Sie unter einer Konfigurationsdatei oder einer API-Grenze wollen. Diese Version packt in 3-Byte-Gruppen gegen eine 64-Zeichen-Tabelle:

#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());
}

Gehen wir die Teile durch. Die reserve-Zeile ist der Mathematik-Abschnitt: (n + 2) / 3 * 4 Zeichen, exakt, also gibt es keine Reallokation mitten in der Schleife. Der reinterpret_cast auf const unsigned char * ist keine Zeremonie - auf Plattformen, wo char signiert ist, wäre ein Byte über 127 andernfalls eine negative Zahl, und in dem Moment, in dem es einen Tabellen-Index berührt, hätten Sie undefiniertes Verhalten, das einen Laborumhang trägt. Jede Iteration holt bis zu drei Bytes in einen 24-Bit-Wert, schiebt die vier 6-Bit-Scheiben durch die Tabelle und gibt am Tail = aus anstelle des Bytes, das nicht da war - die i + 1 < n- und i + 2 < n-Guards sind die gesamte Padding-Logik. Füttern Sie ihm "Mane", und Sie bekommen TWFuZQ==. Füttern Sie ihm ein einzelnes M, und Sie bekommen TQ==. Füttern Sie ihm Bytes über 127 - das 0xCA 0xBF-Paar in der dritten Zeile des Beispiels - und die Ausgabe bleibt reines ASCII (Tcq/), weil ein Byte über 127 einfach ein Byte ist, und die Tabelle kümmert sich nicht darum, was es bedeutet. Vierzig Zeilen, keine Abhängigkeiten, und jeder Edge Case ist eine Zeile, die Sie geschrieben haben, und das ist der ganze Punkt.

Windows CryptoAPI: Der im Betriebssystem eingebaute Packer

Unter Windows gibt es einen Base64-Encoder im Betriebssystem selbst, älter als die meisten Frameworks in diesem Artikel: CryptBinaryToStringA aus wincrypt.h, in crypt32.lib, Teil der CryptoAPI, die seit Jahrzehnten mit Windows ausgeliefert wird. Sie wandelt ein Byte-Array in einen formatierten String um, und ihre Flag-Tabelle liest sich wie ein Menü der gesamten Geschichte des Formats:

Flag Wert Was Sie bekommen
CRYPT_STRING_BASE64HEADER 0x0 Base64, eingewickelt in Zertifikat-BEGIN/END-Header-Zeilen
CRYPT_STRING_BASE64 0x1 Plain Base64, keine Header
CRYPT_STRING_BASE64URI 0xD Das URL-safe-Alphabet: + wird -, / wird _, gemäß RFC 4648, Abschnitt 5
CRYPT_STRING_NOCRLF 0x40000000 Kein Zeilenumbruch wird ans Ende angehängt
CRYPT_STRING_NOCR 0x80000000 Ein nacktes LF anstelle des Standard-CRLF

Das Erste, das man wissen sollte, ist der Standard: Solange Sie nicht CRYPT_STRING_NOCRLF übergeben, hängt die Funktion ein Wagenrücklauf/Zeilenumbruch-Paar ans Ende Ihres Strings - das dokumentierte Verhalten ist, dass jedes nicht-binäre Format eine Zeilenumbruch-Sequenz bekommt - also will ein base64-Token, das auf eine Zeile passen muss, BASE64 | NOCRLF, und diese Kombination ist der idiomatische Aufruf. Das Zweite ist die Aufrufkonvention, die die klassische Windows-Zwei-Schritt-Methode ist: mit einem NULL-Buffer aufrufen, um zu fragen, wie viel Speicher benötigt wird (die Antwort enthält das terminierende NUL), allozieren, erneut aufrufen, und die Länge ohne das NUL zurücklesen:

#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;
}

Noch zwei Anmerkungen. Das URI-Flag ist das einzige native base64url in diesem gesamten Artikel - auf Windows können Sie das Token-Alphabet direkt kodieren, und der Transcode-Ansatz im Abschnitt unten ist strikt für die anderen Plattformen. Und der CRYPT_STRING_BASE64HEADER-Eintrag, mit seinem Wert 0, ist auch das Flag, das Sie bekommen, wenn Sie null übergeben, also wickelt ein Aufruf, der "keine" Flags "meinte", das Payload still in die Zertifikat-Header-Zeilen ein - die Rahmungsgewohnheit aus der PEM-Ära, nützlich zum Erzeugen von .pem-Dateien und eine Überraschung für alles andere. Linken Sie gegen crypt32.lib, und die Funktion ist Ihre für den Rest des Programms.

Base64url: Das Alphabet für Tokens und URLs

Das Standardalphabet 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 dieselbe wie die base64-Kodierung betrachtet werden". Es ist das Alphabet der JWTs, der OAuth-PKCE-Code-Challenges, der YouTube-Video-Identifikatoren und der meisten API-Tokens, und es streicht auch routinemäßig das =-Padding, weil in einem Token die Länge implizit bekannt ist und die Pads nur percent-Escapes wären, die darauf warten, zu passieren.

Von den Encodern in diesem Artikel emittiert nur das Windows-Flag das Alphabet nativ - OpenSSL hat keinen URL-safe-Modus, und auch die Boost-Varianten nicht - also lautet das Rezept auf den meisten Plattformen: Standard kodieren, die zwei Zeichen tauschen, die Pads fallen lassen. Es sind ein Dutzend Zeilen:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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());
}

Die erste Zeile der Ausgabe ist Tcq_, wo das Standardalphabet / geschrieben hätte; die zweite und dritte Zeile zeigen den Pad-Schalter in Aktion - TQ ist standardmäßig ungepaddet, TQ==, wenn der Konsument sie zurückhaben will. Dieses pad-Argument ist das, über das man nachdenken sollte, weil die Konsumenten nicht übereinstimmen: JWT-Segmente wollen keine Pads, PKCE-Challenges wollen keine Pads, aber base64url-Werte, die in einem Feld landen, dessen Decoder strikt über die Länge wacht, könnten sie zurückhaben wollen, und der Schalter ist ein bool, kein Rewrite. Und der Fehlermodus, den man in die entgegengesetzte Richtung im Kopf behalten sollte: Ein - in einem Payload mit Standardalphabet ist schlicht ungültig, also sind die beiden Alphabete nicht auf Byte-Ebene austauschbar - ein Token, das mit dem falschen Alphabet kodiert wurde, dekodiert nicht, es scheitert, und genau das ist der Fehler, den man an einer Sicherheitsgrenze will.

Zeilenumbruch: 64, 76 oder gar nicht

Umgebrochenes Base64 hat in der Wildnis drei Zeilenlängen, jede mit einer Geschichte. Der OpenSSL-Streaming-Encoder ist hart bei 64 Zeichen - die PEM-Gewohnheit, wo der 1987er-Standard für Privacy-Enhanced Mail bei 64 umbrochen hat. MIME, als es die Kodierung 1993 für E-Mail standardisierte, ist auf 76 Zeichen gegangen, und diese Zahl ist der Standardwert des coreutils-base64-Befehls (sein -w-Flag setzt die Breite, und -w 0 schaltet den Umbruch ganz ab) und der meisten Tools des Ökosystems. RFC 4648 selbst schlägt sich nicht auf eine Seite: Es zitiert 76 als MIMEs Grenze und sagt Implementierungen, gar nicht umzubrechen, es sei denn, die verweisende Spezifikation weist sie dazu an. Welches Sie emittieren, hängt davon ab, wer es konsumiert, und der Konsument - nicht das Format - ist die Design-Beschränkung.

Umbruch ist ein Post-Bearbeitungsschritt auf dem kodierten String, nie ein Eingabe-Schritt: Die 4-Zeichen-Gruppen sind die Einheit der Bedeutung, also ist ein Schnitt des Strings bei jedem Vielfachen der Breite ein sicherer Schnitt - jede Zeilengrenze landet zwischen Gruppen. Die C++-Version ist eine Schleife:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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());
}

Die Rechnung: 200 Bytes kodieren zu 268 Zeichen, und bei 76 mit CRLF-Terminatoren umgebrochen sind das 4 Zeilen - drei volle Zeilen und ein 40-Zeichen-Tail - 276 Zeichen auf der Leitung. Die CRLF-Wahl im Snippet ist die der E-Mail; für alles andere ist LF der moderne Standard, und die eine Regel, bei der nicht verhandelbar ist, ist Konsistenz - ein Decoder, der CRLF erwartet, liest ein einzelnes LF als Daten-Zeichen, wenn er streng ist. (MIMEs Regel ist, dass Decoder Zeilenumbrüche ignorieren müssen, und deshalb hat die E-Mail nie unter dem Unterschied gelitten.) Die dritte Gewohnheit, die man kennen sollte: Der openssl base64-Befehl - das enc-Programm im Trenchcoat, das seinen eigenen Namen in argv[0] prüft - bricht ohne -A bei 64 um und emittiert mit -A eine Zeile, und er ist das eine Tool auf der Kommandozeile, dessen Verhalten man pro Lauf prüft, statt es aus dem Gedächtnis zu vertrauen.

Binärdaten in JSON und Config

Ein JSON-String hat eine kleine Liste von Zeichen, die er nicht roh enthalten kann: das Anführungszeichen, den Backslash und die Steuerzeichen unter 0x20. Ein Zertifikat, ein zufälliger Schlüssel, eine Signatur - sie alle sind voller Bytes, die zu einer Escape-Kaskade würden, wenn sie versuchten, in einem rohen String-Feld mitzufahren, und die Steuerzeichen würden manche Parser gleich ganz ersticken lassen. Base64 ist der Fix, und es ist die Standardantwort, die jedes Config-Format gibt, das Binärdaten tragen muss: Der Wert wird als eine Zeile reiner Alphabetzeichen gespeichert, und die Quoting-Regeln der JSON-Bibliothek haben nichts mehr zu tun.

Das C++-Muster ist die komplette Implementierung: Die Bytes lesen (offensichtlich im Binärmodus), kodieren, den String speichern. Der Konsument dekodiert auf der anderen Seite. Die eine JSON-spezifische Falle ist der umgebrochene String: Ein bei 76 Zeichen umgebrochenes Zertifikat, das unverändert in eine JSON-Datei kopiert wird, ist ein String voller Literaler Steuerzeichen, was entweder ein Parse-Fehler oder eine stille Korruption ist, je nach Stimmung des Parsers. Wenn der Wert für Menschen-Augen umgebrochen sein muss, muss er escaped sein oder eine Zeile sein - und für Maschinen-auf-Maschinen-Config ist eine Zeile die Antwort. Die andere Falle ist der unbezeichnete Wert: Eine Config-Spalte, die in einer Manpage von 2014 base64 sagt, ist meistens gepaddetes Standardalphabet, aber Tokens aus der API-Ära sind ungepaddetes URL-safe, und der Vier-Zeichen-Test aus dem Dekodierungs-Leitfaden - enthält es + oder /, - oder _, ein = am Ende? - ist die ganze Diagnose.

Data-URIs: Dateien, die sich in Seiten einfügen

Eine Data-URI ist eine URL, deren Payload direkt in der Adresse steckt: data:, ein optionaler Medientyp, ein optionaler ;base64-Marker, ein Komma und die Daten selbst - das ganze Schema von RFC 2397. Browser nutzen sie, um Bilder, Schriftarten und kleine Skripte ohne zusätzliche Anfrage direkt in HTML und CSS einzubetten, und wenn eine Seite mit deaktiviertem Netzwerk weiterarbeitet, ist eine Data-URI ein starker Verdächtiger. Auf der C++-Seite ist der Kodier-Job, den String zusammenzubauen, was String-Konkatenation mit einer Konstante ist:

#include <cstdio>
#include <string>

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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());
}

Die Stolperfallen liegen alle in den Details. Der ;base64-Marker ist exakt sieben Zeichen lang, und das ist die Länge, die Off-by-one-Bugs ins Visier nehmen: Ein Parser, der sechs prüft, ist ein Parser, der data:text/plain;base4,... akzeptiert und Müll mit geradem Gesicht dekodiert. Und das Payload einer base64-Data-URI ist eine Zeile - Zeilenumbrüche sind Teil der URI-Grammatik nicht, also, wenn Ihr Encoder das Bild bei 76 umgebrochen hat (und MIME-geformte Encoder tun das standardmäßig), ist die URI kaputt, bevor sie den Browser erreicht. Die Regel für diesen Konsumenten: kodieren, nicht umbrechen, und den Medientyp genau halten - ein falsches image/png bei einem JPEG ist die Art von Lüge, die sich nur als kaputtes Thumbnail um 2 Uhr nachts zeigt.

Tokens: JWTs, PKCE und API-Keys

Das Base64 mit dem höchsten Einsatz im Internet steckt in einem Token. Ein JSON Web Token sind drei base64url-Segmente, die mit Punkten verklebt sind: ein header-JSON, ein claims-JSON und eine Signatur, die über den String header.claims berechnet wird. C++ hat keinen eingebauten JWT-Typ, aber einen zu bauen ist der base64url-Encoder von oben plus ein HMAC-Aufruf, weil das ganze Token base64url ist, bis es es nicht mehr ist - bis es eine Signatur ist:

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

/* base64_encode und base64url_encode aus früheren Abschnitten */

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());
}

Führen Sie das Beispiel aus, und das Token, das herauskommt, ist ein Lehrbuch-HS256-Token: Der header dekodiert zu {"alg":"HS256","typ":"JWT"}, die claims zu einem subject, einem name und einem issued-at-Zeitstempel, und die Signatur ist das base64url eines HMAC-SHA256 über die zwei kodierten Segmente. Drei Details tragen das ganze Design. Die Signier-Eingabe sind die kodierten Segmente, nicht das rohe JSON - Signieren Sie das JSON, und Sie haben die falschen Bytes signiert. Die Segmente sind ungepaddetes base64url - die Pads würden mitten in einer URL sitzen, und der ganze Punkt des Alphabets war es, das Token zu einem sauberen String zu halten. Und HS256 bedeutet ein geteiltes Geheimnis, was ein Server-zu-Server-Algorithmus ist: Ein Geheimnis, das im Code eines Clients lebt, ist kein Geheimnis, und das Token, das es signiert, ist keine Zugangsberechtigung. (OAuths PKCE-Flow verwendet dasselbe Alphabet einen Schritt entfernt: ein zufälliger Verifier, mit SHA-256 gehasht, ohne Pads als base64url in eine code challenge verwandelt - der Encoder aus dem base64url-Abschnitt ist die gesamte Client-seitige Implementierung.)

HTTP Basic Auth

Das älteste Base64 in HTTP ist der Credentials-Header: Authorization: Basic gefolgt vom base64 von user:password, ein Schema so alt, dass es vor JSON existiert. Es zu bauen ist eine Konkatenation:

#include <cstdio>
#include <string>

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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());
}

Die Ausgabe ist der String, den Sie wahrscheinlich in einer erfassten Request gesehen haben: Basic dXNlcjpwYXNzd29yZA==. Zwei C++-Anmerkungen. Die Konkatenation user + ":" + pass ist der Ort, an dem ein Passwort mit einem Doppelpunkt einen naiven Parser auf der anderen Seite verunsichern würde - die Parsing-Regel ist "bei dem ersten Doppelpunkt trennen", und deshalb darf die Bau-Seite in jedes der beiden Felder irgendetwas hineinschreiben. Und wenn die Zugangsdaten kein ASCII sind, ist die sichere Lesart des Schemas, die user-id und das password vor dem base64 als UTF-8 zu behandeln, was in C++ bedeutet, dass Ihr std::string die Arbeit schon erledigt - solange Sie ihn mit UTF-8-Bytes gefüllt haben und nicht mit dem, was die Locale entschieden hat. Der Sicherheitshinweis gehört zu jeder Erwähnung dieses Schemas: Basic-Auth ist Verdeckung, kein 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 Maschinen-auf-Maschinen-Aufrufe, nicht für Menschen. (Boost.Beasts Codec - der aus dem detail::-Namespace - erledigt denselben Header-Job innerhalb von Boosts WebSocket-Implementierung und base64-iert den SHA-1-Digest, der zum Sec-WebSocket-Accept-Schlüssel wird, was der stille Beweis ist, dass das Muster das seit 2017 macht.)

E-Mail: Sieben-Bit-Regeln, Base64-Antwort

E-Mail ist der Ort, an dem base64 seine Gewohnheiten gelernt hat, und die Gewohnheiten sind immer noch tragend. SMTP wurde in seiner ursprünglichen Form gebaut, um Sieben-Bit-ASCII zu tragen, also musste alles Binäre vor der Reise als druckbarer Text neu geschrieben werden. Privacy-Enhanced Mail hat es 1987 mit 64-Zeichen-Zeilen und einem RSA-MD2/MD5-Nachrichtenintegritätscheck gemacht, der ans Ende geklebt war, und MIME, als es die Kodierung 1993 für E-Mail standardisierte, lockerte die Grenze auf 76 Zeichen und fügte die Regel hinzu, dass ein konformer Decoder Zeilenumbrüche einfach ignorieren muss. Ein E-Mail-Anhang ist heute immer noch base64, umgebrochen bei 76, und die exakte Arithmetik ergibt 4/3 mal 78/76 - etwa 137 Prozent der ursprünglichen Größe, plus rund 814 Bytes an Headern.

Die C++-Seite ist der Encoder plus die Wrap-Funktion von oben - auf eine Zeile kodieren, bei 76 mit CRLF umbrechen, fertig. Die zwei E-Mail-spezifischen Details: Die letzte Zeile kann einen abschließenden Zeilenumbruch tragen oder auch nicht (Decoder müssen ihn ignorieren, also ist beides legal und beide sind üblich), und der umgebrochene Wert ist kein JSON-Wert, keine Umgebungsvariable und kein Token - er ist ein Blob, der in einen MIME-Körper gehört, und ihn irgendwoanders hin zu bewegen ist der Ort, an dem der Umbruch aufhört, eine Gewohnheit zu sein, und anfängt, ein Bug zu sein. Die umgekehrte Richtung - ein Anhang, der umgebrochen bei 76 ankommt - ist das Terrain des Schwester-Leitfadens, wo sich die vier C++-Decoder über Zeilenumbrüche auf vier verschiedene Weise nicht einigen.

Dateien, Streams und die Zwei-Gigabyte-Obergrenze

Eine Datei zu kodieren ist das Spiegelbild der Datei-Arbeit des Dekodierungs-Leitfadens: im Binärmodus öffnen (auf Windows würde ein Read im Textmodus CRLF-Paare in einzelne Zeilenumbrüche übersetzen und Ihre Daten ändern, bevor der Encoder sie sah), die Bytes lesen, kodieren, binär schreiben. Die Klein-Datei-Version ist ein Ein-Funktions-Job:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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());
}

Die Obergrenze ist die C++-spezifische Tatsache im Abschnittstitel: Jeder Längenparameter in der EVP-API ist ein int. Ein einzelner EVP_EncodeBlock-Aufruf kann daher höchstens etwa 2 GB Eingabe kodieren, und der Ausgabe-Buffer für diesen Aufruf - 1,33 Mal größer - passt überhaupt nicht in ein int. Unterhalb der Obergrenze ist die Block-API für Dateien, die in den Speicher passen, in Ordnung. Darüber, oder für eine Datei, die Sie nicht im Speicher wollen, chunken Sie - und die Chunk-Regel ist die eine base64-spezifische Beschränkung auf die Schleife: Chunks müssen Vielfache von 3 Bytes sein, weil die Gruppierung zu dritt ist und eine Chunk-Grenze mitten in einer Gruppe die Ausgabe ändert. 3072, das sind drei 1024-Byte-Chunk, ist eine bequeme Chunk-Größe, und die Schleife wird:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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;
}

Jeder Chunk kodiert unabhängig, und die Konkatenation ist identisch mit dem One-Shot-Ergebnis - das ist die Eigenschaft, die Chunking überhaupt sicher macht, und sie fällt direkt aus der 3-Byte-Gruppierung heraus. (Der OpenSSL-Streaming-Kontext aus dem Encoder-Abschnitt erledigt denselben Job, während er 64-Zeichen-Zeilenumbruch gratis dazu gibt, was das richtige Werkzeug ist, wenn der Konsument MIME-Form will.) Und die Ausgabe-Seite hat dasselbe Budget wie die Eingabe-Seite: Eine 10-GB-Datei wird zu einem 13,3-GB-String, also wird der Buffer - oder die Datei, die Sie schreiben - mit der Formel aus dem Mathematik-Abschnitt dimensioniert, und die int-Obergrenze sagt, dass der chunk-basierte Pfad über 2 GB keine Bequemlichkeit ist - er ist der einzige Pfad.

Umgebungsvariablen und die Kommandozeile

Umgebungsvariablen haben dasselbe Problem wie JSON-Strings und eine schlechtere Antwort: Sie können überhaupt keine NUL-Bytes tragen, und Steuerzeichen sind auch nicht ihre Freunde. Der Standardtrick ist, das Payload zu base64-ieren, damit es die Shell überlebt, und in C++ ist die Kodier-Richtung ein Einzeiler:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, Null Abhängigkeiten" */

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

Der Wert, der in der Umgebung landet, ist aGVsbG8sIGVudg==: reines Alphabet, sicher für die Shell, sicher für eine .env-Datei, sicher für ein CI-Dashboard und dekodierbar auf jeder Maschine, die einen base64-Decoder hat. Die Kommandozeile selbst hat dieselbe Zwei-Tool-Geschichte wie die Dekodier-Seite, mit den Flags in Kodier-Richtung: base64 von coreutils (oder die uutils-Reimplementierung, die neuere Distributionen ausliefern; mit base64 --version prüfen) bricht standardmäßig bei 76 um, und -w 0 gibt Ihnen eine Zeile; openssl base64 - das enc-Programm, das seinen eigenen Namen in argv[0] prüft und in den base64-Modus schaltet - bricht bei 64 um und nimmt -A für eine einzelne Zeile:

# eine Zeile, für Tokens und Config
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# umgebrochen, für E-Mail und Textdateien
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

Keiner spricht base64url nativ, also bekommt ein Token, das Sie in einer Shell prägen, die Transcode-Behandlung, bevor es in eine URL geht. Und die Kommandozeile ist der Ort, an dem die stille-Fehlschlag-Gewohnheit der Kodier-Seite am gefährlichsten ist: Ein Encoder, der umbricht, während Ihr Konsument eine Zeile erwartet, wird keinen Fehler geben, er wird einfach einen String mit Zeilenumbrüchen darin produzieren - und genau das ist der Fehler, den Sie jetzt in der Produktion jagen. Für alles, was zählt, kodieren Sie in Ihrem Programm, wo der Buffer von der Formel dimensioniert wird und die Zeilenform eine Variable ist, die Sie kontrollieren.

Fallen: Die C++-Edition

  • Das NUL, das Sie nicht bestellt haben. EVP_EncodeBlock hängt nach dem Payload einen NUL-Terminator an. Das Beispiel der Manpage: 16 Bytes rein, 24 kodierte plus das NUL, 25 im Buffer, 24 zurückgegeben. Dimensionieren Sie für das zusätzliche Byte und resize auf den Rückgabewert, sonst endet Ihr Token mit einem Null-Byte.
  • Das harte 64. Die OpenSSL-Streaming-API bricht bei 64 Zeichen um, jeder Block endet in einem Zeilenumbruch, und es gibt kein Flag, das es ändert. Umgebrochene Encoder-Ausgabe in einem einzeiligen Konsumenten ist ein Steuerzeichen-Bug.
  • Der 48-Byte-Block. EVP_EncodeUpdate gibt nur für volle 48-Byte-Eingabe-Blöcke Ausgabe aus; der Rest sitzt im Kontext bis EVP_EncodeFinal. Planen Sie 65 Ausgabe-Bytes pro Block plus das NUL ein, und lesen Sie *outl nicht als "Bytes meines Payloads" - es ist die Anzahl Bytes, die dieser Aufruf geschrieben hat, was für einen kleinen ersten Aufruf null ist.
  • Die fehlenden Pads des Iterators. Die Boost.Serialization-Kette emittiert nie =. Ein 2002er-Iterator, der "Mane" kodiert, gibt Ihnen sechs Zeichen. Hängen Sie die Pads selbst an, sonst wird Ihr strenger Konsument den String ablehnen.
  • Das CRLF, um das Sie nicht gebeten haben. CryptBinaryToStringA hängt ein CR/LF-Paar an, solange Sie nicht CRYPT_STRING_NOCRLF übergeben. Ein mit den Standard-Flags gebautes base64-Token ist zwei Zeichen länger, als es sein sollte, und das vorletzte Zeichen ist ein Wagenrücklauf.
  • Der NULL-Aufruf zählt das NUL. Die Windows-Größen-Sonde gibt die benötigte Länge einschließlich des terminierenden null zurück; der echte Aufruf gibt die Länge ohne ihn zurück. Die beiden zu verwechseln ist der klassische Off-by-one, und er schreibt ein Byte über den Buffer hinaus oder verliert das letzte Zeichen.
  • Erst danach umbrechen, nicht während. Zeilenumbruch ist ein Post-Bearbeitungsschritt auf dem kodierten String. Schneiden Sie bei Vielfachen der Breite - immer sicher, weil jede 4-Zeichen-Gruppe in sich geschlossen ist - und brechen Sie niemals die rohen Bytes um, wo Zeilenumbrüche nicht hingehören.
  • Chunk zu dritt. Wenn Sie ein großes Payload in Stücken kodieren, müssen die Stück-Grenzen auf 3-Byte-Gruppen landen, sonst ändert sich die Gruppierung - und die Ausgabe. 3072 ist ein freundlicher Chunk; 3071 ist ein Bug.
  • int, nicht size_t. Jeder EVP-Längenparameter ist ein int. Die Ein-Aufruf-Obergrenze ist etwa 2 GB Eingabe, und die Ausgabe für diese Eingabe passt überhaupt nicht in ein int. Über der Obergrenze ist der chunk-basierte oder Streaming-Pfad keine Präferenz.
  • Signierter char. Wenn Sie aus einem char * packen, ohne den unsigned-Cast, ist ein Byte über 127 auf Plattformen, wo char signiert ist, eine negative Zahl, und eine Tabelle damit zu indexieren ist undefiniertes Verhalten. const unsigned char * ist keine Zeremonie.
  • Der umgebrochene JSON-String. Ein bei 76 Zeichen umgebrochener Wert, der in eine JSON-Datei kopiert wird, ist ein String aus Literalen Steuerzeichen. Entweder er ist eine Zeile, oder er ist escaped, oder er ist nicht in JSON.
  • Pads sind ein Vertrag. Manche Konsumenten wollen Padding (MIME, die meisten Decoder), manche nicht (JWT, PKCE, Tokens in URLs), und ein paar strenge lehnen fehlende oder nicht-kanonische Pads einfach ab. Das Pad ist keine Dekoration; es ist Teil der Format-Vereinbarung.
  • Die beiden Alphabete. Ein - oder _ in einem Payload mit Standardalphabet ist ungültig, und ein + oder / in einem URL-safe-Payload ist ungültig. Die Alphabete sind nicht auf Byte-Ebene austauschbar - kodieren Sie mit dem richtigen für das Ziel, und transcodieren Sie bewusst.
  • std::string und strlen. std::string trägt Null-Bytes fröhlich, aber in dem Moment, in dem Sie einer Legacy-API einen C-String händigen, stoppt strlen beim ersten NUL. Übergeben Sie Zeiger und Länge, nie einen nackten Zeiger.
  • Das Budget. Die Ausgabe ist 4/3 der Eingabe: Wenn die Eingabe 1,5 GB ist, ist die Ausgabe 2 GB - was auch die int-Obergrenze ist. Dimensionieren Sie den empfangenden Buffer, die Spalte und die Leitung mit der Formel, nicht mit einem Raten.

Wie C++ zu seinem Base64 kam

Die Geschichte des Formats ist älter als die moderne Ära der Sprache, und die C++-Geschichte ist die Geschichte einer Sprache, die es immer wieder nicht ausliefert. Die erste standardisierte Verwendung der heute MIME-base64 genannten Kodierung war das Privacy-Enhanced-Mail-Protokoll, vorgeschlagen 1987 mit 64-Zeichen-Zeilen und einem RSA-MD2/MD5-Nachrichtenintegritätscheck, der ans Ende geklebt war; der Name "base64" selbst kam erst 1993 an, als die MIME-Standards ihn benannten. C++ kam als C++98 im Jahr 1998 - fünf Jahre nach MIME - und der erste Base64-Code, nach dem die Entwickler der Sprache griffen, war das C-Paar von Rene Nyffenegger aus 2004-2008, das eine Stack-Overflow-Frage vom 4. Dezember 2008 quer durch das Web verbreitete. Der schönste Teil dieser Geschichte: Eine Antwort verlinkte Nyffeneggers eigene Seite und trug die Implementierung über, Lizenzkopf und alles, und eine Top-abgestimmte Antwort benchmarkete seine Lösung gegen den Rest des Feldes. Das Volkslied hat einen Lizenzkopf - der Komponist selbst tauchte nie in den Kommentaren auf.

Dann tat das Ökosystem, was Ökosysteme tun. 2002 lieferte Robert Rameys Boost.Serialization die Iterator-Adapter aus - das älteste Base64 in der C++-Werkzeugkiste, streng in der Dekodier-Richtung und berüchtigt ungepaddet in der Kodier-Richtung, ein Jahr, bevor RFC 3548 die Alphabetregeln kodifizierte, die es bereits durchsetzte. 2017 brachte Boost 1.66 Beast, und mit ihm den header-only-Codec, der heute noch ausgeliefert wird, mit der Nyffenegger-Zuschreibung im Footer. OpenSSLs EVP_EncodeBlock und Kollegen sind in jeder OpenSSL-Version dabei, also ist das Arbeitstier in der Werkzeugkiste so lange, wie die Sprache darüber streitet, ob es in den Standard gehöre. Auf Windows ist die Geschichte einfach, dass das Betriebssystem es auslieferte: eine Funktion, eine Flag-Tabelle, überhaupt kein Standard beteiligt. Derweil ging der Standard selbst C++11, C++14, C++17, C++20, C++23 (veröffentlicht 2024) und jetzt C++26 durch, und jedes einzelne von ihnen schaute auf das 64-Zeichen-Alphabet und ging weiter. Der technische Inhalt von C++26 wurde fertiggestellt und auf dem ISO-C++-Meeting im März 2026 in Croydon, UK, mit 114-12-3 angenommen, und es fügt tatsächlich einen neuen <text_encoding>-Header für Text-Codec-Arbeit hinzu; die folgenden Meetings des Ausschusses, im Juni 2026 (Brno) und November 2026 (Búzios, Brasilien), eröffnen den C++29-Arbeitsentwurf, statt C++26 noch einmal anzuschauen. Base64 ist nicht im Standard. Acht Standards, drei Jahrzehnte, ein Header für Textkodierung - und der Ausschuss hatte jetzt jeden denkbaren Vorwand, base64 hinzuzufügen, und hat alle ausgeschlagen. Die praktische Geschichte von Base64 in C++ ist und bleibt die Geschichte seiner Bibliotheken: ein EVP-Paar, zwei Boost-Varianten, ein Windows-Flag und ein vierzigzeiliges Snippet, das Sie besitzen.

Kuratiositäten, die man wissen sollte

  • Der 48-Byte-Block des OpenSSL-Streaming-Encoders ist eine Zahl, die in keinem RFC auftaucht. Es sind 16 Base64-Gruppen, gewählt, damit die Ausgabe-Zeile exakt 64 Zeichen hat - die PEM-Gewohnheit - und es ist einer der letzten Orte, wo 1987 2026 noch tragende Arbeit leistet.
  • Boost.Beasts encoded_size ist der Mathematik-Abschnitt als constexpr-Funktion: 4 * ((n + 2) / 3), zur Kompilierzeit ausgewertet, wenn Sie eine Konstante geben. Die Standardbibliothek hat diesen Einzeiler nie bekommen; Boost hat ihn stattdessen in einem detail::-Namespace ausgeliefert.
  • Das kleinste gepaddete Base64 sind vier Zeichen, QQ==: ein Byte im zwei-Zeichen-Kostüm. Das kleinste ungepaddete sind zwei Zeichen, QQ. Die Pad-Anzahl ist auch eine Botschaft: Zwei Pads bedeuten, die letzte Gruppe hatte ein Byte, ein Pad bedeutet, sie hatte zwei, und keine Pads bedeutet, sie hatte drei - der Empfänger kann die Eingabelänge allein aus dem Tail rekonstruieren.
  • MIMEs Overhead-Mathematik ist exakt: 4/3 mal 78/76, deshalb trifft ein E-Mail-Anhang etwa 137 Prozent seiner ursprünglichen Größe ein, plus rund 814 Bytes an Headern. Jeder Encoder in diesem Artikel zahlt dieselbe Steuer; die Umbruch-Breite ändert nur, wie sie in Rechnung gestellt wird.
  • Auf einem typischen libstdc++ oder MSVC trägt std::string kleine Payloads in einem Stack-Buffer über die Small-String-Optimierung, statt zu allozieren. Eine 9-Byte-Eingabe kodiert zu 12 Zeichen und berührt nie den Heap. Die Base64-Form Ihres Tokens kann wörtlich in einem Stack-Frame leben, und das ist die Art von Gratis-Lunch, die die Standardbibliothek nicht bewirbt.
  • Der openssl base64-Befehl, nach dem Sie in einer Shell greifen könnten, ist überhaupt kein Befehl. Es ist das enc-Programm, das seinen eigenen Namen in argv[0] prüft und die Persönlichkeit wechselt. Ein Alias durch String-Vergleich, was die C++-Art ist, Dinge zu machen, in C.
  • YouTube-Video-Identifikatoren sind base64url: elf Zeichen, kein Padding, kein + oder / in der Nähe einer URL. Das meistangesehene Kodierformat auf dem Planeten läuft auf der "URL- und Dateinamen-sicheren" Variante, die RFC 4648 in einem Abschnitt hinzugefügt hat, der auf eine Seite passt.
  • Vier As - AAAA - kodieren drei Null-Bytes, weil A die Null des Alphabets ist. Wenn Sie je einen Base64-Blob gesehen haben, der vollständig aus einem einzigen Zeichen besteht, wissen Sie jetzt, was er sagte: nichts.
  • Dasselbe Funktionspaar taucht in den Antworten auf eine Stack-Overflow-Frage aus 2008 auf, in der Quelle von Boost.Beast mit einem Zuschreibungs-Footer und in den Header-Dateien unzähliger privater Codebases. Fragen Sie einen C++-Entwickler, woher sein base64 stammt, und die ehrlichste Antwort ist "Das weiß ich nicht, und das Internet auch nicht".

Die andere Richtung

Alles, was Sie gerade verpackt haben, wird von derselben Werkzeugkiste auf der anderen Seite ausgepackt, und die Auspack-Seite hat ihren eigenen Satz an Gewohnheiten: die One-Shot-OpenSSL-Funktion, die ihren Tail mit Nullen füllt, der 2025er-Bugfix, der verändert hat, was der Streaming-Decoder für gepaddete Eingabe zurückgibt, der Boost.Beast-Decode, der bei einem versehentlichen Zeichen stoppt und kein Wort sagt, der Iterator, der bei einem einzelnen Leerzeichen wirft, und der vierzigzeilige strenge Decoder, der auf das exakte Byte zeigt, das wehgetan hat. Die komplette Auspack-Geschichte - die Temperamente der vier Decoder, das base64url-Transcoding, Dateien, die 76-Zeichen-Gewohnheit von MIME und die zwei Kommandozeilen-Tools, die still versagen - lebt im C++-Dekodierungs-Leitfaden auf der Schwester-Site. Gehen Sie ihn lesen, und kommen Sie dann zurück und verpacken Sie etwas Großes. Das ist das ganze Spiel: keine Standardbibliothek, vier Anbieter mit vier unterschiedlichen Meinungen über Zeilenumbrüche und NULs, eine Formel, die jeden Buffer im Artikel dimensioniert, und eine 33-Prozent-Steuer, die jeder Empfänger erstattet bekommt. Viel Spaß beim Verpacken.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Dekodierung in C++ (Cpp): Ein vollständiger Leitfaden