APCO P25 KVL Analysis

These are my efforts to analyze the Keyfill protocol communications exchange between a Motorola1 KVL-3000 Plus and a Motorola1 XTS-2500 P25 portable. This played out while VE7DSS and I spent an evening hacking in the summer of 2018.

The key was an arbitrary AES 256-bit key and the key fill is done in the clear, in ASTRO2 mode - not in FIPS Level 3 encryption mode. I suspect that the FIPS mode uses a symmetric encryption (OTAR shadow key).

Bitstream Capture

An oscilloscope showed the smallest observable data bit appearing to have a period of ~25uS - approximately 4000 bps. Without access to a logic analyzer, a DFRobot FireBeetle 32-bit microcontroller was wired up to an XTS jig and a quick program was written in C to sample the KVL line at 100 kHz.

Sampling at 100 kHz for any period of time required compression in both memory and over the wire. I chose to write a custom Run-length Encoding algorithm as the actual data transferred over the KVL line appeared relatively low. Instead of recording a logic value repeatedly, the RLE encoding records duration of each logic states. When the logic state transitions, the timer is reset and a new line is created.

Following the data capture, I dumped the results as ASCII over a UART to be captured by a PC. The compressed output is 9,162 bytes versus the raw samples being 13,961,142 bytes or a compression ratio of 1523:1. The ASCII format is {Record #},{0,1},{duration}:


Quick and dirty Python script to convert custom run-length-encoded (RLE) output to samples for Sigrok.



# convert RLE data file to samples

sample_rate = 1000000 # 100 kHz

duration_unit_s = (1/1000000)
input_file_path = "rle.csv"
output_file_path = "output.csv"

input_file = open(input_file_path, 'r')
output_file = open(output_file_path, 'w')

input_lines = 0
output_lines = 0

for line in input_file:

	input_lines += 1
	com = line.split(',')
	state = com[1]
	duration = int(com[2])
	# duration was uS - so we convert to samples

	samples = int(round((duration*duration_unit_s)/(1/sample_rate),0))
	for sample in range(samples):
		output_lines += 1

	# write initial sample

print("read", input_lines, "lines.")


PulseView Analysis

The raw data dump was converted to a data format compatible with Sigrok PulseView. PulseView is an open source logic analyzer with protocol decoder support.

The KVL keyfill protocol is proprietary and does not follow conventional UART timings or structure.

KVL Decoder Plugin

The UART decoder in PulseView is written in Python and can be modified to decode the KVL communications.

Create a copy of the UART decoder and modify for KVL.


# critical protocol constants

baudrate = 4000
num_data_bits = 8
parity_type = 'even'
parity_check = 'yes'
num_stop_bits = 0.0
bit_order = 'msb-first'
invert_rx = 'no'

End Result:

PulseView Screenshot


  1. MOTOROLA is a trademark or registered trademark of Motorola Trademark Holdings, LLC. This site is in no way affiliated with Motorola. [return]
  2. ASTRO 25 and ASTRO Digital Solutions (1991) is digital two-way radio communications by Motorola Solutions. [return]