Repository navigation
AirShield frames and ciphers
AirShield seals every DataX message after link setup. This page covers the frame layout byte by byte, the difference between DATA and COMMAND frames, the padding, and the cipher with its per-direction state. It also covers what the phone does with a bad frame. The keys come from Link-setup-and-AirShield.
Every frame after the handshake starts with one byte. The top two bits give the frame type and the low six bits give the stream id.
| Bits | Byte range | Type | Body |
|---|---|---|---|
| 01 | 0x40 to 0x7F | DATA | Sealed frame, described below |
| 10 | 0x80 to 0xBF | COMMAND | One code byte after the header byte |
| 00 | 0x00 to 0x3F | not pinned | No value observed |
| 11 | 0xC0 to 0xFF | not pinned | No value observed |
The phone sends every DATA frame on stream 0, so the header byte is 0x40. The protocol has relay sub-streams with ids up to 63. The phone does not use them.
A DATA frame is one header byte, an 8-byte tag, a one-byte block count and the ciphertext. The table shows the offsets for a frame with n ciphertext blocks, where the block count byte holds n - 1.
| Offset | Length | Field | Value |
|---|---|---|---|
| 0 | 1 | header | 0x40 OR stream id, 0x40 for stream 0 |
| 1 | 8 | tag | First 8 bytes of HMAC-SHA-256, see MAC input below |
| 9 | 1 | block count |
n - 1, 0 to 255 |
| 10 | 16 x n | ciphertext | AES-256-CBC output, whole blocks |
The SDU length of a DATA frame is 10 + 16 x n. The first byte and the block count at offset 9 give the length.
A frame holds at most 256 blocks, so at most 4095 bytes of plaintext fit in one frame, because the padding always takes at least one byte.
Example: the sealed EndLinkSetup with a 46-byte plaintext:
40 header byte, stream 0
<8 bytes> tag
02 block count, 3 blocks
<48 bytes> ciphertext of 46 bytes plus 2 pad bytes C2 C2
That is 58 bytes on the wire.
The tag covers a counter, the block count and the ciphertext. The counter is never sent. It is built as follows:
| Part | Bytes | Notes |
|---|---|---|
| Counter prefix | 02 02 00 00 |
The constant 0x00000202, little endian |
| Sequence | 4 bytes | Per-direction counter, little endian |
| Block count | 1 byte | The byte at offset 9 |
| Ciphertext | 16 x n bytes | Exactly what sits at offset 10 |
The HMAC key is the direction's HMAC key. The tag is the first 8 bytes of the output. For sequence 0x00001234 the counter is 02 02 00 00 34 12 00 00.
A DATA frame goes to the DataX layer after it is unsealed. A COMMAND frame is cleartext control. Its body is one code byte, and the frame header carries the stream id, so the frame looks like [0x80 | stream][code] on the wire. The phone skips every frame whose type is not DATA.
The stream command codes:
| Code | Name | Meaning |
|---|---|---|
| 0 | OPEN | Open a stream |
| 1 | LOST | Stream lost, the glasses' teardown signal |
| 2 | REJECTED | Stream refused |
| 3 | NO_ROUTE | No route to the stream |
| 4 | OPEN_TO_ADVERTISE | Open to advertise |
| 5 | CONNECT | Connect |
| 0x80 | ADVERTISEMENT | Long form: a two-byte length, then up to 1024 bytes |
The transport measures every SDU before the protocol sees it. That measure treats any first byte with bit 7 set as a DataX header, so a COMMAND header byte reads as a DataX length:
- DataX length = 4 + (((first & 0x3F) << 8) | second).
- For stream 0 with LOST,
80 01, the length is 4 + 1 = 5 bytes. - The reader takes 5 bytes, so it consumes 3 bytes of the next SDU, and the stream is out of step after that.
The length rule alone cannot tell the two framings apart. Unconfirmed: whether the glasses ever send a COMMAND frame on a byte stream, and so whether the collision happens.
A first byte in 0x00 to 0x3F cannot be framed, and the reader raises an error. The reader treats 0xC0 to 0xFF as DataX, because bit 7 is set. A frame with type bits 11 would therefore be read as DataX. Unconfirmed: whether the glasses send type bits 00 or 11.
The plaintext gets a modified PKCS#7 pad before encryption. The pad length is 16 - (n mod 16), so it runs from 1 to 16, and an aligned plaintext gains a whole block. Every pad byte holds pad_len + 0xC0:
| Plaintext length | Pad length | Pad bytes | Ciphertext length |
|---|---|---|---|
| 46 | 2 | C2 C2 |
48 |
| 32 | 16 |
D0 repeated 16 times |
48 |
| 8 | 8 |
C8 repeated 8 times |
16 |
To read a pad, the decoder takes the last byte and computes pad_len = last + 0x40, modulo 256. The decoder checks that the pad length is in range and that every pad byte matches.
The phone does not check padding on receive. The glasses pad only when the plaintext is not block aligned. The DataX layer finds message ends from the header lengths. It strips a tail only when the last byte fits the rule and the run matches.
The cipher is AES-256 in CBC mode, with the padding done by hand, and HMAC-SHA-256 over the ciphertext. The construction is encrypt then MAC. The tag check runs in constant time.
Each direction has its own state. Nothing is shared between the two directions except the shared secret that produced their keys:
| State | Send | Receive |
|---|---|---|
| AES key | Send key, 32 bytes, from the glasses' challenge and the phone's seed | Receive key, 32 bytes, from the glasses' seed and the phone's challenge |
| HMAC key | Send key, 32 bytes, derived with the "hmac_derive" salt | Receive key, 32 bytes, derived with the "hmac_derive" salt |
| CBC IV | Starts at the phone's IV from EnableEncryption field 3, then the last ciphertext block of each frame | Starts at the glasses' IV from EnableEncryption field 3, then the last ciphertext block of each frame |
| Sequence | Starts at the phone's random base, plus 1 per frame | Starts at the glasses' EnableEncryption field 4, plus 1 per frame |
The sequence starts at a random value, not at zero. The phone draws its base from four random bytes, read as little endian. The glasses send theirs in EnableEncryption field 4. The counter never resets during a link.
A relay sub-stream copies the keys and starts its sequence at 0. Unconfirmed: whether the glasses use relay sub-streams.
The send path takes a lock, so the IV and the sequence move in step. Two writers at once would corrupt every later frame in both directions, with no error.
This flowchart shows how the phone turns one DataX SDU into a frame. The IV and the sequence advance only after the frame is built.
flowchart TD
A["DataX SDU as plaintext"] --> B["Take the transmit lock"]
B --> C["Pad to 16 bytes, pad byte = pad_len + 0xC0"]
C --> D["AES-256-CBC with the send key and send IV"]
D --> E["Block count = blocks - 1"]
E --> F["MAC input: 02 02 00 00, sequence LE, block count, ciphertext"]
F --> G["Tag = first 8 bytes of HMAC-SHA-256"]
G --> H["Frame = 0x40, tag, block count, ciphertext"]
H --> I["Send IV = last ciphertext block, sequence plus 1"]
I --> J["Write to the transport without a cancel point"]
The write cannot be cancelled, so the phone never leaves a half-written frame.
This flowchart shows how the phone handles one SDU from the glasses. Every failure stops before the state changes.
flowchart TD
A["SDU from the transport"] --> B{"Type bits 0x40?"}
B -->|no| C["Skip the COMMAND frame"]
C --> A
B -->|yes| D{"Length matches the block count?"}
D -->|no| E["Bad frame, the attempt ends"]
D -->|yes| F{"Tag matches the HMAC?"}
F -->|no| E
F -->|yes| G["AES-256-CBC decrypt with the receive IV"]
G --> H["Advance receive IV and sequence, return plaintext"]
A frame body needs at least 25 bytes: 8 for the tag, 1 for the block count and 16 for one block of ciphertext. A shorter body is a bad frame.
- Frame too short. The phone rejects the frame. No state changes.
- Length mismatch. The ciphertext length must equal
16 x (block count + 1). Otherwise the phone rejects the frame. No state changes. - MAC mismatch. The phone rejects the frame. The sequence and the IV do not advance.
- Padding. Inbound padding is not checked, so a bad pad does not raise here. It can show up later as a misframed DataX message.
- Send side. A plaintext over 4095 bytes does not fit in one frame. The phone refuses before anything is sent.
A bad frame ends the link attempt, and the phone makes new keys on the next one. The frame is not skipped.
Because the MAC covers the sequence and the IV chains, one lost or changed frame makes every later frame fail.
Reference code: Cipher.kt, SecureLink.kt, Framing.kt, SduStream.kt
- Unknown: whether COMMAND frames arrive over Bluetooth at all. If they do, the length collision decides what the reader does.
- Unknown: the effect of the length collision on hardware. Whether the reader blocks, desyncs or reports an error.
- Unknown: whether the glasses send COMMAND codes other than the stream-0 LOST.
- Unknown: what the glasses do after a MAC failure on their side, and whether they close the link or answer.
- Unknown: whether the glasses ever send frames with type bits 00 or 11.
- Unknown: whether the glasses use relay sub-streams.
- Unknown: whether the glasses' 4096-byte Wi-Fi chunks have the same frame limit as Bluetooth chunks. See Wi-Fi-Direct.
The glasses
The link
Identity
Media
Settings
Assistant