Zwei lange Texte auf Gleichheit zu prüfen kostet Zeit. Zwei kurze Zahlen zu vergleichen kostet praktisch nichts. Genau diesen Tausch ermöglicht PHP crc32: die Funktion verdichtet beliebige Daten zu einer einzigen 32-Bit-Zahl, die sich blitzschnell vergleichen lässt.
Damit dieser Tausch aufgeht, muss man wissen, was eine Prüfsumme leisten kann und was nicht.
Was eine Prüfsumme mit PHP crc32() leistet
Eine Prüfsumme ist ein Fingerabdruck. Gleiche Eingabe ergibt immer denselben Wert, das ist die Grundlage. Daraus folgt eine Richtung sicher: sind zwei Prüfsummen verschieden, sind auch die Daten verschieden. Es gibt keine Ausnahme davon.
Die Gegenrichtung gilt nicht. Gleiche Prüfsummen bedeuten nur, dass die Daten gleich sein können. Da eine 32-Bit-Zahl rund 4,29 Milliarden verschiedene Werte annehmen kann, beliebige Daten aber unbegrenzt viele Formen haben, müssen sich zwangsläufig irgendwann zwei Eingaben denselben Wert teilen. Solche Zusammenstösse heissen Kollisionen, und weiter unten steht, ab wann sie praktisch werden.
Wie der Wert zustande kommt, lässt sich in einem Satz sagen: die Daten werden als eine sehr lange Bitfolge betrachtet und durch eine feste Zahl geteilt, und der Rest dieser Division ist die Prüfsumme. In der Praxis rechnet niemand das Bit für Bit durch, sondern arbeitet mit einer vorberechneten Tabelle von 256 Werten, die den Rest byteweise fortschreibt. Genau deshalb ist das Verfahren so schnell. Wer es selbst nachbauen will, findet die Tabelle in jeder Referenz, für den Einsatz in PHP braucht man sie nicht.
Das Verfahren hinter PHP crc32 stammt aus der Datenübertragung. Die zyklische Redundanzprüfung wurde entwickelt, um zufällige Bitfehler auf einer Leitung zu erkennen, und darin ist sie ausgezeichnet. Sie wurde nicht entwickelt, um absichtliche Manipulation zu verhindern, und darin ist sie wertlos. Diese Unterscheidung zieht sich durch das ganze Tutorial.
PHP crc32() im einfachsten Fall
PHP crc32 nimmt eine Zeichenkette und gibt eine Ganzzahl zurück. Mehr Parameter gibt es nicht.
<?php
$text = 'Hallo Welt';
echo crc32($text); /* 1344803957 */
echo crc32('Hallo Welt!'); /* voellig anderer Wert */
/* Gleiche Eingabe, gleiches Ergebnis, jedes Mal */
var_dump(crc32($text) === crc32('Hallo Welt')); /* true */
Auffällig ist, wie stark sich der Wert bei einer winzigen Änderung unterscheidet. Ein einziges zusätzliches Ausrufezeichen erzeugt eine völlig andere Zahl. Genau das macht das Verfahren als Änderungserkennung brauchbar.
So einfach der Aufruf ist, so zuverlässig führt er in eine Falle, sobald das Ergebnis gespeichert oder zwischen Systemen ausgetauscht wird.
PHP crc32(): das Vorzeichenproblem und seine Lösung
Der Rückgabewert von PHP crc32 ist eine vorzeichenbehaftete Ganzzahl. Auf einem 32-Bit-System reicht deren Wertebereich nur bis rund 2,1 Milliarden, während die Prüfsumme bis 4,29 Milliarden gehen kann. Alles darüber kippt ins Negative. Auf einem 64-Bit-System passiert das nicht, weil dort genug Platz ist.
Das Ergebnis sind zwei unvereinbare Zahlenwelten: derselbe Text liefert auf dem Entwicklungsrechner einen anderen Wert als auf dem Produktivserver. Wer damit Cache-Schlüssel bildet, sucht den Fehler lange an der falschen Stelle.
<?php
$daten = 'Datensatz 4711';
/* Kann auf 32-Bit-Systemen negativ sein */
$roh = crc32($daten);
/* Immer positiv, auf jedem System gleich */
$sicher = sprintf('%u', $roh);
echo $sicher; /* 4243883777 */
Der Formatbezeichner %u steht für "unsigned" und interpretiert die Bits als vorzeichenlose Zahl. Wichtig ist die Wahl der Funktion: sprintf() gibt den Wert zurück, printf() gibt ihn direkt aus. Zum Speichern wird die erste Variante gebraucht. Ergebnis ist eine Zeichenkette, was für Vergleiche und Datenbankspalten unproblematisch ist. Mehr zu diesen Formatierungen steht im Tutorial zu PHP printf und sprintf.
Praxis: geänderte Datensätze mit PHP crc32() finden
Der ergiebigste Einsatz ist der Abgleich grosser Datenmengen. Statt bei jedem Import sämtliche Felder eines Datensatzes zu vergleichen, wird einmal eine Prüfsumme gebildet und gespeichert. Beim nächsten Lauf genügt der Vergleich dieser einen Zahl.
<?php
function pruefsumme(array $datensatz): string
{
/* Feste Reihenfolge, sonst aendert sich der Wert grundlos */
ksort($datensatz);
return sprintf('%u', crc32(json_encode($datensatz)));
}
$alt = ['id' => 12, 'name' => 'Schraube', 'preis' => '2.40'];
$neu = ['id' => 12, 'name' => 'Schraube', 'preis' => '2.60'];
if (pruefsumme($alt) !== pruefsumme($neu)) {
echo 'Datensatz hat sich geaendert, Update noetig';
}
Das ksort() ist der unscheinbarste und zugleich wichtigste Teil. Ein Array mit denselben Werten in anderer Reihenfolge erzeugt ein anderes JSON und damit eine andere Prüfsumme. Ohne feste Sortierung meldet der Abgleich Änderungen, wo keine sind.
Bei zehntausend Datensätzen spart dieses Vorgehen den Grossteil der Laufzeit. Es wirft allerdings eine Frage auf, die man vorher beantworten sollte.
Wie wahrscheinlich sind Kollisionen?
Die Antwort fällt niedriger aus, als die Zahl 4,29 Milliarden vermuten lässt. Entscheidend ist nicht, wie viele Werte es gibt, sondern wie viele Paare sich gegenseitig treffen können. Nach demselben Prinzip, nach dem in einer Schulklasse mit 23 Kindern zwei am selben Tag Geburtstag haben, liegt die Wahrscheinlichkeit einer Kollision bereits bei rund 65.000 verschiedenen Eingaben bei etwa 50 Prozent.
Für die Praxis heisst das: unterhalb einiger tausend Einträge kann man Kollisionen ignorieren. Im Bereich von hunderttausend Einträgen sind sie zu erwarten, und eine Deduplizierung, die allein auf PHP crc32 vertraut, wirft dann Daten weg. Das richtige Muster ist ein zweistufiger Vergleich.
<?php
$gespeichert = [];
function inhaltBekannt(string $inhalt, array &$gespeichert): bool
{
$schluessel = sprintf('%u', crc32($inhalt));
if (!isset($gespeichert[$schluessel])) {
$gespeichert[$schluessel] = $inhalt;
return false;
}
/* Pruefsumme gleich, jetzt erst genau vergleichen */
return $gespeichert[$schluessel] === $inhalt;
}
Die Prüfsumme arbeitet hier als Vorfilter. Sie schliesst den grossen Rest in einem Schritt aus, und der teure Volltextvergleich läuft nur noch in den wenigen Fällen, in denen die Zahlen übereinstimmen. Damit ist das Verfahren schnell und trotzdem korrekt.
Wofür PHP crc32() nicht gedacht ist
Es gibt einen Einsatzbereich, in dem die Funktion nicht nur ungeeignet, sondern gefährlich ist: überall dort, wo jemand ein Interesse daran hat, die Prüfsumme zu treffen.
Zu einem gegebenen CRC32-Wert lassen sich passende Eingaben gezielt konstruieren. Das ist kein theoretisches Problem und braucht keine besondere Rechenleistung, denn das Verfahren wurde nie darauf ausgelegt, so etwas zu verhindern. Wer damit Passwörter prüft, eine Signatur nachbildet oder Dateien gegen Manipulation absichert, baut eine Tür ohne Schloss.
| Funktion | Tempo | Sicher gegen Angriff | Typischer Einsatz |
crc32() | sehr hoch | nein | Änderungserkennung, Cache-Schlüssel, Vorfilter |
md5() | hoch | nein | Dateiabgleich, Schlüsselbildung, nicht für Sicherheit |
hash('sha256', ...) | mittel | ja | Integrität, Signaturen, Nachweise |
password_hash() | bewusst langsam | ja | Ausschliesslich für Passwörter |
Die letzte Zeile ist die wichtigste. Passwörter gehören in password_hash(), und zwar ohne Ausnahme. Die Funktion ist absichtlich langsam, damit sich Rateversuche nicht lohnen. Wie das im Einzelnen funktioniert, steht im Tutorial zu PHP password_hash. Geht es um Integrität gegen Manipulation, ist PHP hash() mit SHA-256 der richtige Weg.
Grosse Dateien mit hash_file() prüfen
Eine naheliegende Schreibweise scheitert bei grossen Dateien. Der Inhalt muss dabei vollständig in den Speicher, und bei einem Archiv von mehreren Gigabyte reisst das Speicherlimit.
<?php
$datei = '/var/backups/archiv.tar';
/* Schlecht: laedt die komplette Datei in den Speicher */
/* $wert = crc32(file_get_contents($datei)); */
/* Gut: arbeitet blockweise */
$wert = hash_file('crc32b', $datei);
echo $wert; /* Hexadezimal, z.B. 3d4f8a1c */
echo hexdec($wert); /* als Zahl, falls noetig */
An dieser Stelle lauert eine Verwechslung, die regelmässig eine halbe Stunde kostet. In der hash()-Familie gibt es zwei ähnlich benannte Algorithmen, und nur einer davon entspricht der Funktion PHP crc32.
<?php
$text = 'Beispiel';
$direkt = sprintf('%u', crc32($text));
$ueberHash = hexdec(hash('crc32b', $text));
var_dump($direkt === (string) $ueberHash); /* true */
/* hash('crc32', ...) ist ein ANDERER Algorithmus */
var_dump(hash('crc32', $text) === hash('crc32b', $text)); /* false */
Wer Werte aus beiden Welten vergleicht, muss also crc32b verwenden. Das b ist kein Tippfehler in der Dokumentation, sondern bezeichnet die Variante, die PHP auch in der eigenständigen Funktion nutzt.
Die folgende Übersicht fasst zusammen, welcher Weg zu welcher Aufgabe passt.
flowchart TD
A[Daten pruefen] --> B{Wogegen schuetzen?}
B -->|Zufaellige Fehler| C[crc32 genuegt]
B -->|Manipulation| D[SHA-256 mit hash]
B -->|Passwoerter| E[Nur password_hash]
C --> F{Wie gross?}
F -->|Datei| G[hash_file crc32b]
F -->|String| H[crc32 mit sprintf]
H --> I{Viele Eintraege?}
I -->|Ja| J[Vorfilter plus Vergleich]
I -->|Nein| K[Direkt vergleichen]
Fazit
PHP crc32 beantwortet eine einzige Frage, und das sehr schnell: hat sich etwas geändert? Für Datenabgleiche, Cache-Schlüssel und Vorfilter beim Suchen von Dubletten ist die Funktion damit ein nützliches Werkzeug.
Drei Punkte gehören in jede Umsetzung. Der Wert muss mit sprintf('%u', ...) vorzeichenlos gemacht werden, sonst unterscheiden sich die Ergebnisse zwischen Systemen. Ab etwa hunderttausend Einträgen braucht es den zweistufigen Vergleich, weil Kollisionen dann real werden. Und grosse Dateien gehören über hash_file('crc32b', ...) verarbeitet statt über den Umweg über den Speicher.
Der wichtigste Satz steht am Schluss, weil er sich einprägen soll: eine Prüfsumme schützt gegen Zufall, nicht gegen Absicht. Wo jemand ein Interesse daran hat, den Wert zu treffen, ist sie das falsche Werkzeug.