Part 10 of Protocol Decoders — and the first half of the series capstone. Back in Part 1 we flagged one field that every other decoder in this series can parse but none can fully read: the Motorola talker-alias, an obfuscated display name riding P25 link control. It’s the same emitter the Crypto Lab series calls Mercury. This post frames it; Part 11 attacks it. The honest headline up front: it is not cracked, and this is a clean-room, authorized-use-only investigation (issue #773).
TL;DR: A Motorola talker alias reassembles to
WACN | System | RadioID | encoded-alias | CRC-16. The encoded-alias field is exactly 2n bytes for an n-character alias, and the last 2 bytes are a CRC-16/GSM, not cipher output — feeding them into the cryptanalysis was the mistake that stalled earlier work. Strip them, and a clean per-byte decode model emerges:decoded[i] = int8(M·LUT[enc[i]] + c), with the 256-entry substitution LUT fully recovered. The cipher stays gated (CipherVerified = false) — a recovered table is not a verified decode.
Key takeaways
- The alias framing is verified: a 7-byte SUID header, a
2n-byte encoded alias, and a trailing CRC-16/GSM. - The single most expensive early error was treating the CRC bytes as ciphertext — ~11% contamination per message.
- The per-byte decode is
decoded[i] = int8(M·LUT[enc[i]] + c); even positions decode to0x00because aliases are UTF-16BE. - The 256-entry
LUTis fully recovered (round-trips all 3,607 corpus aliases) — yet the cipher is still not verified end-to-end.
Cheat sheet
| Field | Bits / bytes | Role |
|---|---|---|
| WACN | bits 0..19 | wide-area network ID |
| System ID | bits 20..31 | system ID |
| Radio ID | bits 32..55 | source subscriber (SUID = 56 bits = 7 bytes) |
| Encoded alias | bits 56..end−16 | 2n bytes, proprietary per-byte cipher |
| CRC-16 | last 16 bits | CRC-16/GSM — not cipher output |
In this post
- Where the alias comes from — three carriers, one reassembled message.
- The framing — SUID + encoded alias + CRC, and why the CRC bites.
- The decode model — the per-byte affine form and the recovered LUT.
- The gate — why a recovered table is still
CipherVerified = false.
Where the alias comes from
A talker alias is a radio’s human-readable display name — too long for a single message, so Motorola systems fragment it across link-control words. GopherTrunk sees it on three different carriers, and the key design fact is that all three reassemble to the same message once their fragments are concatenated:
- LDU1 link control — voice-channel LC on the traffic channel.
- TDULC link control — the terminator LC (where Motorola actually rides most aliases, per SDRTrunk ground truth on real systems).
- Phase 2 FACCH-S MAC — the TDMA fast-associated control channel.
The transport differs — LCO 0x15 is the Motorola talker-alias header, LCO 0x17
the data blocks, each contributing a 44-bit fragment — but reassembly is
carriage-independent. A TalkerAliasAssembler buffers numbered fragments per source
unit, tolerates out-of-order arrival, evicts stale partials, and emits the complete
message when every block is in. Keeping the cipher, the framing, and the CRC
in one shared internal/radio/p25/motorola package means each carrier owns only its
fragment transport and shares the decode.
The framing, and the CRC that bites
The reassembled message is exactly:
// internal/radio/p25/motorola/alias.go (shape)
// bits 0..19 : WACN
// bits 20..31 : System ID
// bits 32..55 : Radio (subscriber) ID -> SUID = 56 bits = 7 bytes
// bits 56..end-16 : encoded alias bytes (proprietary per-byte cipher)
// last 16 bits : CRC-16/GSM over the preceding bits
The SUID part is verified — the #778 reassembly fix reproduces SDRTrunk’s fragment
byte stream exactly, which is why WACN / System / Radio ID fall out correctly on
real traffic. crc16GSM (poly 0x1021, init 0x0000, xorout 0xFFFF, no
reflection) matches the CRC the reference decoder runs.
Here is the lesson that cost weeks. The encoded-alias field for an n-character
alias is exactly 2n bytes, and the encoded_hex you capture is 2n + 2 bytes
— because the last two bytes are the CRC, not cipher output. The proof is
beautifully direct: the alias P18 appears on two different radio IDs with
encoded_hex 956AB19AE437D7FB and 956AB19AE43799BA — identical
first 6 bytes (the cipher-encoded alias, which depends only on the plaintext),
differing last 2 bytes (the RID-dependent CRC).
An earlier passive analysis fed those trailing CRC bytes straight into the cipher fit — about 11% contamination per message — and concluded the cipher had a “~13.6% non-deterministic high byte.” Strip the last two bytes first and that non-determinism drops to 3.2%, and the real structure appears. The rule is now written in the research notes in bold: strip the trailing 2 CRC bytes before any cryptanalysis. It’s the single most important framing fact in the whole hunt, and it’s the kind of error that looks like cipher noise when it’s actually a boundary bug.
The per-byte decode model
With the CRC gone, the decode is a clean per-byte affine form over a fixed substitution table:
decoded[i] = int8( M_i · LUT[ encoded[i] ] + c_i )
LUTis a fixed 256-entry int8 substitution table, and it has been fully recovered — it round-trips every byte of all 3,607 corpus aliases given the per-character keystream, with zero inconsistencies.(M_i, c_i)is a per-character keystream:M_ian odd multiplier (a modular inverse mod 256, carrying 7 bits of the accumulator’s low byte),c_iadditive (carrying the high byte).- The decoded stream is UTF-16BE +
0x00padding, so even byte positions decode to0x00for ASCII aliases. That meansM·LUT[enc] + c = 0at even positions, which makes the accumulator high byte readable directly from the ciphertext:H_k = LUT[encoded[2k]]. That single observation — the high byte is in plain sight — is what turns Part 11 from a black-box search into a structural attack.
The gate: recovered is not verified
Here is the discipline that separates this from a “we cracked it” blog post.
GopherTrunk ships a Motorola alias decoder — DecodeAliasBytes — but it is gated
behind a single constant:
// internal/radio/p25/motorola/alias.go
const CipherVerified = false
While that is false, DecodeMessage never reports an alias as reliable, and every
caller — Phase 1 and Phase 2 — treats the output as suspect. The shipped table and
constants (accum·293 + 0x72E9, plus a placeholder LUT) are an unverified
algebraic placeholder; they decode nothing on live traffic. The rule for flipping
the gate is explicit in the code: only together with a committed regression fixture
mapping real encoded bytes to the correct plaintext, never on inference alone.
Why so strict? Because a wrong table can decode to coincidentally clean ASCII — a
plausible-looking name that is pure fiction. Surfacing a fabricated name as a
confirmed talker alias is worse than surfacing nothing: it’s misinformation an
operator might act on. So the recovered LUT from the corpus (which genuinely
round-trips 3,607 aliases) lives in the research toolkit, and the shipped decoder
stays gated until a real frame confirms an end-to-end decode. This is the same
“honesty over polish” instinct that runs through the EDACS FEC docs and the
issue-closing policy — a claim isn’t true until it’s verified.
Where this goes next
Part 11 takes the fight to the cipher itself: the per-character affine keystream, the long list of structural hypotheses that were ruled out, the low-byte accumulator attack and the “observability floor” that keeps it uncracked, the chosen-plaintext capture procedure, and the clean-room ethics that gate the whole thing. For the protocol context, the P25 Phase 1 and P25 CAI references cover the link control this rides on; the Crypto Lab series follows the same emitter, Mercury, from the attacker’s side.
FAQ
What is a P25 talker alias? A radio’s human-readable display name, sent over the air so other radios can show “who’s talking.” Motorola fragments it across link-control words and obfuscates the alias bytes with a proprietary per-byte cipher; the SUID (WACN/System/RadioID) around it is standard and decodes cleanly.
Why strip the last two bytes before analyzing the cipher? Because they’re a CRC-16/GSM, not cipher output. The same alias on two different radios shares identical cipher bytes but different CRCs. Feeding the CRC into the cipher fit contaminates ~11% of each message and manufactures fake “non-determinism”; stripping it drops that from 13.6% to 3.2% and reveals the real structure.
Is the talker-alias cipher decoded in GopherTrunk?
No. The framing (SUID + CRC) is verified and decodes, but the per-byte cipher is
gated behind CipherVerified = false. The shipped table is an unverified placeholder
that decodes no live traffic, and the code refuses to present any alias as a
confirmed name until a real capture validates an end-to-end decode.
If the LUT is fully recovered, why isn’t it solved? Recovering the output substitution table is necessary but not sufficient. The per-character keystream still comes from a nonlinear state update whose closed form isn’t known — recovering the table lets you read the corpus, not decode a new alias. Part 11 is about that gap.
Series navigation
Part 10 of 12 · ← Part 9: Conventional, Wideband & the Symbol Scope · Next → Part 11: The Alias Hunt II