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 Mail-Header, eine URL, eine Umgebungsvariable. Die Startseite dieser Site erklärt das Format im Detail, also hier nur die Kurzversion: Drei Bytes werden zu vier Alphabetzeichen, ein kurzer Tail bekommt ein oder zwei =-Zeichen, und die kodierte Form ist etwa 33 Prozent größer als das Original. Kodieren ist die wachsende Richtung, also wird jeder Puffer in diesem Artikel danach bemessen, 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 nicht ein einziges Byte für Sie kodieren. 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 Encoder mit - ein Aufgebot aus vier sehr verschiedenen Persönlichkeiten, plus die Option, etwa vierzig eigene Zeilen zu schreiben. Einer ist ein Arbeitstier, das TLS seit den 1990ern trägt und das Padding, Null-Terminierung und Zeilenumbrüche von sich aus macht, ohne um Erlaubnis zu bitten. Einer ist ein schneller header-only Codec, der sich in einem Namespace versteckt, den seine Autoren "detail" nannten. Einer ist ein Iterator von 2002, der offenbar noch nie ein Padding-Zeichen getroffen hat. Einer ist eine Funktion, die das Betriebssystem seit Jahrzehnten mitliefert und die CRLF ans Ende Ihres Tokens hängt. Und die fünfte Option ist Ihre. Wenn Sie wissen, was jeder hinzufügt, ablehnt oder still und leise anhängt, ist das Kodieren keine Quelle für off-by-one-Bugs mehr. Los geht's mit dem Packen.

Der Standard hat noch nie einen Packer ausgeliefert

Jeder Standard seit C++98 - und es gab acht davon, bis hin zu C++26 - hat sich das 64-Zeichen-Alphabet angesehen und weitergezogen. Es gibt kein <base64>, kein std::base64, nichts in <string> oder <vector>, das Ihre Bytes packt. Die technische Arbeit an C++26 ist fertiggestellt und auf der ISO-C++-Tagung im März 2026 in Croydon, Großbritannien, mit 114-12-3 durchgewählt worden, und tatsächlich fügt sie einen <text_encoding>-Header für Text-Codec-Arbeit hinzu; die nächsten Tagungen des Ausschusses, im Juni 2026 (Brno) und November 2026 (Búzios, Brasilien), öffnen den Arbeits-Entwurf von C++29, statt C++26 noch einmal aufzugraben. Base64 ist nicht im Standard, und es ist schwer, den Ausschuss dafür zu kritisieren: Text-Kodierung dreht sich um Zeichensätze, und Base64 dreht sich um Bytes, also war der neue Header nie die richtige Heimat. In der Praxis hat das Ökosystem die Arbeit erledigt. OpenSSLs EVP-Base64-Routinen sind in jedem OpenSSL-Release, die Boost-Bibliotheken tragen zwei unabhängige Encoder, Windows liefert eine CryptoAPI-Funktion mit einer Flag-Tabelle für den Job mit, und ein vierzigzeiliges Snippet wird seit 2008 über die ganze Sprache kopiert und eingefügt. Wenn Ihr Projekt CMake-basiert ist, besteht die komplette Abhängigkeits-Einrichtung aus drei Zeilen:

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

Das Boost-Release, das sich zu kennen lohnt, ist 1.92.0 von August 2026, von einem Projekt, das 1998 gegründet wurde und Bibliotheken seit seinem ersten Release 1999 ausliefert. Beide Boost-Encoder unten sind header-only - es gibt gar nichts zum Linken - während OpenSSL -lcrypto will, das die meisten C++-Programme, die TLS berühren, schon im Binär haben.

Zuerst die Mathematik: So wird jeder Puffer in diesem Artikel bemessen

Base64 gruppiert Bytes zu dritt, also hat die Ausgabegröße eine Form, die Sie nicht mehr überrascht, wenn Sie sie kennen: Für je 3 Eingabe-Bytes kommen 4 Zeichen heraus, und ein kurzer Tail wird auf eine volle Gruppe gepaddet. Die exakte Anzahl für n Eingabe-Bytes ist:

4 * ((n + 2) / 3)

Das +2 ist der Aufrundungs-Trick: Ganzzahl-Division rundet ab, also sorgt das vorherige Addieren von 2 dafür, dass auf das nächste Vielfache von drei aufgerundet wird. Von dort aus ist jede Puffergröße in diesem Artikel eine Einsetzung. OpenSSLs One-Shot-Funktion will einen Puffer, der die kodierten Daten plus das NUL fasst, das sie am Ende anhängt - die Man-Seite veranschaulicht den Vertrag mit 16 Eingabe-Bytes, die zu 24 kodierten Bytes plus 1 NUL werden, 25 Bytes insgesamt, und die Funktion gibt die Länge ohne das NUL zurück. Ihr Streaming-Pfad verarbeitet Eingabe in 48-Byte-Blöcken, und die Man-Seite bemisst die Ausgabe mit 65 Bytes pro Block (64 Zeichen plus der Zeilenumbruch, den jeder Block immer erzeugt) plus ein Byte für das NUL. Der Boost.Beast-Header gibt Ihnen die exakte Formel als constexpr-Funktion. Und Ihr eigener Code reserviert (n + 2) / 3 * 4 und nennt es erledigt. Hier sind die Zahlen, die Sie tatsächlich treffen werden:

Eingabe Ausgabe (gepaddet) Was aufzufallen hat
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 umgebrochen sind es 11 Zeilen, 679 Zeichen mit Zeilenumbrüchen
1 GB etwa 1,33 GB Kalkulieren Sie Spalte, Datei und Draht für die Steuer ein

Wenn die empfangende Seite eine Spalte mit fester Größe, ein Puffer 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 Dekodier-Puffer, der mit der Kodier-Formel bemessen ist, ist eine klassische Über-Allokation, die zu einem Speicher-Bug-Ticket heranwächst. Die schrumpfende Richtung zu bemessen, ist das Problem des Schwesterguides; hier wächst man nur.

Hier ist das Bild, denn die Unterschiede liegen alle in den Extras - dem Padding, den Zeilenumbrüchen, den NULs - und nicht im Kern-Packen, das jede Zeile identisch implementiert:

Encoder Herkunft Padding Extra-Bytes, die eingeplant werden müssen Die Eigenheit, die Sie im Kopf behalten
EVP_EncodeBlock <openssl/evp.h>, Link -lcrypto Immer 1 (ein NUL im Puffer) Das 16-Byte-Beispiel der Man-Seite ist der Vertrag
EVP_EncodeUpdate + Final gleiches Immer 65 pro 48-Byte-Block Harte Umbrüche 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 - die 1 oder 2 Pads fügen Sie selbst hinzu Ältester Encoder in der Werkzeugkiste, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib Immer 2 (ein CRLF), außer mit NOCRLF Hat ein URL-sicheres Flag, das der Rest der Werkzeugkiste nicht hat
Ihre eigenen vierzig Zeilen Nirgendwo: Es gehört Ihnen Ihre Wahl Ihre Wahl Sie besitzen jeden Randfall für immer

Der Kern-Algorithmus ist in jeder Zeile identisch - das ist der tröstliche Teil eines Formats von 1987. Was sich unterscheidet, ist, was jede Implementierung um den Payload herum hinzufügt, und beinahe jede Falle in diesem Artikel ist einer dieser Zusätze, der auf einen Verbraucher trifft, der es nicht erwartet hat.

OpenSSL: Der Encoder, den Ihr TLS-Stack schon linked hat

Wenn Ihr Programm OpenSSL schon für TLS linkt, brauchen Sie nichts hinzuzufügen. Die One-Shot-Funktion ist ein einziger Aufruf:

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

Geben Sie ihm die Quell-Bytes und die Länge, und er schreibt die gepaddete, einzeilige Kodierung. Der Vertrag lohnt sich zum Auswendiglernen, denn die Man-Seite stellt ihn mit einem Beispiel auf: Für je 3 Eingabe-Bytes gibt es 4 Ausgabe-Bytes; ein Tail, der nicht durch 3 teilbar ist, wird gepaddet, sodass die Ausgabe immer durch 4 teilbar ist; und darüber hinaus wird ein NUL-Terminierungszeichen hinzugefügt. Das dokumentierte Beispiel: 16 Bytes rein, 24 kodiert plus 1 NUL, 25 Bytes insgesamt im Puffer, und die Funktion gibt 24 zurück - die Länge ohne das NUL. Bemessen Sie den Puffer 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 das std::string tut, was C Ihnen aufzwingen würde: Es wächst auf exakt die zurückgegebene Länge, also liegt das NUL, das OpenSSL angehängt hat, einfach jenseits der verwalteten Länge und wird nie Teil des Payloads. Kodieren Sie "Mane", und Sie bekommen TWFuZQ==, den klassischen vier Zeichen langen Tail mit seinem einzelnen Pad; kodieren Sie ein Byte, und Sie bekommen ein zwei-Zeichen-Datenpaar in einem zwei-Zeichen-Padding-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 der 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 abschließen, und die Block-Arithmetik der Man-Seite ist ungewöhnlich explizit. Nur volle Blöcke von 48 Bytes werden sofort verarbeitet; jeder Rest wird im Kontext gehalten und von einem späteren Aufruf oder dem finalen freigegeben. Jeder verarbeitete Block schreibt 64 Zeichen plus Zeilenumbruch - 65 Bytes - und der finale Aufruf behandelt den Teil-Block, deshalb ist seine dokumentierte Decke 65 Bytes plus das NUL. Die Konsequenz, die Sie kennen sollten, bevor Sie aufrufen: Diese API bricht bei 64 Zeichen um. Sie ist nicht konfigurierbar. Das ist der Streaming-Encoder.

#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 Abrechnung kommt exakt so heraus, wie die Man-Seite 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. Dieser nachgestellte Zeilenumbruch ist der, der Verbraucher kaputt macht: Fügen Sie das Ergebnis in einen JSON-String ein, und Sie haben ein Steuerzeichen dort, wo ein Anführungszeichen sein sollte; verwenden Sie es als Token-Segment, und Sie haben ein neues Segment erfunden. Die Daumen-Regel: die Block-API für einzeilige Payloads (Tokens, Header, Config-Werte), die Streaming-API, wenn der Verbraucher MIME-geformte, umgebrochene Ausgabe will, und bei Zweifel den nachgestellten Zeilenumbruch mit einer while (out.back() == '\n')-Schleife streichen, bevor der Payload eine Grenze überschreitet, die ihn nicht erwartet.

Boost.Beast: Ein schneller Packer im 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 unsere interne Angelegenheit", und die Maintainer haben es abgelehnt, den Codec zu einer öffentlichen API zu machen. Trotzdem nutzt ihn jeder: Er ist klein, er ist schnell, er ist header-only (definieren Sie BOOST_BEAST_HEADER_ONLY vor dem Include, und es gibt nichts zum Linken), und er ist derselbe Codec, den der eigene WebSocket-Handshake von Boost.Beast für die Sec-WebSocket-Accept-Berechnung verwendet, was heißt, dass er seit Jahren echten Verkehr kaut.

Auf der Kodierungs-Seite ist die API fast beleidigend ruhig. Ein constexpr-Helfer gibt Ihnen die exakte Ausgabegröße - 4 * ((n + 2) / 3), dieselbe Formel wie der Mathematik-Abschnitt, jetzt mit einem Compiler, der sie prüft - und die encode-Funktion schreibt das gepaddete Ergebnis in Ihren Puffer und sagt Ihnen, wie viele Zeichen sie verwendet hat. Es gibt keinen Fehlerkanal, denn Kodieren kann nicht scheitern: Jedes Byte ist gültige Eingabe, und die Ausgabegröße ist eine reine Funktion der Eingabegröße. 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 erzeugt hat, ohne NUL, um das Sie sich sorgen müssten, und ohne Zeilen, die Sie streichen müssten. Zwei Dinge zum Abheften. Erstens die Herkunft: Der Quellcode ist 2016-2019 von Vinnie Falco copyrighted, mit einer Fußzeile, die Teile einem Snippet von Rene Nyffenegger aus 2004-2008 zuschreibt - dasselbe Volkslied, das die C++-Base64-Geschichte begann, jetzt in Boost ausgeliefert, in Ihrem Binär, und macht WebSocket-Handshakes für das ganze Web. Zweitens das Praktische: Weil der Codec paddelt und nie umbrochen, 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 Puffer, 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 Reihe von kombinierbaren Iterator-Adaptern, geschrieben von Robert Ramey 2002 für Boosts Serialisierung-Bibliothek. Die Kodier-Richtung ist eine Zwei-Adapter-Kette: ein Breiten-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 Kern-Packen und nichts anderes - kein Padding, kein NUL, keine Zeilenumbrüche und kein Fehlerkanal, denn das Kern-Packen kann nicht scheitern. Kodieren Sie "Mane", und der Iterator reicht Ihnen sechs Zeichen, TWFuZQ, mit geradem Gesicht: Eine echte Kodierung von vier Bytes sind acht Zeichen, und einem Iterator von 2002 ist es nie eingefallen, sich darum zu kümmern. Deshalb ist die switch-Anweisung tragend, nicht dekorativ: Ein Byte weniger als eine Gruppe bekommt zwei Pads, zwei Bytes weniger bekommt eines. Dieselbe Kette minus switch ist das Ergebnis, wenn Sie diesen Schritt vergessen, und das Ergebnis ist ein String, der unter einem nachsichtigen Dekodierer sauber dekodiert, unter einem strengen scheitert und die Fehlermeldung Ihres API-Verbrauchers zu einem Rätsel macht. (Die Dekodier-Seite derselben Iterator-Familie ist die, die bei einem einzigen verirrten Leerzeichen eine Exception wirft - mehr im Schwesterguide.)

Vierzig Zeilen, keine Abhängigkeiten

Base64 ist klein genug, dass ein korrekter Encoder 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, die Formel gibt Ihnen die exakte Größe von vornherein, und ein handgebauter 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 Sie die Teile durch. Die reserve-Zeile ist der Mathematik-Abschnitt: (n + 2) / 3 * 4 Zeichen, exakt, also gibt es keine Re-Allokation 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 im Moment, in dem es einen Tabellen-Index berührte, hätten Sie undefiniertes Verhalten, das sich einen Laborkittel überstreift. Jede Iteration zieht bis zu drei Bytes in einen 24-Bit-Wert, schiebt die vier 6-Bit-Scheiben in die Tabelle und gibt am Tail = anstelle des Bytes aus, das nicht da war - die Guards i + 1 < n und i + 2 < n sind die komplette 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/), denn ein Byte über 127 ist einfach ein Byte, und die Tabelle kümmert sich nicht darum, was es bedeutet. Vierzig Zeilen, keine Abhängigkeiten, und jeder Randfall ist eine Zeile, die Sie geschrieben haben, was der ganze Punkt ist.

Windows CryptoAPI: Der vom Betriebssystem mitgelieferte Packer

Auf 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, umwickelt in Zertifikat-BEGIN/END-Header-Zeilen
CRYPT_STRING_BASE64 0x1 Glatte Base64, keine Header
CRYPT_STRING_BASE64URI 0xD Das URL-sichere Alphabet: + wird -, / wird _, gemäß RFC 4648 Abschnitt 5
CRYPT_STRING_NOCRLF 0x40000000 Kein am Ende angehängter Zeilenumbruch
CRYPT_STRING_NOCR 0x80000000 Ein nacktes LF statt des Standard-CRLF

Das Erste, was Sie wissen müssen, ist der Standard: außer Sie geben CRYPT_STRING_NOCRLF, hängt die Funktion ein Wagenrücksetzer/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 Aufruf-Konvention, die der klassische Windows-Zweischritt ist: Aufrufen mit einem NULL-Puffer, um zu fragen, wie viel Platz gebraucht 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;
}

Zwei weitere Notizen. Das URI-Flag ist das einzige native base64url in diesem ganzen Artikel - auf Windows können Sie das Token-Alphabet direkt kodieren, und der Transkodieren-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 geben, also wickelt ein Aufruf, der "keine" Flags meinte, still und leise den Payload in die Zertifikat-Header-Zeilen ein - die PEM-Ära-Gewohnheit des Rahmens, nützlich zum Erzeugen von .pem-Dateien und eine Überraschung für alles andere. Linken Sie gegen crypt32.lib, und die Funktion gehört Ihnen für den Rest des Programms.

Base64url: Das Alphabet für Tokens und URLs

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 gleich mit der base64-Kodierung angesehen 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 auch das =-Padding weg, denn in einem Token ist die Länge implizit bekannt, und die Pads wären nur Prozent-Escapes, die darauf warten, zu passieren.

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

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, keine 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 Ausgabe-Zeile ist Tcq_, wo das Standard-Alphabet / geschrieben hätte; die zweiten zwei Zeilen zeigen den Pad-Schalter in Aktion - TQ standardmäßig ungepaddet, TQ==, wenn der Verbraucher sie zurückwill. Das pad-Argument ist das, worüber man nachdenken muss, denn die Verbraucher sind sich uneins: JWT-Segmente wollen keine Pads, PKCE-Challenges wollen keine Pads, aber ein base64url-Wert, der in einem Feld landet, wo der Dekodierer streng mit der Länge ist, will sie vielleicht zurück, und der Schalter ist ein bool, kein Umbau. Und der Fehlschlag-Modus, den man sich in die andere Richtung merken sollte: Ein - in einem Standard-Alphabet-Payload ist einfach 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 das ist der Fehlschlag, den Sie an einer Sicherheits-Grenze wollen.

Zeilenumbruch: 64, 76 oder nie

Umegebrochenes 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 1993 die Kodierung für E-Mail standardisierte, ging zu 76 Zeichen über, und diese Zahl ist der Standardwert des coreutils-base64-Befehls (sein -w-Flag setzt die Breite, und -w 0 schaltet das Umbrechen komplett aus) und der meisten Tools des Ökosystems. RFC 4648 selbst nimmt keine Seite ein: Es zitiert 76 als MIMEs Grenze und sagt Implementierungen, gar nicht umzubrechen, es sei denn, die referenzierende Spezifikation weist sie dazu an. Welche Sie emittieren, hängt davon ab, wer es verbraucht, und der Verbraucher - nicht das Format - ist die Design-Beschränkung.

Umbrechen ist ein Nachbearbeitungsschritt auf den kodierten String, nie ein Eingangsschritt: Die 4-Zeichen-Gruppen sind die Einheit der Bedeutung, also ist ein Schnitt des Strings an 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, keine 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 Abrechnung: 200 Bytes kodieren zu 268 Zeichen, und bei 76 umgebrochen mit CRLF-Terminatoren sind das 4 Zeilen - drei volle Zeilen und ein 40-Zeichen-Tail - 276 Zeichen auf dem Draht. Die CRLF-Wahl im Snippet ist die Wahl der E-Mail; für alles andere ist LF der moderne Standard, und die eine nicht verhandelbare Regel ist Konsistenz - ein Dekodierer, der CRLF erwartet, liest ein einzelnes LF als Datenzeichen, wenn er streng ist. (MIMEs Regel ist, dass Dekodierer Zeilenumbrüche ignorieren müssen, deshalb hat E-Mail nie unter dem Unterschied gelitten.) Die dritte Gewohnheit, die Sie kennen sollten: Der openssl base64-Befehl - das ist das enc-Programm in einem Trenchcoat, das seinen eigenen Namen in argv[0] prüft - bricht ohne -A bei 64 um und emittiert mit -A eine Zeile, und es ist das eine Tool auf der Kommandozeile, dessen Verhalten Sie bei jedem Lauf prüfen, statt es aus dem Gedächtnis heraus zu vertrauen.

Binär in JSON und Config

Ein JSON-String hat eine kleine Liste von Zeichen, die er roh nicht enthalten kann: das Anführungszeichen, den Backslash und die Steuerzeichen unter 0x20. Ein Zertifikat, ein zufälliger Key, eine Signatur - alle voller Bytes, die zu einer Kaskade von Escapes werden würden, wenn sie es wagten, in einem rohen String-Feld zu reisen, und die Steuerzeichen würden manche Parser direkt ersticken lassen. Base64 ist der Fix, und es ist die Standard-Antwort, die jedes Config-Format gibt, das Binäres tragen muss: Der Wert wird als eine Zeile reiner Alphabetzeichen gespeichert, und die Quote-Regeln der JSON-Bibliothek haben nichts mehr zu tun.

Das C++-Muster ist die komplette Implementierung: Die Bytes lesen (im Binärmodus, selbstverständlich), kodieren, den String speichern. Der Verbraucher dekodiert auf der anderen Seite. Die eine JSON-spezifische Falle ist der umgebrochene String: Ein bei 76 Zeichen umgebrochenes Zertifikat, das direkt in eine JSON-Datei kopiert wird, ist ein String voller wörtlicher Steuerzeichen, was entweder ein Parse-Fehler oder eine stille Korruption ist, je nach Stimmung des Parsers. Wenn der Wert für menschliche Augen umgebrochen sein muss, muss er escaped werden oder eine Zeile sein - und für Maschine-zu-Maschine-Config ist eine Zeile die Antwort. Die andere Falle ist der unbezeichnete Wert: Eine Config-Spalte, die in einer Man-Seite von 2014 base64 sagt, ist meistens gepaddetes Standard-Alphabet, aber Tokens aus der API-Ära sind ungepaddetes URL-sicher, und der Vier-Zeichen-Test aus dem Dekodier-Guide - enthält es + oder /, - oder _, ein = am Ende? - ist die gesamte Diagnose.

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

Eine data URI ist eine URL, deren Payload direkt in der Adresse steht: data:, ein optionaler Media-Type, ein optionaler ;base64-Marker, ein Komma und die 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 eine Seite mit deaktiviertem Netzwerk weiterarbeitet, ist eine data URI eine starke Verdächtige. 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, keine 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 Fallen liegen alle in den Details. Der ;base64-Marker ist exakt sieben Zeichen lang, was die Länge ist, auf die off-by-one-Bugs es abgesehen haben: Ein Parser, der sechs prüft, ist ein Parser, der data:text/plain;base4,... akzeptiert und Müll mit geradem Gesicht dekodiert. Und der Payload einer base64-data-URI ist eine Zeile - Zeilenumbrüche sind nicht Teil der URI-Grammatik, also wenn Ihr Encoder das Bild bei 76 umbrochen hat (und MIME-geformte Encoder tun das standardmäßig), ist die URI kaputt, bevor sie den Browser erreicht. Die Regel für diesen Verbraucher: Kodieren, nicht umbrechen, und den Media-Type korrekt halten - ein falsches image/png auf 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 ist 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 das Bauen eines ist der base64url-Encoder von oben plus ein HMAC-Aufruf, denn das ganze Token ist base64url, bis es es nicht 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 den vorherigen 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 Namen und einem issued-at-Zeitstempel, und die Signatur ist das base64url eines HMAC-SHA256 über die beiden kodierten Segmente. Drei Details tragen das ganze Design. Die Signier-Eingabe sind die kodierten Segmente, nicht das rohe JSON - Sie signieren 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, das Token als einen sauberen String zu halten. Und HS256 bedeutet ein geteiltes Secret, was ein Server-zu-Server-Algorithmus ist: Ein Secret, das im Code eines Clients lebt, ist kein Secret, und das Token, das es signiert, ist keine Berechtigung. (OAuths PKCE-Flow verwendet dasselbe Alphabet einen Schritt weiter entfernt: ein zufälliger Verifier, mit SHA-256 gehasht, ungepaddet base64url-kodiert zu einer Code-Challenge - der Encoder aus dem base64url-Abschnitt ist die komplette 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 JSON vorausgeht. Das Bauen davon ist eine Konkatenation:

#include <cstdio>
#include <string>

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, keine 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 Anfrage gesehen haben: Basic dXNlcjpwYXNzd29yZA==. Zwei C++-Notizen. Die Konkatenation user + ":" + pass ist der Ort, an dem ein Passwort, das einen Doppelpunkt enthält, einen naiven Parser auf der anderen Seite verwirren würde - die Parse-Regel lautet "beim ersten Doppelpunkt teilen", deshalb ist die Bau-Seite frei, in jedes der beiden Felder irgendetwas zu schreiben. Und wenn die Credentials kein ASCII sind, ist die sichere Lesart des Schemas, die user-id und das Passwort vor dem Base64 als UTF-8 zu behandeln, was in C++ heißt, dass Ihr std::string die Arbeit schon erledigt - solange Sie es mit UTF-8-Bytes gefüllt haben und nicht mit dem, was die Locale beschlossen hat. Der Sicherheits-Hinweis gehört zu jeder Erwähnung dieses Schemas: 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. (Boost.Beasts Codec - der aus dem detail::-Namespace - macht denselben Header-Job in Boosts WebSocket-Implementierung, base64-kodiert den SHA-1-Digest, der zum Sec-WebSocket-Accept-Key wird, was der stille Beweis ist, dass das Muster das seit 2017 tut.)

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

E-Mail ist der Ort, an dem Base64 seine Gewohnheiten gelernt hat, und die Gewohnheiten sind noch immer tragend. 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 einer RSA-MD2/MD5-Nachrichten-Integritätsprüfung, die am Ende angeklebt war, 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. 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 rund 814 Bytes Header.

Die C++-Seite ist der Encoder plus die Wrap-Funktion von oben - eine Zeile kodieren, bei 76 mit CRLF umbrechen, fertig. Die zwei E-Mail-spezifischen Details: Die letzte Zeile kann einen nachgestellten Zeilenumbruch tragen oder auch nicht (Dekodierer 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-Body gehört, und ihn irgendwo anders hinzubewegen ist der Punkt, an dem das Umbrechen 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 Schwesterguides, wo die vier C++-Dekodierer sich bei Zeilenumbrüchen auf vier verschiedene Weise uneins sind.

Dateien, Streams und die Zwei-Gigabyte-Decke

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

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, keine 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 Decke ist die C++-spezifische Tatsache in der Abschnitts-Überschrift: Jeder Längen-Parameter in der EVP-API ist ein int. Ein einzelner EVP_EncodeBlock-Aufruf kann daher höchstens etwa 2 GB Eingabe kodieren, und der Ausgabe-Puffer für diesen Aufruf - 1,33-mal größer - passt überhaupt nicht in ein int. Unter der Decke ist die Block-API für Dateien, die in den Speicher passen, gut. 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 der Schleife: Chunks müssen Vielfache von 3 Bytes sein, denn die Gruppierung geht zu dritt und eine Chunk-Grenze mitten in einer Gruppe ändert die Ausgabe. 3072 - drei 1024-Byte-Chunks - ist eine angenehme Chunk-Größe, und die Schleife wird zu:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, keine 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 und fügt dabei 64-Zeichen-Zeilenumbruch für umsonst hinzu, was das richtige Tool ist, wenn der Verbraucher 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 Puffer - oder die Datei, die Sie schreiben - mit der Formel aus dem Mathematik-Abschnitt bemessen, und die int-Decke 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 Standard-Trick ist, den Payload als Base64 zu kodieren, damit er die Shell übersteht, und in C++ ist die Kodier-Richtung ein Einzeiler:

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

/* base64_encode aus dem Abschnitt "Vierzig Zeilen, keine 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 auf jeder Maschine dekodierbar, die einen Base64-Dekodierer hat. Die Kommandozeile selbst hat dieselbe Zwei-Tool-Geschichte wie die Dekodier-Seite, mit den Kodier-Richtungs-Flags: base64 aus coreutils (oder den uutils-Nachbau, den neuere Distributionen mitliefern; prüfen mit base64 --version) 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 wechselt - bricht bei 64 um und nimmt -A für eine einzige 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 Transkodieren-Behandlung, bevor es in eine URL geht. Und die Kommandozeile ist der Ort, an dem die stille-Fehlschlag-Gewohnheit der Kodierungs-Seite am gefährlichsten ist: Ein Encoder, der umgebrochen hat, wenn Ihr Verbraucher eine Zeile erwartet hat, wird keinen Fehler geben, er wird einfach einen String mit Zeilenumbrüchen darin produzieren - was genau der Fehlschlag ist, den Sie jetzt in Produktion jagen. Für alles, was zählt, kodieren Sie in Ihrem Programm, wo der Puffer mit der Formel bemessen ist und die Zeilenform eine Variable ist, die Sie kontrollieren.

Fallen: Die C++-Ausgabe

  • Das NUL, das Sie nicht bestellt haben. EVP_EncodeBlock hängt ein NUL-Terminierungszeichen nach dem Payload an. Das Beispiel der Man-Seite: 16 Bytes rein, 24 kodiert plus das NUL, 25 im Puffer, 24 zurückgegeben. Bessern Sie das Extra-Byte ein und resize auf den Rückgabewert, oder Ihr Token endet 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. Umegebrochene Encoder-Ausgabe in einem Ein-Zeilen-Verbraucher 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. Stellen Sie 65 Ausgabe-Bytes pro Block plus das NUL ein, und lesen Sie *outl nicht als "Bytes meines Payloads" - es sind die Bytes, die dieser Aufruf geschrieben hat, was für einen kleinen ersten Aufruf null ist.
  • Die fehlenden Pads des Iterators. Die Boost.Serialization-Kette gibt nie = aus. Ein Iterator von 2002, der "Mane" kodiert, gibt Ihnen sechs Zeichen. Hängen Sie die Pads selbst an, oder Ihr strenger Verbraucher lehnt den String ab.
  • Das CRLF, das Sie nicht verlangt haben. CryptBinaryToStringA hängt ein CR/LF-Paar an, außer Sie geben CRYPT_STRING_NOCRLF. Ein Base64-Token, das mit den Standard-Flags gebaut wurde, ist zwei Zeichen länger als es sein sollte, und das vorletzte Zeichen ist ein Wagenrücksetzer.
  • Der NULL-Aufruf zählt das NUL. Die Windows-Größen-Sonde gibt die benötigte Länge einschließlich des terminierenden Null-Bytes zurück; der echte Aufruf gibt die Länge ohne es zurück. Beides zu verwechseln ist der klassische off-by-one, und er schreibt ein Byte über den Puffer hinaus oder verliert das letzte Zeichen.
  • Nachher umbrechen, nicht während. Zeilenumbruch ist ein Nachbearbeitungsschritt auf den kodierten String. Schneiden Sie an Vielfachen der Breite - immer sicher, denn jede 4-Zeichen-Gruppe ist in sich geschlossen - und brechen Sie niemals die rohen Bytes um, denn dort gehören Zeilenumbrüche nicht hin.
  • Chunks von drei. Wenn Sie einen großen 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ängen-Parameter ist ein int. Die Ein-Aufruf-Decke ist etwa 2 GB Eingabe, und die Ausgabe für diese Eingabe passt überhaupt nicht in ein int. Über der Decke ist der chunk-basierte oder Streaming-Pfad keine Frage des Geschmacks.
  • Signierter char. Wenn Sie aus einem char * packen, ohne den unsigned-Cast, ist ein Byte über 127 eine negative Zahl auf Plattformen, wo char signiert ist, 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, in eine JSON-Datei kopiert, ist ein String aus wörtlichen Steuerzeichen. Entweder ist er eine Zeile, oder er ist escaped, oder er ist nicht in JSON.
  • Pads sind ein Vertrag. Manche Verbraucher wollen Padding (MIME, die meisten Dekodierer), manche nicht (JWT, PKCE, Tokens in URLs), und einige 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 Standard-Alphabet-Payload ist ungültig, und ein + oder / in einem URL-sicheren ist ungültig. Die Alphabete sind nicht auf Byte-Ebene austauschbar - kodieren Sie mit dem richtigen für das Ziel, und transkodieren Sie absichtlich.
  • std::string und strlen. std::string trägt Null-Bytes gerne, aber im Moment, in dem Sie eine C-String an eine Legacy-API geben, hält strlen beim ersten NUL an. Geben Sie Zeiger und Länge weiter, 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-Decke ist. Bessern Sie den empfangenden Puffer, die Spalte und den Draht mit der Formel ein, nicht mit einer Schätzung.

Wie C++ sein Base64 bekam

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 mitliefert. 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 aus 2004-2008, das eine Stack-Overflow-Frage vom 4. Dezember 2008 über das Web verbreitete. Der schönste Teil dieser Geschichte: Eine Antwort verlinkte Nyffeneggers eigene Seite und übernahm die Implementierung, Lizenz-Header und alles, und eine top-voted Antwort benchmarkte seine Lösung gegen den Rest des Feldes. Das Volkslied hat einen Lizenz-Header - 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 - das älteste Base64 in der C++-Werkzeugkiste, streng in der Dekodier-Richtung und berühmt-berüchtigt ungepaddet in der Kodier-Richtung, ein Jahr, bevor RFC 3548 die Alphabet-Regeln 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. OpenSSLs EVP_EncodeBlock und Freunde sind in jedem OpenSSL-Release, also ist das Arbeitstier in der Werkzeugkiste, solange die Sprache darüber streitet, ob es im Standard sein sollte. Auf Windows ist die Geschichte einfach, dass das Betriebssystem es mitgeliefert hat: eine Funktion, eine Flag-Tabelle, überhaupt kein Standard beteiligt. Inzwischen ging der Standard selbst C++11, C++14, C++17, C++20, C++23 (veröffentlicht 2024) und jetzt C++26, und jedes einzelne von ihnen schaute sich das 64-Zeichen-Alphabet an und ging weiter. C++26s technischer Inhalt ist fertiggestellt und wurde auf der ISO-C++-Tagung im März 2026 in Croydon, Großbritannien, mit 114-12-3 durchgewählt, und tatsächlich fügt er einen neuen <text_encoding>-Header für Text-Codec-Arbeit hinzu; die folgenden Tagungen des Ausschusses, im Juni 2026 (Brno) und November 2026 (Búzios, Brasilien), öffnen den Arbeits-Entwurf von C++29, statt C++26 noch einmal aufzugraben. Base64 ist nicht im Standard. Acht 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: ein EVP-Paar, zwei Boost-Varianten, ein Windows-Flag und ein vierzigzeiliges Snippet, das Sie besitzen.

Kleinigkeiten, die sich zu wissen lohnen

  • Der 48-Byte-Block des OpenSSL-Streaming-Encoders ist eine Zahl, die in keinem RFC erscheint. 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, an denen 1987 2026 noch tragende Arbeit leistet.
  • Boost.Beasts encoded_size ist der Mathematik-Abschnitt als constexpr-Funktion: 4 * ((n + 2) / 3), zur Compile-Zeit ausgewertet, wenn Sie ihm eine Konstante geben. Die Standardbibliothek kam nie in den Genuss dieses Einzeilers; Boost lieferte ihn stattdessen in einem detail::-Namespace aus.
  • Das kleinste gepaddete Base64 sind vier Zeichen, QQ==: ein Byte in einem zwei-Zeichen-Kostüm. Das kleinste ungepaddete sind zwei Zeichen, QQ. Die Pad-Zahl ist auch eine Botschaft: Zwei Pads bedeutet, die letzte Gruppe hatte ein Byte, ein Pad bedeutet, sie hatte zwei, und keine Pads bedeutet, sie hatte drei - der Empfänger kann die Eingabegröße aus dem Tail allein zurückgewinnen.
  • MIMEs Overhead-Mathematik ist exakt: 4/3 mal 78/76, deshalb kommt ein E-Mail-Anhang bei etwa 137 Prozent seiner Originalgröße an, plus rund 814 Bytes Header. Jeder Encoder in diesem Artikel zahlt dieselbe Steuer; die Wrap-Breite ändert nur, wie sie abgerechnet wird.
  • Auf einem typischen libstdc++ oder MSVC trägt std::string kleine Payloads in einem Stapel-Puffer durch 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, 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 das enc-Programm, das seinen eigenen Namen in argv[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.
  • Vier A's - AAAA - kodieren drei Null-Bytes, denn A ist die Null des Alphabets. Wenn Sie je einen Base64-Blob gesehen haben, der ganz aus einem einzigen Zeichen besteht, wissen Sie jetzt, was er sagte: nichts.
  • 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".

Die andere Richtung

Alles, was Sie gerade verpackt haben, wird von derselben Werkzeugkiste auf der anderen Seite entpackt, und die Entpackungs-Seite hat ihre eigene Schar an Gewohnheiten: die One-Shot-OpenSSL-Funktion, die ihren Tail mit Nullen auffüllt, der 2025-Bugfix, der änderte, was der Streaming-Dekodierer für gepaddete Eingabe zurückgibt, der Boost.Beast-Decode, der bei einem verirrten Zeichen stehenbleibt und nie ein Wort sagt, der Iterator, der bei einem einzelnen Leerzeichen wirft, und der vierzigzeilige strenge Dekodierer, der auf das exakte Byte zeigt, das geschadet hat. Die komplette Entpackungs-Geschichte - die Temperamente der vier Dekodierer, base64url-Transkodieren, Dateien, MIMEs 76-Zeichen-Gewohnheit und die zwei Kommandozeilen-Tools, die still scheitern - lebt im C++-Dekodier-Guide auf der Schwestersite. Gehen Sie ihn lesen, dann kommen Sie zurück und verpacken Sie etwas Großes. Das ist das ganze Spiel: keine Standardbibliothek, vier Anbieter mit vier verschiedenen Meinungen über Zeilenumbrüche und NULs, eine Formel, die jeden Puffer im Artikel bemisst, und eine 33-Prozent-Steuer, die jeder Empfänger erstattet bekommt. Viel Freude beim Verpacken.

Zuletzt aktualisiert: 2026-09-08

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