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-Dekodierung in C: Ein vollständiger Leitfaden

Es steckt in einer API-Antwort, in einer Konfigurationsdatei, in einem E-Mail-Anhang oder mitten in einer URL: ein langer String aus Buchstaben und Ziffern, hin und wieder ein + oder /, und vielleicht ein = oder zwei am Ende. Sie erkennen es im Handumdrehen, und jetzt brauchen Sie die originalen Bytes zurück - in C. Das ist der ganze Job des Base64-Dekodierens: Vier Alphabetzeichen gehen rein, drei Rohbytes kommen raus, immer wieder, bis die =-Zeichen Ihnen sagen, wo die echten Daten zu Ende waren. Die Startseite dieser Site erklärt das Format Schritt für Schritt, also widmet sich dieser Artikel dem Ort, an dem die eigentliche Arbeit liegt: bei Puffern, Bibliotheken und den Fallen, die dazwischen lauern.

Zwei Dinge, die Sie vor dem ersten malloc kennen sollten. Erstens: Dekodieren ist die schrumpfende Richtung. Die Ausgabe ist drei Viertel so groß wie die Eingabe, also braucht ein Dekodierer nie mehr Speicher als der Payload, den er bereits hält. Zweitens - und das ist die Schlagzeile - C liefert keinen Base64-Dekodierer mit. Die Standardbibliothek der Sprache ist eingefroren, lange bevor es Base64 gab, und kein Standard seitdem hat die Lücke gefüllt. Also stützt sich jedes C-Programm, das Base64 dekodiert, auf eine Bibliothek, und die vier, die in der Praxis zählen, sind OpenSSL, Mbed TLS, APR-Util und GLib. Jede hat eine andere Persönlichkeit: was sie verzeiht, wie sie Fehler meldet und was sie still und leise mit Ihrer Ausgabe anstellt. Sobald Sie die Persönlichkeit Ihres Dekodierers kennen, wird das Dekodieren von Base64 in C keine Quelle mehr für mysteriöse Bugs, sondern eine Routine, die Sie im Schlaf schreiben.

Die Werkzeugkiste: Vier Wege, Ihre Bytes zurückzubekommen

Hier ist das Bild auf einen Blick. Alle vier decken das Standard-Alphabet ab; die Unterschiede liegen an den Rändern, und an den Rändern kommen die Bugs her.

Bibliothek Header Fehlermodell Ausgabe-Eigenheit, die Sie im Kopf behalten
OpenSSL (libcrypto) <openssl/evp.h> Gibt bei schlechter Eingabe -1 zurück Der One-Shot-Dekodierer füllt das Ende mit Nullen auf
Mbed TLS <mbedtls/base64.h> Rückgabecodes (-0x002C, -0x002A) Die strengsten Eingabe-Regeln von allen vier
APR-Util <apr-1.0/apr_base64.h> Keine: hält beim ersten verdächtigen Zeichen an API mit int-Längen, also ist 2 GB die Decke
GLib <glib.h> Gibt nur bei hartem Fehlschlag NULL zurück Ignoriert lockeren Müll still und leise

Die Installation ist ein Paketname pro Distro. Für OpenSSL: libssl-dev auf Debian und Ubuntu, openssl-devel auf Fedora und RHEL, openssl auf Arch und brew install openssl auf macOS. Für Mbed TLS: libmbedtls-dev (oder mbedtls). Für APR-Util: libaprutil1-dev plus libapr1-dev. Für GLib: glib2.0-dev. Danach linken Sie mit -lcrypto, -lmbedcrypto, -laprutil-1 bzw. -lglib-2.0. Welche wählen Sie? Wenn Sie OpenSSL ohnehin schon für TLS oder Hashing linked haben (die meisten Server tun das), nehmen Sie OpenSSL. Für Embedded- und ressourcenarme Builds ist Mbed TLS der kleine, strenge Mitbewohner. Wenn Sie im Apache-Ökosystem unterwegs sind, ist APR-Util schon da. Wenn Ihr Codebase auf GNOME oder GTK aufbaut, hält GLib alles in einer einzigen Runtime zusammen.

OpenSSL: Der Dekodierer, der die Lücken mit Nullen füllt

OpenSSL bringt Base64 in zwei Varianten mit. Die One-Shot-Funktion ist der Star in den meisten Programmen:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char out[16];
  const char *payload = "TWFuZQ==";
  int n = EVP_DecodeBlock(out, (const unsigned char *)payload,
                          (int)strlen(payload));
  if (n < 0) {
    printf("not base64\n");
    return 1;
  }
  printf("%d bytes\n", n);
  return 0;
}

Geben Sie ihm einen Puffer mit Base64-Zeichen und eine Länge, und er schreibt die dekodierten Bytes nach out und gibt zurück, wie viele es waren. Er schneidet vorangestellten Weißraum weg, schneidet nachgestellten Weißraum und Zeilenumbrüche weg, und er lehnt Eingabe ab, die nach dem Abschneiden kein Vielfaches von vier Zeichen ist oder ein Zeichen außerhalb des Alphabets enthält. Bisher ein absolut vernünftiger Vertrag. Nur ein Detail, das still und leise mehr als einen Datenbank-Import ruiniert hat: der Rückgabewert ist nicht die echte Datenlänge.

Führen Sie das Programm aus, und Sie bekommen 4 bytes... na, warte. TWFuZQ== sind zwei Vierer-Gruppen, also gibt die Funktion 6 zurück, und der Puffer hält 4d 61 6e 65 00 00: das Wort "Mane" plus zwei Null-Bytes. Der One-Shot-Dekodierer von OpenSSL arbeitet in festen Quanta - jede vier Eingabezeichen erzeugen immer exakt drei Ausgabe-Bytes - und wenn die letzte Gruppe nur ein echtes Byte trug, werden die anderen beiden Slots mit Nullen gefüllt. Das Manual erwähnt das in einem einzigen ruhigen Satz ("die Ausgabe wird bei Bedarf mit 0-Bits aufgefüllt"), und dieser eine Satz ist der wichtigste Satz der ganzen Man-Seite dieser Funktion.

Die echte Länge wird aus dem Padding zurückgerechnet, und das ist eine Rechnung in zwei Zeilen:

size_t real_length(const char *b64) {
  size_t len = strlen(b64);
  while (len > 0 && b64[len - 1] == '=') len--;
  return len * 3 / 4;
}

Zählen Sie die Alphabetzeichen, streichen Sie die nachgestellten Pads, multiplizieren Sie mit drei, dividieren Sie durch vier. Für TQ== (der Buchstabe M, kodiert) ergibt das (2 * 3) / 4 = 1 echtes Byte - während EVP_DecodeBlock drei meldet. Halten Sie das Paar (Zeiger, Länge) immer zusammen, und verwenden Sie nie strlen auf dekodierten Daten, denn die Bytes, die Sie zurückbekommen, dürfen ein JPEG sein, und das erste davon kann ein NUL sein.

Der Streaming-Dekodierer: Ein Dekodierer, der weiß, wann er aufhört

Für alles andere bietet OpenSSL das Streaming-Paar EVP_DecodeUpdate plus EVP_DecodeFinal. Das Kontext-Objekt ist das, was den Zustand zwischen den Aufrufen weiterträgt: Es hält eines bis drei Zeichen einer unvollständigen Gruppe, damit Sie den Payload in Chunks füttern können. Das Verhalten, das zählt, ist dieses: Weißraum (Leerzeichen, Tabs, Wagenrücksetzer, Zeilenumbrüche) wird an jeder Stelle des Streams übersprungen, jedes andere Nicht-Alphabetzeichen oder ein = mitten in den Daten geben sofort -1 zurück, und eine Rückgabe von 0 durch ein Update bedeutet "das Padding wurde gesehen, es wird nichts mehr erwartet". EVP_DecodeFinal lehnt dann mit -1 ab, wenn noch eine Teilgruppe wartet, denn eine Länge, die (nach dem Weißraum) kein Vielfaches von vier ist, ist kein gültiger Payload.

Ein Versions-Hinweis vor dem Code, denn alte Tutorials werden Sie stolpern lassen: In OpenSSL 3.x ist der Kontext-Typ EVP_ENCODE_CTX opak, also kompiliert das Stapel-Muster EVP_ENCODE_CTX ctx;, das in viel Internet-Code zu finden ist, nicht mehr. Allozieren und freigeben Sie explizit:

static int decode_b64(const unsigned char *in, int in_len,
                      unsigned char *out, int *out_len) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return -1;
  }
  *out_len = 0;
  EVP_DecodeInit(ctx);
  int r = EVP_DecodeUpdate(ctx, out, out_len, in, in_len);
  if (r < 0) {
    EVP_ENCODE_CTX_free(ctx);
    return -1;
  }
  int tail = 0;
  r = EVP_DecodeFinal(ctx, out + *out_len, &tail);
  EVP_ENCODE_CTX_free(ctx);
  if (r < 0) {
    return -1;
  }
  *out_len += tail;
  return 0;
}

Dimensionieren Sie den Ausgabe-Puffer auf in_len * 3 / 4 + 3, und der Aufruf ist für jede Eingabe sicher. Sehen Sie ihm dabei zu, wie er einen MIME-umgebrochenen Payload verarbeitet, bei dem der Zeilenumbruch mitten in einer Gruppe landet:

const char *wrapped = "TWFu\nZQ==";
unsigned char out[16];
int out_len = 0;
if (decode_b64((const unsigned char *)wrapped,
    (int)strlen(wrapped), out, &out_len) != 0) {
  printf("invalid base64\n");
  return 1;
}
printf("%.*s\n", out_len, out); /* Mane */

Der Umbruch verschwindet, die vier Bytes kommen raus, und niemand musste die Eingabe vorher säubern. Es gibt einen Bonus-Unterschied zur One-Shot-Funktion: Der Streaming-Weg zählt die Bytes ehrlich. Füttern Sie ihm TQ==, und er gibt exakt ein Byte (4d) zurück, ohne Null-Padding, denn er versteht, dass zwei Pads bedeuten, dass zwei der drei Ausgabe-Slots nie gefüllt wurden. Wenn Ihr Payload je eine vertrauenswürdige Länge von OpenSSL braucht, ist das der Weg, den Sie nehmen.

Mbed TLS: Der Strenge

Mbed TLS (die Krypto-Bibliothek, die als PolarSSL anfing und heute in ARM Embedded-Stacks mitliefert) gibt Ihnen zwei Funktionen mit einem sehr sauberen Vertrag:

int mbedtls_base64_encode(unsigned char *dst, size_t dlen, size_t *olen,
                          const unsigned char *src, size_t slen);
int mbedtls_base64_decode(unsigned char *dst, size_t dlen, size_t *olen,
                          const unsigned char *src, size_t slen);

Dekodieren Sie wie eine sorgfältige Person es tun würde. Rufen Sie die Funktion mit dst auf NULL (oder dlen auf null) gesetzt auf, und sie teilt Ihnen die benötigte Größe in *olen mit, ohne irgendetwas zu tun; rufen Sie sie richtig auf, und Sie bekommen 0 bei Erfolg, MBEDTLS_ERR_BASE64_INVALID_CHARACTER (das ist -0x002C), wenn in der Eingabe etwas nicht stimmt, oder MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (das ist -0x002A), wenn das Ziel zu klein ist. Die dekodierte Länge landet in *olen, und anders als bei der One-Shot-Funktion von OpenSSL ist sie immer die ehrliche Zahl: Das Dekodieren von TQ== gibt Ihnen ein Byte, 4d, nichts weiter.

Die Eingabe-Regeln sind die strengsten der vier Bibliotheken und auswendig lernenswert, denn sie definieren, was "gültig" für Mbed TLS bedeutet:

  • CRLF- und LF-Zeilenumbrüche dürfen zwischen Gruppen auftreten - E-Mail-Payloads funktionieren so, wie sie sind.
  • Leerzeichen sind erlaubt kurz vor einem Zeilenumbruch und ganz am Ende des Puffers, aber ein Leerzeichen nach einem Zeilenumbruch oder mitten in einer Gruppe ist ein Fehler.
  • Höchstens zwei =-Zeichen, und nur am Ende; Daten nach einem Pad sind ein Fehler.
  • Jedes Byte über 127 (Umlaute, UTF-8-Fragmente, binärer Müll) ist ein Fehler.

Diese letzte Regel ist die, die beißt: Wenn ein Payload von einer Quelle kommt, die die Zeichencodierung vermurkst hat, lehnt Mbed TLS ihn ab, wo ein trögerer Dekodierer ihn mit einer Achselzuckung dekodiert hätte. Für alles, was unvertraute Eingabe berührt, ist Strenge ein Feature. Ein vollständiges Dekodieren sieht so aus:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
  const char *payload = "TWFuZQ==";
  size_t need = 0;
  int rc = mbedtls_base64_decode(NULL, 0, &need,
      (const unsigned char *)payload,
      strlen(payload));
  if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
    printf("size query failed: %d\n", rc);
    return 1;
  }
  unsigned char *out = malloc(need);
  size_t olen = 0;
  rc = mbedtls_base64_decode(out, need, &olen,
      (const unsigned char *)payload,
      strlen(payload));
  if (rc != 0) {
    printf("decode failed: %d\n", rc);
    free(out);
    return 1;
  }
  printf("%.*s\n", (int)olen, out);
  free(out);
  return 0;
}

(Dass die Größenanfrage den "zu klein"-Code zurückgibt, ist so gewollt: Das ist die Art und Weise, wie die Funktion meldet, was sie geschrieben hätte. Beide Rückgabecodes oben kommen aus <mbedtls/base64.h>, demselben Header, in dem die Funktion deklariert ist.)

APR-Util und GLib: Zwei weitere Stühle

APR-Util - die Utility-Bibliothek des Apache Portable Runtime, dem Fundament, auf dem der Apache HTTP Server gebaut ist - trägt Base64 seitdem der Server Basic-Auth-Header dekodieren muss. Die API ist eine kleine Familie von int-basierten Funktionen:

#include <apr-1.0/apr_base64.h>
int apr_base64_encode_len(int len);
int apr_base64_encode(char *coded_dst, const char *plain_src,
                      int len_plain_src);
int apr_base64_decode_len(const char *coded_src);
int apr_base64_decode(char *plain_dst, const char *coded_src);

Zwei Dinge, bevor Sie danach greifen. Erstens: Die Längen sind int: 32-Bit, also ist die praktische Decke 2 GB pro Aufruf, was gut ist für Header und Konfig-Werte und nicht gut für das Dekodieren einer 4-GB-Datei. Zweitens - und das ist das Große - die Decode-Funktion hat überhaupt keine Fehler-Rückgabe. Das Verhalten ist nur in der Implementierung sichtbar, nicht im Header: Der Dekodierer behandelt jedes ungültige Zeichen, einschließlich Weißraum und NUL, als Abschluss. Er dekodiert, bis er auf das erste trifft, das er nicht erkennt, gibt zurück, wie weit er kam, und sagt nichts. Ein abgeschnittener Payload, ein Einfügen mit nachgestelltem Kommentar, ein beschädigtes Byte in der Mitte - all das erzeugt eine still zu kurze Ausgabe. Wenn Sie den APR-Dekodierer verwenden, müssen Sie die zurückgegebene Länge mit dem vergleichen, was der Payload versprochen hat; die Funktion wird es nicht für Sie tun. Es gibt keinen Wrapper, der aus dem Pool alloziert - Sie stellen den Ziel-Puffer, also allozieren Sie in Pool-getriebenem Code plain_dst selbst aus dem Pool. Es gibt auch einen EBCDIC-Winkel, den Sie an keiner anderen Stelle dieses Artikels finden: Auf EBCDIC-Maschinen wandeln die Funktionen die Eingabe vor der Kodierung nach ASCII um und nach dem Dekodieren zurück, damit derselbe Code auf den Mainframes läuft, die noch httpd betreiben.

GLib, die Runtime hinter GTK und den meisten GNOME-Anwendungen, nimmt die entgegengesetzte Persönlichkeit. Sein Dekodierer akzeptiert einen String und gibt immer einen frisch allozierten Puffer zurück (NULL nur, wenn Sie einen NULL-Zeiger übergeben), dekodiert, was er kann, und überspringt den Rest still:

#include <glib.h>
gsize out_len = 0;
guchar *bytes = g_base64_decode(payload, &out_len);
if (bytes == NULL) {
  printf("not base64\n");
} else {
  printf("%u bytes\n", (unsigned)out_len);
  g_free(bytes);
}

Die Falle steckt im Wort "immer". Der GLib-Dekodierer gehört zur nachsichtigen Schule: Zeichen außerhalb des Alphabets werden übersprungen, nicht zum Verhängnis. Füttern Sie ihm TWFuZ@==, und er wird Ihnen die drei Bytes von "Man" überreichen, ohne den kleinen Finger zu rühren. Es gibt auch eine bequeme In-Place-Variante, g_base64_decode_inplace(), die direkt über dem Eingabe-Puffer dekodiert (sicher, weil die Ausgabe kürzer ist als die Eingabe) und denselben Zeiger zurückgibt, so dass das Ergebnis am Anfang des Puffers beginnt - ein schöner Trick für Code mit wenig Speicher, und er frisst CRLF-umgebrochene Eingabe ohne Murren. Die Erkenntnis für C-Entwickler: Wenn Ihre Daten unvertraut sind, wird GLib Sie vor einem beschädigten Payload nicht retten. Die _step-Varianten (g_base64_decode_step mit einer Zustands-Ganzzahl) sind verfügbar, wenn Sie inkrementelles Dekodieren brauchen, und das passende Paar g_base64_encode_step/g_base64_encode_close lebt auf der Kodierungsseite.

URL-sicheres Base64: Das andere Alphabet

Irgendwo zwischen dem Standard-Alphabet und Ihren URLs hat es jemand erwischt. Standard-Base64 verwendet + und / als seine zwei höchsten Symbole, und beide sind Ärger in URLs: Ein + in einem Query-String wird zuverlässig als Leerzeichen interpretiert, lange bevor Ihr Server ihn sieht, und / ist ein Pfad-Trennzeichen. RFC 4648, Abschnitt 5, definiert den Fix, genannt base64url: dieselbe Kodierung, aber + wird durch - ersetzt, / durch _, und das nachgestellte =-Padding wird weggelassen, wenn die Länge auf andere Weise bekannt ist. JSON Web Tokens, OAuth-State-Parameter und eine ganze Menge API-Session-IDs leben in diesem Dialekt.

Keine der vier C-Bibliotheken dekodiert base64url nativ, also ist die Umwandlung ein kleiner Helfer, den Sie einmal schreiben und immer wieder verwenden: die beiden Sonderzeichen zurückmappen, fehlendes Padding wieder ergänzen, und dann das Ergebnis an Ihren Standard-Dekodierer reichen. Erst die Längenprüfung, denn eine Länge, die eins mehr als ein Vielfaches von vier ist, ist in jedem Base64-Dialekt unmöglich:

int base64url_decode(const char *url_safe, unsigned char *out,
    size_t out_cap, size_t *out_len) {
  size_t len = strlen(url_safe);
  if (len % 4 == 1) {
    return -1;
  }
  size_t needed = (len * 3) / 4;
  if (needed > out_cap) {
    return -2;
  }
  char *std = malloc(len + 4);
  if (std == NULL) {
    return -3;
  }
  for (size_t i = 0; i < len; i++) {
    char c = url_safe[i];
    if (c == '-') c = '+';
    if (c == '_') c = '/';
    std[i] = c;
  }
  size_t pad = (4 - len % 4) % 4;
  for (size_t i = 0; i < pad; i++) {
    std[len + i] = '=';
  }
  int n = EVP_DecodeBlock(out, (const unsigned char *)std,
                          (int)(len + pad));
  free(std);
  if (n < 0) {
    return -1;
  }
  *out_len = needed;
  return 0;
}

Zwei Stolperfallen bewachen diesen Weg. Die erste ist die Richtung: Wenn Sie einen URL-sicheren Payload ohne den Zeichen-Tausch in den Standard-Dekodierer füttern, lehnen OpenSSL und Mbed TLS ihn ab (diese Zeichen sind nicht in ihrem Alphabet), während GLib still die - und _ überspringen und einen String zurückgeben, der kürzer ist, als er sein sollte - ohne Fehler. Gehen Sie immer durch den Helfer. Die zweite ist die Warnung des RFC selbst, und sie ist es wert, ernst genommen zu werden: base64url "sollte nicht als dasselbe wie die base64-Kodierung betrachtet werden". Wenn ein Payload zufällig keine - oder _-Zeichen enthält, sind die beiden Dialekte für diese Daten byte-identisch, und eine Verwechslung ist unsichtbar - genau deshalb überlebt die Verwechslung, bis sie auf einen Payload trifft, der eines enthält.

Dateien: Das Original wiederherstellen

Der häufigste Datei-Job ist das Umgekehrte von dem, was eine Export-Routine gemacht hat: Eine .b64-Textdatei trifft ein, und Sie brauchen die Originaldatei zurück. Lesen Sie den ganzen Text, dekodieren Sie ihn, und lassen Sie dann die Bytes sich selbst ankündigen, bevor Sie einem Etikett vertrauen. C hat kein finfo, also ist der praktische Test ein Magic-Number-Sniff über die ersten paar Bytes:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("upload.b64", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  char *text = malloc((size_t)size + 1);
  size_t got = fread(text, 1, (size_t)size, f);
  fclose(f);
  text[got] = '\0';
  unsigned char *out = malloc((got * 3) / 4 + 3);
  int out_len = 0;
  if (decode_b64((const unsigned char *)text, (int)got,
      out, &out_len) != 0) {
    printf("not valid base64\n");
    free(text);
    free(out);
    return 1;
  }
  free(text);
  const char *kind = "unknown binary";
  if (out_len >= 4 && memcmp(out, "\x89PNG", 4) == 0) kind = "png";
  else if (out_len >= 5 && memcmp(out, "%PDF-", 5) == 0) kind = "pdf";
  else if (out_len >= 4 && memcmp(out, "PK\x03\x04", 4) == 0) kind = "zip";
  else if (out_len >= 3 && memcmp(out, "\xff\xd8\xff", 3) == 0) kind = "jpeg";
  printf("looks like a %s, %d real bytes\n", kind, out_len);
  free(out);
  return 0;
}

Notizen zu den Rändern: Öffnen Sie die Datei im Binär-Modus (rb/wb), auch für die textuelle Hälfte, denn Text-Modus übersetzt Zeilenumbrüche auf einigen Plattformen und ruiniert Ihre Zeichenanzahl; und geben Sie den dekodierten Puffer nie mit printf("%s") aus, um "mal zu schauen, was es ist". Der Magic-Sniff ist die ehrliche Art, diese Frage zu stellen, und wenn Sie die wiederhergestellte Datei später an einen Browser liefern, sollte der Content-Type aus demselben Sniff kommen, nicht aus dem Dateinamen.

Data-URIs: Das Bild in der URL

Ein Liebling unter den Ankünften aus der Web-Welt: Jemand fügt ein Bild in ein Formular ein, und das Front End reicht Ihrem Server eine komplette Data-URI wie data:image/png;base64,iVBORw0KGgo.... RFC 2397 definiert die Form: data:, ein optionaler Medientyp, eine optionale ;base64-Flagge, ein Komma und dann der Payload. Wenn die Flagge vorhanden ist, ist der Payload Base64; wenn nicht, ist der Payload percent-kodierter Klartext - seltener, aber legal. Wenn der Medientyp weggelassen wird, ist der Standard text/plain;charset=US-ASCII. Das Parsen in C besteht darin, das Komma zu finden und zu schauen, was direkt davor sitzt:

int split_data_uri(const char *uri, char *mime, size_t mime_cap,
    int *is_b64, const char **payload) {
  if (strncmp(uri, "data:", 5) != 0) {
    return -1;
  }
  const char *comma = strchr(uri, ',');
  if (comma == NULL) {
    return -1;
  }
  *is_b64 = 0;
  const char *meta = uri + 5;
  size_t meta_len = (size_t)(comma - meta);
  if (meta_len >= 7 && strncmp(comma - 7, ";base64", 7) == 0) {
    *is_b64 = 1;
    meta_len -= 7;
  }
  if (meta_len == 0) {
    snprintf(mime, mime_cap, "text/plain;charset=US-ASCII");
  } else {
    snprintf(mime, mime_cap, "%.*s", (int)meta_len, meta);
  }
  *payload = comma + 1;
  return 0;
}

Und der Aufrufer liest sich wie ein Satz:

char mime[256];
int is_b64 = 0;
const char *payload = NULL;
const char *uri = "data:image/png;base64,iVBORw0KGgo...";
if (split_data_uri(uri, mime, sizeof(mime), &is_b64, &payload) == 0) {
  printf("mime=%s base64=%d\n", mime, is_b64);
  /* jetzt den Payload mit Ihrer Bibliothek der Wahl dekodieren */
}

Drei Stolperfallen leben in diesem Format. Die fehlende ;base64-Flagge ist die erste: eine legale Data-URI ohne sie trägt einen percent-kodierten Payload, und wenn man den durch einen Base64-Dekodierer jagt, entsteht Müll - prüfen Sie die Flagge, dann wählen Sie Ihren Dekodierer. Der behauptete Medientyp ist die zweite: Er ist ein Hinweis vom Absender, keine Tatsache; der Magic-Number-Sniff aus dem Datei-Abschnitt ist Ihre Tatsache. Die dritte ist die Größe: Der Rat des RFC selbst lautet, dass Data-URIs für kurze Werte gedacht sind, also ist ein mehrere Megabytes großes Bild, das in einer URL reist, ein schlechter Geruch in Ihrer Architektur, kein Muster, das man feiern muss.

JWTs: Die nicht so geheimen Teile lesen

Der berühmteste Base64-Payload im Web ist das JSON Web Token, und der am wenigsten beängstigende, sobald man seine Form kennt. Laut RFC 7519 besteht ein kompaktes JWT aus drei base64url-Teilen, die durch Punkte verbunden sind: ein Header, ein Payload und eine Signatur - jeder davon kodiert ohne Padding und ohne Zeilenumbrüche. Die ersten beiden Teile sind schlichtes JSON, deshalb kann sie jeder lesen, und deshalb sollte jeder weiterlesen, bevor er einen Token anfässt.

Die ersten beiden Teile zu lesen, ist ein paar Zeilen mit dem base64url-Helfer von oben, und das ist der schnellste Weg, einem Token den Schrecken zu nehmen:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int base64url_decode(const char *url_safe, unsigned char *out,
    size_t out_cap, size_t *out_len);
int main(void) {
  const char *token =
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
    "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
    "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
  const char *dot1 = strchr(token, '.');
  if (dot1 == NULL) {
    return 1;
  }
  const char *part2 = dot1 + 1;
  const char *dot2 = strchr(part2, '.');
  if (dot2 == NULL) {
    return 1;
  }
  const char *part3 = dot2 + 1;
  char seg[512];
  char buf[1024];
  size_t n = 0;
  size_t hlen = (size_t)(dot1 - token);
  memcpy(seg, token, hlen);
  seg[hlen] = '\0';
  if (base64url_decode(seg, (unsigned char *)buf,
      sizeof(buf), &n) == 0) {
    printf("header:  %.*s\n", (int)n, buf);
  }
  size_t plen = (size_t)(dot2 - part2);
  memcpy(seg, part2, plen);
  seg[plen] = '\0';
  if (base64url_decode(seg, (unsigned char *)buf,
      sizeof(buf), &n) == 0) {
    printf("payload: %.*s\n", (int)n, buf);
  }
  printf("signature: %s (encoded, verify before trusting!)\n", part3);
  return 0;
}

Ausgedruckt ist der Header {"alg":"HS256","typ":"JWT"} und der Payload ist {"sub":"1234567890","name":"John Doe"}. Und jetzt der Teil, der zählt: Der dritte Teil ist eine Signatur, und die zwei Teile, die Sie gerade dekodiert haben, sind weder geheim noch authentifiziert. Jeder mit einem Packet-Capture kann sie lesen, und jeder mit einem Texteditor kann sie umschreiben. Ein JWT-Payload in C zu vertrauen, bevor die Signatur verifiziert ist, ist der klassische Authentifizierungs-Bug, und Base64 macht es leicht, das nicht zu merken - der Token sieht aus wie ein unkaputtbarer Klumpen, während er eine Postkarte ist. Um einen HS256-Token zu verifizieren, berechnen Sie den HMAC-SHA256 über header.part neu, mit Ihrem Geheimnis, mit HMAC() aus <openssl/hmac.h>, und vergleichen in konstanter Zeit mit CRYPTO_memcmp(); wenn sich die Digests nicht einig sind, wird der Token abgelehnt, was immer er behauptet. In C gibt es keine de-facto-Standard-JWT-Bibliothek, also bauen Sie diesen kleinen Verifikationsschritt entweder selbst oder übernehmen eine der Community-Bibliotheken - aber die Base64-Seite des Jobs ist der Splitten-und-Dekodieren-Tanz von oben, und Sie sollten ihn in all seinen Details verstehen.

Basic Auth: Der Header, der die Privatsphäre nie gelernt hat

Der älteste Authentifizierungs-Header im Web reist immer noch auf Base64: Authorization: Basic, gefolgt von der Standard-Alphabet-Kodierung von username:password (RFC 7617, den RFC 911 für das Basic-Schema referenziert). Der RFC stellt ausdrücklich klar, dass das Kodierung ist, nicht Schutz - jeder mit einem Packet-Capture kann beide Hälften mit einem einzigen Befehl dekodieren - also ist der Job auf der Dekodierseite in C, den Header zu parsen, streng zu dekodieren, beim ersten Doppelpunkt aufzuteilen (Passwörter dürfen legal Doppelpunkte enthalten) und mit einer zeitlich sicheren Funktion zu vergleichen:

#include <string.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
static size_t real_length(const char *b64);
int basic_auth_ok(const char *header, const char *expected_user,
                  const char *expected_pass) {
  if (strncmp(header, "Basic ", 6) != 0) {
    return 0;
  }
  const char *b64 = header + 6;
  unsigned char out[256];
  int n = EVP_DecodeBlock(out, (const unsigned char *)b64,
                          (int)strlen(b64));
  if (n < 0) {
    return 0;
  }
  size_t real = real_length(b64);
  size_t u_len = strlen(expected_user);
  size_t p_len = strlen(expected_pass);
  if (real != u_len + 1 + p_len) {
    return 0;
  }
  if (memcmp(out, expected_user, u_len) != 0) {
    return 0;
  }
  if (out[u_len] != ':') {
    return 0;
  }
  return CRYPTO_memcmp(out + u_len + 1, expected_pass, p_len) == 0;
}

Die Längenprüfung leistet echte Arbeit: Sie stoppt einen Payload, der zu "alice:secret" mit nachgestelltem Müll dekodiert, oder einen abgeschnittenen "alice:secre", daran, zu passen. Und CRYPTO_memcmp (oder memcmp nur, wenn Sie die zeitlichen Implikationen verstehen) ist das, was einem Angreifer den Weg verbaut, sich über Ihre Benutzerliste mit Messungen durchzuhangeln. Servieren Sie diesen Header über HTTPS oder gar nicht - auf einer klaren Verbindung ist die Base64-Ebene Fensterputz.

E-Mail und PEM: Das ursprüngliche Zuhause

Base64 wurde für ein sehr spezifisches Problem geboren: Der Mail-Transport trug nur 7-Bit-ASCII, und die Leute wollten Binärdaten durch ihn schicken. MIME (RFC 2045) machte Base64 zu einer der standardmäßigen Transfer-Kodierungen und fügte zwei Haus-Regeln hinzu: kodierte Zeilen dürfen 76 Zeichen nicht überschreiten, und Dekodier-Software muss Zeichen außerhalb des Alphabets ignorieren - Zeilenumbrüche inklusive. Diese zweite Regel ist der Grund, warum die Streaming-Dekodierer von oben einen umgebrochenen Anhang ohne jede Vorverarbeitung kauen, und der Grund, warum die 76-Zeichen-Gewohnheit in jede Mail-Bibliothek der Erde eingebrannt ist. Der Ahn war PEM (Privacy Enhanced Mail, RFC 1421), der stattdessen 64-Zeichen-Zeilen verwendete - die 64/76-Spaltung, die Sie in Werkzeugen sehen, ist diese Geschichte, und beide Grenzen wurden letztlich von SMTP auferlegt.

PEM-Rüstung - das Format, in dem Schlüssel und Zertifikate reisen - ist nur beschriftetes Base64: eine -----BEGIN ... ------Zeile, der Body in 64-Zeichen-Zeilen und eine passende END-Zeile. Die Rüstung in C abzuziehen ist ein Zeilen-Scan, und dann macht der Dekodierer den Rest:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("server.key", "r");
  if (f == NULL) {
    return 1;
  }
  char line[256];
  char b64[8192];
  size_t pos = 0;
  int in_body = 0;
  while (fgets(line, sizeof(line), f) != NULL) {
    if (strncmp(line, "-----BEGIN", 10) == 0) {
      in_body = 1;
      continue;
    }
    if (strncmp(line, "-----END", 8) == 0) {
      in_body = 0;
      break;
    }
    if (in_body) {
      size_t l = strlen(line);
      while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) {
        l--;
      }
      memcpy(b64 + pos, line, l);
      pos += l;
    }
  }
  fclose(f);
  unsigned char der[8192];
  int out_len = 0;
  if (decode_b64((const unsigned char *)b64, (int)pos,
      der, &out_len) != 0) {
    printf("armor contained no valid base64\n");
    return 1;
  }
  printf("DER payload decoded\n");
  return 0;
}

Die dekodierten Bytes sind DER, eine kompakte binäre Serialisierung, und das ist es, was die Zertifikat- und Schlüssel-Funktionen von OpenSSL letztlich verzehren. Zwei Notizen: Sammeln Sie den Body ohne seine Zeilenumbrüche (wie die Schleife es tut), damit Ihre Länge ein Vielfaches von vier ist, und wenn eine Datei mehrere Blöcke trägt, passen Sie das END-Label an das BEGIN-Label an, das Sie geöffnet haben - eine einfache Flagge reicht, wenn Sie nur den ersten Block wollen, wie hier.

Geheimnisse, Konfigs und Datenbank-Spalten

Base64 ist ein Text-Container, deshalb taucht es immer wieder an Orten auf, die man nicht erwarten würde. In Konfigurationsdateien und Umgebungsvariablen ist es der Trick, Werte unterzuschleusen, die das Format sonst kaputtmachen würden: ein Datenbank-DSN mit Semikolons, ein Passwort mit Anführungszeichen, ein Wert mit einem Zeilenumbruch. In Datenbanken kann ein binärer Blob als Base64 in einer Text-Spalte leben und jedes Werkzeug überleben, das Text annimmt - allerdings zu einem Preis von etwa einem Drittel zusätzlicher Größe, also dimensionieren Sie Ihre Spalten entsprechend (oder fragen Sie, warum der Wert nicht ohnehin in einer BLOB-Spalte steht).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
static size_t real_length(const char *b64) {
  size_t len = strlen(b64);
  while (len > 0 && b64[len - 1] == '=') len--;
  return len * 3 / 4;
}
int main(void) {
  const char *b64 = getenv("API_KEY_B64");
  if (b64 == NULL) {
    printf("API_KEY_B64 is not set\n");
    return 1;
  }
  size_t cap = strlen(b64);
  unsigned char *out = malloc(cap);
  int n = EVP_DecodeBlock(out, (const unsigned char *)b64, (int)cap);
  if (n < 0) {
    printf("API_KEY_B64 is not valid base64\n");
    free(out);
    return 1;
  }
  size_t real = real_length(b64);
  printf("key is %zu bytes\n", real);
  free(out);
  return 0;
}

Die Vorsicht gilt doppelt. Erstens: Das ist Format-Sicherheit, nicht Verschwiegenheit. Im Moment, in dem ein Entwickler die Konfigurationsdatei lesen kann, kann er den Wert mit einem einzigen Aufruf dekodieren, und die Sicherheitssektion des RFC dokumentiert echte Vorfälle, in denen Leute einen Protokoll-Austausch an den Support berichteten und "versehentlich das Passwort preisgaben", weil Base64 visuell tarnt, aber nicht rechnerisch schützt. Speichern Sie nie ein Geheimnis als Base64 und nennen Sie es verschlüsselt. Zweitens: Validieren Sie beim Start: ein halb eingefügter Umgebungs-Wert ist ein -1 vom strengen Aufruf, und eine Ein-Zeilen-Prüfung verwandelt einen kryptischen Fehlschlag drei Stunden später in eine umsetzbare Meldung beim Boot.

Dekodieren aus der Shell

Nicht alles Dekodieren passiert in Ihrem Programm. CLI-Skripte, Cron-Jobs und One-Liner dekodieren Base64 ständig, und C-Entwickler sollten die zwei Werkzeuge kennen, die ohnehin auf jeder Linux-Kiste existieren. Das coreutils-Tool ist das allgemeine: base64 -d dekodiert, -i lässt es Müll-Zeichen ignorieren statt zu scheitern, und -w setzt die Umbruch-Spalte (was nur die Kodierung betrifft, nicht das Dekodieren):

base64 -d < blob.b64 > blob.bin
base64 -d -i < messy.b64 > blob.bin

OpenSSL liefert sein eigenes mit, erreichbar als openssl base64 (der freundliche Alias von openssl enc -base64):

openssl base64 -d < blob.b64 > blob.bin
openssl base64 -d -A < blob.b64 > blob.bin

Die -A-Flagge bedeutet "eine Zeile": kodieren ohne den 64-Zeichen-Umbruch und die Eingabe ebenfalls als eine einzige Zeile erwarten. Und hier ist eine CLI-Falle, die Ihnen einen Abend kostet, wenn Sie sie nicht lesen: Das OpenSSL base64-Dekodieren ist zeilenorientiert, und ein Payload, der ohne jeden Zeilenumbruch ankommt, dekodiert zu gar nichts, still und leise:

printf 'TQ=='  | openssl base64 -d | wc -c   # 0
printf 'TQ==\n' | openssl base64 -d | wc -c  # 1

Der coreutils-Dekodierer hat diese Erwartung nicht, und das ist einer der Gründe, warum er die sichere Standardwahl für Klempen-Arbeit ist. Noch eine Dialekt-Notiz: BSD-abgeleitete Systeme (älteres macOS insbesondere) schrieben die Decode-Flagge historisch als -D; moderne Releases folgen der GNU-Konvention von -d, also schauen Sie in die Man-Seite auf der Maschine, auf der Sie tatsächlich sind.

Große Payloads, kleiner Speicher

Dekodieren ist die Richtung, die Ihnen hilft: Die Ausgabe ist drei Viertel so groß wie die Eingabe, also ist Speicherdruck durch Base64 selten. Trotzdem, wenn eine mehrere Hundert Megabytes große .b64-Datei auf der Platte landet, ist der Streaming-Weg von oben Ihr Werkzeug, und er ist einfacher, als er aussieht. Lesen Sie die kodierte Datei in Chunks, füttern Sie jedes Chunk an EVP_DecodeUpdate, und schreiben Sie die dekodierten Bytes, so wie sie ankommen. Der Kontext hält eines bis drei Zeichen jeder unvollständigen Gruppe zwischen den Aufrufen, also können Chunk-Grenzen überall fallen - Sie müssen sie nicht ausrichten:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_DecodeInit(ctx);
  FILE *in = fopen("huge.b64", "rb");
  FILE *outf = fopen("huge.bin", "wb");
  char inbuf[65536];
  unsigned char outbuf[49152 + 4];
  size_t got;
  int ok = 1;
  while (ok && (got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
    int outl = 0;
    int r = EVP_DecodeUpdate(ctx, outbuf, &outl,
        (const unsigned char *)inbuf, (int)got);
    if (r < 0) {
      ok = 0;
    } else if (outl > 0) {
      fwrite(outbuf, 1, (size_t)outl, outf);
    }
  }
  int tail = 0;
  if (ok && EVP_DecodeFinal(ctx, outbuf, &tail) == 1 && tail > 0) {
    fwrite(outbuf, 1, (size_t)tail, outf);
  }
  EVP_ENCODE_CTX_free(ctx);
  fclose(in);
  fclose(outf);
  return ok ? 0 : 1;
}

Der Peak-Speicher liegt bei zwei Puffern in der Größenordnung von wenigen Dutzend Kilobytes, unabhängig von der Dateigröße, und eine beschädigte Datei scheitert schnell - EVP_DecodeUpdate gibt -1 an dem Chunk zurück, an dem der Schaden ist, also können Sie einen Offset berichten statt sich die Schultern zu zucken. Ein Bibliothek-Hinweis für diesen Weg: Der APR-Util-Dekodierer arbeitet auf NUL-terminierten Strings mit int-großem Buchhaltungswesen (sein Rückgabewert ist ein int und die Eingabe ist durch eine interne Konstante knapp unter 3 GB gedeckelt), also ist er aus dem Rennen für Dateien im Gigabyte-Bereich. Wenn Sie Fortschrittsmeldungen brauchen, zählen Sie die Bytes, die Sie geschrieben haben - das ist Ihre Position in der Ausgabe, und die Eingabe-Position ist ungefähr vier Drittel davon.

Die Fallen, alle C-spezifisch

Zusammen an einem Ort, die Fallen, die spezifisch sind für diese Arbeit in C:

  • Der null-aufgefüllte One-Shot. EVP_DecodeBlock gibt die Quantum-Länge zurück, nicht die Datenlänge. TQ== meldet drei Bytes, trägt aber eins. Berechnen Sie die echte Länge immer aus den nachgestellten Pads neu, oder verwenden Sie das Streaming-Paar.
  • Dekodierte Bytes sind kein String. Das Ergebnis kann NUL-Bytes enthalten und muss kein UTF-8 sein. Kein strlen, kein printf("%s"), keine Übergabe an Funktionen, die Text annehmen. Tragen Sie (Zeiger, Länge) überall bei sich.
  • Puffer-Größe ist Ihr Job. C wird Ihren Ausgabe-Puffer nicht vergrößern, und die Dekodierer auch nicht - das Update von OpenSSL schreibt, was es dekodiert, in den Platz, den Sie ihm gegeben haben. Dimensionieren Sie ihn auf in_len * 3 / 4 + 3 (plus Umbruch-Overhead, wenn die Eingabe umgebrochen ist und Sie mit einem Helfer dekodieren, der den Weißraum nicht streicht) und behalten Sie eine Obergrenzen-Prüfung in jedem Wrapper.
  • Signierte char-Lookups. Wenn Sie jemals einen Dekodierer selbst schreiben, ist der klassische Bug, das Eingabe-Byte als Index in eine 256-Einträge-Tabelle mit einem simplen char auf einer Plattform zu verwenden, auf der char signiert ist: Byte 0xFF wird zu -1, und Sie indexen rückwärts durch den Speicher. Indexen Sie immer mit unsigned char- oder unsigned-Werten.
  • Die stillen sind die gefährlichen. APR-Util hält beim ersten ungültigen Zeichen an und sagt nichts; GLib überspringt Müll und sagt nichts. OpenSSL und Mbed TLS scheitern laut. Wenn Ihre Eingabe unvertraut ist, ist die Stille der Bibliothek ein Bug in Ihrem Programm, nicht in der Bibliothek.
  • Die Kommandozeile frisst Zeilenumbrüche. openssl base64 -d dekodiert null Bytes, wenn die Eingabe keinen Zeilenumbruch hat. Shell-Pipelines, die nachgestellte Zeilenumbrüche streifen (tr -d '\n', xargs, Editor-Speichervorgänge ohne abschließenden Zeilenumbruch), erzeugen leere Ausgabe ohne Fehler.
  • int versus size_t. Die One-Shot-API von OpenSSL nimmt eine int-Länge, APR-Util verwendet durchgängig int, und die Mbed-TLS- und GLib-APIs verwenden size_t. Gemischte Längen-Rechnerei zwischen ihnen ist der Ort, an dem signierte/unsigned-Warnungen echte Bugs verstecken - und an dem APRs 2-GB-Decke lebt.
  • Weißraum ist nicht einheitlich. OpenSSL überspringt sämtlichen Weißraum überall; Mbed TLS erlaubt CRLF/LF zwischen Gruppen und Leerzeichen kurz vor einem Umbruch, aber nicht danach oder mitten in der Zeile; die CLI-Tools variieren. Ein Payload, der für einen Dekodierer gültig ist, kann für einen anderen ungültig sein, und "es lief auf meiner Maschine" bedeutet meistens "mein Dekodierer war tröger".

Gute Gewohnheiten, gesammelt

Validieren Sie, bevor Sie vertrauen: Eine Form-Prüfung (Alphabetzeichen, höchstens zwei nachgestellte Pads) fängt offensichtlichen Müll ab, bevor überhaupt dekodiert wird, aber nur ein echtes Dekodieren versteht die Base64-Semantik, also hat der strenge Dekodierer das letzte Wort. Verwenden Sie das Streaming-Paar von OpenSSL, wenn Sie ehrliche Längen oder chunkweise Eingabe brauchen, und den One-Shot, wenn der Payload klein ist und Sie seine Länge sofort korrigieren. Halten Sie (Zeiger, Länge)-Paare zusammen und lassen Sie einen dekodierten Puffer nie auf eine String-Funktion treffen. Vergleichen Sie Authentifizierungs-Material mit CRYPTO_memcmp. Sniffen Sie die Magic-Bytes, bevor Sie einem Dateinamen oder einem behaupteten MIME-Typ glauben. Und behandeln Sie Base64 so, wie es ist - ein Verpackungsformat, eine kleine Box für Bytes - nicht als ein Schloss: Nichts an diesen 64 Buchstaben macht Ihre Daten privat.

Eine kurze Geschichte von Base64 in C

Die Geschichte beginnt mit der E-Mail. In den Jahren 1990 und 1991 entwarfen eine Gruppe von Kryptografen Privacy Enhanced Mail, ein System für signierte und verschlüsselte E-Mail, und sie brauchten einen Weg, Binärdaten durch ein 7-Bit-Netzwerk zu tragen. Ihre Antwort, standardisiert als RFC 1421 im Jahr 1993, kodierte Daten zu je sechs Bits pro Zeichen - "base 64" - in 64-Zeichen-Zeilen, und die Implementierung war, wie üblich, C. Um die gleiche Zeit kam das Web mit seinem eigenen MIME, RFC 1521 (1993) und dann RFC 2045 (1996), das dasselbe Alphabet behielt, die Zeilenlänge auf 76 lockerte und Base64 zum Anhang-Format des jungen Internets machte.

Die C-Standardbibliothek hat die ganze Party verpasst. Der C89-Standard wurde 1990 veröffentlicht, drei Jahre vor MIME, und das Sprach-Komitee hat seitdem nie eine Base64-Funktion hinzugefügt - nicht in C99, nicht in C11, nicht in C23 (die 2024er Revision). Also wuchs das Ökosystem um die Bibliotheken herum: OpenSSL trägt die EVP-Kodier- und Dekodier-Routinen in libcrypto, seit jemand OpenSSL für TLS linkt, Mbed TLS (2015 von PolarSSL umbenannt) behielt ein kleines strenges Paar für eingebettete Systeme, APR-Util wurde mit Apache ausgeliefert, als der Server seine eigenen Auth-Header dekodieren musste, und GLib fügte sein Trio für den Desktop hinzu. Die Standards jagten die Implementierungen: RFC 3548 im Jahr 2003 räumte die alten Definitionen auf, und RFC 4648 im Jahr 2006 (Base-N Encodings) formalisierte die Alphabete, die URL-sichere Variante und die Sicherheits-Regeln, auf die dieser Artikel sich stützt. Passenderweise verweist Abschnitt 11 dieses RFCs auf eine ISO-C99-Referenzimplementierung - der Beispiel-Dekodierer des Standards selbst ist in C geschrieben, und das sagt Ihnen alles darüber, wo dieses Format zu Hause ist.

Schöne Fakten, C-Ausgabe

Ein paar C-gefärbte Fakten, die einfach Spaß machen zu wissen:

  • Der Name ist Mathematik, nicht Marketing: Jedes Ausgabezeichen trägt exakt sechs Bits, und 2 hoch 6 ist 64. "Base64" ist die Basis, laut vorgelesen.
  • Das Alphabet hat 65 Zeichen, nicht 64: die 64 Symbole plus =, das RFC 4648 "das zusätzliche 65ste Zeichen" nennt, verwendet für eine besondere Verarbeitungs-Funktion. Das Pad ist ein Arbeiter, kein Buchstabe.
  • OpenSSL bricht kodierte Ausgabe bei 64 Zeichen um (die PEM-Gewohnheit), während coreutils bei 76 umbricht (die MIME-Gewohnheit). Der 12-Zeichen-Unterschied sind zwei Jahrzehnte Mail-Geschichte, die Sie in der Ausgabe von zwei Befehlen auf derselben Maschine sehen können.
  • Der Autor des GNU-coreutils-Befehls base64 ist Simon Josefsson - dieselbe Person, die RFC 4648 geschrieben hat. Der Standard und eine seiner meistgenutzten Implementierungen teilen sich einen Autor, und genau so kommen die beiden dazu, sich über jeden Randfall einig zu sein.
  • Mbed TLS macht seine Tabellen-Lookups über konstantzeitige Helfer (mbedtls_ct_base64_*), also verrät die Dekodier-Geschwindigkeit nicht, welche Zeichen es gesehen hat. Ein Detail, das Sie nie bemerken werden und für dessen Existenz Sie froh sein dürfen.
  • TQ== ist der kleinste nicht-triviale Payload: ein echtes Byte, zwei Pads. Es ist der perfekte Test-Vektor - der One-Shot-Dekodierer von OpenSSL gibt dafür drei Bytes zurück, sein Streaming-Dekodierer eins, Mbed TLS eins und GLib eins. Vier Bibliotheken, zwei Antworten, und der Unterschied ist das Null-Padding.
  • APRs base64-Funktionen sind die einzigen in diesem Artikel, die sich um EBCDIC kümmern, denn httpd läuft noch immer auf Maschinen, auf denen Buchstaben kein ASCII sind. Die C-Standardbibliothek hat nie einen Mainframe getroffen; APR schon.
  • Der leere Payload ist die universelle Identität: Jede Bibliothek kodiert und dekodiert Eingabe mit null Länge zu Ausgabe mit null Länge, ohne Fehler. Wenn Ihr Dekodierer an einem leeren String erstickt, haben Sie einen Bug, kein Format.

Der Wechsel auf die Kodierer-Seite

Das war die Dekodierer-Seite, und dort wohnt der größte Teil des Schmerzes, denn beim Dekodieren treffen Sie auf die Daten anderer Leute: ihre Padding-Entscheidungen, ihre Zeilenumbrüche, ihre beschädigten Bytes, ihre Tokens. Die entgegengesetzte Richtung - Bytes in einen Base64-String umzuwandeln - ist ein ruhigeres Tier mit seiner eigenen Schar an Fallen: exakte Buffer-Mathematik, die Frage des Zeilenumbruchs und die Größen-Abrechnung, die jeden Absender trifft. Die Base64-Kodierung in C wird im verwandten Artikel, der von dieser Seite verlinkt ist, eingehend behandelt, und er passt zu diesem, so wie der Dekodierer zum Kodierer passt: Lesen Sie beide, und keine der beiden Richtungen wird Sie je wieder überraschen.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Kodierung in C: Ein vollständiger Leitfaden