Chapter 4 · practice
Masks and Control Frames
Mask and Unmask a Payload
Your learning codec reads every
81 05 48 65 6c 6c 6f
81 85 37 fa 21 3d 7f 9f 4d 51 58
The first byte is the same, 81: FIN set, 05 in one and 85 in the other: the same length 5, plus the MASK bit 0x80 you read in Chapter 3. Then come four bytes the unmasked frame does not have, 37 fa 21 3d. That is the masking key. Last come five payload bytes, and in the masked frame they no longer spell “Hello”. They are “Hello” combined with the key, byte by byte. This lesson writes that combination; the next one puts it into frames.
XOR with a rotating key
RFC 6455 section 5.3 defines masking in one rule: byte i of the masked payload is byte i of the payload XOR byte i % 4 of the key. The key has four bytes, and i % 4 walks through them in turn, 0, 1, 2, 3, then back to 0, for as long as the payload lasts. Here is “Hello” with the RFC’s key:
i | Payload byte | Key byte (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 wraps around and reuses key[0], the same key byte as the H. The two ls at positions 2 and 3 give 4d and 51, because their positions use different key bytes, so equal letters do not give equal masked bytes. bytes int (Networks with Python Chapter 2), so payload[i] ^ key[i % 4] is a number from 0 to 255, and a bytes again with bytes(...).
With a key whose four bytes are all ff, every key byte is the same, so the position does not matter, and XOR with ff flips all eight bits of each byte. Predict two such flips first:
What does this print?
print(hex(0x71 ^ 0xFF), hex(0x8E ^ 0xFF))
This block masks the four bytes of b"quiz" that way:
The second b'quiz'. A real key has four different bytes, so your i to pick the right one.
Which key byte masks payload byte 9?
Its own inverse
Chapter 3’s first lesson asked what (0x48 ^ 0x37) ^ 0x37 prints. The answer was 72, which is 0x48, the H of “Hello” masked with key[0] and unmasked again. XOR with the same value twice always gives back what you started with: x ^ k ^ k == x. So one function does both jobs. The client calls it with the key to mask a payload; the server reads the key from the frame and calls the same function with the same key to get the payload back.
Why clients mask
Masking is not there to protect the message. It protects the network between the client and the server. RFC 6455 section 10.3 explains the worry: a proxy or cache on the path that does not understand
A fresh, unpredictable key for every frame takes that choice away. The script picks the payload, but the browser picks the key, so the script cannot know which bytes will cross the network. That is why the rule applies to clients: a browser runs code from websites it does not trust, and a server does not.
Masking hides nothing from anyone who can read the frame, because the key travels in the same frame, four bytes before the payload. Anyone who sees the frame can unmask it, exactly as your server will. Keeping a conversation private is the job of wss:// scheme you meet in Chapter 9.
Why is masking not encryption?
Your task
This chapter starts from the course’s own finished Chapter 3 files, whatever your Chapter 3 code looked like. ws_frames.py is the Chapter 3 codec with this chapter’s new functions added as stubs; each docstring names the lesson that writes it, and the Chapter 3 functions say “Chapter 3 wrote it.” ws_handshake.py is the course’s finished handshake from Chapter 2, and tests/test_frames.py is the course’s version of the suite from Chapter 3 lesson 6, with its @pytest.mark.parametrize decorators. Both are there to read; no lesson here edits them. These lessons have an editor and no python -m pytest in its folder runs it. Its 15 tests cover unmasked frames, and every change in this chapter keeps all 15 passing.
In ws_frames.py, write apply_mask(payload, key). Loop over the positions of payload with range(len(payload)), XOR byte i with key[i % 4], and return the results as bytes. The same function must also unmask.
Press Run. Run starts frames_demo.py again, now with one section per lesson of this chapter; its last lines run them only when the file is started as a program, behind the if __name__ == "__main__": guard. With apply_mask written, the output begins:
Lesson 1: Hello masked with 37 fa 21 3d
7f 9f 4d 51 58
Lesson 2: a masked text frame
not written yet
Then press Submit. The check masks the RFC’s “Hello” and masks the result again, then does the same with keys and payloads you have not seen: a 7-byte text, an empty payload, and 65,536 bytes. It expects bytes back every time. In the next lesson, encode_frame and decode_frame use your function to build and read masked frames.
Task
Write apply_mask(payload, key) in ws_frames.py. Byte i of the result is byte i of payload XOR byte i % 4 of the 4-byte key. range(len(payload)), use ^ and % 4, and return the result as bytes. Called again with the same key on its own result, the
Press Run: the Lesson 1 section of frames_demo.py should print 7f 9f 4d 51 58. Then press Submit. The check 37 fa 21 3d and masks the result again, then does the same for keys and payloads you have not seen: a 7-byte text, an empty payload, and 65,536 bytes. It expects bytes of the payload’s length every time. Leave the other functions in ws_frames.py as they are.