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 mag. Ein binärer Blob, der in ein JSON-Feld 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 reisen wird. Das ist der tägliche Umgang mit Base64-Kodierung: Sie schreibt jeweils drei Bytes Rohdaten als vier Zeichen aus einem Alphabet mit 64 Zeichen um, und ein oder zwei =-Zeichen runden das Ende ab, sodass das Ergebnis ein simpler Text ist, den alles tragen kann. Die Startseite dieser Site erklärt das Format vollständig, also konzentriert sich dieser Artikel darauf, was PHP Ihnen gibt, was es still und leise für Sie entscheidet und wo die Fallen sind.

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

Am Ende dieses Artikels wissen Sie, wie man jeden Geschmack von Base64 erzeugt, den ein PHP-Entwickler tatsächlich trifft: Einzeilen-Ausgabe, MIME-umbrachte E-Mails, PEM-umrüstete Schlüssel, URL-sichere Tokens und Data-URIs, dazu die Streaming-Tricks für den Fall, dass die Daten zu groß sind, um im Speicher gehalten zu werden.

Eine Funktion, null Optionen

Die komplette API, genau so, wie sie modernes PHP meldet:

base64_encode(string $string): string

Lesen Sie das noch einmal. Ein Parameter, ein Rückgabewert, keine Flags. Das Handbuch beschreibt es als MIME-Base64, "entworfen, damit Binärdaten den Transport durch Transport-Ebenen überstehen, die nicht 8-bit-sauber sind, wie etwa Mail-Körper". Beachten Sie, was diese Formulierung nicht verspricht: keine Zeilenbrüche, kein Umbruch, keine Meinung dazu, wo die Ausgabe leben wird. Die Funktion emittiert eine lange Zeile, und welchen Umbruch das Ziel auch immer will, ist Ihre Aufgabe 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 coaleszen Sie jeden nullable Wert zuerst zu ''.

Die Ausgabe-Größe folgt einem festen Muster, das Sie vor dem Aufruf vorhersagen können:

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

Das Muster ist vier Zeichen für jede komplette Gruppe von drei Bytes, plus eine letzte Teilgruppe, die mit ein 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 exakte Eingabe-Größe. Sie können sie schätzen (durch vier teilen, mit drei multiplizieren, die Pads abziehen), aber Sie können sie nicht exakt ablesen.

Wo die Zeilenbrüche hingehören

Da base64_encode() nie von allein umbricht, ist die Umbruch-Entscheidung ein Ziel-Problem. In der Praxis gibt es drei Antworten.

Keine Zeilenbrüche. Die rohe Funktionsausgabe, genau eine Zeile. Das ist es, was Sie für URLs, JSON-Payloads, Header, Datenbankwerte und alles andere wollen, wo ein Zeilenbruch ein Bug wäre. Das meinen auch die meisten mit "gib mir einfach das Base64".

MIME-Wrapping: 76 Zeichen plus CRLF. Die E-Mail-Konvention aus RFC 2045, Abschnitt 6.8: kodierte Zeilen dürfen 76 Zeichen nicht überschreiten, und Dekodierer müssen die Zeilenbrüche ignorieren. Der klassische Begleitauftritt ist chunk_split(), den das Handbuch genau aus diesem Grund in seiner "Siehe auch"-Liste mit base64_encode() zusammenführt:

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

PEM-Wrapping: 64 Zeichen plus LF. Schlüssel und Zertifikate nutzen die ältere Privacy-Enhanced-Mail-Konvention (RFC 1421): kürzere 64-Zeichen-Zeilen. Gleiches 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()-Haken gilt für die zwei umbrachten Geschmacksrichtungen: Die Funktion hängt das Trennzeichen ans Ende des Ergebnisses, auch wenn die Eingabe-Länge ein exaktes Vielfaches der Zeilenlänge ist. Wenn ein nachgelagerter Verbraucher über eine leere Zeile am Ende stolpert, ist das der Grund; ein rtrim()-Aufruf auf das Trennzeichen behebt es. Beachten Sie auch die Asymmetrie, die Sie irgendwann retten wird: Dekodierer ignorieren Zeilenbrüche komplett, also dekodieren ein MIME-umbrachtes und ein unumbrachtes 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 Ärger. Ein + in einer formular-kodierten Query-String wird zu einem Leerzeichen, bevor Ihre Anwendung es je sieht, und / ist das Pfad-Trennzeichen in URLs. Dateinamen und Tokens haben ihre eigenen Beschwerden. RFC 4648, Abschnitt 5, löst das mit dem URL- und Dateinamen-sicheren Alphabet: + wird zu -, / wird zu _, und das abschließende =-Padding wird meist weggelassen. Der RFC besteht darauf, dass dies "nicht als dasselbe wie die base64-Kodierung betrachtet werden sollte", also behandeln Sie es als eigenständiges Format, das gemeinhin base64url heißt.

Es zu erzeugen sind 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 Sie es verwenden: JSON-Web-Token-Teile, OAuth-State- und Nonce-Parameter, API-IDs, die Sie in URL-Pfade legen, und alles, was in eine Adressleiste oder einen Dateinamen kopiert wird. Wann Sie es nicht verwenden: E-Mail-Körper, PEM-Rüstung und überall, wo am anderen Ende ein Verbraucher mit Standard-Alphabet sitzt, denn - und _ sind nicht in ihrem Wortschatz. Und mischen Sie die beiden Alphabete nie still und leise: Ein URL-sicher kodierter Token muss überall und für immer URL-sicher dekodiert werden. Das ist die komplette Interoperabilitäts-Regel von base64url.

Unicode und die Bytes, die Sie meinten

PHP-Strings sind Byte-Folgen, und base64_encode() kodiert, welche Bytes es bekommt, ohne zu fragen, was sie bedeuten. Das ist ein Feature bis zum Tag, an dem Ihr "Text" nicht tatsächlich die Kodierung ist, die Sie glauben. Der klassische Fehler: Ein String, der in Ihrem Editor wie UTF-8 aussieht, aber von einer Legacy-Quelle als Windows-1252 ankam. Kodieren Sie diese Bytes unverändert, und der Empfänger, der dekodiert und UTF-8 annimmt, bekommt Mojibake statt Ihrer umgelauten Buchstaben.

Die Lösung ist, vor der Kodierung 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 schon UTF-8 ist, können Sie die Konvertierung überspringen, und ein billiger Sanitäts-Check ist mb_check_encoding($utf8, 'UTF-8'). Ein Satz voller Rat: Versuchen Sie nie, einen bereits kodierten Base64-String zu "beheben", indem Sie ihn als Text erneut kodieren. Das ist die Doppelkodierungs-Falle aus dem Fehlerquellen-Abschnitt unten, und sie ist der mit Abstand häufigste Base64-Bug in PHP-Codebases.

Dateien, Blobs und die .b64-Konvention

Die geradeste Kodierungsaufgabe: Eine Datei wird zu Text. PHP-Strings sind Bytes, also gibt es keinen "Binärmodus", um den Sie sich sorgen müssten; 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%-Rechnung, 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ößen-Rechnung, wenn Sie 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 große-Daten-Abschnitt unten.

Data-URIs: Ein Bild in die Seite bringen

Eine Data-URI bettet das Payload direkt in die URL ein, sodass keine zweite Anfrage nötig ist, um es zu holen. RFC 2397 definiert die Form: data:, ein optionaler Medientyp, ein optionales ;base64-Flag, ein Komma und die Daten. Bei binären Medien wie Bildern ist das Flag vorhanden, also ist das Payload genau das, was base64_encode() produziert 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 eine URI keine rohen Bytes oder Kommas sicher enthalten kann, und das Base64-Alphabet überhaupt kein Escaping braucht. Die Abwägungen sind allerdings real. Das kodierte Payload ist etwa 33 Prozent größer als die Datei, wodurch das HTML-Dokument selbst größer wird. Browser cachen eine Data-URI nicht so, wie sie eine Datei-URL cachen, also werden bei jedem Seitenaufruf die Bytes neu heruntergeladen. Und der RFC selbst sagt, dass Data-URIs nur für kurze Werte nützlich sind; alte HTML-Parser hatten harte Limits für die Attribut-Länge, und moderne Browser, die zwar großzügiger sind, mögen Megabytes in einem Tag trotzdem nicht. Verwenden Sie sie für Avatare, Icons und kleine Inline-Grafiken; für alles andere nehmen Sie echte Dateien.

JWTs und API-Tokens

JSON Web Tokens sind der prominenteste Base64-Verbraucher in modernen APIs, und sie nutzen den URL-sicheren, unpaddierten Dialekt aus dem Abschnitt oben. Laut RFC 7519 besteht ein kompakter JWT aus drei durch Punkte getrennten base64url-Teilen: Header, Payload, Signatur. Der Header und das Payload sind einfaches JSON; die Signatur sind rohe Bytes. Einen von Hand zu bauen ist eine angenehme Art, 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 zu beachten. Die Signatur ist die base64url-Kodierung roher HMAC-Bytes, deshalb wird hash_hmac() mit true für rohe Ausgabe aufgerufen. Und der Header und das Payload sind für jeden lesbar, was so gewollt ist: Ein JWT ist eine signierte Eintrittskarte, kein Geheimnis. In der Produktion basteln Sie weder Signierung noch Verifizierung selbst. Das Community-Paket ist firebase/php-jwt (v7, benötigt 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

Versionshinweis für die v7-Linie der Bibliothek: Die HMAC-Algorithmen setzen eine minimale Schlüssel-Länge durch, daher wird ein HS256-Secret, das kürzer als 32 Bytes ist, abgelehnt, bevor überhaupt kodiert wird. Lange Secrets sind ohnehin der Normalfall; das sorgt nur dafür, dass die Bibliothek keinen lockeren Umgang damit duldet.

Die Bibliothek erledigt die base64url-Konvertierung, die Signierung und die Ablauf-Checks für Sie, und sie wirft typisierte Ausnahmen, statt halbvertraute Daten zurückzugeben. Wenn Sie damit Tokens schreiben, berühren Sie base64_encode() nie direkt, was genau so sein soll.

HTTP: Basic Auth und der WebSocket-Handshake

Zwei Header-Bau-Aufgaben, 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. Es zu bauen ist eine String-Konkatenation:

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

Sagen Sie es einmal laut, denn der RFC verlangt es: Das ist Kodierung, kein Schutz. Jeder mit einer Packet-Capture holt beide Hälften mit einer einzigen Taste zurück, 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 transformierten Key zurücksendet. Er konkateniert den Sec-WebSocket-Key des Clients mit einer festen magischen GUID, nimmt den SHA-1 des Ergebnisses und base64-kodiert das Digest. Das ist Standard-Base64, Padding inklusive, weil es in einem Header lebt, 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: Wenn Ihre Implementation dieselben 28 Zeichen produziert, spricht die WebSocket-Ebene korrekt.

E-Mail: der Original-Use-Case

Alles andere in diesem Artikel ist ein Nachfahre einer einzigen Tatsache: SMTP wurde zum Transport von 7-Bit-ASCII entworfen, 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 kennen: Zeilen von höchstens 76 Zeichen und Dekodierer, die jedes Zeichen außerhalb des Alphabets ignorieren. So reist ein PDF-Anhang:

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

Die praktischen Zahlen: Die Kodierung selbst kostet 33 Prozent, und das CRLF alle 76 Zeichen kostet ein bisschen mehr, also verschickt ein 100-KB-Anhang als rund 137 KB Text. Wenn Sie aus PHP Mails schreiben, machen die Bibliotheken (PHPMailer und seine verwandte Sippe) das Wrapping für Sie, und Sie geben ihnen das rohe Binär. Wenn Sie in einer rohen .eml-Datei je eine 76 Zeichen breite Wand aus Buchstaben sehen, kennen Sie jetzt 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 Labels. PEM-Rüstung ist eine BEGIN-Zeile, ein 64-Zeichen-umbrachter Base64-Block und eine END-Zeile, eine Konvention, die aus Privacy-Enhanced Mail (RFC 1421) stammt und von OpenSSL am Leben gehalten wird. PHPs openssl-Erweiterung erzeugt und verarbeitet 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-Label, 64-Zeichen-Zeilen, END-Label

Der interessante Fall ist, wenn die Rüstung von Hand neu aufgebaut werden muss, zum Beispiel wenn Sie rohe DER-Bytes von einer API erhalten und eine PEM-Datei für ein Tool brauchen, das nur PEM liest. Die Konvention ist 64 Zeichen pro Zeile, LF-Zeilenbrüche und ein Label, 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

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

Konfigurationsdateien, Umgebungsvariablen und Datenbanken

Base64 ist ein Textcontainer, was es zu einem Schmuggelwerkzeug für Werte macht, die sonst ihren Container kaputtmachen würden. Eine Datenbank-DSN voller Semikolons und Anführungszeichen, ein JWT in einer .env-Datei, ein binärer Blob in einer TEXT-Spalte: All das wird zu einem langen, sicheren String.

Die Umgebungsvariablen-Geschmacksrichtung ist ein zweistufiges Ritual. Einmal, auf dem Rechner, der die Konfiguration baut, kodieren Sie:

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

Dann, bei jedem Start der Anwendung, dekodieren und validieren Sie beim Hochfahren, damit eine halb eingefügte Konfiguration deutlich scheitert, statt kryptisch:

$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ößen-Rechnung gilt: Der gespeicherte Wert ist etwa 33 Prozent größer als der Blob, also belegt eine 1-MB-Datei in der Spalte etwa 1,33 MB, und Sie sollten den Spaltentyp damit im Hinterkopf wählen. Und dieselbe Warnung wie überall: Das ist Formatsicherheit, kein Schutz. Jeder, der die Konfiguration lesen oder die Spalte abfragen kann, kann es in einem einzigen Aufruf rückgängig machen. Wenn der Wert sensibel ist, verschlüsseln Sie ihn; Base64 macht ihn nur portabel.

Große Daten und stabiler Speicher

Kodieren ist die Richtung, die Sie was kostet: Die Ausgabe ist ein Drittel größer als die Eingabe, also möchte ein 2-GB-Binär 2,66 GB kodierten String im Speicher. Auf einem langlebigen Web-Prozess oder einem Host mit engen Speicherlimits ist das ein Grund zu streamen statt alles auf einmal einzulesen, und PHP gibt Ihnen 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 das Trennzeichen, was den chunk_split()-Effekt 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-Überlieferung. Eine 76-Zeichen-MIME-Zeile hält genau 57 Bytes Ausgangsdaten, wenn Sie also die Eingabedatei in Chunks eines Vielfachen von 57 Bytes lesen, kodiert jeder Chunk unabhängig, ohne Rest-Bits, die zwischen den Chunks getragen werden müssen. Das Lesen in 8151-Byte-Chunks (57 mal 143: 143 volle 76-Zeichen-Zeilen Ausgabe, nah an PHPs traditionellem 8192-Byte-I/O-Puffer) hält den Speicher flach, während die Datei MIME-perfekt herausstreamt:

$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ählen Sie? Den Filter, wenn Sie PHP die Kleinarbeit übernehmen lassen und Ihnen die exakten Chunk-Grenzen egal sind; die 57-Byte-Schleife, wenn Sie deterministische MIME-Ausgabe, Fortschritts-Haken oder ein hartes Limit für die Puffergröße wollen. Im einen wie im anderen Fall bleibt der Speicher-Fußabdruck bei einem Chunk, nicht einer Datei.

Fehlerquellen mit PHP-Akzent

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

  • Doppelkodierung. Der Klassiker: Ein Wert, der schon Base64 ist (aus einer Umgebungsvariable, einer Datenbank, einem früheren Skript), wird nochmal durch base64_encode() geschickt, weil niemand nachgeschaut hat. Das Ergebnis dekodiert sich einmal und ergibt... mehr Base64. Das Heilmittel ist ein Round-Trip-Check an der Grenze oder eine einzelne, bekannte Funktion, die die gesamte Kodierung in der Codebase besitzt.
  • Das + in URLs. Die Standard-Ausgabe enthält + und /. In einer formular-kodierten Query-String wird das Plus zu einem Leerzeichen, bevor Ihr Code es sieht; in einem Pfad ist es ein Trennzeichen. Für alles, was an URLs gebunden ist, emittieren Sie base64url oder percent-kodieren Sie den ganzen Wert mit rawurlencode().
  • Das Trennzeichen am Ende. chunk_split() beendet sein Ergebnis mit dem Trennzeichen, auch bei exakten Vielfachen der Zeilenlänge. Eine leere Zeile am Ende ist meist harmlos (Dekodierer ignorieren sie), aber sie bringt naive Zeilenzähler und Diff-Tools aus der Fassung. rtrim()en Sie das Trennzeichen, wenn der Verbraucher anspruchsvoll ist.
  • Umbruch-Mismatch. Mit 76-Zeichen-MIME-Zeilen zu schreiben und einen Verbraucher zu haben, der 64-Zeichen-PEM-Zeilen erwartet (oder umgekehrt), ist kein Dekodierungsproblem, denn Dekodierer ignorieren Brüche, aber es ist ein Zeilen-Tooling- und Menschen-Leseproblem. Wählen Sie die Konvention, die Ihr Ziel erwartet, und bleiben Sie dabei.
  • Der Zeilenumbruch am Ende ist Daten. base64_encode() kodiert jedes Byte, einschließlich eines Zeilenumbruchs am Ende einer Textdatei. Wenn zwei Systeme für scheinbar denselben Text "unterschiedliche" Base64-Werte erzeugen, ist ein \n am Ende der übliche Verdächtige.
  • Base64 ist keine Verschlüsselung. Ein Passwort zu kodieren, bevor es die Datenbank erreicht, schützt es nicht; es formatiert es. Die "verschlüsselte" Spalte ist für jeden mit Abfrage-Zugriff einen einzigen Funktionsaufruf vom Klartext entfernt. Verschlüsseln oder hashen Sie echte Secrets; Base64 ist eine Transport-Verkleidung.
  • Der Speicher ist ein Drittel größer. Auf einem 32-Bit-PHP-Build oder einem Host mit engen Speicherlimits kann das Kodieren eines großen Binärs komplett scheitern. Streamen Sie es, wie oben gezeigt, bevor Sie memory_limit justieren.
  • Es werden niemals Zeilenbrüche hinzugefügt. "MIME base64" in der Funktionsbeschreibung bedeutet nicht "MIME-umbrachte 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, auf die beste Art. base64_encode() kam in PHP 4 als Kernfunktion mit einem Parameter und ohne Optionen, und sie hat seitdem nicht einen einzigen dazubekommen. Ein Strict-Mode war nie nötig (es gibt nichts, wofür man strikt sein müsste, wenn man selbst die Daten erzeugt), es wurde nie eine Padding-Option hinzugefügt, und die Umbruch-Arbeit wurde ab dem ersten Tag an chunk_split() delegiert, daher sitzen die beiden Funktionen noch immer zusammen in den "Siehe auch"-Listen des Handbuchs.

Das Handbuch trägt die 33-Prozent-Zahl, seit je jemand sich 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 überhaupt in diesem Artikel auftaucht. Der Stream-Filter convert.base64-encode kam später dazu, und er hatte seinen eigenen Bug, aus dem er herauswachsen musste: Im Jahr 2015 behob PHP einen Defekt (Bug #68532), bei dem der Filter im Lese-Modus auf Memory-Streams das letzte Padding-Zeichen auslassen konnte, was genau die Art von stiller Beschädigung ist, von der der funktionsbasierte Weg nie betroffen ist. PHP 8.0 fügte den nativen string-Parameter und die Rückgabe-Typen hinzu, und dort endet der Changelog. Eine Funktion, ein Parameter, zwanzig Jahre, null Optionen: ein Denkmal dafür, die Oberfläche gleich beim ersten Mal richtig zu machen.

Lustige PHP-Fakten

Denn ohne die Kuriositäten ist eine Referenz nicht komplett:

  • Die leere Identität. base64_encode('') ist ''. Kein Padding, keine Ausgabe, keine Überraschungen: Leere hinein, Leere raus.
  • Eine seltsame Adresse. Das PHP-Handbuch führt base64_encode() im Buch "Other Basic Extensions" unter "URLs". Es gibt kein "Encoding"-Kapitel; "URLs" ist der Ort, an dem Sie es finden, neben parse_url().
  • Das Alphabet ist nie umgezogen. Dieselben 64 Zeichen kommen seit PHP 4 aus PHPs Encoder. Ein Base64-String, den 2001 ein PHP-4-Skript auf einem Windows-Rechner erzeugt hat, dekodiert heute auf PHP 8.4 unter Linux identisch. Das ist Interoperabilität mit 25 Jahren Erfahrung.
  • 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 hält genau 57 Bytes Ausgangsdaten, und genau das ist es, was die Streaming-Chunk-Schleife im großen-Daten-Abschnitt ermöglicht, ohne Zustand zwischen den Lesevorgängen zu tragen.
  • Es hat ein Dial-up-Geschwister. Das "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 standardisiert hat. Es lebt im String-Functions-Kapitel, aber es ist die fossile Aufzeichnung des ursprünglichen Zwecks dieser Funktion.
  • Alte Browser waren pingelig mit dem Padding. Eine php.net-Anmerkung aus dem Jahr 2004 berichtet, dass Internet Explorer Cookie-Namen mit = ablehnte, daher streicht erfahrener Code manchmal die Pads am Ende von Base64, das in Cookies gespeichert wird. Moderne Setups brauchen den Trick nicht, aber er erklärt seltsame rtrim($x, '=')-Aufrufe, die Sie vielleicht erben.

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 Sie von Anfang bis Ende kontrollieren. Die schwierigere Richtung ist die, in der Sie das Base64 anderer empfangen: ihre Padding-Entscheidungen, ihre Zeilenbrüche, ihre URL-sicheren Dialekte, ihre kaputten Einfügungen. Dort braucht ein Dekodierer den Strict-Mode, eine Validierungspipeline und eine gesunde Skepsis gegenüber allem. Base64-Dekodierung in PHP, verlinkt von dieser Seite, behandelt die Dekodierungs-Seite in derselben Tiefe.

Zuletzt aktualisiert: 2026-09-08

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