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

Er sitzt in einer API-Antwort, in einer Konfigurationsdatei, in einem E-Mail-Anhang oder mitten in einer URL: ein langer String aus Buchstaben und Ziffern, gelegentlich ein + oder /, und vielleicht noch ein = oder zwei am Ende. Sie erkennen ihn sofort, und jetzt brauchen Sie die Original-Bytes zurück - in C. Genau das ist der gesamte Job des Base64-Dekodierens: Vier Alphabetzeichen gehen rein, drei rohe Bytes kommen raus, immer wieder, bis die =-Zeichen Ihnen sagen, wo die echten Daten endeten. Die Startseite dieser Site erklärt das Format Schritt für Schritt, deshalb gibt dieser Artikel seine Energie dort aus, wo die eigentliche Arbeit liegt: bei Buffern, Bibliotheken und den Fallen, die dazwischen warten.

Zwei Dinge zu wissen, bevor der erste malloc kommt. Erstens: Dekodieren ist die schrumpfende Richtung: Die Ausgabe ist drei Viertel so groß wie die Eingabe, also braucht ein Decoder nie mehr Speicher als das Payload, das er bereits hält. Zweitens - und das ist die Schlagzeile - C liefert keinen Base64-Decoder mit. Die Standardbibliothek der Sprache erstarrte lange bevor Base64 existierte, und kein Standard seitdem hat die Lücke gefüllt. Also verlässt 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 heimlich mit Ihrer Ausgabe anstellt. Sobald Sie die Persönlichkeit Ihres Decoders kennen, ist das Base64-Dekodieren in C keine Quelle für Mysterien-Bugs mehr, sondern eine Routine, die Sie im Schlaf schreiben können.

Die Werkzeugkiste: Vier Wege, Ihre Bytes zurückzubekommen

Hier ist die Lage auf einen Blick. Alle vier decken das Standardalphabet ab; die Unterschiede liegen an den Rändern, und genau dort kommen die Bugs her.

Bibliothek Header Fehlermodell Ausgabekuriosität zum Merken
OpenSSL (libcrypto) <openssl/evp.h> Gibt -1 bei ungültiger Eingabe zurück Der One-Shot-Decoder füllt den Tail mit Nullen
Mbed TLS <mbedtls/base64.h> Rückgabecodes (-0x002C, -0x002A) Die strengsten Eingaberegeln von allen vier
APR-Util <apr-1.0/apr_base64.h> Keine: Stoppt beim ersten seltsamen Zeichen int-basierte Längen-API, also ist 2 GB die Obergrenze
GLib <glib.h> Gibt nur bei hartem Fehlschlag NULL zurück Ignoriert versprengten Müll still

Die Installation ist ein Paketname pro Distribution. 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. Dann linken Sie mit -lcrypto, -lmbedcrypto, -laprutil-1 beziehungsweise -lglib-2.0. Welche wählen Sie? Wenn Sie OpenSSL bereits für TLS oder Hashing linken (die meisten Server tun das), nehmen Sie OpenSSL. Für eingebettete und ressourcenknappe Builds ist Mbed TLS der kleine, strenge Bürger. 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 Decoder, der Lücken mit Nullen füllt

OpenSSL führt Base64 in zwei Varianten. Die One-Shot-Funktion ist der Star in den meisten Codes:

#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 Buffer mit Base64-Zeichen und eine Länge, und er schreibt die dekodierten Bytes nach out und gibt zurück, wie viele es waren. Er streicht führende Leerzeichen, streicht abschließende Leerzeichen und Zeilenumbrüche, und er lehnt Eingaben ab, die nach dem Streichen kein Vielfaches von vier Zeichen sind oder die ein Zeichen außerhalb des Alphabets enthalten. Bisher ein völlig vernünftiger Vertrag. Mit einer einzigen Ausnahme, die still und heimlich schon so manchen Datenbank-Import kaputt gemacht hat: der Rückgabewert ist nicht die echte Datenlänge.

Führen Sie das Programm aus, und Sie erhalten 4 bytes... nein, Moment. TWFuZQ== sind zwei Vierer-Gruppen, also gibt die Funktion 6 zurück, und der Buffer enthält 4d 61 6e 65 00 00: das Wort "Mane" plus zwei Null-Bytes. Der One-Shot-Decoder von OpenSSL arbeitet in festen Quanten - jedes vier Eingabezeichen produzieren immer genau 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 genau dieser eine Satz ist der wichtigste im gesamten Manual dieser Funktion.

Die echte Länge wird aus dem Padding zurückgewonnen, und das ist eine Zweizeiler-Berechnung:

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 abschließenden 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 melden würde. Halten Sie das Paar (Zeiger, Länge) immer zusammen, und verwenden Sie nie strlen auf dekodierten Daten, denn die Bytes, die Sie zurückbekommen, können ein JPEG sein, und das erste davon kann ein NUL sein.

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

Für alles andere bietet OpenSSL das Streaming-Paar EVP_DecodeUpdate plus EVP_DecodeFinal. Das Kontextobjekt ist es, das den Zustand zwischen den Aufrufen bewegt: Es hält ein bis drei Zeichen einer unvollständigen Gruppe zurück, damit Sie das Payload in chunks füttern können. Das Verhalten, das zählt, ist dieses: Leerzeichen (Spaces, Tabs, Carriage Returns, Line Feeds) werden an jeder Stelle des Streams übersprungen, jedes andere Nicht-Alphabet-Zeichen oder ein = mitten in den Daten gibt sofort -1 zurück, und eine Rückgabe von 0 aus einem Update bedeutet "das Padding wurde gesehen, es wird nichts mehr erwartet". EVP_DecodeFinal lehnt dann mit -1 ab, wenn noch eine partielle Gruppe hängt, denn eine Länge, die (nach Leerzeichen) kein Vielfaches von vier ist, ist kein gültiges Payload.

Ein Versionshinweis vor dem Code, denn alte Tutorials werden Sie ausbremsen: In OpenSSL 3.x ist der Kontexttyp EVP_ENCODE_CTX opak, deshalb kompiliert das Stack-Muster EVP_ENCODE_CTX ctx;, das in viel Internet-Code zu finden ist, nicht mehr. Allozieren und freigeben Sie ausdrücklich:

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

Bemessen Sie den Ausgabe-Buffer auf in_len * 3 / 4 + 3, und der Aufruf ist sicher für jede Eingabe. Sehen Sie zu, wie er ein umgebrochenes MIME-Payload behandelt, 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 reinigen. Es gibt einen Bonus-Unterschied zur One-Shot-Funktion: Der Streaming-Pfad zählt Bytes ehrlich. Füttern Sie ihm TQ==, und er gibt genau 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, dann ist das der Pfad, den Sie nehmen sollten.

Mbed TLS: Der Strikteste

Mbed TLS (die Krypto-Bibliothek, die als PolarSSL das Licht der Welt erblickte und heute in den eingebetteten Stacks von ARM mitgeliefert wird) 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 es eine sorgfältige Person tun würde. Rufen Sie die Funktion mit dst auf NULL (oder dlen auf null) auf, und sie teilt Ihnen die benötigte Größe in *olen mit, ohne Arbeit zu verrichten; rufen Sie sie für den Ernstfall auf, und Sie erhalten bei Erfolg 0, MBEDTLS_ERR_BASE64_INVALID_CHARACTER (das ist -0x002C), wenn irgendetwas in der Eingabe falsch ist, oder MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (das ist -0x002A), wenn das Ziel zu klein ist. Die dekodierte Länge landet in *olen, und im Gegensatz zur One-Shot-Funktion von OpenSSL ist es immer die ehrliche Zahl: Das Dekodieren von TQ== liefert ein Byte, 4d, nichts mehr.

Die Eingaberegeln sind die strengsten der vier Bibliotheken und es lohnt sich, sie auswendig zu lernen, denn sie definieren, was für Mbed TLS "gültig" bedeutet:

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

Genau diese letzte Regel beißt: Wenn ein Payload von einer Quelle kommt, die die Zeichencodierung verzerrt hat, lehnt Mbed TLS es ab, wo ein faulerer Decoder es mit einer Schulterzuckung dekodiert hätte. Für alles, was unzuverlässige Eingaben berührt, ist Strenge ein Feature. Ein komplettes 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ößenabfrage den "zu klein"-Code zurückgibt, ist gewollt: So meldet die Funktion, 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, des Fundaments, 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 zu wissen, bevor Sie danach greifen. Erstens: Die Längen sind int: 32-Bit, also liegt die praktische Obergrenze bei 2 GB pro Aufruf, was für Header und Konfigurationswerte gut ist und zum Dekodieren einer 4-GB-Datei nicht gut ist. Zweitens - und das ist das Große - die Decode-Funktion hat gar keinen Fehler-Rückgabewert. Das Verhalten ist nur in der Implementierung sichtbar, nicht im Header: Der Decoder nimmt jedes ungültige Zeichen, einschließlich Leerzeichen und NUL, als Terminierzeichen. Er dekodiert bis zur ersten Sache, die er nicht erkennt, gibt zurück, wie weit er gekommen ist, und sagt nichts. Ein abgeschnittenes Payload, eine Einfügung mit einem anhängenden Kommentar, ein beschädigtes Byte in der Mitte - all das produziert eine still zu kurze Ausgabe. Wenn Sie den Decoder von APR verwenden, müssen Sie die zurückgegebene Länge mit dem vergleichen, was das Payload versprochen hat; die Funktion wird es für Sie nicht tun. Es gibt keinen Wrapper, der aus dem Pool alloziert - Sie liefern den Ziel-Buffer selbst, also allozieren Sie in pool-gesteuertem Code plain_dst selbst aus dem Pool. Es gibt auch einen EBCDIC-Aspekt, den Sie an keiner anderen Stelle in diesem Artikel finden: Auf EBCDIC-Maschinen wandeln die Funktionen die Eingabe vor dem Encodieren nach ASCII um und nach dem Dekodieren zurück, damit läuft derselbe Code auf den Mainframes, die noch immer httpd betreiben.

GLib, die Runtime hinter GTK und den meisten GNOME-Anwendungen, hat die entgegengesetzte Persönlichkeit. Ihr Decoder nimmt einen String an und gibt immer einen frisch allozierten Buffer zurück (NULL nur, wenn Sie einen NULL-Zeiger übergeben), dekodiert alles, 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 Decoder von GLib gehört zur Schule der Nachsichtigen: Zeichen außerhalb des Alphabets werden übersprungen, nicht fatal. Füttern Sie ihm TWFuZ@==, und er gibt die drei Bytes von "Man" zurück, ohne einen Finger zu rühren. Es gibt auch eine praktische In-Place-Variante, g_base64_decode_inplace(), die über dem Eingabe-Buffer dekodiert (sicher, weil die Ausgabe kürzer ist als die Eingabe) und denselben Zeiger zurückgibt, so dass das Ergebnis am Anfang des Buffers beginnt - ein netter Trick für speicherknappe Codes, und er isst CRLF-umgebrochene Eingaben gern. Die Botschaft für C-Entwickler: Wenn Ihre Daten nicht vertrauenswürdig sind, wird GLib Sie vor einem beschädigten Payload nicht retten. Die _step-Varianten (g_base64_decode_step mit einer Zustands-Ganzzahl) stehen bereit, wenn Sie inkrementelles Dekodieren brauchen, und das passende Paar g_base64_encode_step/g_base64_encode_close lebt auf der Encoder-Seite.

URL-sicheres Base64: Das andere Alphabet

Irgendwo zwischen dem Standardalphabet und Ihren URLs wurde jemand verletzt. Standard-Base64 verwendet + und / als seine beiden höchsten Symbole, und beide sind in URLs Ärger: Ein + in einem Query-String wird routinemäßig als Leerzeichen interpretiert, bevor Ihr Server es sieht, und / ist ein Pfad-Trennzeichen. RFC 4648, Abschnitt 5, definiert die Lösung, genannt base64url: dieselbe Kodierung mit + ersetzt durch -, / ersetzt durch _, und das abschließende =-Padding wird weggelassen, wenn die Länge auf andere Weise bekannt ist. JSON Web Tokens, OAuth-State-Parameter und sehr viele 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 wiederverwenden: die beiden Sonderzeichen zurückmappen, fehlendes Padding wieder anfügen, und dann das Ergebnis an Ihren Standard-Decoder geben. Längenprüfung zuerst, denn eine Länge, die eins mehr als ein Vielfaches von vier ist, ist in keinem Base64-Dialekt mö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 Fallstricke bewachen diesen Weg. Der erste ist die Richtung: Wenn Sie ein URL-sicheres Payload in den Standard-Decoder geben, ohne den Zeichen-Tausch, lehnen OpenSSL und Mbed TLS es ab (diese Zeichen sind nicht in ihrem Alphabet), während GLib das - und _ still überspringt und einen String zurückgibt, der kürzer ist, als er sein sollte - ohne Fehler. Gehen Sie immer durch den Helfer. Der zweite ist die Warnung des RFC selbst, die es ernst zu nehmen lohnt: 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 ein Verwechseln ist unsichtbar - genau deshalb überlebt das Verwechseln, bis es auf ein Payload trifft, das eines davon enthält.

Dateien: Das Original wiederherstellen

Der häufigste Datei-Job ist das Gegenteil davon, was irgendeine Export-Routine gemacht hat: Eine .b64-Textdatei trifft ein, und Sie brauchen die Originaldatei zurück. Lesen Sie den gesamten Text, dekodieren Sie ihn, und lassen Sie die Bytes dann sich selbst ankündigen, bevor Sie einem Label 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;
}

Anmerkungen an den Rändern: Öffnen Sie die Datei im Binärmodus (rb/wb), selbst für die Text-Hälfte, denn im Textmodus werden Zeilenumbrüche auf manchen Plattformen übersetzt und Ihre Zeichenzählung beschädigt; und geben Sie nie den dekodierten Buffer mit printf("%s") aus, um "zu sehen, was es ist". Der Magic-Sniff ist der ehrliche Weg, diese Frage zu stellen, und wenn Sie die wiederhergestellte Datei später an einen Browser liefern, sollte das Content-Type aus demselben Sniff kommen, nicht aus dem Dateinamen.

Data-URIs: Das Bild in der URL

Ein beliebter Eintreff aus der Web-Welt: Jemand fügt ein Bild in ein Formular ein, und das Frontend reicht Ihrem Server einen kompletten Data-URI wie data:image/png;base64,iVBORw0KGgo... weiter. RFC 2397 definiert die Form: data:, ein optionaler Medientyp, eine optionale ;base64-Flag, ein Komma, und dann das Payload. Wenn die Flag vorhanden ist, ist das Payload Base64; wenn nicht, ist das Payload percent-kodierter Klartext - seltener, aber legal. Wenn der Medientyp weggelassen wird, ist der Standard text/plain;charset=US-ASCII. Das Parsen in C ist eine Frage des Auffindens des Kommas und des Blicks darauf, 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 Payload mit Ihrer bevorzugten Bibliothek dekodieren */
}

Drei Fallgruben leben in diesem Format. Die fehlende ;base64-Flag ist die erste: ein legaler Data-URI ohne sie trägt ein percent-kodiertes Payload, und das durch einen Base64-Decoder zu jagen produziert Müll - prüfen Sie die Flag, dann wählen Sie Ihren Decoder. 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 eigene Rat des RFC ist, dass Data-URIs für kurze Werte sind, also ist ein mehrere Megabytes großes Bild, das in einer URL reitet, ein Gestank in Ihrer Architektur, kein Muster, das man feiern sollte.

JWTs: Die Nicht-Geheimen-Teile lesen

Das berühmteste Base64-Payload im Web ist der JSON Web Token, und der am wenigsten beängstigende, wenn Sie seine Form kennen. Gemäß RFC 7519 ist ein kompakter JWT drei base64url-Teile, verbunden durch Punkte: ein Header, ein Payload und eine Signatur - jeweils kodiert ohne Padding, ohne Zeilenumbrüche. Die ersten beiden Teile sind klares JSON, deshalb kann jeder sie lesen, und deshalb sollte jeder weiterlesen, bevor er einen Token anfasst.

Die ersten beiden Teile zu lesen sind ein paar Zeilen mit dem base64url-Helfer von oben, und es ist der schnellste Weg, einen Token zu entmystifizieren:

#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 das Payload {"sub":"1234567890","name":"John Doe"}. 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. Dem JWT-Payload in C zu vertrauen, bevor die Signatur verifiziert ist, ist der klassische Authentifizierungs-Bug, und Base64 macht es leicht, ihn nicht zu bemerken - der Token sieht aus wie ein unbrechbarer Blob, ist aber eine Postkarte. Um einen HS256-Token zu verifizieren, berechnen Sie den HMAC-SHA256 über header.part mit Ihrem Secret neu, verwenden HMAC() aus <openssl/hmac.h> und vergleichen in konstanter Zeit mit CRYPTO_memcmp(); wenn die Digests nicht übereinstimmen, wird der Token abgelehnt, was auch immer er behauptet. Es gibt keine de-facto-Standard-JWT-Bibliothek in C, also bauen Sie für die Produktion entweder diesen kleinen Verifizierungsschritt selbst oder übernehmen Sie eine der Community-Bibliotheken - aber die Base64-Seite des Jobs ist der Split-and-Decode-Tanz von oben, und Sie sollten ihn ganz verstehen.

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

Der älteste Authentifizierungs-Header im Web reitet noch immer auf Base64: Authorization: Basic, gefolgt von der Standardalphabet-Kodierung von username:password (RFC 7617, auf den RFC 9110 für das Basic-Schema verweist). Der RFC ist explizit: Das hier ist Kodierung, nicht Schutz - jeder mit einem Packet-Capture kann beide Hälften mit einem einzigen Befehl dekodieren - also ist der Decode-Teil des Jobs in C, den Header zu parsen, streng zu dekodieren, beim ersten Doppelpunkt zu teilen (Passwörter dürfen legal Doppelpunkte enthalten) und mit einer timing-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 macht echte Arbeit: Sie hält ein Payload davon ab zu matchen, das zu "alice:secret" mit anhängendem Müll dekodiert, oder "alice:secre" abgeschnitten. Und CRYPTO_memcmp (oder memcmp nur, wenn Sie die Timing-Implikationen verstehen) ist es, was einen Angreifer daran hindert, sich per Timing durch Ihre User-Liste zu tasten. Servieren Sie diesen Header über HTTPS oder gar nicht - auf einer Klartext-Verbindung ist die Base64-Ebene Fensterschmuck.

E-Mail und PEM: Der ursprüngliche Wohnsitz

Base64 wurde für ein sehr spezifisches Problem geboren: Der Mail-Transport trug nur 7-Bit-ASCII, und die Leute wollten Binärdaten durchschicken. MIME (RFC 2045) machte Base64 zu einer der Standard-Transfer-Kodierungen und fügte zwei Hausregeln hinzu: Kodierte Zeilen dürfen nicht länger als 76 Zeichen sein, und Decode-Software muss Zeichen außerhalb des Alphabets ignorieren - Zeilenumbrüche eingeschlossen. Genau diese zweite Regel ist der Grund, warum die Streaming-Decoder von oben ein umgebrochenes Anhang-File mit null Vorverarbeitung durchkauen, und sie ist der Grund, warum die 76-Zeichen-Gewohnheit noch immer in jeder Mail-Bibliothek der Welt eingebacken ist. Der Vorfahre war PEM (Privacy Enhanced Mail, RFC 1421), das stattdessen 64-Zeichen-Zeilen verwendete - die 64/76-Spaltung, die Sie in Tools sehen, ist genau diese Geschichte, und beide Grenzen wurden letztlich von SMTP auferlegt.

Die PEM-Rüstung - das Format, in dem Keys und Zertifikate reisen - ist nur beschriftetes Base64: eine -----BEGIN ... ------Zeile, der Körper in 64-Zeichen-Zeilen, und eine passende END-Zeile. Das Streifen der Rüstung in C ist ein Zeilen-Scan, und dann macht der Decoder 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 genau das verbrauchen die Zertifikats- und Key-Funktionen von OpenSSL letztlich. Zwei Anmerkungen: Sammeln Sie den Körper ohne seine Zeilenumbrüche (so wie es die Schleife 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 Flag funktioniert, wenn Sie nur den ersten Block wollen, wie hier.

Geheimnisse, Konfigurationen und Datenbank-Spalten

Base64 ist ein Text-Container, deshalb taucht es immer wieder an Orten auf, die Sie nicht erwarten würden. In Konfigurationsdateien und Umgebungsvariablen ist es der Trick, Werte zu schmuggeln, die sonst das Format brechen würden: eine Datenbank-DSN mit Semikolons, ein Passwort mit Anführungszeichen, ein Wert mit einem Zeilenumbruch. In Datenbanken kann ein Binär-Blob als Base64 in einer Text-Spalte leben und jedes Tool überstehen, das Text annimmt - allerdings zum 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 liegt).

#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 hier ist Format-Sicherheit, nicht Geheimhaltung: In dem Moment, in dem ein Entwickler die Konfigurationsdatei lesen kann, kann er den Wert mit einem einzigen Aufruf dekodieren, und die Sicherheits-Sektion des RFC dokumentiert echte Vorfälle, in denen Leute einen Protokoll-Austausch an den Support meldeten und "versehentlich das Passwort preisgaben", weil Base64 visuell tarnt, aber nicht rechnerisch schützt. Speichern Sie nie ein Secret als Base64 und nennen Sie es verschlüsselt. Zweitens: Validieren Sie beim Start: Ein halb eingefügter Umgebungs-Wert ist ein -1 vom strikten Aufruf, und eine Einzeilen-Prüfung verwandelt einen kryptischen Fehlschlag drei Stunden später in eine handlungsrelevante Meldung beim Boot.

Dekodieren aus der Shell

Nicht alles Dekodieren passiert in Ihrem Programm. CLI-Skripte, Cron-Jobs und One-Liner dekodieren ständig Base64, und C-Entwickler sollten die beiden Tools kennen, die auf jeder Linux-Kiste schon 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 (die nur das Encodieren betrifft, nicht das Dekodieren):

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

OpenSSL liefert sein eigenes, erreichbar als openssl base64 (ein freundlicherer Alias für openssl enc -base64):

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

Die -A-Flag bedeutet "eine Zeile": encodieren ohne den 64-Zeichen-Umbruch, und die Eingabe wird ebenfalls als einzelne Zeile erwartet. Und hier ist eine CLI-Falle, die Sie einen Abend kosten wird, wenn Sie sie nicht lesen: Das base64-Decode von OpenSSL ist zeilenorientiert, und ein Payload, das ganz ohne 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-Decoder hat diese Erwartung nicht, was einer der Gründe ist, warum er die sicherere Standardwahl für Klebe-Arbeit ist. Noch ein Dialekt-Hinweis: BSD-abgeleitete Systeme (ältere macOS-Versionen besonders) schrieben die Decode-Flag historisch als -D; moderne Releases folgen der GNU-Konvention -d, also schauen Sie in die Manpage auf der Maschine, auf der Sie wirklich sind.

Große Payloads, wenig 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 vorherige Streaming-Pfad Ihr Tool, und er ist einfacher, als er aussieht. Lesen Sie die kodierte Datei in chunks, geben Sie jedes Chunk an EVP_DecodeUpdate, und schreiben Sie die dekodierten Bytes, wenn sie ankommen. Der Kontext hält die ein bis drei Zeichen jeder unvollständigen Gruppe zwischen den Aufrufen zurück, 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 Spitzen-Speicher ist zwei Buffer in der Größenordnung von einigen Dutzend Kilobytes, unabhängig von der Dateigröße, und eine beschädigte Datei scheitert schnell - EVP_DecodeUpdate gibt -1 an dem Chunk zurück, wo der Schaden ist, also können Sie ein Offset melden statt mit den Schultern zu zucken. Ein Bibliothek-Hinweis für diesen Pfad: Der Decoder von APR-Util arbeitet mit NUL-terminierten Strings und 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 für multi-gigabyte-große Dateien aus dem Rennen. Wenn Sie eine Fortschrittsanzeige 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

An einem Ort gesammelt, die Fallen, die speziell für diese Arbeit in C sind:

  • Der nullgefü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 immer die echte Länge aus den abschließenden 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"), kein Weitergeben an Funktionen, die Text annehmen. Tragen Sie (Zeiger, Länge) überall mit.
  • Buffer-Größe ist Ihr Job. C vergrößert Ihren Ausgabe-Buffer nicht, und die Decoder auch nicht - OpenSSLs Update schreibt, was es dekodiert, in den Raum, den Sie ihm gegeben haben. Bemessen Sie ihn auf in_len * 3 / 4 + 3 (plus Umbruch-Overhead, wenn die Eingabe umgebrochen ist und Sie mit einem Helfer dekodieren, der nicht streicht), und halten Sie eine Cap-Prüfung in jedem Wrapper.
  • Signierte char-Lookups. Wenn Sie je einen Decoder selbst schreiben, ist der klassische Bug, das Eingabe-Byte als Index in eine 256-einträgige Tabelle mit einem normalen char auf einer Plattform zu verwenden, wo char signiert ist: Byte 0xFF wird -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 stoppt beim ersten ungültigen Zeichen und sagt nichts; GLib überspringt Müll und sagt nichts. OpenSSL und Mbed TLS scheitern laut. Wenn Ihre Eingabe nicht vertrauenswürdig 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 abschließende Zeilenumbrüche streichen (tr -d '\n', xargs, Editor-Speichervorgänge ohne abschließende Zeile), produzieren leere Ausgabe ohne Fehler.
  • int versus size_t. OpenSSLs One-Shot-API nimmt eine int-Länge, APR-Util verwendet durchweg int, und die Mbed-TLS- und GLib-APIs verwenden size_t. Gemischte Längenarithmetik zwischen ihnen ist der Ort, an dem signierte/unsigned-Warnungen echte Bugs verstecken - und an dem APRs 2-GB-Obergrenze lebt.
  • Leerzeichen sind nicht einheitlich. OpenSSL überspringt alle Leerzeichen überall; Mbed TLS erlaubt CRLF/LF zwischen Gruppen und Leerzeichen direkt vor einem Umbruch, aber nicht danach oder mitten in der Zeile; die CLI-Tools variieren. Ein Payload, das für einen Decoder gültig ist, kann für einen anderen ungültig sein, und "es lief auf meiner Maschine" bedeutet meistens "mein Decoder war fauler".

Gute Gewohnheiten, gesammelt

Validieren Sie, bevor Sie vertrauen: Eine Formprüfung (Alphabetzeichen, höchstens zwei abschließende Pads) fängt offensichtlichen Müll vor jedem Decode ab, aber nur ein echter Decode versteht Base64-Semantik, also hat der strenge Decoder das letzte Wort. Verwenden Sie das Streaming-Paar von OpenSSL, wenn Sie ehrliche Längen oder chunkige Eingaben brauchen, und den One-Shot, wenn das Payload klein ist und Sie seine Länge sofort korrigieren. Halten Sie (Zeiger, Länge)-Paare zusammen, und lassen Sie einen dekodierten Buffer 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 Schloss: Nichts an diesen 64 Zeichen macht Ihre Daten privat.

Eine kurze Geschichte von Base64 in C

Die Geschichte beginnt mit Mail. 1990 und 1991 skizzierte eine Gruppe von Kryptografen Privacy Enhanced Mail, ein System für signierte und verschlüsselte E-Mails, und sie brauchten einen Weg, Binärdaten durch ein 7-Bit-Netzwerk zu tragen. Ihre Antwort, 1993 als RFC 1421 standardisiert, kodierte Daten mit sechs Bits pro Zeichen - "base 64" - in 64-Zeichen-Zeilen, und die Implementierung war, selbstverständlich, 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 Anhangsformat des jungen Internets machte.

Die C-Standardbibliothek verpasste das ganze Boot. Der C89-Standard wurde 1990 veröffentlicht, drei Jahre vor MIME, und der Ausschuss der Sprache 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: OpenSSL trägt die EVP-Encode-/Decode-Routinen in libcrypto seitdem jemand OpenSSL für TLS linkt, Mbed TLS (2015 aus PolarSSL umbenannt) hielt ein kleines striktes 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 Sicherheitsregeln, auf die sich dieser Artikel stützt. Passenderweise verweist Abschnitt 11 dieses RFC auf eine ISO-C99-Referenzimplementierung - der eigene Beispieldecoder des Standards ist in C geschrieben, und das sagt alles darüber aus, wo dieses Format zuhause ist.

Spaßfakten, C-Edition

Ein paar C-getönte Fakten, die schlicht Spaß machen zu wissen:

  • Der Name ist Mathematik, nicht Marketing: Jedes Ausgabe-Zeichen trägt genau sechs Bits, und 2 hoch 6 ist 64. "Base64" ist die Basis, laut ausgesprochen.
  • Das Alphabet hat 65 Zeichen, nicht 64: die 64 Symbole plus =, den RFC 4648 "das zusätzliche 65. Zeichen" nennt, das für eine spezielle Verarbeitungsfunktion verwendet wird. 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). Die 12-Zeichen-Differenz sind zwei Jahrzehnte Mail-Geschichte, die Sie in der Ausgabe von zwei Befehlen auf derselben Maschine sehen können.
  • Der Autor des GNU-coreutils-base64-Befehls ist Simon Josefsson - dieselbe Person, die RFC 4648 schrieb. Der Standard und eine seiner meistgenutzten Implementierungen teilen sich einen Autor, und so kamen die beiden dazu, sich bei jedem Randfall einig zu sein.
  • Mbed TLS macht seine Tabellen-Lookups über konstantzeitige Helfer (mbedtls_ct_base64_*), also gibt die Decode-Geschwindigkeit nicht preis, welche Zeichen es gesehen hat. Ein Detail, das Sie nie bemerken werden und dessen Existenz Sie freut.
  • TQ== ist das kleinste nicht-triviale Payload: ein echtes Byte, zwei Pads. Es ist der perfekte Testvektor - OpenSSLs One-Shot-Decoder gibt dafür drei Bytes zurück, sein Streaming-Decoder 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, weil httpd noch immer auf Maschinen läuft, wo Buchstaben nicht ASCII sind. Die C-Standardbibliothek hat nie einen Mainframe getroffen; APR schon.
  • Das leere Payload ist die universelle Identität: Jede Bibliothek kodiert und dekodiert Eingaben der Länge null zu Ausgaben der Länge null, ohne Fehler. Wenn Ihr Decoder an einem leeren String erstickt, haben Sie einen Bug, kein Format.

Der Blick auf die Encoder-Seite

Das war die Decoder-Seite, und hier lebt der größte Teil des Schmerzes, denn Dekodieren ist der Ort, an dem Sie auf die Daten anderer treffen: ihre Padding-Entscheidungen, ihre Zeilenumbrüche, ihre beschädigten Bytes, ihre Tokens. Die entgegengesetzte Richtung - Bytes in einen Base64-String verwandeln - ist ein ruhigeres Tier mit seiner eigenen Besetzung an Fallen: exakte Buffer-Mathematik, die Frage des Zeilenumbruchs und die Größen-Rechnung, die auf jedem Absender landet. Base64-Kodierung in C wird im verwandten Artikel, der von dieser Seite aus verlinkt ist, im Detail behandelt, und er passt zu diesem hier, so wie ein Decoder zu einem Encoder 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