Wer Zugangsdaten zu einem fremden Dienst oder personenbezogene Spalten in der Datenbank absichert, braucht die Inhalte später wieder im Klartext. Genau dafür ist PHP openssl_encrypt gemacht: die Funktion verschlüsselt einen String symmetrisch mit AES, und derselbe Schlüssel macht ihn wieder lesbar. Dieses Tutorial geht den ganzen Weg bis zu der Frage, wo dieser Schlüssel liegen darf.
Das Bild zeigt den Weg vom Klartext zum Geheimtext samt der beiden Beipacks, die dabei entstehen. Bevor der erste Code steht, fällt die Entscheidung, für welche Daten PHP openssl_encrypt überhaupt zuständig ist.
Verschlüsseln oder hashen? Die Weiche zuerst stellen
Wer Passwörter prüfen will, braucht keine Verschlüsselung, sondern einen Hash. Wie das mit crypt() funktioniert, steht im Tutorial Verschlüsselung von Zeichenketten nach DES, der heutige Standard dafür ist PHP password_hash(). PHP openssl_encrypt ist für den anderen Fall gemacht: für Daten, die später wieder lesbar sein müssen. Ein Hash ist eine Einbahnstraße, und das ist dort die volle Absicht. Hier ist das Gegenteil gefragt.
Was PHP openssl_encrypt() macht
PHP openssl_encrypt() arbeitet symmetrisch. Derselbe Schlüssel verschlüsselt und entschlüsselt, ein öffentliches Gegenstück wie bei Public-Key-Verfahren gibt es nicht. Heraus kommt der Geheimtext, und openssl_decrypt() holt daraus den Klartext. Das kleinste vollständige Beispiel braucht drei Zeilen Vorbereitung und zwei Aufrufe.
<?php
$klartext = 'IBAN DE02 1203 0000 0000 2020 51';
$verfahren = 'aes-256-gcm';
/* 32 Rohbytes fuer AES-256, hier nur zur Veranschaulichung erzeugt */
$schluessel = random_bytes(32);
/* Die IV-Laenge gibt das Verfahren vor, sie wird nie geraten */
$iv = random_bytes(openssl_cipher_iv_length($verfahren));
/* $tag wird per Referenz gefuellt und muss vorher existieren */
$tag = '';
$geheim = openssl_encrypt(
$klartext,
$verfahren,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
$zurueck = openssl_decrypt(
$geheim,
$verfahren,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
var_dump($zurueck === $klartext); /* bool(true) */
Drei Dinge fallen auf. Der Schlüssel ist kein Text, sondern eine Folge von 32 Rohbytes. Der Initialisierungsvektor kommt aus random_bytes() und hat exakt die vom Verfahren geforderte Länge. Und der Auth-Tag wird per Referenz gefüllt, die Variable muss vorher existieren. Wer das beherrscht, hat bei PHP openssl_encrypt das Wesentliche verstanden.
Die acht Parameter von PHP openssl_encrypt()
Die Signatur wirkt überladen, weil in jedem Beispiel im Netz andere Werte darin stehen. Pflicht sind nur die ersten drei, brauchbar wird der Aufruf ab dem fünften.
openssl_encrypt(
string $data,
string $cipher_algo,
string $passphrase,
int $options = 0,
string $iv = "",
string &$tag = null,
string $aad = "",
int $tag_length = 16
): string|false
| Nr. | Name | Pflicht | Bedeutung |
| 1 | $data | ja | Der Klartext, auch als Binärdaten. |
| 2 | $cipher_algo | ja | Das Verfahren als Zeichenkette, etwa aes-256-gcm. |
| 3 | $passphrase | ja | Der Schlüssel als Rohbytes, bei AES-256 genau 32 Bytes lang. |
| 4 | $options | nein | Rückgabeform: mit OPENSSL_RAW_DATA Rohbytes, sonst Base64. |
| 5 | $iv | praktisch ja | Der Initialisierungsvektor. Formal optional, ohne ihn vorhersagbar. |
| 6 | $tag | bei GCM ja | Referenzparameter, bei GCM mit 16 Bytes Auth-Tag gefüllt, bei CBC leer. |
| 7 | $aad | nein | Zusatzangaben, die nur in den Auth-Tag einfließen und nicht verschlüsselt werden. Beim Entschlüsseln muss derselbe Wert anliegen. |
| 8 | $tag_length | nein | Länge des Auth-Tags in Bytes, Vorgabe 16. Ein kürzerer Tag schwächt die Echtheitsprüfung. |
Der dritte Parameter trägt in der Dokumentation den Namen passphrase, ist aber der Schlüssel und wird als Rohbytes erwartet. Ein kürzerer Wert wird stillschweigend mit Nullbytes aufgefüllt, ein längerer abgeschnitten. Eine Warnung gibt es nicht, nur eine schwächere Verschlüsselung. Die beiden letzten Parameter bleiben in den meisten Aufrufen auf ihren Vorgabewerten. Wo $aad doch gebraucht wird, steht der Fall weiter unten beim blockweisen Verschlüsseln.
Der Initialisierungsvektor und warum er zufällig sein muss
Der Initialisierungsvektor, kurz IV, sorgt dafür, dass derselbe Klartext bei jedem Durchgang einen anderen Geheimtext ergibt. Seine Länge gibt das Verfahren vor, openssl_cipher_iv_length() liefert sie: 16 Bytes bei aes-256-cbc, 12 bei aes-256-gcm. Diese Zahl wird nie geraten.
Der IV ist kein Geheimnis und wird mit dem Geheimtext gespeichert. Er muss aber für jeden Datensatz neu aus random_bytes() kommen. Warum weder rand() noch uniqid() taugen, zeigt das Tutorial PHP uniqid() und random_bytes(). Steht der IV fest im Code, wird PHP openssl_encrypt vorhersagbar: gleiche Eingabe, gleicher Geheimtext. Wer zwei identische Werte in der Datenbank sieht, weiß ohne Schlüssel, dass beide Datensätze dasselbe enthalten.
OPENSSL_RAW_DATA und die Base64-Frage
Ohne dieses Flag gibt PHP openssl_encrypt einen fertigen Base64-String aus, mit dem Flag reine Rohbytes. Beides ist zulässig, aber beim Entschlüsseln muss dieselbe Einstellung gelten. Sonst kommt false heraus, ohne Hinweis auf die Ursache.
<?php
$verfahren = 'aes-256-cbc';
$schluessel = random_bytes(32);
$iv = random_bytes(openssl_cipher_iv_length($verfahren));
/* Ohne Flag: die Rueckgabe ist bereits Base64 und textsicher */
$mitBase64 = openssl_encrypt('Hallo Welt', $verfahren, $schluessel, 0, $iv);
/* Mit Flag: die Rueckgabe sind Rohbytes, kuerzer, aber binaer */
$roh = openssl_encrypt(
'Hallo Welt',
$verfahren,
$schluessel,
OPENSSL_RAW_DATA,
$iv
);
var_dump(base64_decode($mitBase64) === $roh); /* bool(true) */
Bewährt hat sich diese Regel: OPENSSL_RAW_DATA setzen, alle Bestandteile selbst zusammensetzen und am Ende einmal base64_encode() über das fertige Paket legen. Dann kodiert genau eine Stelle im Code, nicht zwei. Wie stark Base64 die Datenmenge aufbläht, behandelt das Tutorial PHP base64_encode() und base64_decode().
GCM oder CBC? Die wichtigste Entscheidung
Welches Verfahren PHP openssl_encrypt benutzt, steht im zweiten Parameter. Am Code ist der Unterschied fast unsichtbar, inhaltlich trennt er zwei Welten. aes-256-gcm ist ein authentifiziertes Verfahren: es liefert einen Auth-Tag von 16 Bytes, und openssl_decrypt() gibt false aus, sobald an IV, Tag oder Geheimtext ein Bit verändert wurde. aes-256-cbc prüft nichts davon.
| Merkmal | aes-256-gcm | aes-256-cbc |
| Länge des IV | 12 Bytes | 16 Bytes |
| Echtheitsprüfung eingebaut | ja, über den Auth-Tag | nein |
| Zusätzlicher HMAC nötig | nein | ja, sonst bleibt der Geheimtext veränderbar |
| Rückgabe bei Manipulation | false | veränderter Klartext ohne Fehler |
| Empfehlung | Standardwahl | nur bei Vorgabe, dann mit HMAC |
Die vorletzte Zeile ist die entscheidende. Bei CBC lässt sich der Geheimtext gezielt verändern, ohne den Schlüssel zu kennen, und beim Entschlüsseln kommt kein Fehler heraus, sondern ein anderer Klartext. Wo eine Vorgabe CBC erzwingt, kommt deshalb ein HMAC über IV und Geheimtext dazu. Die Hash-Verfahren dahinter zeigt das Tutorial PHP hash().
<?php
$verfahren = 'aes-256-cbc';
/* Zwei getrennte Schluessel, einer zum Verschluesseln, einer zum Signieren */
$schluesselEnc = hex2bin(getenv('KEY_ENC'));
$schluesselMac = hex2bin(getenv('KEY_MAC'));
$iv = random_bytes(openssl_cipher_iv_length($verfahren));
$geheim = openssl_encrypt(
'Kontostand 1234,56',
$verfahren,
$schluesselEnc,
OPENSSL_RAW_DATA,
$iv
);
/* CBC prueft nichts. Ohne diese Zeile ist der Geheimtext veraenderbar */
$mac = hash_hmac('sha256', $iv . $geheim, $schluesselMac, true);
$paket = base64_encode($iv . $mac . $geheim);
/* Beim Lesen zuerst pruefen, dann erst entschluesseln */
$roh = base64_decode($paket, true);
$gelesenIv = substr($roh, 0, 16);
$gelesenMac = substr($roh, 16, 32);
$gelesenGeheim = substr($roh, 48);
$erwartet = hash_hmac(
'sha256',
$gelesenIv . $gelesenGeheim,
$schluesselMac,
true
);
/* hash_equals statt ===, sonst verraet die Laufzeit den Vergleich */
if (!hash_equals($erwartet, $gelesenMac)) {
throw new RuntimeException('Daten wurden veraendert');
}
Warum dort hash_equals() steht und nicht ===, hat einen messbaren Grund. Ein gewöhnlicher Stringvergleich bricht beim ersten abweichenden Byte ab und verrät über die Laufzeit, wie weit ein geratener Wert gestimmt hat. Bei CBC füllt PHP openssl_encrypt keinen Auth-Tag, diese Prüfung muss also von Hand danebengestellt werden.
Verschlüsselte Daten richtig speichern
IV und Auth-Tag sind kein Geheimnis, aber ohne sie sind die Daten verloren. Beide wandern also mit in die Ablage. Bewährt hat sich ein einziges Feld, in dem IV, Tag und Geheimtext hintereinander liegen, einmal Base64-kodiert. Beim Lesen trennen die bekannten Längen die drei Teile wieder.
<?php
const VERFAHREN = 'aes-256-gcm';
const TAG_LAENGE = 16;
function verschluesseln(string $klartext, string $schluessel): string
{
$ivLaenge = openssl_cipher_iv_length(VERFAHREN);
$iv = random_bytes($ivLaenge);
$tag = '';
$geheim = openssl_encrypt(
$klartext,
VERFAHREN,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($geheim === false) {
throw new RuntimeException('Verschluesselung fehlgeschlagen');
}
/* Reihenfolge festlegen und nie wieder aendern */
return base64_encode($iv . $tag . $geheim);
}
function entschluesseln(string $paket, string $schluessel): string
{
$roh = base64_decode($paket, true);
if ($roh === false) {
throw new RuntimeException('Kein gueltiges Base64');
}
$ivLaenge = openssl_cipher_iv_length(VERFAHREN);
$iv = substr($roh, 0, $ivLaenge);
$tag = substr($roh, $ivLaenge, TAG_LAENGE);
$geheim = substr($roh, $ivLaenge + TAG_LAENGE);
$klar = openssl_decrypt(
$geheim,
VERFAHREN,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($klar === false) {
/* Falscher Schluessel oder veraenderte Daten */
throw new RuntimeException('Entschluesselung fehlgeschlagen');
}
return $klar;
}
Die Reihenfolge im Paket wird einmal festgelegt und nie wieder angetastet, sonst sind alle Altdaten unlesbar. Die Spalte ist BLOB oder TEXT, nicht VARCHAR mit knapper Länge, denn Base64 vergrößert die Daten um rund ein Drittel. Und was mit PHP openssl_encrypt geschützt in der Datenbank liegt, lässt sich weder durchsuchen noch sortieren.
Woher der Schlüssel kommt
Hier kippt Kryptografie in der Praxis am häufigsten. PHP openssl_encrypt fragt nicht nach, woher der Schlüssel stammt, also muss die Anwendung das regeln. Im Quelltext hat er nichts zu suchen und erst recht nichts im Repository, sondern in einer Umgebungsvariablen, einer Datei außerhalb des Web-Roots oder einem Schlüsseldienst. Erzeugt wird er einmal mit bin2hex(random_bytes(32)), und hex2bin() macht aus den 64 Hexzeichen wieder 32 Rohbytes. Die Umrechnung behandelt das Tutorial PHP Zahlensysteme mit bin2hex().
<?php
/* Einmalig auf der Kommandozeile erzeugen und sicher hinterlegen: */
/* php -r "echo bin2hex(random_bytes(32));" */
$hex = getenv('APP_SCHLUESSEL');
if ($hex === false || strlen($hex) !== 64) {
throw new RuntimeException('APP_SCHLUESSEL fehlt oder ist zu kurz');
}
/* 64 Hexzeichen ergeben genau 32 Rohbytes fuer AES-256 */
$schluessel = hex2bin($hex);
$paket = verschluesseln('vertraulicher Inhalt', $schluessel);
Muss sich ein Mensch das Geheimnis merken, wird der Schlüssel abgeleitet. hash_pbkdf2() macht aus Passwort und mitgespeichertem Salt 32 Rohbytes und kostet dabei absichtlich Rechenzeit.
<?php
$passwort = 'ein langes Passwort aus dem Passwortmanager';
/* Das Salt ist kein Geheimnis und wird mitgespeichert */
$salt = random_bytes(16);
/* Letzter Parameter true liefert 32 Rohbytes statt Hexzeichen */
$schluessel = hash_pbkdf2('sha256', $passwort, $salt, 310000, 32, true);
/* Falsch und leider weit verbreitet: */
/* $schluessel = md5($passwort); */
/* md5 ist auf Tempo gebaut und laesst sich massenhaft durchprobieren */
Ein Satz dazu, der ungern ausgesprochen wird: ohne Schlüssel sind die Daten weg, es gibt kein Wiederherstellungsverfahren. Das ist kein Mangel des Verfahrens, sondern sein Zweck.
Vom ersten Zweifel bis zur Ablage des Schlüssels sind es fünf Entscheidungen, und die erste fällt noch vor der Wahl des Verfahrens.
flowchart TD
A[Daten schuetzen] --> B{Wieder lesbar?}
B -->|Nein, nur pruefen| C[password_hash]
B -->|Ja, entschluesseln| D[openssl_encrypt]
D --> E{Welches Verfahren?}
E -->|Standard| F[aes-256-gcm]
E -->|Vorgabe| G[aes-256-cbc]
F --> H[IV und Tag mitspeichern]
G --> I[HMAC ergaenzen]
I --> J[IV und MAC mitspeichern]
H --> K[Schluessel aus Umgebung]
J --> K
Wenn openssl_decrypt() nur false liefert
Die Funktion sagt nicht, was schiefgelaufen ist. Sie gibt false aus, damit endet die Auskunft. Vier Ursachen decken fast alle Fälle ab, und jede davon ist eine Abweichung von dem, was PHP openssl_encrypt beim Verschlüsseln bekommen oder erzeugt hat: falscher Schlüssel, falscher oder abgeschnittener IV, fehlender oder veränderter Auth-Tag, und eine unterschiedliche Einstellung bei OPENSSL_RAW_DATA zwischen den beiden Aufrufen.
<?php
$klar = openssl_decrypt(
$geheim,
'aes-256-gcm',
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($klar === false) {
/* Es koennen mehrere Meldungen anstehen, deshalb die Schleife */
while ($meldung = openssl_error_string()) {
error_log('OpenSSL: ' . $meldung);
}
throw new RuntimeException('Entschluesselung fehlgeschlagen');
}
/* Vorab pruefen, ob der Server das Verfahren ueberhaupt kennt */
$gcm = array_filter(
openssl_get_cipher_methods(),
static fn (string $m): bool => str_contains($m, 'gcm')
);
print_r($gcm);
/* aes-128-gcm, aes-192-gcm, aes-256-gcm, aria-128-gcm, ... */
Ein ehrlicher Hinweis: die Meldungsliste bleibt manchmal leer. Ein fehlgeschlagener Tag-Vergleich bei GCM hinterlässt keine Meldung, eine unpassende Blocklänge bei CBC dagegen schon, dann steht dort etwa error:1C80006B:Provider routines::wrong final block length. Bevor der Verdacht auf den eigenen Code fällt, lohnt der Blick auf openssl_get_cipher_methods(): auf älterem Shared Hosting fehlt GCM gelegentlich.
Stolperfallen, die immer wieder auftauchen
Keiner der fünf folgenden Fehler löst eine Meldung aus. Das Skript verschlüsselt, die Datenbank füllt sich, und beim Entschlüsseln kommt der Klartext zurück. Die Schwäche sitzt jedes Mal neben dem Aufruf von PHP openssl_encrypt und nicht in ihm, deshalb kann die Funktion sie auch nicht anzeigen.
Fester Initialisierungsvektor im Quelltext Meist entstanden aus der Not, den IV beim Entschlüsseln wiederzufinden. Die Lösung ist nicht ein fester Wert, sondern das Mitspeichern des zufälligen im selben Feld.
Hartkodierte Länge von 16 Bytes Bei CBC stimmt die Zahl, bei GCM sind es 12. Wer sie fest einsetzt und später das Verfahren wechselt, bekommt keinen Hinweis, sondern nur einen Aufruf, der scheitert.
Ein Passwort direkt als Schlüssel Ein md5()-Wert liefert zwar 32 Zeichen, aber nur 16 Byte Entropie in Hexdarstellung. Der richtige Weg ist hash_pbkdf2() mit Salt.
Auth-Tag ohne vorherige Deklaration Der sechste Parameter wird per Referenz gefüllt. Fehlt die Variable davor, meldet PHP eine Warnung, und beim Entschlüsseln fehlt der Tag ohne weiteren Hinweis.
Binärer Geheimtext in JSON oder in einer URL Rohbytes überstehen weder eine JSON-Kodierung noch eine Adresszeile. Wer sie dort hindurchschickt, verliert einzelne Bytes und damit den ganzen Datensatz.
Von mcrypt auf PHP openssl_encrypt() umstellen
Wer eine alte Anwendung anhebt, findet dort mcrypt_encrypt() mit rijndael-128. Bei einem 32 Byte langen Schlüssel entspricht das aes-256-*, der Modus steht im alten Aufruf daneben. Ein Fallstrick bleibt: mcrypt füllte den letzten Block mit Nullbytes auf, OpenSSL nutzt PKCS#7. Der ruhigere Weg ist, jeden Altdatensatz beim nächsten Zugriff einmal zu entschlüsseln und sofort mit PHP openssl_encrypt im neuen Format zurückzuschreiben, statt zwei Wege dauerhaft zu pflegen.
Große Dateien blockweise verschlüsseln
Bei einer Datenbankspalte spielt der Speicher keine Rolle, bei einer Datei sofort. Der naheliegende Aufruf openssl_encrypt(file_get_contents($gross)) zieht die komplette Datei in den Arbeitsspeicher und legt den Geheimtext gleich daneben. Gemessen an einer Protokolldatei von 37,7 MB ergibt das eine Speicherspitze von 78,0 MB. Bei einem memory_limit von 32M scheitert schon file_get_contents() mit Allowed memory size of 33554432 bytes exhausted, PHP openssl_encrypt kommt gar nicht mehr an die Reihe.
Ein Streaming-Modus würde das lösen, nur gibt es hier keinen. PHP openssl_encrypt nimmt einen fertigen String entgegen und liefert einen fertigen String zurück. Es gibt keinen Zustand, den man über mehrere Aufrufe hinweg mitschleppen könnte. Der andere gern genannte Weg, ein Verschlüsselungsfilter auf einem geöffneten Datenstrom, ist heute ebenfalls versperrt: stream_get_filters() meldet auf PHP 8.4 nur convert.iconv.*, zlib.*, string.rot13, string.toupper, string.tolower, convert.*, consumed und dechunk. Die früheren mcrypt.*-Filter sind mit der Erweiterung verschwunden, ein Gegenstück aus dem OpenSSL-Lager hat PHP nie bekommen.
Übrig bleibt der Weg, der ohnehin der durchschaubarste ist: die Datei in Abschnitte fester Größe lesen und jeden Abschnitt einzeln durch PHP openssl_encrypt schicken. Das passt auch zum Auth-Tag aus dem GCM-Abschnitt weiter oben, denn der entsteht immer erst am Ende eines Aufrufs. Ein Block, ein Initialisierungsvektor, ein Tag. Damit ist jeder Abschnitt für sich prüfbar, und beim Entschlüsseln fällt ein verändertes Byte sofort auf, statt erst nach 37 MB.
<?php
$verfahren = 'aes-256-gcm';
$schluessel = hex2bin(getenv('APP_SCHLUESSEL'));
const BLOCKGROESSE = 65536;
const TAGLAENGE = 16;
$quelle = 'logs/gross.log'; /* 39.500.000 Bytes, also 37,7 MB */
$ziel = 'logs/gross.log.enc';
$ivLaenge = openssl_cipher_iv_length($verfahren); /* 12 bei GCM */
$ein = fopen($quelle, 'rb');
$aus = fopen($ziel, 'wb');
while (!feof($ein)) {
$block = fread($ein, BLOCKGROESSE);
if ($block === '' || $block === false) {
break;
}
/* Jeder Block bekommt einen eigenen IV und einen eigenen Tag */
$iv = random_bytes($ivLaenge);
$tag = '';
$geheim = openssl_encrypt(
$block,
$verfahren,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($geheim === false) {
throw new RuntimeException('Block konnte nicht verschluesselt werden');
}
/* Rahmen je Block: vier Bytes Laenge, dann IV, Tag, Geheimtext */
fwrite($aus, pack('N', strlen($geheim)) . $iv . $tag . $geheim);
}
fclose($ein);
fclose($aus);
printf(
"%d Bytes => %d Bytes, Speicherspitze %.1f MB\n",
filesize($quelle),
filesize($ziel),
memory_get_peak_usage(true) / 1048576
);
/* 39500000 Bytes => 39519296 Bytes, Speicherspitze 2.0 MB */
Aus 78,0 MB Spitze werden 2,0 MB, und die Datei wächst dabei um 19.296 Bytes. Das sind genau 32 Bytes je Block, verteilt auf vier Bytes Längenangabe, zwölf Bytes Initialisierungsvektor und sechzehn Bytes Auth-Tag, bei 603 Blöcken. Der Rahmen ist der eigentliche Trick an der Sache: ohne die Längenangabe weiß der Leseweg nicht, wo ein Block aufhört. pack('N', ...) schreibt sie als vier Bytes in fester Reihenfolge, unabhängig davon, auf welchem System die Datei später gelesen wird.
Der Rückweg liest denselben Rahmen in derselben Reihenfolge wieder ab, mit genau den Angaben, die PHP openssl_encrypt je Block erzeugt hat. Ohne ihn wäre die ganze Übung wertlos, deshalb gehört er ins selbe Beispiel.
<?php
$ein = fopen('logs/gross.log.enc', 'rb');
$aus = fopen('logs/gross.log.zurueck', 'wb');
$ivLaenge = openssl_cipher_iv_length($verfahren);
/* Vier Bytes lesen, daraus die Blocklaenge gewinnen, dann
genau so weit weiterlesen. Am Dateiende liefert fread
eine leere Zeichenkette, das beendet die Schleife. */
while (($kopf = fread($ein, 4)) !== '' && strlen($kopf) === 4) {
$laenge = unpack('N', $kopf)[1];
$iv = fread($ein, $ivLaenge);
$tag = fread($ein, TAGLAENGE);
$geheim = fread($ein, $laenge);
$klar = openssl_decrypt(
$geheim,
$verfahren,
$schluessel,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($klar === false) {
throw new RuntimeException('Block veraendert oder Schluessel falsch');
}
fwrite($aus, $klar);
}
fclose($ein);
fclose($aus);
/* Der Beleg: dieselbe Pruefsumme wie vor dem Verschluesseln */
var_dump(
hash_file('sha256', 'logs/gross.log')
=== hash_file('sha256', 'logs/gross.log.zurueck')
);
/* bool(true) */
Die Prüfsumme stimmt, es kommt Byte für Byte dieselbe Datei heraus. Und die Echtheitsprüfung arbeitet wirklich je Block: kippt man in einem Geheimtext ein einziges Bit, liefert openssl_decrypt() für genau diesen Block false, während alle anderen sauber durchlaufen. Zwei Punkte gehören trotzdem dazu. Die Blockgröße wird einmal festgelegt und wandert nicht später in eine Konfigurationsdatei, sonst sind Altdateien unlesbar. Und wer die Reihenfolge der Blöcke absichern will, gibt den Blockzähler bei jedem Aufruf von PHP openssl_encrypt als siebten Parameter $aad mit, denn sonst lassen sich zwei Blöcke vertauschen, ohne dass ein Tag das bemerkt.
Bleibt openssl_seal(), das bei dieser Frage regelmäßig empfohlen wird. Es löst eine andere Aufgabe. Die Funktion erzeugt einen zufälligen Sitzungsschlüssel, verschlüsselt damit den Inhalt und verpackt diesen Sitzungsschlüssel anschließend für jeden Empfänger einzeln mit dessen öffentlichem Schlüssel. Sie erwartet deshalb ein Array von öffentlichen Schlüsseln und füllt ein Array verschlüsselter Schlüssel, openssl_open() macht das beim Empfänger wieder auf. Das ist das richtige Werkzeug, wenn ein Inhalt mehrere Empfänger hat, die untereinander kein gemeinsames Geheimnis teilen sollen. Gegen den Speicherverbrauch hilft es nicht, denn auch dort geht der komplette Inhalt als ein String hinein.
Fazit
PHP openssl_encrypt ist die richtige Wahl, wenn Daten später wieder im Klartext gebraucht werden, und die falsche, sobald es um Passwörter geht. Als Verfahren steht aes-256-gcm an erster Stelle, der Initialisierungsvektor kommt für jeden Datensatz neu aus random_bytes(), und IV samt Auth-Tag wandern mit dem Geheimtext in ein Base64-Feld. Der Schlüssel liegt in der Serverumgebung, abgeleitet wird er bei Bedarf mit hash_pbkdf2() und niemals mit md5(). Erzwingt eine Vorgabe CBC, kommt ein HMAC dazu, verglichen mit hash_equals(). Und geht es statt um ein Datenbankfeld um eine Datei, die nicht in den Speicher passt, wandert sie in Blöcken fester Größe durch die Funktion, jeder mit eigenem Initialisierungsvektor und eigenem Auth-Tag. Wer diese sechs Punkte einhält, hat mit PHP openssl_encrypt eine Umsetzung, die auch einer Prüfung standhält.