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 PHP: Ein vollständiger Leitfaden

Sie haben Daten, die einen Kanal überleben müssen, der sie nicht leiden kann. Einen binären Blob, der in einem JSON-Feld sitzen muss. Ein Bild, das in einem HTML-Tag leben muss. Ein Zertifikat, das in eine Konfigurationsdatei gehört. Ein Token, das durch URLs, Header und Cookies wandern wird. Das ist der Alltag der Base64-Kodierung: Drei Bytes roher Daten werden jeweils in vier Zeichen aus einem Alphabet von 64 Buchstaben umgeschrieben, ein oder zwei =-Zeichen runden den Schwanz ab, und das Ergebnis ist reiner Text, den alles tragen kann. Die Startseite dieser Seite erklärt das Format im Ganzen, daher konzentriert sich dieser Artikel darauf, was PHP einem gibt, was es still und leise für einen entscheidet, und wo die Fallen sind.

Auf der PHP-Seite beginnt die Geschichte mit guter Nachricht: base64_encode() lebt seit PHP 4 im Kern, es nimmt ein Argument, es gibt immer einen String zurück, und es kann nicht scheitern. Es gibt keinen Strict-Modus, keinen Fehlerpfad, keine Konfiguration. Die Kodierung ist deterministisch: dieselben Bytes erzeugen immer dieselben Buchstaben. Ihre Aufgabe als Entwickler ist nicht, die Funktion zum Laufen zu bringen, sondern dafür zu sorgen, dass sich die umgebende Welt benimmt: das richtige Alphabet für das Ziel wählen, die richtigen Zeilenumbrüche setzen, den richtigen Zeichensatz wandeln und die 33-prozentige Größenrechnung mit offenen Augen tragen. (Base64 vergrößert Daten normalerweise um etwa ein Drittel, vier Zeichen pro drei Eingabe-Bytes; behalten Sie das im Hinterkopf, denn es taucht immer wieder auf.)

Bis zum Ende dieses Artikels wissen Sie, wie man jede Variante von Base64 erzeugt, auf die ein PHP-Entwickler tatsächlich trifft: Einzeilen-Ausgabe, MIME-umwickelte E-Mails, PEM-umwickelte Schlüssel, URL-sichere Tokens und Data-URIs, plus die Streaming-Tricks für den Fall, dass die Daten zu groß sind, um sie im Speicher zu halten.

Eine Funktion, null Optionen

Die gesamte API, genau so, wie sie das moderne PHP meldet:

base64_encode(string $string): string

Lesen Sie das noch einmal. Ein Parameter, ein Rückgabewert, keine Flags. Das Manual beschreibt es als MIME-Base64, "deshalb entworfen, damit Binärdaten den Transport durch Transportebenen überstehen, die nicht 8-bit sauber sind, wie etwa Mail-Body". Beachten Sie, was diese Formulierung nicht verspricht: keine Zeilenumbrüche, kein Umbruch, keine Meinung dazu, wo die Ausgabe leben wird. Die Funktion emittiert eine einzige lange Zeile, und egal welchen Umbruch das Ziel will, ist das Ihre Sache mit einem zweiten Aufruf. Seit PHP 8.0 trägt die Signatur native Typen; seit PHP 8.1 löst das Übergeben von null eine Deprecation-Warnung aus, also koaleszieren Sie jeden Wert, der null sein kann, zuerst auf ''.

Die Ausgabegröße folgt einem festen Muster, das man vor dem Aufruf vorhersagen kann:

Eingabe-Bytes Ausgabe-Zeichen Padding
0 0 keines
1 4 zwei =
2 4 ein =
3 4 keines
3,000,000 4,000,000 keines
100,000 133,336 zwei =

Das Muster: vier Zeichen für jede vollständige Gruppe von drei Bytes, plus eine letzte Teilgruppe, die mit einem oder zwei =-Zeichen aufgefüllt wird. Eine Folge, die man kennen sollte: Ein Byte und drei Bytes erzeugen beide vier Zeichen, also versteckt die kodierte Länge die genaue Eingabegröße. Man kann sie schätzen (durch vier teilen, mit drei multiplizieren, die Pads abziehen), aber man kann sie nicht exakt ablesen.

Wohin mit den Zeilenumbrüchen

Da base64_encode() nie von selbst umbreicht, ist die Umbruch-Entscheidung eine Frage des Ziels. In der Praxis gibt es drei Antworten.

Keine Zeilenumbrüche. Die rohe Funktion-Ausgabe, genau eine Zeile. Das will man für URLs, JSON-Payloads, Header, Datenbankwerte und alles, wo ein Zeilenumbruch ein Bug wäre. Das meinen auch die meisten, wenn sie "gib mir einfach das Base64" sagen.

MIME-Umbruch: 76 Zeichen plus CRLF. Die E-Mail-Konvention aus RFC 2045, Abschnitt 6.8: Kodierte Zeilen dürfen nicht mehr als 76 Zeichen lang sein, und Decoder müssen die Zeilenumbrüche ignorieren. Der klassische Begleit-Aufruf ist chunk_split(), den das Manual in seiner "Siehe auch"-Liste genau aus diesem Grund mit base64_encode() paart:

$binary = file_get_contents('/var/www/uploads/report.pdf');
$wrapped = chunk_split(base64_encode($binary), 76, "\r\n");

PEM-Umbruch: 64 Zeichen plus LF. Schlüssel und Zertifikate verwenden die ältere Privacy-Enhanced-Mail-Konvention (RFC 1421): kürzere 64-Zeichen-Zeilen. Dasselbe Werkzeug, andere Zahlen:

$der = file_get_contents('/etc/ssl/raw-key.der');
$armor = "-----BEGIN PRIVATE KEY-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END PRIVATE KEY-----\n";

Ein chunk_split()-Stolperstein gilt für beide Umbruch-Varianten: Die Funktion hängt den Trenner ans Ergebnis an, selbst wenn die Eingabelänge ein exaktes Vielfaches der Zeilenlänge ist. Wenn ein nachgelagerter Consumer über eine trailing Leerzeile stolpert, ist das der Grund; ein rtrim()-Aufruf auf den Trenner behebt es. Beachten Sie auch die Asymmetrie, die Ihnen eines Tages das Leben retten wird: Decoder ignorieren Zeilenumbrüche vollständig, also dekodieren ein MIME-umwickelter und ein nicht umwickelter Payload zu denselben Bytes. Umbruch ist eine Höflichkeit gegenüber zeilenbasierten Tools und Menschen, kein semantischer Unterschied.

Die Ausgabe URL-sicher machen

Das Standard-Alphabet enthält + und /, und beide sind außerhalb einer Textdatei problematisch. Ein + in einem formular-kodierten Query-String wird zu einem Leerzeichen, bevor Ihre Anwendung es überhaupt sieht, und / ist der Pfadtrenner in URLs. Dateinamen und Tokens haben noch ihre eigenen Beschwerden. RFC 4648, Abschnitt 5, löst das mit dem für URLs und Dateinamen sicheren Alphabet: + wird zu -, / zu _, und das =-Padding am Ende wird meist weggelassen. Der RFC besteht darauf, dass dies "nicht als das Gleiche wie die base64-Kodierung betrachtet werden sollte", also behandeln Sie es als eigenes Format, üblicherweise bekannt als base64url.

Die Erzeugung braucht zwei String-Operationen:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
var_dump(base64url_encode("hi>?there\x00bin")); // string(18) "aGk-P3RoZXJlAGJpbg"

Wann man es verwendet: JSON-Web-Token-Teile, OAuth-State- und Nonce-Parameter, API-IDs, die man in URL-Pfade setzt, und alles, was in eine Adressleiste oder einen Dateinamen kopiert wird. Wann man es nicht verwendet: E-Mail-Body, PEM-Rüstung und überall dort, wo am anderen Ende ein Consumer des Standard-Alphabets sitzt, denn - und _ sind nicht in deren Vokabular. Und mischen Sie die beiden Alphabete nicht still und leise: Ein URL-sicher kodiertes Token muss überall und für immer URL-sicher dekodiert werden. Das ist die ganze Interoperabilitäts-Regel von base64url.

Unicode und die Bytes, die Sie meinten

PHP-Strings sind Byte-Folgen, und base64_encode() kodiert die Bytes, die es bekommt, ohne zu fragen, was sie bedeuten. Das ist eine Stärke, bis zu dem Tag, an dem Ihr "Text" nicht wirklich die Kodierung ist, die Sie meinen. Der klassische Fehlschlag: ein String, der im Editor wie UTF-8 aussieht, aber als Windows-1252 aus einer Legacy-Quelle ankam. Kodiert man diese Bytes so wie sie sind, bekommt der Empfänger, der dekodiert und UTF-8 annimmt, Mojibake statt Ihrer Akzente.

Die Lösung ist, vor dem Kodieren zu normalisieren, mit der mbstring-Erweiterung (sie wird mit der PHP-Quelle ausgeliefert, ist aber nicht standardmäßig aktiviert):

$fromLegacy = "caf\xE9 au lait"; // Windows-1252-Bytes: Das é ist 0xE9
$utf8 = mb_convert_encoding($fromLegacy, 'UTF-8', 'Windows-1252');
$encoded = base64_encode($utf8);
var_dump($utf8); // string(13) "café au lait": Das é ist jetzt zwei UTF-8-Bytes

Wenn die Quelle bereits UTF-8 ist, kann man die Wandlung überspringen, und eine billige Plausibilitätsprüfung ist mb_check_encoding($utf8, 'UTF-8'). Ein Satz Rat: Versuchen Sie niemals, einen bereits kodierten Base64-String zu "reparieren", indem Sie ihn als Text neu kodieren. Das ist die Doppel-Kodierungs-Falle aus dem Stolperfallen-Abschnitt unten, und sie ist der mit Abstand häufigste Base64-Bug in PHP-Codebasen.

Dateien, Blobs und die .b64-Konvention

Der geradlinigste Kodier-Job: Eine Datei wird zu Text. PHP-Strings sind Bytes, also gibt es keinen "Binärmodus", um den man sich Sorgen machen müsste; file_get_contents() gibt Ihnen die exakten Bytes, und base64_encode() gibt Ihnen den exakten Text:

$binary = file_get_contents('/var/www/uploads/photo.png');
$encoded = base64_encode($binary);
file_put_contents('/var/www/uploads/photo.png.b64', $encoded);
var_dump(strlen($binary), strlen($encoded)); // die 33-prozentige Größenrechnung, jedes Mal

Zwei Gewohnheiten halten das sicher. Erstens: Wissen Sie, was Sie kodieren. Die finfo-Klasse (die fileinfo-Erweiterung, in Standard-PHP-Builds enthalten) sagt Ihnen den echten Typ aus den Bytes, nicht aus dem Dateinamen:

$mime = (new finfo(FILEINFO_MIME_TYPE))->file('/var/www/uploads/photo.png');
var_dump($mime); // string(9) "image/png"

Zweitens: Denken Sie an die Größenrechnung, wenn Sie den Speicher planen: Ein 500-KB-Bild wird zu einer 670-KB-Textdatei, und ein 1-GB-Video zu einer 1,33-GB-Textdatei. Deshalb existiert der Abschnitt über große Daten unten.

Data-URIs: Ein Bild in die Seite setzen

Ein Data-URI bettet den Payload direkt in die URL ein, damit keine zweite Anfrage nötig ist, um ihn abzuholen. RFC 2397 definiert die Form: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma und dann die Daten. Bei binären Medien wie Bildern ist das Flag vorhanden, also ist der Payload genau das, was base64_encode() erzeugt hat:

function data_uri_for(string $path): string
{
  $mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
  $payload = base64_encode(file_get_contents($path));
  return 'data:' . $mime . ';base64,' . $payload;
}
$uri = data_uri_for('/var/www/uploads/avatar.png');
echo '<img src="' . $uri . '" alt="avatar" />' . "\n";

Warum hier Base64? Weil ein URI keine rohen Bytes oder Kommas sicher enthalten kann, und das Base64-Alphabet keinerlei Maskierung braucht. Die Kompromisse sind real. Der kodierte Payload ist etwa 33 Prozent größer als die Datei, wodurch das HTML-Dokument selbst größer wird. Browser cachen einen Data-URI nicht so wie eine Datei-URL, also werden die Bytes bei jedem Seitenaufruf neu heruntergeladen. Und der RFC selbst sagt, Data-URIs seien nur für kurze Werte nützlich; alte HTML-Parser hatten harte Limits auf die Attributlänge, und moderne Browser, zwar deutlich großzügiger, mögen Megabytes innerhalb eines Tags trotzdem nicht. Verwenden Sie sie für Avatare, Icons und kleine Inline-Grafiken; für alles andere echte Dateien.

JWTs und API-Tokens

JSON Web Tokens sind das Flaggschiff unter den Base64-Nutzern in modernen APIs, und sie verwenden den URL-sicheren, ungepaddeten Dialekt aus dem Abschnitt oben. Laut RFC 7519 besteht ein kompakter JWT aus drei durch Punkte getrennten base64url-Teilen: Header, Payload, Signatur. Header und Payload sind schlichtes JSON; die Signatur sind rohe Bytes. Ein JWT von Hand zu bauen ist ein angenehmer Weg, jedes bewegliche Teil zu sehen:

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'iat' => 1516239022,
]));
$signingInput = $header . '.' . $payload;
$signature = base64url_encode(hash_hmac('sha256', $signingInput, $secret, true));
$token = $signingInput . '.' . $signature;

Zwei Dinge fallen auf. Die Signatur ist die base64url-Kodierung der rohen HMAC-Bytes, deshalb wird hash_hmac() mit true für die rohe Ausgabe aufgerufen. Und der Header und der Payload sind für jeden lesbar, und das ist so gewollt: Ein JWT ist ein signierter Inhaberschein, kein Geheimnis. In der Produktion baut man Signierung oder Verifikation nicht von Hand. Das Community-Paket ist firebase/php-jwt (v7, erfordert PHP 8.0 oder neuer), installiert mit Composer:

composer require firebase/php-jwt
use Firebase\JWT\JWT;
$secret = 'correct-horse-battery-staple-long-enough-secret';
$token = JWT::encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'exp' => time() + 3600,
], $secret, 'HS256');
var_dump(substr_count($token, '.')); // int(2): header, payload, Signatur

Ein Versionshinweis zur v7-Linie der Bibliothek: Die HMAC-Algorithmen erzwingen eine minimale Schlüssellänge, also wird ein HS256-Secret, das kürzer als 32 Bytes ist, abgelehnt, bevor überhaupt kodiert wird. Lange Secrets sind ohnehin die Norm; das sorgt nur dafür, dass die Bibliothek nicht nachlässig damit umgeht.

Die Bibliothek übernimmt die base64url-Wandlung, die Signierung und die Ablauf-Checks für Sie, und wirft typisierte Ausnahmen, statt halb vertrauenswürdige Daten zurückzugeben. Wenn man mit ihr Tokens schreibt, berührt man base64_encode() nie direkt, und genau so soll es sein.

HTTP: Basic Auth und der WebSocket-Handshake

Zwei Header-Bau-Jobs, bei denen PHP das Base64 macht und das Protokoll den Rest.

HTTP-Basic-Auth (RFC 7617): Der Client sendet Authorization: Basic plus das Base64 von username:password. Bauen ist eine einzige String-Konkatenation:

function basic_authorization_header(string $username, string $password): string
{
  return 'Basic ' . base64_encode($username . ':' . $password);
}
$headers[] = 'Authorization: ' . basic_authorization_header('alice', 'secret123');

Sprechen Sie es einmal laut aus, denn der RFC zwingt Sie dazu: Dies ist Kodierung, kein Schutz. Jeder, der eine Paket-Verfolgung hat, stellt beide Hälften mit einer einzigen Taste wieder her, also gehört Basic Auth nur auf HTTPS-Verbindungen.

Der WebSocket-Handshake (RFC 6455): Der Server beweist, dass er den Client gehört hat, indem er einen verwandelten Schlüssel zurückechoed. Er konkateniert den Sec-WebSocket-Key des Clients mit einer festen Magic-GUID, nimmt den SHA-1 des Ergebnisses und base64-kodiert das Digest. Das ist Standard-Base64, Padding inklusive, denn es lebt in einem Header, nicht in einer URL:

function websocket_accept_key(string $clientKey): string
{
  return base64_encode(hash('sha1', $clientKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
$accept = websocket_accept_key('dGhlIHNhbXBsZSBub25jZQ==');
var_dump($accept); // string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

Dieses Beispiel ist das aus dem RFC selbst, was es zu einem praktischen Selbsttest macht: Erzeugt Ihre Implementierung dieselben 28 Zeichen, so spricht die WebSocket-Ebene korrekt.

E-Mail: der ursprüngliche Anwendungsfall

Alles andere in diesem Artikel ist ein Nachkomme einer einzigen Tatsache: SMTP wurde entwickelt, um 7-Bit-ASCII zu tragen, und die Leute wollten Binärdaten senden. Die Antwort des MIME-Standards, in RFC 2045, Abschnitt 6.8, war Base64 als Content-Transfer-Encoding, mit den zwei Hausregeln, die Sie bereits getroffen haben: Zeilen von höchstens 76 Zeichen und Decoder, die jedes Zeichen außerhalb des Alphabets ignorieren. Also reist ein PDF-Anhang so:

$pdf = file_get_contents('/var/www/uploads/report.pdf');
$attachment = chunk_split(base64_encode($pdf), 76, "\r\n");
// die Mail-Bibliothek legt jetzt $attachment in das MIME-Teil,
// mit Content-Transfer-Encoding: base64

Die praktischen Zahlen: Die Kodierung selbst kostet 33 Prozent, und das CRLF alle 76 Zeichen kostet ein wenig mehr, also wird ein 100-KB-Anhang als grob 137 KB Text verschickt. Wenn man E-Mail aus PHP schreibt, erledigen die Bibliotheken (PHPMailer und seine stabilen Verwandten) das Umwickeln für einen, und man reicht ihnen das rohe Binär. Wenn man jemals eine 76 Zeichen breite Wand aus Buchstaben in einer rohen .eml-Datei sieht, kennt man nun den exakten Algorithmus, der sie erzeugt hat.

PEM-Rüstung für Schlüssel und Zertifikate

Schlüssel und Zertifikate brauchen mehr als eine Wand aus Buchstaben; sie brauchen Etiketten. PEM-Rüstung ist eine BEGIN-Zeile, ein 64 Zeichen umwickelter Base64-Block und eine END-Zeile, eine Konvention, die aus Privacy-Enhanced Mail (RFC 1421) stammt und von OpenSSL am Leben gehalten wird. Die openssl-Erweiterung von PHP erzeugt und konsumiert diese Form direkt:

$res = openssl_pkey_new([
  'private_key_bits' => 2048,
  'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $pem);
// $pem ist bereits gerüstet: BEGIN-Etikett, 64-Zeichen-Zeilen, END-Etikett

Der interessante Fall ist, wenn die Rüstung von Hand neu aufgebaut werden muss, zum Beispiel, wenn man rohe DER-Bytes von einer API erhält und eine PEM-Datei für ein Tool braucht, das nur PEM liest. Die Konvention lautet 64 Zeichen pro Zeile, LF-Zeilenumbrüche und ein Etikett, das den Inhalt benennt:

$rearmored = "-----BEGIN CERTIFICATE-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END CERTIFICATE-----\n";
var_dump(openssl_x509_parse($rearmored) !== false); // bool(true): die Rüstung ist gültig

Das Etikett falsch, und die Datei ist Müll, egal wie perfekt das Base64 ist. Und die Zeilenlänge falsch, und die meisten Tools lesen sie trotzdem, denn Decoder ignorieren Zeilenumbrüche, aber Diff-Tools und Menschen werden leiden. Sechsundsechzig ist die Zahl.

Konfigurationsdateien, Umgebungsvariablen und Datenbanken

Base64 ist ein Text-Container, was es zu einem Schmuggelwerkzeug für Werte macht, die sonst ihren Container sprengen würden. Eine Datenbank-DSN voller Semikolons und Anführungszeichen, ein JWT in einer .env-Datei, ein binärer Blob in einer TEXT-Spalte: Sie alle werden zu einer einzigen langen, sicheren Zeichenkette.

Die Umgebungsvariable ist ein Zweistufen-Ritual. Einmal, auf der Maschine, die die Konfiguration baut, kodiert man:

echo 'DB_DSN_B64=' . base64_encode('pg:host=db;password=qu"ote') . PHP_EOL;

Dann, bei jedem Start der Anwendung, dekodiert und validiert man beim Hochfahren, damit eine halb hineingeklebte Konfiguration laut statt kryptisch scheitert:

$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}

Für Datenbanken speichert dieselbe Idee Binärdaten in Text-Spalten. Die Größenrechnung gilt: Der gespeicherte Wert ist etwa 33 Prozent größer als der Blob, also belegt eine 1-MB-Datei grob 1,33 MB in der Spalte, und man sollte den Spaltentyp mit im Blick wählen. Und dieselbe Warnung wie überall sonst: Dies ist Format-Sicherheit, keine Geheimhaltung. Jeder, der die Konfiguration lesen oder die Spalte abfragen kann, kehrt sie in einem einzigen Aufruf um. Wenn der Wert sensibel ist, verschlüsseln Sie ihn; Base64 macht ihn nur transportfähig.

Große Daten, ruhiger Speicher

Kodieren ist die Richtung, die einem was kostet: Die Ausgabe ist ein Drittel größer als die Eingabe, also will ein 2-GB-Binär 2,66 GB kodierten String im Speicher. In einem langlebigen Web-Prozess oder auf einem Host mit engen Speicherlimits ist das ein Grund zum Streamen statt zum Einlesen, und PHP gibt einem zwei Wege.

Der erste Weg ist der convert.base64-encode-Stream-Filter, der Streaming-Zwilling der Funktion. Er unterstützt Parameter als assoziatives Array: line-length für die Umbruch-Breite und line-break-chars für den Trenner, was den Effekt von chunk_split() nachbildet, ohne den ganzen String zu halten:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, [
  'line-length' => 64,
  'line-break-chars' => "\n",
]);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

Der zweite Weg ist der klassische 57-Byte-Trick, und er ist ein kleines Stück PHP-Folklore. Eine 76-Zeichen-MIME-Zeile fasst genau 57 Bytes Originaldaten, also wenn man die Eingabedatei in Chunks liest, die ein Vielfaches von 57 Bytes sind, kodiert jedes Chunk unabhängig, ohne Rest-Bits zwischen den Chunks zu tragen. Lesen in 8151-Byte-Chunks (57 mal 143: eine Ausgabe aus 143 vollen 76-Zeichen-Zeilen, nah am traditionellen 8192-Byte-I/O-Puffer von PHP) hält den Speicher flach, während die Datei MIME-perfekt herausströmt:

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
while (!feof($in)) {
  $plain = fread($in, 57 * 143);
  $encoded = chunk_split(base64_encode($plain), 76, "\r\n");
  fwrite($out, $encoded);
}
fclose($in);
fclose($out);

Welchen wählt man? Den Filter, wenn man PHP die Rohrleitung besitzen lassen will und die exakten Chunk-Grenzen egal sind; die 57-Byte-Schleife, wenn man deterministische MIME-Ausgabe, Fortschritts-Hooks oder eine harte Obergrenze auf die Puffergröße will. Entweder so oder so bleibt der Fußabdruck bei einem Chunk, nicht bei einer Datei.

Stolperfallen mit PHP-Akzent

Die Fallen, die in echten PHP-Codebasen auftauchen, an einem Ort gesammelt:

  • Doppel-Kodierung. Der Klassiker: Ein Wert, der bereits Base64 ist (aus einer Umgebungsvariable, einer Datenbank, einem früheren Skript), geht nochmal durch base64_encode(), weil niemand gecheckt hat. Das Ergebnis dekodiert sich einmal und liefert... mehr Base64. Die Heilung ist eine Roundtrip-Prüfung an der Grenze, oder eine einzige bekannte Funktion, der im Codebase die gesamte Kodierung überlassen ist.
  • Das + in URLs. Die Standard-Ausgabe enthält + und /. In einem formular-kodierten Query-String wird das Plus zu einem Leerzeichen, bevor der Code es sieht, und in einem Pfad ist es ein Trenner. Für alles, was an eine URL gebunden ist: base64url ausgeben oder den ganzen Wert mit rawurlencode() percent-kodieren.
  • Der Schluss-Trenner. chunk_split() beendet das Ergebnis mit dem Trenner, selbst bei exakten Vielfachen der Zeilenlänge. Eine Leerzeile am Ende ist meist harmlos (Decoder ignorieren sie), aber sie trippt naive Zeilen-Zähler und Diff-Tools. rtrim() den Trenner, wenn der Consumer empfindlich ist.
  • Umbruch-Mismatch. Mit 76-Zeichen-MIME-Zeilen zu schreiben, während der Consumer 64-Zeichen-PEM-Zeilen erwartet (oder umgekehrt), ist kein Dekodier-Problem, denn Decoder ignorieren Brüche, aber es ist ein Problem für Zeilen-Tooling und für Menschen, die lesen. Wählen Sie die Konvention, die Ihr Ziel erwartet, und halten Sie sich daran.
  • Das trailing Newline ist Daten. base64_encode() kodiert jedes Byte, einschließlich des Zeilenumbruchs am Ende einer Textdatei. Wenn zwei Systeme "unterschiedliches" Base64 für gleich aussehenden Text erzeugen, ist der übliche Verdächtige ein trailing \n.
  • Base64 ist keine Verschlüsselung. Ein Passwort vor dem Eintreffen in der Datenbank zu kodieren schützt es nicht, es formatiert es. Die "verschlüsselte" Spalte ist für jeden mit Query-Zugriff nur ein Funktionsaufruf vom Klartext entfernt. Verschlüsseln oder Hashen Sie echte Secrets. Base64 ist ein Transport-Kostüm.
  • Der Speicher wird um ein Drittel größer. Auf einem 32-Bit-PHP-Build oder einem Host mit engen Speicherlimits kann das Kodieren eines großen Binärs schlicht fehlschlagen. Streamen Sie es, wie oben gezeigt, bevor Sie memory_limit justieren.
  • Zeilenumbrüche werden nie hinzugefügt, nie. "MIME-Base64" in der Funktionsbeschreibung bedeutet nicht "MIME-umwickelte Ausgabe". Wenn Ihre Ausgabe 76-Zeichen-Zeilen braucht, fügen Sie sie mit chunk_split() oder dem Filter hinzu.

Eine kurze Geschichte von base64_encode

Die Kodierungs-Seite der PHP-Geschichte ist fast erfrischend langweilig, im besten Sinne. base64_encode() kam in PHP 4 als Kernfunktion mit einem Parameter und ohne Optionen, und sie hat seitdem keinen einzigen davon dazubekommen. Strict-Modus war nie nötig (wenn man selbst die Daten produziert, gibt es nichts, worüber man streng sein könnte), eine Padding-Option wurde nie hinzugefügt, und der Umbruch-Job wurde von Tag eins an chunk_split() übergeben, deshalb sitzen die beiden Funktionen immer noch Seite an Seite in den "Siehe auch"-Listen des Manuals.

Das Manual trägt die 33-Prozent-Zahl so lange, wie sich jemand erinnern kann: "Base64-kodierte Daten brauchen etwa 33 % mehr Platz als die Originaldaten." Dieser Satz ist heute noch da, und er ist der Grund, warum die Zahl in diesem Artikel überhaupt erscheint. Der Stream-Filter convert.base64-encode kam später dazu, und er hatte seinen eigenen Bug, aus dem er herauswachsen musste: 2015 behob PHP einen Defekt (Bug #68532), bei dem der Filter im Lese-Modus auf Speicher-Streams das letzte Padding-Zeichen auslassen konnte, was genau die Art von stiller Korruption ist, die der funktionsbasierte Weg nie erleidet. PHP 8.0 fügte die nativen string-Parameter- und Rückgabewerttypen hinzu, und da endet das Changelog. Eine Funktion, ein Parameter, zwanzig Jahre, null Optionen: ein Denkmal dafür, die Oberfläche schon beim ersten Mal richtig zu bekommen.

Lustige PHP-Fakten

Denn eine Referenz ist ohne Kuriositäten nicht komplett:

  • Die leere Identität. base64_encode('') ist ''. Kein Padding, keine Ausgabe, keine Überraschungen: Leere rein, Leere raus.
  • Eine seltsame Adresse. Das PHP-Manual listet base64_encode() im Buch "Weitere Basis-Erweiterungen" unter "URLs" auf. Es gibt kein "Kodierung"-Kapitel; unter "URLs" findet man es, neben parse_url().
  • Das Alphabet hat sich nie bewegt. Dieselben 64 Zeichen kommen seit PHP 4 aus dem PHP-Encoder. Ein Base64-String, den ein PHP-4-Skript auf einer Windows-Kiste im Jahr 2001 erzeugt hat, dekodiert heute auf PHP 8.4 unter Linux identisch. Das ist Interoperabilität mit einer 25-jährigen Erfolgsbilanz.
  • Ein Byte und drei Bytes sehen in der Länge identisch aus. Beide erzeugen vier Zeichen, nur das Padding unterscheidet sie. Deshalb existiert die Größentabelle in diesem Artikel.
  • Siebenundfünfzig ist eine magische Zahl. Eine 76-Zeichen-MIME-Zeile fasst genau 57 Bytes Originaldaten, und genau diese Übereinstimmung macht die Streaming-Chunk-Schleife im großen Daten-Abschnitt möglich, ohne Zustand zwischen den Lesezugriffen zu tragen.
  • Es hat ein Dial-Up-Geschwister. Die "Siehe auch" von base64_encode() listet sogar convert_uuencode(), den PHP-Wrapper für uuencode, das Format, das Binärdaten für E-Mail kodiert hat, bevor MIME Base64 standardisierte. Es lebt im Kapitel "String-Funktionen", aber es ist der fossile Nachweis des ursprünglichen Zwecks dieser Funktion.
  • Alte Browser waren picky beim Padding. Ein php.net-Hinweis aus dem Jahr 2004 berichtet, dass Internet Explorer Cookie-Namen mit = ablehnte, deshalb streicht erfahrener Code manchmal die trailing Pads von Base64, das in Cookies gespeichert ist. Moderne Setups brauchen diesen Trick nicht, aber er erklärt die seltsamen rtrim($x, '=')-Aufrufe, die man erben kann.

Die andere Seite

Das ist die Kodierungs-Seite, und sie ist die einfachere der beiden: Die Funktion kann nicht scheitern, die Ausgabe ist deterministisch, und das Format ist eines, das man von Anfang bis Ende kontrolliert. Die schwierigere Richtung ist die, in der man das Base64 anderer erhält: deren Padding-Entscheidungen, deren Zeilenumbrüche, deren URL-sichere Dialekte, deren beschädigtes Einfügen. Genau dort braucht ein Decoder den Strict-Modus, eine Validierungs-Pipeline und eine gesunde Skepsis gegenüber allem. Base64-Dekodierung in PHP, von dieser Seite verlinkt, deckt die Dekodierungs-Seite in derselben Tiefe ab.

Zuletzt aktualisiert: 2026-09-08

Verwandter Artikel: Base64-Dekodierung in PHP: Ein vollständiger Leitfaden