Kapitel 4 · Übung
Masken und Steuerframes
Eine Payload maskieren und demaskieren
Dein Lern-Codec liest jeden
81 05 48 65 6c 6c 6f
81 85 37 fa 21 3d 7f 9f 4d 51 58
Das erste Byte ist dasselbe, 81: FIN gesetzt, 05 und im anderen 85: dieselbe Länge 5, plus das MASK-Bit 0x80, das du in Kapitel 3 gelesen hast. Dann kommen vier Bytes, die der unmaskierte Frame nicht hat, 37 fa 21 3d. Das ist der
XOR mit einem rotierenden Schlüssel
RFC 6455, Abschnitt 5.3, definiert das Maskieren in einer Regel: Byte i der maskierten Payload ist Byte i der Payload XOR Byte i % 4 des Schlüssels. Der Schlüssel hat vier Bytes, und i % 4 geht sie der Reihe nach durch, 0, 1, 2, 3, dann wieder 0, so lange die Payload reicht. Hier ist “Hello” mit dem Schlüssel des RFC:
i | Payload-Byte | Schlüsselbyte (i % 4) | XOR |
|---|---|---|---|
| 0 | 48 (H) | 37 (key[0]) | 7f |
| 1 | 65 (e) | fa (key[1]) | 9f |
| 2 | 6c (l) | 21 (key[2]) | 4d |
| 3 | 6c (l) | 3d (key[3]) | 51 |
| 4 | 6f (o) | 37 (key[0]) | 58 |
Position 4 fängt wieder vorne an und verwendet erneut key[0], dasselbe Schlüsselbyte wie beim H. Die beiden l an den Positionen 2 und 3 ergeben 4d und 51, weil ihre Positionen verschiedene Schlüsselbytes verwenden, also ergeben gleiche Buchstaben keine gleichen maskierten Bytes. Das Indizieren eines bytes-int (Networks with Python, Kapitel 2), also ist payload[i] ^ key[i % 4] eine Zahl von 0 bis 255, und eine bytes(...) wieder zu bytes.
Bei einem Schlüssel, dessen vier Bytes alle ff sind, ist jedes Schlüsselbyte gleich, also spielt die Position keine Rolle, und XOR mit ff kehrt alle acht Bits jedes Bytes um. Sag zuerst zwei solche Umkehrungen voraus:
Was gibt das aus?
print(hex(0x71 ^ 0xFF), hex(0x8E ^ 0xFF))
Dieser Block maskiert die vier Bytes von b"quiz" auf diese Weise:
Die zweite b'quiz' aus. Ein echter Schlüssel hat vier verschiedene Bytes, also braucht deine i, um das richtige auszuwählen.
Welches Schlüsselbyte maskiert Payload-Byte 9?
Ihre eigene Umkehrung
Die erste Lektion von Kapitel 3 hat gefragt, was (0x48 ^ 0x37) ^ 0x37 ausgibt. Die Antwort war 72, also 0x48, das H von “Hello”, mit key[0] maskiert und wieder demaskiert. XOR mit demselben Wert zweimal ergibt immer den Ausgangswert: x ^ k ^ k == x. Also erledigt eine Funktion beide Aufgaben. Der Client ruft sie mit dem Schlüssel auf, um eine Payload zu maskieren; der Server liest den Schlüssel aus dem Frame und ruft dieselbe Funktion mit demselben Schlüssel auf, um die Payload zurückzubekommen.
Warum Clients maskieren
Das Maskieren ist nicht dazu da, die Nachricht zu schützen. Es schützt das Netzwerk zwischen Client und Server. RFC 6455, Abschnitt 10.3, erklärt die Sorge: Ein Proxy oder Cache auf dem Weg, der
Ein neuer, unvorhersehbarer Schlüssel für jeden Frame nimmt diese Wahl weg. Das Skript wählt die Payload, aber der Browser wählt den Schlüssel, also kann das Skript nicht wissen, welche Bytes das Netzwerk durchqueren. Deshalb gilt die Regel für Clients: Ein Browser führt Code von Websites aus, denen er nicht vertraut, und ein Server tut das nicht.
Das Maskieren verbirgt nichts vor jemandem, der den Frame lesen kann, weil der Schlüssel im selben Frame reist, vier Bytes vor der Payload. Jeder, der den Frame sieht, kann ihn demaskieren, genau wie es dein Server tun wird. Ein Gespräch privat zu halten ist die Aufgabe von wss://, das dir in Kapitel 9 begegnet.
Warum ist Maskieren keine Verschlüsselung?
Deine Aufgabe
Dieses Kapitel beginnt mit den fertigen Dateien des Kurses aus Kapitel 3, egal wie dein Code aus Kapitel 3 aussah. ws_frames.py ist der Codec aus Kapitel 3, mit den neuen Funktionen dieses Kapitels als Platzhalter; jeder Docstring nennt die Lektion, die die Funktion schreibt, und die Funktionen aus Kapitel 3 sagen “Chapter 3 wrote it.” ws_handshake.py ist der fertige Handshake des Kurses aus Kapitel 2, und tests/test_frames.py ist die Version des Kurses der Suite aus Kapitel 3, Lektion 6, mit ihren @pytest.mark.parametrize-Decorators. Beide sind zum Lesen da; keine Lektion hier ändert sie. Diese Lektionen haben einen Editor und kein python -m pytest in seinem Ordner sie aus. Ihre 15 Tests decken unmaskierte Frames ab, und jede Änderung in diesem Kapitel lässt alle 15 bestehen.
Schreibe in ws_frames.py die Funktion apply_mask(payload, key). Durchlaufe die Positionen von payload mit range(len(payload)), verknüpfe Byte i per XOR mit key[i % 4] und gib die Ergebnisse als bytes zurück. Dieselbe Funktion muss auch demaskieren.
Klicke auf Run. Run startet wieder frames_demo.py, jetzt mit einem Abschnitt pro Lektion dieses Kapitels; seine letzten Zeilen führen sie nur aus, wenn die Datei als Programm gestartet wird, hinter dem Wächter if __name__ == "__main__":. Mit fertigem apply_mask beginnt die Ausgabe so:
Lesson 1: Hello masked with 37 fa 21 3d
7f 9f 4d 51 58
Lesson 2: a masked text frame
not written yet
Klicke dann auf Submit. Die Prüfung maskiert das “Hello” des RFC und maskiert das Ergebnis erneut, dann macht sie dasselbe mit Schlüsseln und Payloads, die du nicht gesehen hast: ein 7 Byte langer Text, eine leere Payload und 65.536 Bytes. Sie erwartet jedes Mal bytes zurück. In der nächsten Lektion verwenden encode_frame und decode_frame deine Funktion, um maskierte Frames zu bauen und zu lesen.
Aufgabe
Schreibe apply_mask(payload, key) in ws_frames.py. Byte i des Ergebnisses ist Byte i von payload XOR Byte i % 4 des 4 Byte langen key. Durchlaufe die Positionen mit range(len(payload)), verwende ^ und % 4 und gib das Ergebnis als bytes zurück. Erneut mit demselben Schlüssel auf ihr eigenes Ergebnis aufgerufen, gibt die
Klicke auf Run: Der Abschnitt zu Lektion 1 von frames_demo.py sollte 7f 9f 4d 51 58 ausgeben. Klicke dann auf Submit. Die Prüfung maskiert das “Hello” des RFC mit 37 fa 21 3d und maskiert das Ergebnis erneut, dann macht sie dasselbe für Schlüssel und Payloads, die du nicht gesehen hast: ein 7 Byte langer Text, eine leere Payload und 65.536 Bytes. Sie erwartet jedes Mal bytes mit der Länge der Payload. Lass die anderen Funktionen in ws_frames.py, wie sie sind.