From e3cede064a801cdef7f4183d1f83806031ee5563 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 22 Sep 2023 11:34:13 +0200 Subject: [PATCH] [LoRaW] Improve session persistence, check frame counters & Nonces, multiple MAC commands --- keywords.txt | 8 +- src/BuildOpt.h | 2 +- src/Hal.h | 58 +++--- src/TypeDef.h | 17 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 292 ++++++++++++++++++++++-------- src/protocols/LoRaWAN/LoRaWAN.h | 22 ++- 6 files changed, 286 insertions(+), 113 deletions(-) diff --git a/keywords.txt b/keywords.txt index 36bfb988..2a5cc1e3 100644 --- a/keywords.txt +++ b/keywords.txt @@ -289,8 +289,9 @@ setModem KEYWORD2 # LoRaWAN wipe KEYWORD2 +restoreOTAA KEYWORD2 beginOTAA KEYWORD2 -beginAPB KEYWORD2 +beginABP KEYWORD2 uplink KEYWORD2 downlink KEYWORD2 configureChannel KEYWORD2 @@ -399,5 +400,10 @@ RADIOLIB_ERR_INVALID_PORT LITERAL1 RADIOLIB_ERR_NO_RX_WINDOW LITERAL1 RADIOLIB_ERR_INVALID_CHANNEL LITERAL1 RADIOLIB_ERR_INVALID_CID LITERAL1 +RADIOLIB_ERR_UPLINK_UNAVAILABLE LITERAL1 RADIOLIB_ERR_COMMAND_QUEUE_FULL LITERAL1 RADIOLIB_ERR_COMMAND_QUEUE_EMPTY LITERAL1 +RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND LITERAL1 +RADIOLIB_ERR_JOIN_NONCE_INVALID LITERAL1 +RADIOLIB_ERR_N_FCNT_DOWN_INVALID LITERAL1 +RADIOLIB_ERR_A_FCNT_DOWN_INVALID LITERAL1 \ No newline at end of file diff --git a/src/BuildOpt.h b/src/BuildOpt.h index 5fe2e0ba..dbf3448e 100644 --- a/src/BuildOpt.h +++ b/src/BuildOpt.h @@ -443,7 +443,7 @@ // the amount of space allocated to the persistent storage #if !defined(RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE) - #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x60) + #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0xD0) #endif // This only compiles on STM32 boards with SUBGHZ module, but also diff --git a/src/Hal.h b/src/Hal.h index 7fa37aa4..9d89811f 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -7,43 +7,47 @@ #include "BuildOpt.h" // list of persistent parameters -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID (0) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID (1) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID (2) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID (3) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID (4) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID (5) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID (6) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID (7) - +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID (0) // this is NOT the LoRaWAN version, but version of this table +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID (1) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID (2) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID (3) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID (4) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID (5) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID (6) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID (7) #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID (8) #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID (9) #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DL_SETTINGS_ID (10) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX_DELAY_ID (11) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CF_LIST_ID (12) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID (13) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID (14) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_ID (15) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CF_LIST_ID (11) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX_DELAY_ID (12) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID (13) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID (14) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID (15) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_ID (16) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_ID (17) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID (18) static const uint32_t RadioLibPersistentParamTable[] = { - 0x00, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID - 0x04, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID - 0x08, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID - 0x0C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID + 0x00, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION + 0x08, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID + 0x0C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID 0x10, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID 0x20, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID 0x30, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID 0x40, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID - - 0x50, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID - 0x54, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID - 0x58, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DL_SETTINGS_ID - 0x5C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX_DELAY_ID + 0x50, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID + 0x54, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID + 0x58, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID + 0x5C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DL_SETTINGS_ID 0x60, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_CF_LIST - 0x70, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID - 0x74, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID - 0x78, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_ID - 0x7C, // end + 0x70, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX_DELAY_ID + 0x74, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID + 0x78, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID + 0x7C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID + 0x80, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_ID + 0x84, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_ID + 0x88, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID + 0xD0, // end }; /*! diff --git a/src/TypeDef.h b/src/TypeDef.h index a2cb3a62..0ebcbbb1 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -533,10 +533,25 @@ */ #define RADIOLIB_ERR_COMMAND_QUEUE_EMPTY (-1110) +/*! + \brief Unable to delete MAC command because it was not found in the queue. +*/ +#define RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND (-1111) + /*! \brief Unable to join network because JoinNonce is not higher than saved value. */ -#define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1111) +#define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1112) + +/*! + \brief Received downlink Network frame counter is invalid (lower than last heard value). +*/ +#define RADIOLIB_ERR_N_FCNT_DOWN_INVALID (-1113) + +/*! + \brief Received downlink Application frame counter is invalid (lower than last heard value). +*/ +#define RADIOLIB_ERR_A_FCNT_DOWN_INVALID (-1114) /*! \} diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 328e59d9..2fc1496e 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -44,7 +44,7 @@ void LoRaWANNode::wipe() { mod->hal->wipePersistentStorage(); } -int16_t LoRaWANNode::begin(bool otaa) { +int16_t LoRaWANNode::restoreOTAA() { int16_t state = this->setPhyProperties(); RADIOLIB_ASSERT(state); @@ -55,6 +55,11 @@ int16_t LoRaWANNode::begin(bool otaa) { return(RADIOLIB_ERR_NETWORK_NOT_JOINED); } + uint16_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID); + // if (RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION > nvm_table_version) { + // // set default values for variables that are new or something + // } + // pull all needed information from persistent storage this->devAddr = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID); mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); @@ -63,10 +68,6 @@ int16_t LoRaWANNode::begin(bool otaa) { mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); RADIOLIB_DEBUG_PRINTLN("appSKey:"); RADIOLIB_DEBUG_HEXDUMP(this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); - // in case of ABP, we are done already - if (!otaa) { - return(RADIOLIB_ERR_NONE); - } uint32_t dlSettings = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DL_SETTINGS_ID); this->rev = (dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) >> 7; @@ -138,6 +139,10 @@ int16_t LoRaWANNode::begin(bool otaa) { } } + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); + memcpy(&queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); + state = this->setupChannels(); RADIOLIB_ASSERT(state); @@ -149,7 +154,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe Module* mod = this->phyLayer->getMod(); if(!force && (mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) == RADIOLIB_LORAWAN_MAGIC)) { // the device has joined already, we can just pull the data from persistent storage - return(this->begin()); + return(this->restoreOTAA()); } // set the physical layer configuration @@ -255,10 +260,22 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RADIOLIB_DEBUG_PRINTLN("joinAcceptMsg:"); RADIOLIB_DEBUG_HEXDUMP(joinAcceptMsg, lenRx); + // get current JoinNonce from downlink and previous JoinNonce from NVM + uint32_t joinNonce = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); + uint32_t joinNoncePrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID); + RADIOLIB_DEBUG_PRINTLN("JoinNoncePrev: %d, JoinNonce: %d", joinNoncePrev, joinNonce); + + // JoinNonce received must be greater than the last JoinNonce heard, else error + if(joinNonce <= joinNoncePrev) { + return(RADIOLIB_ERR_JOIN_NONCE_INVALID); + } + // check LoRaWAN revision (the MIC verification depends on this) uint8_t dlSettings = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DL_SETTINGS_POS]; this->rev = (dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) >> 7; RADIOLIB_DEBUG_PRINTLN("LoRaWAN revision: 1.%d", this->rev); + + // verify MIC if(this->rev == 1) { // 1.1 version, first we need to derive the join accept integrity key uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; @@ -294,14 +311,6 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // TODO process the data rate offset (void)rx1DrOffset; - // get & compare JoinNonce: should be higher than value saved in non-volatile storage - uint32_t joinNonce = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); - uint32_t joinNonceOld = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID); - RADIOLIB_DEBUG_PRINTLN("JoinNonceOld: %d, JoinNonce: %d", joinNonceOld, joinNonce); - if(joinNonce <= joinNonceOld) { - return(RADIOLIB_ERR_JOIN_NONCE_INVALID); - } - // parse other contents uint32_t homeNetId = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], 3); this->devAddr = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS]); @@ -428,20 +437,22 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID, homeNetId); mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX_DELAY_ID, this->rxDelays[0]); mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DL_SETTINGS_ID, (uint32_t)dlSettings); + // save cfList (all 0 if none is present) - RADIOLIB_DEBUG_PRINTLN("cfList:"); - RADIOLIB_DEBUG_HEXDUMP(cfList, RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN); mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CF_LIST_ID), cfList, RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN); // all complete, reset device counters and set the magic number mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, 0); mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + + // everything written to NVM, write current version to NVM + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey) { +int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey) { this->devAddr = addr; memcpy(this->appSKey, appSKey, RADIOLIB_AES128_KEY_SIZE); memcpy(this->nwkSEncKey, nwkSKey, RADIOLIB_AES128_KEY_SIZE); @@ -461,7 +472,13 @@ int16_t LoRaWANNode::beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, // setup uplink/downlink frequencies and datarates state = this->setupChannels(); - return(state); + RADIOLIB_ASSERT(state); + + // everything written to NVM, write current version to NVM + Module* mod = this->phyLayer->getMod(); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); + + return(RADIOLIB_ERR_NONE); } #if defined(RADIOLIB_BUILD_ARDUINO) @@ -479,12 +496,24 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { if(port > 0xDF) { return(RADIOLIB_ERR_INVALID_PORT); } + // port 0 is only allowed for MAC-only payloads + if(port == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { + if (!isMACPayload) { + return(RADIOLIB_ERR_INVALID_PORT); + } + // if this is MAC only payload, continue and reset for next uplink + isMACPayload = false; + } + + Module* mod = this->phyLayer->getMod(); - // check if there are some MAC commands to piggyback - size_t foptsLen = 0; - if(this->commandsUp.numCommands > 0) { + // check if there are some MAC commands to piggyback (only when piggybacking onto a application-frame) + uint8_t foptsLen = 0; + size_t foptsBufSize = 0; + if(this->commandsUp.numCommands > 0 && port != RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { // there are, assume the maximum possible FOpts len for buffer allocation - foptsLen = 15; + foptsLen = this->commandsUp.len; + foptsBufSize = 15; } // check maximum payload len as defined in phy @@ -498,7 +527,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { RADIOLIB_ASSERT(state); // check if sufficient time has elapsed since the last uplink - Module* mod = this->phyLayer->getMod(); if(mod->hal->millis() - this->rxDelayStart < rxDelays[1]) { // not enough time elapsed since the last uplink, we may still be in an RX window return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); @@ -506,7 +534,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { // build the uplink message // the first 16 bytes are reserved for MIC calculation blocks - size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); + size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsBufSize); #if defined(RADIOLIB_STATIC_ONLY) uint8_t uplinkMsg[RADIOLIB_STATIC_ARRAY_SIZE]; #else @@ -526,26 +554,40 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, fcnt); LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)fcnt); - // check if we have some MAC command to append - // TODO implement appending multiple MAC commands - LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; - if(popMacCommand(&cmd, &this->commandsUp) == RADIOLIB_ERR_NONE) { - // we do, add it to fopts - uint8_t foptsBuff[RADIOLIB_AES128_BLOCK_SIZE]; - foptsBuff[0] = cmd.cid; - for(size_t i = 1; i < cmd.len; i++) { - foptsBuff[i] = cmd.payload[i]; + // check if we have some MAC commands to append + if(foptsLen > 0) { + uint8_t foptsNum = this->commandsUp.numCommands; + uint8_t foptsBuff[foptsBufSize]; + size_t idx = 0; + for (size_t i = 0; i < foptsNum; i++) { + LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; + popMacCommand(&cmd, &this->commandsUp, i); + if (cmd.cid == 0) { + break; + } + foptsBuff[idx] = cmd.cid; + for(size_t i = 1; i < cmd.len; i++) { + foptsBuff[idx + i] = cmd.payload[i]; + } + idx += cmd.len + 1; } - foptsLen = 1 + cmd.len; + + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); + uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= foptsLen; // encrypt it processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true); + + // write the current MAC command queue to nvm for next uplink + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; + memcpy(&queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } // set the port - // TODO if FOptsLen > 0, then port must be either not present or > 0 (4.3.1.6) uplinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(foptsLen)] = port; // select encryption key based on the target port @@ -555,6 +597,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { } // encrypt the frame payload + // TODO check ctrId --> erratum says it should be 0x01? processAES(data, len, encKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, true); // create blocks for MIC calculation @@ -792,15 +835,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { if(state == RADIOLIB_ERR_LORA_HEADER_DAMAGED) { state = RADIOLIB_ERR_NONE; } - - // get the frame counter and set it to the MIC calculation block - // TODO this will not handle overflow into 32-bits! - // TODO cache the ADR bit? - uint16_t fcnt = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); - LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt); - - RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); - RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); if(state != RADIOLIB_ERR_NONE) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -809,6 +843,55 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(state); } + // get the frame counter and set it to the MIC calculation block + // TODO cache the ADR bit? + uint16_t fcnt16 = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); + LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt16); + uint32_t fcnt32 = fcnt16; // calculate possible rollover once decided if this is network downlink or application downlink + + RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); + RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); + + // calculate length of FOpts and payload + uint8_t foptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; + int payLen = downlinkMsgLen - 8 - foptsLen - sizeof(uint32_t); + + bool isAppDownlink = true; + if (payLen <= 0 && this->rev == 1) { // no payload => MAC commands only => Network frame (LoRaWAN v1.1 only) + isAppDownlink = false; + } + + // check the FcntDown value (Network or Application) + uint32_t fcntDownPrev = 0; + if (isAppDownlink) { + fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID); + } else { + fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); + } + + // assume a 16-bit to 32-bit rollover when difference in LSB is smaller than MAX_FCNT_GAP + // if that isn't the case and the received fcnt is smaller or equal to the last heard fcnt, then error + if (fcnt16 <= fcntDownPrev && 0xFFFF - (uint16_t)fcntDownPrev + fcnt16 > RADIOLIB_LORAWAN_MAX_FCNT_GAP) { + #if !defined(RADIOLIB_STATIC_ONLY) + delete[] downlinkMsg; + #endif + if (isAppDownlink) { + return(RADIOLIB_ERR_A_FCNT_DOWN_INVALID); + } else { + return(RADIOLIB_ERR_N_FCNT_DOWN_INVALID); + } + } else if (fcnt16 <= fcntDownPrev) { + uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover + fcnt32 |= (msb << 16); // add back the MSB part + } + + // save current fcnt to NVM + if (isAppDownlink) { + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, fcnt32); + } else { + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, fcnt32); + } + // check the MIC if(!verifyMIC(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen, this->sNwkSIntKey)) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -827,15 +910,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_DOWNLINK_MALFORMED); } - // check fopts len - uint8_t foptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; + // process FOpts (if there are any) if(foptsLen > 0) { // there are some Fopts, decrypt them uint8_t fopts[RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK]; - // according to the specification, the last two arguments should be 0x00 and false, - // but that will fail even for LoRaWAN 1.1.0 server - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x01, true); + // TODO it COULD be the case that the assumed rollover is incorrect, if possible figure out a way to catch this and retry with just fcnt16 + uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, ctrId, true); RADIOLIB_DEBUG_PRINTLN("fopts:"); RADIOLIB_DEBUG_HEXDUMP(fopts, foptsLen); @@ -860,10 +942,39 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { remLen -= processedLen; foptsPtr += processedLen; } + + // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink + if(this->commandsUp.len > 15) { + uint8_t foptsNum = this->commandsUp.numCommands; + size_t foptsBufSize = this->commandsUp.len; + uint8_t foptsBuff[foptsBufSize]; + size_t idx = 0; + for(size_t i = 0; i < foptsNum; i++) { + LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; + popMacCommand(&cmd, &this->commandsUp, i); + if(cmd.cid == 0) { + break; + } + foptsBuff[idx] = cmd.cid; + for(size_t i = 1; i < cmd.len; i++) { + foptsBuff[idx + i] = cmd.payload[i]; + } + idx += cmd.len + 1; + } + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); + + isMACPayload = true; + this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + } + + // write the MAC command queue to nvm for next uplink + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; + memcpy(&queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } - // fopts are processed or not present, check if there is payload - int payLen = downlinkMsgLen - 8 - foptsLen - sizeof(uint32_t); + // process payload (if there is any) if(payLen <= 0) { // no payload *len = 0; @@ -871,21 +982,15 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { delete[] downlinkMsg; #endif - // increase nFcntDown by 1 (MAC downlink only) - uint32_t nFcntDown = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NFCNT_DOWN_ID, nFcntDown + 1); - return(RADIOLIB_ERR_NONE); } - // increase aFcntDown by 1 (application downlink with payload) - uint32_t aFcntDown = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_AFCNT_DOWN_ID, aFcntDown + 1); - // there is payload, and so there should be a port too // TODO pass the port? *len = payLen - 1; - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); + // TODO it COULD be the case that the assumed rollover is incorrect, then figure out a way to catch this and retry with just fcnt16 + // TODO does the erratum hold here as well? + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); #if !defined(RADIOLIB_STATIC_ONLY) delete[] downlinkMsg; @@ -1238,38 +1343,69 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ queue->commands[queue->numCommands - 1].payload[4], queue->commands[queue->numCommands - 1].repeat);*/ queue->numCommands++; + queue->len += 1 + cmd->len; // 1 byte for command ID, len bytes for payload return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force) { +int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index) { if(queue->numCommands == 0) { return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); } if(cmd) { - /*RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - queue->commands[queue->numCommands - 1].cid, - queue->commands[queue->numCommands - 1].len, - queue->commands[queue->numCommands - 1].payload[0], - queue->commands[queue->numCommands - 1].payload[1], - queue->commands[queue->numCommands - 1].payload[2], - queue->commands[queue->numCommands - 1].payload[3], - queue->commands[queue->numCommands - 1].payload[4], - queue->commands[queue->numCommands - 1].repeat);*/ - memcpy(cmd, &queue->commands[queue->numCommands - 1], sizeof(LoRaWANMacCommand_t)); + // RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + // queue->commands[index].cid, + // queue->commands[index].len, + // queue->commands[index].payload[0], + // queue->commands[index].payload[1], + // queue->commands[index].payload[2], + // queue->commands[index].payload[3], + // queue->commands[index].payload[4], + // queue->commands[index].repeat); + memcpy(cmd, &queue->commands[index], sizeof(LoRaWANMacCommand_t)); } - if((!force) && (queue->commands[queue->numCommands - 1].repeat > 0)) { - queue->commands[queue->numCommands - 1].repeat--; + if(queue->commands[index].repeat > 0) { + queue->commands[index].repeat--; } else { - queue->commands[queue->numCommands - 1].repeat = 0; + queue->len -= (1 + queue->commands[index].len); // 1 byte for command ID, len for payload + memset(&queue->commands[index], 0x00, sizeof(LoRaWANMacCommand_t)); queue->numCommands--; } return(RADIOLIB_ERR_NONE); } +int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue) { + if(queue->numCommands == 0) { + return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); + } + + for(size_t index = 0; index < queue->numCommands; index++) { + if(queue->commands[index].cid == cid) { + // RADIOLIB_DEBUG_PRINTLN("delete MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + // queue->commands[index].cid, + // queue->commands[index].len, + // queue->commands[index].payload[0], + // queue->commands[index].payload[1], + // queue->commands[index].payload[2], + // queue->commands[index].payload[3], + // queue->commands[index].payload[4], + // queue->commands[index].repeat); + queue->len -= (1 + queue->commands[index].len); // 1 byte for command ID, len for payload + if(index < RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1) { + memmove(&queue->commands[index], &queue->commands[index + 1], (RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - index) * sizeof(LoRaWANMacCommand_t)); + } + memset(&queue->commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1], 0x00, sizeof(LoRaWANMacCommand_t)); + queue->numCommands--; + return(RADIOLIB_ERR_NONE); + } + } + + return(RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND); +} + size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len); @@ -1285,7 +1421,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion); if(srvVersion == this->rev) { // valid server version, stop sending the ResetInd MAC command - popMacCommand(NULL, &this->commandsUp, true); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_RESET, &this->commandsUp); } return(1); } break; @@ -1488,7 +1624,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion); if((srvVersion > 0) && (srvVersion <= this->rev)) { // valid server version, stop sending the ReKey MAC command - popMacCommand(NULL, &this->commandsUp, true); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_REKEY, &this->commandsUp); } return(1); } break; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 5ec73946..65efe7eb 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -5,6 +5,9 @@ #include "../PhysicalLayer/PhysicalLayer.h" #include "../../utils/Cryptography.h" +// version of NVM table layout (NOT the LoRaWAN version) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION (0x01) + // preamble format #define RADIOLIB_LORAWAN_LORA_SYNC_WORD (0x34) #define RADIOLIB_LORAWAN_LORA_PREAMBLE_LEN (8) @@ -257,7 +260,7 @@ struct LoRaWANMacCommand_t { uint8_t cid; /*! \brief Length of the payload */ - size_t len; + uint8_t len; /*! \brief Payload buffer (5 bytes is the longest possible) */ uint8_t payload[5]; @@ -269,6 +272,7 @@ struct LoRaWANMacCommand_t { struct LoRaWANMacCommandQueue_t { LoRaWANMacCommand_t commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE]; size_t numCommands; + size_t len; }; /*! @@ -306,10 +310,10 @@ class LoRaWANNode { void wipe(); /*! - \brief Join network by loading information from persistent storage. + \brief Restore OTAA session by loading information from persistent storage. \returns \ref status_codes */ - int16_t begin(bool otaa = true); + int16_t restoreOTAA(); /*! \brief Join network by performing over-the-air activation. By this procedure, @@ -333,7 +337,7 @@ class LoRaWANNode { \param sNwkSIntKey Pointer to the network session S key (LoRaWAN 1.1), unused for LoRaWAN 1.0. \returns \ref status_codes */ - int16_t beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey = NULL, uint8_t* sNwkSIntKey = NULL); + int16_t beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey = NULL, uint8_t* sNwkSIntKey = NULL); #if defined(RADIOLIB_BUILD_ARDUINO) /*! @@ -395,10 +399,12 @@ class LoRaWANNode { LoRaWANMacCommandQueue_t commandsUp = { .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, .numCommands = 0, + .len = 0, }; LoRaWANMacCommandQueue_t commandsDown = { .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, .numCommands = 0, + .len = 0, }; // the following is either provided by the network server (OTAA) @@ -438,6 +444,9 @@ class LoRaWANNode { // device status - battery level uint8_t battLevel = 0xFF; + // indicates whether an uplink has MAC commands as payload + bool isMACPayload = false; + // method to generate message integrity code uint32_t generateMIC(uint8_t* msg, size_t len, uint8_t* key); @@ -475,7 +484,10 @@ class LoRaWANNode { int16_t pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue); // pop MAC command from queue, done by copy unless CMD is NULL - int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force = false); + int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index); + + // delete a specific MAC command from queue, indicated by the command ID + int16_t deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue); // execute mac command, return the number of processed bytes for sequential processing size_t execMacCommand(LoRaWANMacCommand_t* cmd);