Hoofdstuk 4 · oefening
Maskers en controleframes
Een payload maskeren en demaskeren
Je leercodec leest elk
81 05 48 65 6c 6c 6f
81 85 37 fa 21 3d 7f 9f 4d 51 58
De eerste byte is hetzelfde, 81: FIN gezet, 05 in het ene en 85 in het andere: dezelfde lengte 5, plus de MASK-bit 0x80 die je in hoofdstuk 3 las. Dan komen vier bytes die het niet-gemaskeerde frame niet heeft, 37 fa 21 3d. Dat is de
XOR met een roterende sleutel
RFC 6455 sectie 5.3 definieert maskeren in één regel: byte i van de gemaskeerde payload is byte i van de payload XOR byte i % 4 van de sleutel. De sleutel heeft vier bytes, en i % 4 loopt ze om de beurt langs, 0, 1, 2, 3, en dan terug naar 0, zolang de payload duurt. Hier is “Hello” met de sleutel van de RFC:
i | Payloadbyte | Sleutelbyte (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 |
Positie 4 gaat rond en gebruikt key[0] opnieuw, dezelfde sleutelbyte als de H. De twee l’en op positie 2 en 3 geven 4d en 51, omdat hun posities verschillende sleutelbytes gebruiken, dus gelijke letters geven geen gelijke gemaskeerde bytes. Indexeren in een bytes-int (Networks with Python hoofdstuk 2), dus payload[i] ^ key[i % 4] is een getal van 0 tot en met 255, en een bytes(...) weer bytes.
Met een sleutel waarvan alle vier de bytes ff zijn, is elke sleutelbyte hetzelfde, dus doet de positie er niet toe, en XOR met ff draait alle acht bits van elke byte om. Voorspel eerst twee van zulke omdraaiingen:
Wat drukt dit af?
print(hex(0x71 ^ 0xFF), hex(0x8E ^ 0xFF))
Dit blok maskeert de vier bytes van b"quiz" op die manier:
De tweede b'quiz' af. Een echte sleutel heeft vier verschillende bytes, dus je i nodig om de juiste te kiezen.
Welke sleutelbyte maskeert payloadbyte 9?
Zijn eigen inverse
De eerste les van hoofdstuk 3 vroeg wat (0x48 ^ 0x37) ^ 0x37 afdrukt. Het antwoord was 72, en dat is 0x48, de H van “Hello” gemaskeerd met key[0] en weer gedemaskeerd. XOR met twee keer dezelfde waarde geeft altijd terug waarmee je begon: x ^ k ^ k == x. Eén functie doet dus beide klussen. De client roept haar aan met de sleutel om een payload te maskeren; de server leest de sleutel uit het frame en roept dezelfde functie aan met dezelfde sleutel om de payload terug te krijgen.
Waarom clients maskeren
Maskeren is er niet om het bericht te beschermen. Het beschermt het netwerk tussen de client en de server. RFC 6455 sectie 10.3 legt de zorg uit: een proxy of cache op het pad die
Een nieuwe, onvoorspelbare sleutel voor elk frame neemt die keuze weg. Het script kiest de payload, maar de browser kiest de sleutel, dus het script kan niet weten welke bytes het netwerk oversteken. Daarom geldt de regel voor clients: een browser draait code van websites die hij niet vertrouwt, en een server doet dat niet.
Maskeren verbergt niets voor iemand die het frame kan lezen, omdat de sleutel in hetzelfde frame reist, vier bytes vóór de payload. Iedereen die het frame ziet, kan het demaskeren, precies zoals je server zal doen. Een gesprek privé houden is de taak van wss:// dat je in hoofdstuk 9 tegenkomt.
Waarom is maskeren geen versleuteling?
Je opdracht
Dit hoofdstuk begint vanuit de eigen afgewerkte bestanden van de cursus uit hoofdstuk 3, hoe je eigen code uit hoofdstuk 3 er ook uitzag. ws_frames.py is de codec uit hoofdstuk 3 met de nieuwe functies van dit hoofdstuk toegevoegd als stubs; elke docstring noemt de les die haar schrijft, en de functies uit hoofdstuk 3 zeggen “Chapter 3 wrote it.” ws_handshake.py is de afgewerkte handshake van de cursus uit hoofdstuk 2, en tests/test_frames.py is de versie van de cursus van de suite uit hoofdstuk 3 les 6, met haar @pytest.mark.parametrize-decorators. Beide zijn er om te lezen; geen enkele les hier past ze aan. Deze lessen hebben een editor en geen python -m pytest in de map ervan haar. Haar 15 tests dekken niet-gemaskeerde frames, en elke wijziging in dit hoofdstuk houdt alle 15 groen.
Schrijf in ws_frames.py apply_mask(payload, key). Loop over de posities van payload met range(len(payload)), XOR byte i met key[i % 4], en geef de resultaten terug als bytes. Dezelfde functie moet ook demaskeren.
Druk op Run. Run start opnieuw frames_demo.py, nu met één sectie per les van dit hoofdstuk; de laatste regels ervan draaien ze alleen als het bestand als programma wordt gestart, achter de bewaker if __name__ == "__main__":. Met apply_mask geschreven begint de uitvoer zo:
Lesson 1: Hello masked with 37 fa 21 3d
7f 9f 4d 51 58
Lesson 2: a masked text frame
not written yet
Druk daarna op Submit. De controle maskeert het “Hello” van de RFC en maskeert het resultaat opnieuw, en doet daarna hetzelfde met sleutels en payloads die je nog niet hebt gezien: een tekst van 7 bytes, een lege payload, en 65.536 bytes. Ze verwacht elke keer bytes terug. In de volgende les gebruiken encode_frame en decode_frame je functie om gemaskeerde frames te bouwen en te lezen.
Opdracht
Schrijf apply_mask(payload, key) in ws_frames.py. Byte i van het resultaat is byte i van payload XOR byte i % 4 van de key van 4 bytes. Loop over de posities met range(len(payload)), gebruik ^ en % 4, en geef het resultaat terug als bytes. Opnieuw aangeroepen met dezelfde sleutel op haar eigen resultaat geeft de
Druk op Run: de sectie van les 1 in frames_demo.py moet 7f 9f 4d 51 58 afdrukken. Druk daarna op Submit. De controle maskeert het “Hello” van de RFC met 37 fa 21 3d en maskeert het resultaat opnieuw, en doet daarna hetzelfde voor sleutels en payloads die je nog niet hebt gezien: een tekst van 7 bytes, een lege payload, en 65.536 bytes. Ze verwacht elke keer bytes met de lengte van de payload. Laat de andere functies in ws_frames.py zoals ze zijn.