Obligatory Disclaimer
Security is hard. If a product handles sensitive information then it is the engineer's responsibility to his customers to consult a security professional. However, experimenting with security should be encouraged whenever possible; the world needs better security and everyone has to start somewhere.
Introduction
The security features that are built into Bluetooth Low Energy (BLE) are focused on the link between devices. BLE uses authentication to make it possible for a user to pair with his own device regardless of the presence of other peoples' devices. Once the user's devices are paired, encryption is used to hide the contents of the transmitted information from unauthorized eavesdroppers. In general, BLE's security works well for simple devices such as Heart Rate Monitors despite the fact that some versions of the key exchange protocol are notably flawed.
Common justifications for adding application-level security on top of what BLE provides include:
- Compensating for use cases where a secure pairing method can not be used
- Hiding sensitive information from an untrusted intermediate device (e.g. smart phone) while it is being transmitted to a trusted device (e.g. web service)
- Introducing an authentication protocol to allow multiple users to share a single device
Considerations
Here are a few topics to consider whenever security discussions arise:
- Does the user need to be authenticated or is it sufficient to authenticate the device?
- Does any information need to be hidden?
- Does the device need to differentiate between multiple users?
- Do they have different privileges?
- Will it be necessary to add/remove users?
- How will the user names be stored in the device?
- Do they require different passwords?
- Can these passwords be updated?
- How will the passwords be stored in the device?
- Do individual devices require unique IDs and/or passwords?
- How will this information be stored in the device?
- What additional infrastructure will be required (e.g. a database for storing IDs in production)?
Accessing The Cipher
The AES Electronic CodeBook (ECB) peripheral on the nRF51 and nRF52832 devices provides a simple interface to the same 128-bit AES encryption core that is used by the CCM and AAR peripherals. When used correctly, the ECB can provide a reasonable amount of security with only a small amount of overhead.
Accessing the ECB via the SoftDevice APIs is easy. Three 128-bit (16-byte) buffers are required by the cipher and must be located in RAM so nrf_soc.h defines the following struct:
/**@brief AES ECB data structure */
typedef struct
{
uint8_t key[SOC_ECB_KEY_LENGTH]; /**< Encryption key. */
uint8_t cleartext[SOC_ECB_CLEARTEXT_LENGTH]; /**< Clear Text data. */
uint8_t ciphertext[SOC_ECB_CIPHERTEXT_LENGTH]; /**< Cipher Text data. */
} nrf_ecb_hal_data_t;
After the key and cleartext buffers are initialized the sd_ecb_block_encrypt function is used to encrypt the cleartext and produce the ciphertext:
static nrf_ecb_hal_data_t m_ecb_data;
memcpy(&m_ecb_data.key[0], p_ecb_key, SOC_ECB_KEY_LENGTH);
memcpy(&m_ecb_data.cleartext[0], p_cleartext, SOC_ECB_CLEARTEXT_LENGTH);
err_code = sd_ecb_block_encrypt(&m_ecb_data);
APP_ERROR_CHECKK(err_code);
According to the documentation the sd_ecb_block_encrypt function always succeeds.
At this point in time there are two questions that inevitably arise:
- How can the ECB be used to decrypt the ciphertext that it produces?
- Isn't the Electronic CodeBook cipher bad?
The answer to both questions is to use the ECB as a stream cipher.
Block Vs. Stream Cipher
ECB is a block cipher; the result of the encryption depends solely on the contents of the input buffer and the key. The consequence of this is that encrypting two buffers that have identical contents will always yield identical results. The classic demonstration of why this can be a bad thing is created by encrypting the individual pixels of an image using a block cipher:
- The original image.
- ECB was used to individually encrypt each pixel in the image. A lot of information about the original image is still present even though it would be difficult to decipher the true color of any particular pixel without knowing the secret key.
Stream ciphers, on the other hand, cause the result of the current operation to depend on additional factors such as the result of the previous operation (e.g. Cipher Block Chaining (CBC)) or an initial state along with the order in which the operations are performed (e.g. Counter Mode (CM)).
- The ECB was converted to a stream cipher (CM) before the pixels in the image were encrypted.
Converting a block cipher to a stream cipher requires keeping track of extra state variables between invocations of the block cipher. The following interaction between two parties (Alice and Bob) illustrates the extra steps that are required in order to use the ECB as a CM cipher in particular:
- Alice and Bob both have access to the same secret key
- Alice generates a 12-byte random number called a nonce
- Alice sends her nonce to Bob and does not care if anyone else sees it because it is not secret
- Alice writes the nonce to the upper 12-bytes of a 16-byte buffer and zeroes the lower 4 bytes of the buffer so those bytes can be used as a 4-byte counter. Bob does the same on his computer. This buffer will be referred to as the [nonce|ctr] buffer.
- Alice uses the ECB to encrypt her [nonce|ctr] buffer using the secret key
- Alice uses the eXclusive OR (XOR) operation to combine the output buffer from the ECB with a 16-byte buffer that contains the message that she wants to send to Bob. The result of this operation is the ciphertext.
- Alice sends the ciphertext to Bob and increments the counter in her [nonce|ctr] buffer
- Bob also uses the ECB to encrypt his [nonce|ctr] buffer using the secret key
- Bob applies the XOR operation to combine the output buffer from the ECB with the ciphertext. The result of this operation is Alice's original message (because XOR'ing with the same value twice yields the original value).
- Bob increments the counter in his [nonce|ctr] buffer to prepare for the next message
As long as Alice and Bob keep their counters in sync they can continue sending messages until the counters are exhausted. Note: Alice should generate a new 12-byte nonce and send it to Bob before the counters are allowed to wrap.
One of the features of a cipher such as AES is that even a small difference in one of its inputs will lead to a significant difference in its output; in this case, flipping a single bit in the counter value is sufficient to prevent patterns in the stream cipher's output even when encrypting two buffers that have identical contents.
Implementing Counter Mode
Cryptographically secure nonce values can be generated using the Random Number Generator (RNG) peripheral. Note that there is a difference in the amount of time that is required to generate a random byte between the nRF51 and nRF52832 devices. The SoftDevice maintains a random pool but in certain circumstances it may be necessary to wait for that pool to regenerate:
#include "nrf_soc.h"
#include "nrf_delay.h"
#include "app_error.h"
#define ECB_KEY_LEN (16UL)
#define COUNTER_BYTE_LEN (4UL)
#define NONCE_RAND_BYTE_LEN (12UL)
// The RNG wait values are typical and not guaranteed. See Product Specifications for more info.
#ifdef NRF51
#define RNG_BYTE_WAIT_US (677UL)
#elif defined NRF52
#define RNG_BYTE_WAIT_US (124UL)
#else
#error "Either NRF51 or NRF52 must be defined."
#endif
/**
* @brief Uses the RNG to write a 12-byte nonce to a buffer
* @details The 12 bytes will be written to the buffer starting at index 4 to leave
* space for the 4-byte counter value.
*
* @param[in] p_buf An array of length 16
*/
void nonce_generate(uint8_t * p_buf)
{
uint8_t i = COUNTER_BYTE_LEN;
uint8_t remaining = NONCE_RAND_BYTE_LEN;
// The random number pool may not contain enough bytes at the moment so
// a busy wait may be necessary.
while(0 != remaining)
{
uint32_t err_code;
uint8_t available = 0;
err_code = sd_rand_application_bytes_available_get(&available);
APP_ERROR_CHECK(err_code);
available = ((available > remaining) ? remaining : available);
if (0 != available)
{
err_code = sd_rand_application_vector_get((p_buf + i), available);
APP_ERROR_CHECK(err_code);
i += available;
remaining -= available;
}
if (0 != remaining)
{
nrf_delay_us(RNG_BYTE_WAIT_US * remaining);
}
}
}
The only static (module-level) variables that are required are an initialization flag and the nrf_ecb_hal_data_t structure that was introduced earlier:
static bool m_initialized = false;
// NOTE: The ECB data must be located in RAM or a HardFault will be triggered.
static nrf_ecb_hal_data_t m_ecb_data;
/**
* @brief Initializes the module with the given nonce and key
* @details The nonce will be copied to an internal buffer so it does not need to
* be retained after the function returns. Additionally, a 32-bit counter
* will be initialized to zero and placed into the least-significant 4 bytes
* of the internal buffer. The nonce value should be generated in a
* reasonable manner (e.g. using this module's nonce_generate function).
*
* @param[in] p_nonce An array of length 16 containing 12 random bytes
* starting at index 4
* @param[in] p_ecb_key An array of length 16 containing the ECB key
*/
void ctr_init(const uint8_t * p_nonce, const uint8_t * p_ecb_key)
{
m_initialized = true;
// Save the key.
memcpy(&m_ecb_data.key[0], p_ecb_key, ECB_KEY_LEN);
// Copy the nonce.
memcpy(&m_ecb_data.cleartext[COUNTER_BYTE_LEN],
&p_nonce[COUNTER_BYTE_LEN],
NONCE_RAND_BYTE_LEN);
// Zero the counter value.
memset(&m_ecb_data.cleartext[0], 0x00, COUNTER_BYTE_LEN);
}
The same function can be used to encode cleartext and decode ciphertext:
static uint32_t crypt(uint8_t * buf)
{
uint8_t i;
uint32_t err_code;
if (!m_initialized)
{
return NRF_ERROR_INVALID_STATE;
}
err_code = sd_ecb_block_encrypt(&m_ecb_data);
if (NRF_SUCCESS != err_code)
{
return err_code;
}
for (i=0; i < ECB_KEY_LEN; i++)
{
buf[i] ^= m_ecb_data.ciphertext[i];
}
// Increment the counter.
(*((uint32_t*) m_ecb_data.cleartext))++;
return NRF_SUCCESS;
}
The encrypt and decrypt functions can be defined as separate functions to avoid confusion (even though they both call the same function internally):
/**
* @brief Encrypts the given buffer in-situ
* @details The encryption step is done separately (using the nonce, counter, and
* key) and then the result from the encryption is XOR'd with the given
* buffer in-situ. The counter will be incremented only if no error occurs.
*
* @param[in] p_clear_text An array of length 16 containing the clear text
*
* @retval NRF_SUCCESS Success
* @retval NRF_ERROR_INVALID_STATE Module has not been initialized
* @retval NRF_ERROR_SOFTDEVICE_NOT_ENABLED SoftDevice is present, but not enabled
*/
uint32_t ctr_encrypt(uint8_t * p_clear_text)
{
return crypt(p_clear_text);
}
/**
* @brief Decrypts the given buffer in-situ
* @details The encryption step is done separately (using the nonce, counter, and
* key) and then the result from the encryption is XOR'd with the given
* buffer in-situ. The counter will be incremented only if no error occurs.
*
* @param[in] p_cipher_text An array of length 16 containing the cipher text
*
* @retval NRF_SUCCESS Succeess
* @retval NRF_ERROR_INVALID_STATE Module has not been initialized
* @retval NRF_ERROR_SOFTDEVICE_NOT_ENABLED SoftDevice is present, but not enabled
*/
uint32_t ctr_decrypt(uint8_t * p_cipher_text)
{
return crypt(p_cipher_text);
}
Conclusion
Using the Enhanced CodeBook peripheral as part of a stream cipher is the correct way to use it in the majority of applications.
Top Comments