Part 12 of DMR End to End, a 14-part deep dive that follows the world’s most widely deployed digital PMR protocol through GopherTrunk — from a 4FSK carrier to two simultaneous recorded calls, direct-mode handhelds, and decrypted Enhanced Privacy voice. Part 11 ended with clear AMBE+2 frames reaching the vocoder. This part is about the frames that arrive scrambled: where a transmission announces its cipher, how an RC4 keystream is laid onto eighteen vocoder frames, and the convention — the embedded IV names the *next superframe — that no round-trip could have caught and a reporter’s known-key capture did.*
TL;DR: DMR “Enhanced Privacy” (DMRA algorithm
0x21) is RC4 keyed with key‖MI — the operator’s key followed by the 32-bit Message Indicator — with 256 keystream bytes discarded and 7 bytes per 49-bit AMBE+2 frame applied across a superframe (EPKeystream,DescrambleSuperframeininternal/radio/dmr/voice/ep.go). The MI arrives in the Privacy Indicator header (dmr.ParsePIHeader, CRC mask0x9696) and advances per superframe through the LFSR x³²+x⁴+x²+1 (AdvanceMI). Every superframe also embeds an IV as three Golay(24,12) codewords — and that IV is the NEXT superframe’s MI.EPTrackerapplied it to the current one, so everything after the first superframe decoded on the wrong MI; that fix plusRewindMIfor late entry lifted the #1187 captures from ciphertext (b0 continuity ≈0.15) to speech (0.77 / 0.74). Capture-verified; the live daemon call is still the open gate.
Key takeaways
- Encryption is announced in a data burst, then hidden in the voice. The PI header carries algorithm, key ID and MI at keyup; afterwards the voice bursts carry the chain, a nibble per frame.
- The keystream construction was right the whole time. Both defects that hid a working decrypt were receiving-side: an IV applied one superframe too early, and a harness slicing bursts out of the gaps.
- A Golay-verified IV is weak evidence on its own. The radius-3 sphere covers ~57 % of 24-bit words, so clear vocoder bits “verify” ~1 % of the time — a disagreeing IV is trusted only when it decoded clean.
- Judge a decrypt by pitch continuity, never loudness. Random AMBE+2 parameters are loud; only a slowly moving fundamental says the key was right.
Cheat sheet
| Concern | What it does | Where it lives |
|---|---|---|
| Privacy Indicator header | alg / FID / key ID / 32-bit MI / dest, CRC-CCITT ^ 0x9696 |
internal/radio/dmr/pi.go (ParsePIHeader, IsRC4) |
| Finding the header burst | data-sync slice → slot type 0x0 → BPTC → CRC |
dmr/voice/pi_detector.go (PIHeaderDetector) |
| Keystream | RC4(key‖MI), drop 256, 7 bytes/frame, silence frames in clear | dmr/voice/ep.go (EPKeystream, DescrambleSuperframe) |
| MI schedule | x³²+x⁴+x²+1, one step per superframe, invertible | AdvanceMI / RewindMI |
| Embedded IV | 18 nibbles at on-air bits 71/67/63/59 → 3 × Golay(24,12) + CRC-4 | ExtractEmbeddedIV, EPTracker.Next |
| Reporter harness | GT_DMR_EP_IQ / GT_DMR_EP_AUDIO + GT_DMR_EP_KEY, pitch-continuity VERDICT |
cmd/gophertrunk/dmr_ep_replay_test.go |
In this post
- The header that says how — twelve octets and a CRC mask.
- Key‖MI, drop 256, seven bytes a frame — the RC4 construction.
- The IV that names the next superframe — embedded MIs, the tracker, the rewind.
- Composer to recorder — key resolution, events, the crypto-frame bridge.
- The capture that settled it — two receiving-side defects, one verdict.
The header that says how
Authorized use only. Everything here is known-key decryption: the operator supplies a key they are authorized to hold, and GopherTrunk performs no key recovery of any kind — the same model as SDRTrunk, DSD-FME and OP25. Decrypting traffic you are not permitted to touch is illegal in most jurisdictions; the Crypto Lab framing applies unchanged — measure your own deployment, or one you have written permission to assess.
P25 announces encryption twice —
a grant bit and an in-call Encryption Sync.
DMR is similar: the Full Link Control’s service options carry a privacy
bit (Part 5),
and the how rides a dedicated data burst — the Privacy Indicator
header, slot type 0x0 (DTPIHeader), sent after the Voice LC Header. Its BPTC(196,96)-recovered block is twelve octets, pinned against
SDRTrunk and DSD-FME: algorithm
(0x21 RC4, 0x22 DES, 0x24/0x25 AES), feature-set ID (0x10 DMRA,
0x68 Hytera, 0x0A Kirisun), key ID, the 32-bit MI in octets 3–6, a
24-bit destination, and a CRC-CCITT (poly 0x1021, init 0) XORed with the
PI-header mask.
That mask costs newcomers an afternoon. ETSI writes it as 0x6969 but
defines the CRC with its output inverted; framing.CRCCCITTWithInit is the
plain form, so piHeaderCRCMask is ^0x6969 = 0x9696 — the relation
csbkCRCMask (0x5A5A) bears to ETSI’s 0xA5A5 in
Part 4.
TestParsePIHeaderReferenceLiteral pins it with an independent Python CRC’s
vector — the
literal-vector rule,
since a round-trip through AssemblePIHeader agrees with either mask.
Two choices are deliberate. IsRC4 keys on the low three bits of the
algorithm octet, as DSD-FME does — but only under feature set 0x10:
Hytera’s Enhanced Privacy (FID 0x68, 40-bit MI) is a different
construction and is not claimed. And the header has no RS(12,9)
trailer, so its CRC is the only integrity check: a BPTC-clean block that
fails it is dropped, never guessed, and PIHeaderDetector.Rejects keeps its
hex as the instrument for an unnamed vendor layout. The detector runs beside
the voice Decoder exactly like the terminator detector: data-sync slice,
both polarities, slot type, BPTC, ParsePIHeader.
Key‖MI, drop 256, seven bytes a frame
The construction is short, and every line is a protocol fact two open
decoders agree on.
EPKeystream appends the four MI bytes (MSB first) to the key, seeds Go’s
rc4.NewCipher, discards EPKeystreamDrop = 256 bytes and returns the
next n; AdvanceMI clocks a 32-bit LFSR with feedback
(l>>31)^(l>>3)^(l>>1) thirty-two times — x³²+x⁴+x²+1.
DescrambleSuperframe XORs those 126 bytes onto the eighteen FEC-decoded
payloads in place. Two rules keep alignment honest: a frame that failed
FEC (nil) is skipped but still consumes its 7-byte slot, and a frame
carrying the AMBE+2 silence vector (0xF801A99F8CE080,
Part 11’s
subject) is sent in clear and passes through untouched while consuming its
slot — DSD-FME’s silence guard.
The kinship with P25 is close: internal/radio/p25/phase1/adp.go keys RC4
with the key plus the first 8 octets of P25’s 72-bit MI, drops 256, and
lays the stream onto IMBE frames from absolute byte 267 (LDU1) and 368
(LDU2) — OP25’s layout, verified on the same reporter’s ADP call (pitch
track 0.13 → 0.57). One cipher, one warm-up, and — next — one convention.
The IV that names the next superframe
If the header were the only MI source, a scanner tuning in late — grant,
retune, DDC settle, frame lock — would never decrypt anything. So every
superframe also carries an MI: each of the 18
on-air frames donates one nibble in its unprotected C3 sub-vector — bits
71, 67, 63, 59, C3[0..3] after deinterleave. The 72 bits form three
Golay(24,12) codewords —
codeword j takes data from bursts A, B, C and parity from D, E, F — whose
36 data bits are the 32-bit MI plus a CRC-4 (x⁴+x+1, output inverted). That
is ExtractEmbeddedIV.
Now the convention. The embedded IV is the NEXT superframe’s MI — the
P25 LDU2 Encryption Sync
rule. On the #1187 radio every superframe’s IV equals AdvanceMI of the one
before, and the first equals AdvanceMI of the header’s; DSD-FME agrees
(dmr_alg_refresh LFSR-advances at burst F before dmr_late_entry_mi
compares).
EPTracker.Next is the rule as a switch. A verified IV equal to
AdvanceMI(next) confirms the chain; with no chain at all, a verified IV
yields the current MI by RewindMI, so late entry decrypts the very
superframe it verified on. A verified IV that disagrees
is where the Golay gate matters: a radius-3 sphere covers ~57 % of all
24-bit words, so clear vocoder bits pass all three codewords plus the CRC-4
about 1 % of the time, whereas three clean codewords plus CRC are
~2⁻⁴⁰ by chance. A clean disagreement means a missed superframe and the MI
is re-derived from it; a corrected one holds the prediction (Mismatches
counts both).
Composer to recorder
The per-call state (dmrEPState, composer/dmr_ep.go) never guesses a
key and never acts on an embedded IV for a call with no other encryption
evidence unless that IV decoded perfectly clean. onPIHeader seeds the
tracker (SetHeaderMI),
resolves the key through KeyResolver when IsRC4, and publishes
events.KindCallEncryption with Protocol: "dmr"; superframe runs
ExtractEmbeddedIV → EPTracker.Next → DescrambleSuperframe in
place.
KeyResolver is built from trunking.systems[].encryption_keys
(buildKeyResolver, logging daemon: in-process decryption keys configured
(DMR enhanced privacy / P25 ADP)); algorithm must be rc4 / arc4 /
adp (DES and AES fail at load with “not supported yet”), key is 1–32 hex
bytes, and a key under a different ID is never applied. The header lands at
INFO as composer: dmr PI header — call is encrypted, and the counters —
pi_headers, iv_verified, mi_predicted, mi_mismatches,
descrambled_frames, no_key — ride the debug composer: dmr enhanced
privacy line.
The event is the same KindCallEncryption P25 publishes, so the engine’s
follow / metadata / ignore policy
and its configured-key exemption apply unchanged, and
recordings.skip_encrypted
(Recording & Streaming Part 7)
still decides whether a keyless call reaches disk. Independently of any key,
recordings.crypto_capture_path hands every superframe’s ciphertext plus
its MI to the cryptocap bridge for the
keystream-reuse workflow.
TestComposerDMREnhancedPrivacyDecryptsWithConfiguredKey runs the full
composer and requires the recorder’s .raw sidecar to hold clear
payloads.
The capture that settled it
The #1187 reporter posted two known-key discriminator captures — key IDs 11
and 22, colour code 2, TG 582743, simplex — and
TestDMREnhancedPrivacyReplay judged them: production DDC → dmrrx
receiver → PIHeaderDetector + superframe decoder → EPTracker →
DescrambleSuperframe → ambe2-dmr, printing every header, the IV/MI
timeline and a one-line VERDICT. Knobs: GT_DMR_EP_IQ or
GT_DMR_EP_AUDIO (a discriminator WAV, FM re-modulated), GT_DMR_EP_KEY,
GT_DMR_EP_KEYID, GT_DMR_EP_OUT, and GT_DMR_EP_DUMP, which writes every
superframe before descrambling so a keystream hypothesis is testable
offline.
Both runs failed at first, for opposite reasons; neither was the cipher. First, the harness sliced voice with the back-to-back 132-dibit single-slot decoder; a simplex handheld sends one burst per 60 ms frame, 288 dibits apart (Part 3), so bursts B–F were read out of the gaps: burst A decoded FEC-clean, B–F carried the random-word Golay signature — 73 % needing exactly 3 corrections, 12 % exactly 2 — which looks exactly like post-FEC scrambling and is not — the same defect #1192 had fixed in production the day before. The harness now defaults to the cadence-detecting decoder, and a C0/C1 Golay histogram is the first instrument to read when frames look scrambled. Second, the IV convention: correctly sliced frames decoded on the wrong MI while the wrongly sliced ones had decoded on the right one.
The verdict metric matters because loudness would have lied: random AMBE+2
parameters through the vocoder are loud, so the harness reports RMS but
never trusts it. It measures pitch continuity, the fraction of
consecutive frame pairs whose fundamental index moves by at most 10: speech
≳0.5, ciphertext ≈0.15. Post-fix the captures score 0.77 and 0.74.
TestEPCaptureIssue1187 pins it against literal on-air frames
(voice/testdata/ep_issue1187_ptt1.json): the header→IV chain yields
continuity ≥ 0.55, late entry rewinds to the header’s MI, and decrypting each
superframe with its own IV stays ≤ 0.35 — the old semantics fail it.
Verified-language, as the repo states it: Enhanced Privacy is
capture-verified. Open on #1187 is the daemon path on air — a live
call with encryption_keys configured recording intelligible audio (the
#764/#771 discipline). Not decoded: Hytera EP, Kirisun, DES/AES; the
DMR encryption reference
and RC4 page cover the
landscape, and
docs/dmr-encryption.md
has the capture-contribution recipe.
How the IV convention shaped the Go code
- The tracker decodes on what it already knows.
Nextreturns the current MI from the header, the previous IV or the prediction, and lets the embedded IV steer onlynext— the convention lives in control flow. - Verification strength is a parameter, not a boolean.
ExtractEmbeddedIVreturnscorrectedbesideok; a clean triple and a ~1 % false verify are treated differently. - Instruments outlive the fix.
Mismatches,PIHeaderDetector.RejectsandGT_DMR_EP_DUMPpin the next vendor variant.
Where this goes next
Twice here a receiving-side defect masqueraded as a cipher problem, and instruments caught both. Part 13 makes that the subject: testing DMR without a repeater — committed real-air slices, the replay harnesses and their scrubs, and the fixture lessons that hid direct mode.
FAQ
Can GopherTrunk decrypt DMR Enhanced Privacy?
Yes, with a key the operator supplies under
trunking.systems[].encryption_keys whose key_id matches the Privacy
Indicator header. The voice chain descrambles RC4 “Enhanced Privacy” in
process and records clear audio; without a key the call is recorded as
ciphertext with algorithm and key ID surfaced.
Where does a DMR call carry its Message Indicator? Twice: in the Privacy Indicator header sent after the Voice LC Header at keyup (32 bits, octets 3–6), and embedded in every voice superframe as 18 nibbles forming three Golay(24,12) codewords plus a CRC-4 — the late-entry copy, which names the next superframe’s MI, not the one carrying it.
Why did a working keystream produce garbage at first? Two receiving-side defects. The tracker applied each embedded IV to its own superframe instead of the next, so every superframe after the first used the wrong MI; and the replay harness sliced a simplex handheld’s bursts with the back-to-back single-slot decoder, reading bursts B–F out of the gaps.
How do I know a decrypt actually worked? Not by ear or level — random vocoder parameters are loud. The harness measures pitch continuity, the fraction of consecutive AMBE+2 frames whose fundamental index (bits 0..3 and 37..39) moves by at most 10: speech ≳0.5, ciphertext ≈0.15. The #1187 captures score 0.77 and 0.74.
Is DMR Enhanced Privacy verified on air in GopherTrunk?
Capture-verified: two known-key captures decode to speech through
TestDMREnhancedPrivacyReplay, with literal on-air frames pinned in
TestEPCaptureIssue1187. The remaining gate on #1187 is a live daemon call
with encryption_keys configured that records intelligible audio.
Series navigation
Part 12 of 14 · ← Part 11: AMBE+2 Voice & the Silence-Frame Bug · Next → Part 13: Testing DMR Without a Repeater