BLE with coded PHY and CCM decryption

Hey, I'm trying to get a basic BLE with coded PHY (125Kbps) setup with on the fly encryption/decryption working but I'm running into a strange problem. I'm using a custom SDK which is why I spent a few weeks trying to figure it out my self before wasting people's time with having to deal with 3rd party SDKs. But honestly, I'm at a loss and can't seem to figure out the root cause of the issue.

Setup:

I have 2 nrf52840 dongles, one setup as a transmitter which simply transmits the same frame once every second. The other is setup as a receiver. Both are configured to operate in coded PHY mode (125Kbps). Without encryption, the receiver is able to receive all frames with CRC_OK events being generated after each frame.

Problem:

When I add encryption and decryption using the CCM peripheral, I run into a strange timing problem. Using a debugger, I can see that the transmitter successfully encrypts the frame and on the receiver side we receive the correct encrypted frame (CRC_OK event generated and the contents of the temp packet match the transmitted frame), however the ENDCRYPT event is generated, the MICSTATUS is CheckFailed. When I inspect the contents of the decrypted packet, I can see that all bytes are correctly decrypted except for the LAST byte (this holds true for the few packet sizes that I tried).

Now here is the interesting part, if instead of the hardcoded PPI channel that triggers the CRYPT task using the ADDRESS event, I use a custom PPI channel to trigger the CRYPT task using the PAYLOAD event (so much later in the packet reception), the frame is decrypted correctly. It seems like the CCM module (even though configured with the same data rate of 125Kbps) is running slightly faster than the RADIO and it's processing the last byte before the RADIO has written it to memory.

I've attached all of the relevant code. Please let me know if you have further questions or are missing important parts.

/**
 * The CBC-MAC encryption module for NRF microcontrollers.
 *
 * @author  Ehsan Tofigh
 * @project lcx
 * @file    src/libcortex/ccm.c
 * @license
 */

#include <libcortex/ccm.h>
#include <libcortex/nvic.h>
#include <libcortex/util.h>

#include <string.h>

/** CCM engine state structure. */
static struct {
	uint8_t  key[16U];       /**< 16 byte AES key. */
	uint64_t cnt;            /**< Packet counter (ony 39 LSB bits are used) */
	struct {
		uint8_t dir : 1; /**< Direction. */
		uint8_t pad : 1; /**< Zero padding. */
		uint8_t     : 6;
	};
	uint8_t iv[8U];          /**< 8 byte initialisation vector. */
	uint8_t scratch[278U + 20];   /**< Scratch memory structure. */
} __attribute__((packed)) ccm;

bool lcx_ccm_init (const enum lcx_ccm_mode_datarate datarate,
		   const uint8_t                   *key,
		   const uint8_t                   *iv)
{
	lcx_assert(key == NULL);
	lcx_assert(iv == NULL);
	lcx_assert(datarate >= LCX_CCM_MODE_DATARATE_COUNT);

	/* Prepare the configuration structure: */
	memcpy(ccm.key, key, sizeof (ccm.key));
	memcpy(ccm.iv, iv, sizeof (ccm.iv));
	ccm.cnt = 0U;

	/*Initialise the CCM peripheral: */
	LCX_CCM->CNFPTR.CNFPTR         = (uint32_t) &ccm;
	LCX_CCM->SCRATCHPTR.SCRATCHPTR = (uint32_t) ccm.scratch;
	LCX_CCM->MODE.DATARATE         = lcx_ccm_mode_datarate_map[datarate];
	LCX_CCM->MODE.LENGTH           = lcx_ccm_mode_length_map[LCX_CCM_MODE_LENGTH_EXTENDED];
	// LCX_CCM->MAXPACKETSIZE.MAXPACKETSIZE = 0x1B;

	LCX_CCM->ENABLE.ENABLE = lcx_ccm_enable_enable_map[LCX_CCM_ENABLE_ENABLE_ENABLED];

	return true;
}

bool lcx_ccm_start (const enum lcx_ccm_mode_mode mode,
		    const uint8_t               *in,
		    const uint8_t               *out)
{
	lcx_assert(mode >= LCX_CCM_MODE_MODE_COUNT);
	lcx_assert(in == NULL);
	lcx_assert(out == NULL);

	LCX_CCM->MODE.MODE     = lcx_ccm_mode_mode_map[mode];
	LCX_CCM->INPTR.INPTR   = (uint32_t) in;
	LCX_CCM->OUTPTR.OUTPTR = (uint32_t) out;

	if (mode == LCX_CCM_MODE_MODE_ENCRYPTION) {
		lcx_set_shorts(LCX_CCM, .ENDKSGEN_CRYPT = 1U);
	} else {
		lcx_set_shorts(LCX_CCM, .ENDKSGEN_CRYPT = 0U);
	}

	return true;
}
ale.hccm.h
/**
 * Example file for demonstrating the ALE transmitter functionality.
 *
 * @author  Ehsan Tofigh
 * @project lcx
 * @file    src/examples/ale_tx/main.c
 * @license
 */

#include <libradio/ale.h>
#include <libcortex/nvic.h>
#include <libcortex/radio.h>
#include <libcortex/systick.h>

#include <stdbool.h>
#include <stdint.h>

/** Configuration structure for the ALE module. */
static const struct lrd_ale_handle handle = {
	.key = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF },
};

struct {
	struct lrd_ale_header header;
	char   payload[12U];
} __attribute__((packed)) packet = {
	.payload = "Hello World!",
	.header.length = sizeof(packet.payload),
};

/**
 * Entry point of program.
 *
 * @return Return code of the program.
 */
int main (void)
{
	(void) lcx_nvic_init(1U);
	(void) lrd_ale_init(&handle);

	while (true) {
		(void) lrd_ale_transmit(&packet.header, sizeof(packet), false);
		lcx_systick_sleep_ms(1000U);
	}

	return 0;
}
/**
 * Example file for demonstrating the ALE receiver functionality.
 *
 * @author  Ehsan Tofigh
 * @project lcx
 * @file    src/examples/ale_rx/main.c
 * @license
 */

#include <libradio/ale.h>
#include <libcortex/itm.h>
#include <libcortex/nvic.h>
#include <libcortex/radio.h>
#include <libcortex/systick.h>

#include <stdbool.h>
#include <stdint.h>

/** Configuration structure for the ALE module. */
static const struct lrd_ale_handle handle = {
	.key = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF },
};

/**
 * Entry point of program.
 *
 * @return Return code of the program.
 */
int main (void)
{
	(void) lcx_nvic_init(1U);
	(void) lrd_ale_init(&handle);
	(void) lrd_ale_start_receiver();

	while (true) {
	}

	return 0;
}
/**
 * A Low Energy radio stack for the NRF microcontrollers.
 *
 * @author  Ehsan Tofigh
 * @project lcx
 * @file    src/libradio/ale.c
 * @license
 */

#include <libradio/ale.h>
#include <libcortex/nvic.h>
#include <libcortex/radio.h>
#include <libcortex/rtc.h>
#include <libcortex/rng.h>
#include <libcortex/ppi.h>
#include <libcortex/gpiote.h>
#include <libcortex/util.h>

#include <string.h>

/** Maximum payload size in bytes. */
#define MAX_PAYLOAD_SIZE   251U

/** Access address used for the channel. */
#define ACCESS_ADDRESS     0x41424347U

/**
 * The pre-programmed PPI channel for triggering AES Key generation when the
 * radio is ready.
 */
#define PPI_RADIO_READY_KSGEN   24U

/**
 * The pre-programmed PPI channel for triggering AES decryption when the address
 * is received on the radio.
 */
#define PPI_RADIO_ADDRESS_CRYPT 25U

/** ALE packet structures. */
struct packet {
	struct lrd_ale_header header;          /**< Packet header. */
	union {
		uint8_t raw[MAX_PAYLOAD_SIZE]; /**< Raw view of the packet. */
	};
} __attribute__((packed));

/** ALE state structure. */
static struct {
	struct packet tmp_packet;            /**< Buffer to temporarily keep the packet for encryption/decryption. */
	struct packet rx_packet;             /**< Buffer to keep the decrypted packet. */
} ale;

/** Packet structure configuration. */
static const struct lcx_radio_packet_config packet_config = {
	.pcnf0 = {
		.PLEN    = lcx_radio_pcnf0_plen_map[LCX_RADIO_PCNF0_PLEN_LONGRANGE],
		.LFLEN   = 8U,
		.S0LEN   = 1U,
		.S1LEN   = 8U,
		.S1INCL  = lcx_radio_pcnf0_s1incl_map[LCX_RADIO_PCNF0_S1INCL_INCLUDE],
		.CILEN   = 2U,
		.TERMLEN = 3U,
	},
	.pcnf1 = {
		.WHITEEN = lcx_radio_pcnf1_whiteen_map[LCX_RADIO_PCNF1_WHITEEN_ENABLED],
		.BALEN   = 3U, /* Base address length in number of bytes minus 1. */
		.MAXLEN  = MAX_PAYLOAD_SIZE + 4U,
	},
	.crccnf = {
		.LEN      = lcx_radio_crccnf_len_map[LCX_RADIO_CRCCNF_LEN_THREE],
		.SKIPADDR = lcx_radio_crccnf_skipaddr_map[LCX_RADIO_CRCCNF_SKIPADDR_SKIP],
	},
	.crcpoly = { .CRCPOLY = 0x000065BU },
	.crcinit = { .CRCINIT = 0x0555555U },
	.tifs    = { .TIFS    = 150U       }, /* Interframe spacing in us. */
};

/** List of ALE channel frequencies. */
static const uint16_t ch_freq[] = {
	[36U] = 2400U,
	[37U] = 2402U,
	[38U] = 2426U,
	[39U] = 2480U,
};

/**
 * Set the access address.
 *
 * @param[in] addr 4 byte access address.
 */
static void set_access_address (const uint32_t addr)
{
	LCX_RADIO->BASE0.BASE0       = (addr << 8U);
	LCX_RADIO->PREFIX0.AP0       = (uint8_t) (addr >> 24U);
	LCX_RADIO->RXADDRESSES.ADDR0 = 1U;
}

/**
 * Main radio interrupt handler.
 */
static void radio_irq_handler (void)
{
	if (lcx_radio_test_and_clear_event(LCX_RADIO_EVENTS_PHYEND)) {
	}

	if (lcx_radio_test_and_clear_event(LCX_RADIO_EVENTS_ADDRESS)) {
	}

	if (lcx_radio_test_and_clear_event(LCX_RADIO_EVENTS_CRCOK)) {
	}

	if (lcx_radio_test_and_clear_event(LCX_RADIO_EVENTS_CCABUSY)) {
	}
}

bool lrd_ale_init (const struct lrd_ale_handle *handle)
{
	lcx_assert(handle == NULL);

	/* Enable the radio: */
	lcx_assert(lcx_radio_init() == false);

	/* Fix for errata 191: */
	*(volatile uint32_t *) 0x40001740U =
		((*((volatile uint32_t *) 0x40001740U)) & 0x7FFF00FFU) | 0x80000000U | (((uint32_t)(196U)) << 8U);

	/* Configure the radio: */
	lcx_assert(lcx_radio_set_packet_config(&packet_config) == false);
	lcx_assert(lcx_radio_set_mode(LCX_RADIO_MODE_MODE_BLE_LR125KBIT) == false);
	lcx_assert(lcx_radio_set_freq(ch_freq[36U]) == false);
	LCX_RADIO->DATAWHITEIV.DATAWHITEIV = 36U;
	LCX_RADIO->TXADDRESS.TXADDRESS = 0U;

	/* Configure the cryptography engine: */
	lcx_assert(lcx_ccm_init(LCX_CCM_MODE_DATARATE_125KBPS, handle->key, handle->iv) == false);

	/* Set the access address: */
	set_access_address(ACCESS_ADDRESS);

	/* Configure the clear channel assessment: */
	LCX_RADIO->CCACTRL.CCAEDTHRES = 30U;

	/* Start the RNG: */
	lcx_rng_start();

	/* Enable the interrupts: */
	lcx_radio_enable_irq(LCX_RADIO_IRQ_PHYEND);
	lcx_radio_enable_irq(LCX_RADIO_IRQ_CCABUSY);
	lcx_nvic_enable_src(LCX_RADIO_NVIC_SRC, 1U, 0U, &radio_irq_handler);

	return true;
}

bool lrd_ale_start_receiver (void)
{
	/* Configure the encryption engine: */
	lcx_assert(lcx_ccm_start(LCX_CCM_MODE_MODE_DECRYPTION,
				 (const uint8_t *) &ale.tmp_packet,
				 (const uint8_t *) &ale.rx_packet) == false);
	lcx_assert(lcx_radio_set_ptr((const uint8_t *) &ale.tmp_packet) == false);
	lcx_ccm_trigger_task(LCX_CCM_TASKS_KSGEN);

	/* *************** This does not work! ***************** */
	lcx_assert(lcx_ppi_ch_enable(PPI_RADIO_ADDRESS_CRYPT, true) == false);

	/* ******************* This workS! ********************* */
	// lcx_ppi_ch_init(0U, lcx_radio_get_event(LCX_RADIO_EVENTS_PAYLOAD),
	//       lcx_ccm_get_task(LCX_CCM_TASKS_CRYPT), NULL);
	// lcx_assert(lcx_ppi_ch_enable(0U, true) == false);

	/* Configure the shortcuts: */
	lcx_set_shorts(LCX_RADIO, .RXREADY_START = 1U, .PHYEND_DISABLE = 1U, .DISABLED_RXEN = 0U, .ADDRESS_RSSISTART = 1U, .DISABLED_RSSISTOP = 1U);

	lcx_radio_trigger_task(LCX_RADIO_TASKS_RXEN);

	return true;
}

uint8_t lrd_ale_read_edlevel (const uint32_t cnt)
{
	/* Set the number of measurements: */
	LCX_RADIO->EDCNT.EDCNT = cnt;

	/* Configure the shortcuts: */
	lcx_set_shorts(LCX_RADIO, .READY_EDSTART = 1U, .EDEND_DISABLE = 1U);

	lcx_radio_trigger_task(LCX_RADIO_TASKS_RXEN);
	while (lcx_radio_test_and_clear_event(LCX_RADIO_EVENTS_EDEND) == false);

	return (uint8_t) LCX_RADIO->EDSAMPLE.EDLVL;
}

bool lrd_ale_transmit (struct lrd_ale_header *header,
		       const size_t           len,
		       const bool             cca)
{
	lcx_assert(header == NULL);
	lcx_assert(LCX_RADIO->STATE.STATE != lcx_radio_state_state_map[LCX_RADIO_STATE_STATE_DISABLED]);

	/* Check the size of the payload: */
	size_t l = len - sizeof(*header);
	lcx_assert(l > MAX_PAYLOAD_SIZE);
	header->length = (uint8_t) l;

	/* Configure the encryption engine: */
	lcx_assert(lcx_ppi_ch_enable(PPI_RADIO_ADDRESS_CRYPT, false) == false);
	lcx_assert(lcx_ccm_start(LCX_CCM_MODE_MODE_ENCRYPTION,
				 (const uint8_t *) header,
				 (const uint8_t *) &ale.tmp_packet) == false);
	lcx_assert(lcx_radio_set_ptr((const uint8_t *) &ale.tmp_packet) == false);
	lcx_ccm_trigger_task(LCX_CCM_TASKS_KSGEN);

	/* Configure the shortcuts: */
	if (cca) {
		lcx_set_shorts(LCX_RADIO, .RXREADY_CCASTART = 1U, .TXREADY_START = 1U, .PHYEND_DISABLE = 1U, .CCAIDLE_TXEN = 1U, .CCABUSY_DISABLE = 1U);
		lcx_radio_trigger_task(LCX_RADIO_TASKS_RXEN);
	} else {
		lcx_set_shorts(LCX_RADIO, .TXREADY_START = 1U, .PHYEND_DISABLE = 1U);
		lcx_radio_trigger_task(LCX_RADIO_TASKS_TXEN);
	}

	return true;
}

  • Hi,

    I am sorry for the delay. Have you looked at the low-level implementation for Nordic in the Zephyr LL and used that as a reference? Unfortunately I have not had a chance to dig into it, but I suspect you may be able to find some inspiration in how things are done there.

  • Hey Einar, Sorry for the very late reply. Unfortunately I got Covid and had to self quarantine for a while.

    At last I'm back in the office and managed to do some testing.

    Thanks for sending the link for the Zephyr implementation. I went through it and couldn't spot any differences at first. Then went back to the code and started to experiment some more, until I realized that by setting PCNF0.S1LEN = 0, I could get the decryption to work!.

    Investigating this further and going back to the reference manual, showed that S1 field is marked as "Reserved for future use" in the CCM chapter. I took this as that I can use this field for my own purposes, however this is not the case. When looking at the zephyr code, they also set the S1LEN to either 0 or 8 depending on whether the device supports CTE (which is an extension for direction finding). Since the nrf52840 doesn't support direction finding, I assume the CCM module is not configured in a way to allow the use of the S1 field. If this field is transmitted by the radio, the CCM module will lag behind by 1 byte and thus not complete the decryption in sequence with the Radio module.

    At least that's my theory.

    In any case, I seem to have a good work around for my problem and can continue. Thanks for all of the help!

Related