Wiki / Concepts

Data whitening

A fixed pseudo-random sequence XORed over a radio frame before it is keyed, so long runs of identical bytes do not put a spike in the spectrum. It is a spectral tool, not a secret, and undoing it is a lookup, not an attack.

Where whitening sits in the transmit chain
Frame: length, address, payload, CRC
Whitener: XOR with a 9-bit LFSR sequence
Preamble and sync word prepended, NOT whitened
The preamble and the sync word stay in the clear because the receiver has to find them before it can start the whitener. Everything after the sync word, including the length byte and the CRC, comes off the air looking random.

Data whitening (also called scrambling) is a fixed pseudo-random byte sequence that a radio XORs over a frame just before modulating it, and that the receiver XORs off again on the way in. It exists to shape the spectrum, not to hide anything, and it is one of the most common reasons a perfectly demodulated sub-GHz payload still looks like random bytes.

What it is

A packet engine takes the frame it is about to send, generates a deterministic sequence from a linear feedback shift register, XORs one byte of that sequence into each byte of the frame, and keys the result. The receiver runs the identical register from the identical seed and XORs the same sequence back out. Nothing is keyed, nothing is negotiated, and the sequence is printed in the datasheet.

The classic is the PN9 sequence used by the Texas Instruments sub-GHz family and by many chips that copied its packet engine: a 9-bit register with the polynomial x^9 + x^5 + 1, seeded to all ones at the start of every frame, XORing its low byte into each data byte and then clocking eight times.

def pn9(data, seed=0x1FF):
    lfsr, out = seed, bytearray()
    for byte in data:
        out.append(byte ^ (lfsr & 0xFF))
        for _ in range(8):
            fb = ((lfsr & 1) ^ ((lfsr >> 5) & 1)) & 1
            lfsr = ((lfsr >> 1) | (fb << 8)) & 0x1FF
    return bytes(out)

It is its own inverse: run a whitened frame back through the same function and you get the frame.

Why it matters

A transmitter sending a long run of 0x00 or 0xFF keys the same symbol thousands of times in a row. That concentrates energy at a few frequencies, drifts the receiver's DC estimate and clock recovery, and can push the device outside the spectral mask its regulator requires. Whitening breaks up the run so the transmitted energy is spread evenly across the channel, and the receiver's bit synchroniser keeps seeing transitions to lock onto.

For an analyst it matters for the opposite reason. Whitening is the step that makes a correctly demodulated capture look like it failed. Byte histograms go flat, no ASCII appears, entropy looks high, and the natural conclusion is that the payload is encrypted. It is not, and the fix costs ten lines.

How you recognise it

  • The preamble and sync word are readable and everything after them is not. If AA AA AA ... D3 91 comes out clean and the rest is flat, suspect the packet engine rather than a cipher.
  • Two frames with nearly identical content differ in only a few bytes. A stream cipher with a per-frame nonce would change everything. A fixed sequence restarted every frame changes only what actually differs.
  • A CRC confirms it instantly. De-whiten, then check the frame's own CRC. If it agrees you were right; if it does not, your register variant or your seed is wrong, and there are only a handful of conventions to try.
  • The whitener is in the datasheet. For a chip with a documented packet engine there is no reverse engineering to do, only reading.

Pitfalls

  • Whitening is not encryption. Treating it as a security control is a finding in its own right: anyone with the datasheet reads the frame.
  • Conventions differ. Which end of the register the output byte is taken from, whether the register is clocked before or after the XOR, and whether the byte is bit-reflected all vary between vendors. Use the frame's CRC as the arbiter rather than guessing.
  • The whitener usually covers the length byte and the CRC. Parsing the length before de-whitening gives nonsense and often a huge frame length.
  • The seed restarts every frame. That is convenient for the receiver and convenient for you: the same byte offset in two frames carries the same sequence byte.

Further reading