diff --git a/docs/faq.md b/docs/faq.md index ec145fe3..a015f9fd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -4,7 +4,7 @@ This project is still pretty young but moving at a pretty good pace. Not all fea Most of these problems should be solved by the beta release (within three months): - We don't make these devices and they haven't been tested by UL or the FCC. If you use them you are experimenting and we can't promise they won't burn your house down ;-) -- Encryption is turned off for now +- The encryption [implementation](software/crypto.md) has not been reviewed by an expert. (Are you an expert? Please help us) - A number of (straightforward) software work items have to be completed before battery life matches our measurements, currently battery life is about three days. Join us on chat if you want the spreadsheet of power measurements/calculations. - The Android API needs to be documented better - No one has written an iOS app yet. But some good souls [are talking about it](https://github.com/meshtastic/Meshtastic-esp32/issues/14) ;-) diff --git a/docs/software/TODO.md b/docs/software/TODO.md index 2d80da3c..23efd6ee 100644 --- a/docs/software/TODO.md +++ b/docs/software/TODO.md @@ -29,7 +29,6 @@ fetches the fresh nodedb. - rx signal measurements -3 marginal, -9 bad, 10 great, -10 means almost unusable. So scale this into % signal strength. preferably as a graph, with an X indicating loss of comms. - assign every "channel" a random shared 8 bit sync word (per 4.2.13.6 of datasheet) - use that word to filter packets before even checking CRC. This will ensure our CPU will only wake for packets on our "channel" - Note: we do not do address filtering at the chip level, because we might need to route for the mesh -- add basic crypto - https://github.com/chegewara/esp32-mbedtls-aes-test/blob/master/main/main.c https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation - use ECB at first (though it is shit) because it doesn't require us to send 16 bytes of IV with each packet. Then OFB per example. Possibly do this crypto at the data payload level only, so that all of the packet routing metadata is in cleartext (so that nodes will route for other radios that are cryptoed with a key we don't know) - add frequency hopping, dependent on the gps time, make the switch moment far from the time anyone is going to be transmitting - share channel settings over Signal (or qr code) by embedding an an URL which is handled by the MeshUtil app. diff --git a/docs/software/cypto.md b/docs/software/cypto.md new file mode 100644 index 00000000..f5f7d04a --- /dev/null +++ b/docs/software/cypto.md @@ -0,0 +1,38 @@ +# Encryption in Meshtastic + +Cryptography is tricky, so we've tried to 'simply' apply standard crypto solutions to our implementation. However, +the project developers are not cryptography experts. Therefore we ask two things: + +- If you are a cryptography expert, please review these notes and our questions below. Can you help us by reviewing our + notes below and offering advice? We will happily give as much or as little credit as you wish as our thanks ;-). +- Consider our existing solution 'alpha' and probably fairly secure against an not very aggressive adversary. But until + it is reviewed by someone smarter than us, assume it might have flaws. + +## Notes on implementation + +- We do all crypto at the SubPacket (payload) level only, so that all meshtastic nodes will route for others - even those channels which are encrypted with a different key. +- Mostly based on reading [Wikipedia]() and using the modes the ESP32 provides support for in hardware. +- We use AES256-CTR as a stream cypher (with zero padding on the last BLOCK) because it is well supported with hardware acceleration. + +Parameters for our CTR implementation: + +- Our AES key is 256 bits, shared as part of the 'Channel' specification. +- Each SubPacket will be sent as a series of 16 byte BLOCKS. +- The node number concatenated with the packet number is used as the NONCE. This counter will be stored in flash in the device and should essentially never repeat. If the user makes a new 'Channel' (i.e. picking a new random 256 bit key), the packet number will start at zero. The packet number is sent + in cleartext with each packet. The node number can be derived from the "from" field of each packet. +- Each BLOCK for a packet has an incrementing COUNTER. COUNTER starts at zero for the first block of each packet. +- The IV for each block is constructed by concatenating the NONCE as the upper 96 bits of the IV and the COUNTER as the bottom 32 bits. Note: since our packets are small counter will really never be higher than 32 (five bits). + +``` +You can encrypt separate messages by dividing the nonce_counter buffer in two areas: the first one used for a per-message nonce, handled by yourself, and the second one updated by this function internally. +For example, you might reserve the first 12 bytes for the per-message nonce, and the last 4 bytes for internal use. In that case, before calling this function on a new message you need to set the first 12 bytes of nonce_counter to your chosen nonce value, the last 4 to 0, and nc_off to 0 (which will cause stream_block to be ignored). That way, you can encrypt at most 2**96 messages of up to 2**32 blocks each with the same key. + +The per-message nonce (or information sufficient to reconstruct it) needs to be communicated with the ciphertext and must be unique. The recommended way to ensure uniqueness is to use a message counter. An alternative is to generate random nonces, but this limits the number of messages that can be securely encrypted: for example, with 96-bit random nonces, you should not encrypt more than 2**32 messages with the same key. + +Note that for both stategies, sizes are measured in blocks and that an AES block is 16 bytes. +``` + +## Remaining todo + +- Make the packet numbers 32 bit +- Implement for NRF52 [NRF52](https://infocenter.nordicsemi.com/topic/com.nordic.infocenter.sdk5.v15.0.0/lib_crypto_aes.html#sub_aes_ctr) diff --git a/docs/software/nrf52-TODO.md b/docs/software/nrf52-TODO.md index a60cf2af..4f4b6c40 100644 --- a/docs/software/nrf52-TODO.md +++ b/docs/software/nrf52-TODO.md @@ -62,6 +62,7 @@ Needed to be fully functional at least at the same level of the ESP32 boards. At Nice ideas worth considering someday... +- use the Jumper simulator to run meshes of simulated hardware: https://docs.jumper.io/docs/install.html - make/find a multithread safe debug logging class (include remote logging and timestamps and levels). make each log event atomic. - turn on freertos stack size checking - Currently we use Nordic's vendor ID, which is apparently okay: https://devzone.nordicsemi.com/f/nordic-q-a/44014/using-nordic-vid-and-pid-for-nrf52840 and I just picked a PID of 0x4403 diff --git a/lib/BluetoothOTA/src/BluetoothUtil.cpp b/lib/BluetoothOTA/src/BluetoothUtil.cpp index 0da8df85..ccaf4163 100644 --- a/lib/BluetoothOTA/src/BluetoothUtil.cpp +++ b/lib/BluetoothOTA/src/BluetoothUtil.cpp @@ -184,7 +184,7 @@ class MySecurity : public BLESecurityCallbacks void onPassKeyNotify(uint32_t pass_key) { - Serial.printf("onPassKeyNotify %u\n", pass_key); + Serial.printf("onPassKeyNotify %06u\n", pass_key); startCb(pass_key); } diff --git a/proto b/proto index b35e7fb1..5e2df6c9 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit b35e7fb17e80a9761145d69a288a9e87af862cab +Subproject commit 5e2df6c9986cd75f0af4eab1ba0d2aacf258aaab diff --git a/src/esp32/ESP32CryptoEngine.cpp b/src/esp32/ESP32CryptoEngine.cpp new file mode 100644 index 00000000..bccfae55 --- /dev/null +++ b/src/esp32/ESP32CryptoEngine.cpp @@ -0,0 +1,83 @@ +#include "CryptoEngine.h" +#include "configuration.h" + +#include "crypto/includes.h" + +#include "crypto/common.h" + +// #include "esp_system.h" + +#include "crypto/aes.h" +#include "crypto/aes_wrap.h" +#include "mbedtls/aes.h" + +#define MAX_BLOCKSIZE 256 + +class ESP32CryptoEngine : public CryptoEngine +{ + + mbedtls_aes_context aes; + + /// How many bytes in our key + uint8_t keySize = 0; + + public: + ESP32CryptoEngine() { mbedtls_aes_init(&aes); } + + ~ESP32CryptoEngine() { mbedtls_aes_free(&aes); } + + /** + * Set the key used for encrypt, decrypt. + * + * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. + * + * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) + * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will cache the + * provided pointer) + */ + virtual void setKey(size_t numBytes, uint8_t *bytes) + { + keySize = numBytes; + DEBUG_MSG("Installing AES%d key!\n", numBytes * 8); + if (numBytes != 0) { + auto res = mbedtls_aes_setkey_enc(&aes, bytes, numBytes * 8); + assert(!res); + } + } + + /** + * Encrypt a packet + * + * @param bytes is updated in place + */ + virtual void encrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes) + { + if (keySize != 0) { + uint8_t stream_block[16]; + static uint8_t scratch[MAX_BLOCKSIZE]; + size_t nc_off = 0; + + // DEBUG_MSG("ESP32 encrypt!\n"); + initNonce(fromNode, packetNum); + assert(numBytes <= MAX_BLOCKSIZE); + memcpy(scratch, bytes, numBytes); + memset(scratch + numBytes, 0, + sizeof(scratch) - numBytes); // Fill rest of buffer with zero (in case cypher looks at it) + + auto res = mbedtls_aes_crypt_ctr(&aes, numBytes, &nc_off, nonce, stream_block, scratch, bytes); + assert(!res); + } + } + + virtual void decrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes) + { + // DEBUG_MSG("ESP32 decrypt!\n"); + + // For CTR, the implementation is the same + encrypt(fromNode, packetNum, numBytes, bytes); + } + + private: +}; + +CryptoEngine *crypto = new ESP32CryptoEngine(); diff --git a/src/main.cpp b/src/main.cpp index 3971ae9b..b6c03846 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -104,8 +104,6 @@ const char *getDeviceName() return name; } -static MeshRadio *radio = NULL; - static uint32_t ledBlinker() { static bool ledOn; @@ -231,10 +229,10 @@ void setup() #else new SimRadio(); #endif - radio = new MeshRadio(rIf); - router.addInterface(&radio->radioIf); - if (radio && !radio->init()) + router.addInterface(rIf); + + if (!rIf->init()) recordCriticalError(ErrNoRadio); // This must be _after_ service.init because we need our preferences loaded from flash to have proper timeout values diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp new file mode 100644 index 00000000..d72be111 --- /dev/null +++ b/src/mesh/CryptoEngine.cpp @@ -0,0 +1,32 @@ +#include "CryptoEngine.h" +#include "configuration.h" + +void CryptoEngine::setKey(size_t numBytes, uint8_t *bytes) +{ + DEBUG_MSG("WARNING: Using stub crypto - all crypto is sent in plaintext!\n"); +} + +/** + * Encrypt a packet + * + * @param bytes is updated in place + */ +void CryptoEngine::encrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes) +{ + DEBUG_MSG("WARNING: noop encryption!\n"); +} + +void CryptoEngine::decrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes) +{ + DEBUG_MSG("WARNING: noop decryption!\n"); +} + +/** + * Init our 128 bit nonce for a new packet + */ +void CryptoEngine::initNonce(uint32_t fromNode, uint64_t packetNum) +{ + memset(nonce, 0, sizeof(nonce)); + *((uint64_t *)&nonce[0]) = packetNum; + *((uint32_t *)&nonce[8]) = fromNode; +} \ No newline at end of file diff --git a/src/mesh/CryptoEngine.h b/src/mesh/CryptoEngine.h new file mode 100644 index 00000000..04e592e2 --- /dev/null +++ b/src/mesh/CryptoEngine.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +/** + * see docs/software/crypto.md for details. + * + */ + +class CryptoEngine +{ + protected: + /** Our per packet nonce */ + uint8_t nonce[16]; + + public: + virtual ~CryptoEngine() {} + + /** + * Set the key used for encrypt, decrypt. + * + * As a special case: If all bytes are zero, we assume _no encryption_ and send all data in cleartext. + * + * @param numBytes must be 16 (AES128), 32 (AES256) or 0 (no crypt) + * @param bytes a _static_ buffer that will remain valid for the life of this crypto instance (i.e. this class will cache the + * provided pointer) + */ + virtual void setKey(size_t numBytes, uint8_t *bytes); + + /** + * Encrypt a packet + * + * @param bytes is updated in place + */ + virtual void encrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes); + virtual void decrypt(uint32_t fromNode, uint64_t packetNum, size_t numBytes, uint8_t *bytes); + + protected: + /** + * Init our 128 bit nonce for a new packet + * + * The NONCE is constructed by concatenating (from MSB to LSB): + * a 64 bit packet number (stored in little endian order) + * a 32 bit sending node number (stored in little endian order) + * a 32 bit block counter (starts at zero) + */ + void initNonce(uint32_t fromNode, uint64_t packetNum); +}; + +extern CryptoEngine *crypto; diff --git a/src/mesh/MeshRadio.cpp b/src/mesh/MeshRadio.cpp deleted file mode 100644 index 53c052be..00000000 --- a/src/mesh/MeshRadio.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "error.h" -#include -#include - -#include "MeshRadio.h" -#include "MeshService.h" -#include "NodeDB.h" -#include "configuration.h" -#include "sleep.h" -#include -#include - -/** - * ## LoRaWAN for North America - -LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments. - -The maximum output power for North America is +30 dBM. - -The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are -separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. -*/ - -/// Sometimes while debugging it is useful to set this false, to disable rf95 accesses -bool useHardware = true; - -MeshRadio::MeshRadio(RadioInterface *rIf) : radioIf(*rIf) // , manager(radioIf) -{ - myNodeInfo.num_channels = NUM_CHANNELS; - - // Can't print strings this early - serial not setup yet - // DEBUG_MSG("Set meshradio defaults name=%s\n", channelSettings.name); -} - -bool MeshRadio::init() -{ - if (!useHardware) - return true; - - DEBUG_MSG("Starting meshradio init...\n"); - - configChangedObserver.observe(&service.configChanged); - preflightSleepObserver.observe(&preflightSleep); - notifyDeepSleepObserver.observe(¬ifyDeepSleep); - -#ifdef RESET_GPIO - pinMode(RESET_GPIO, OUTPUT); // Deassert reset - digitalWrite(RESET_GPIO, HIGH); - - // pulse reset - digitalWrite(RESET_GPIO, LOW); - delay(10); - digitalWrite(RESET_GPIO, HIGH); - delay(10); -#endif - - // we now expect interfaces to operate in promiscous mode - // radioIf.setThisAddress(nodeDB.getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at constructor - // time. - - applySettings(); - - if (!radioIf.init()) { - DEBUG_MSG("LoRa radio init failed\n"); - return false; - } - - // No need to call this now, init is supposed to do same. reloadConfig(); - - return true; -} - -/** hash a string into an integer - * - * djb2 by Dan Bernstein. - * http://www.cse.yorku.ca/~oz/hash.html - */ -unsigned long hash(char *str) -{ - unsigned long hash = 5381; - int c; - - while ((c = *str++) != 0) - hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ - - return hash; -} - -/** - * Pull our channel settings etc... from protobufs to the dumb interface settings - */ -void MeshRadio::applySettings() -{ - // Set up default configuration - // No Sync Words in LORA mode. - radioIf.modemConfig = (ModemConfigChoice)channelSettings.modem_config; - - // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM - int channel_num = hash(channelSettings.name) % NUM_CHANNELS; - radioIf.freq = CH0 + CH_SPACING * channel_num; - radioIf.power = channelSettings.tx_power; - - DEBUG_MSG("Set radio: name=%s, config=%u, ch=%d, txpower=%d\n", channelSettings.name, channelSettings.modem_config, - channel_num, channelSettings.tx_power); -} - -int MeshRadio::reloadConfig(void *unused) -{ - applySettings(); - radioIf.reconfigure(); - - return 0; -} diff --git a/src/mesh/MeshRadio.h b/src/mesh/MeshRadio.h index 85c3e77f..3d41d23c 100644 --- a/src/mesh/MeshRadio.h +++ b/src/mesh/MeshRadio.h @@ -2,9 +2,7 @@ #include "MemoryPool.h" #include "MeshTypes.h" -#include "Observer.h" #include "PointerQueue.h" -#include "RadioInterface.h" #include "configuration.h" #include "mesh.pb.h" @@ -61,48 +59,3 @@ #define NUM_CHANNELS NUM_CHANNELS_US #endif -/** - * A raw low level interface to our mesh. Only understands nodenums and bytes (not protobufs or node ids) - * FIXME - REMOVE THIS CLASS - */ -class MeshRadio -{ - public: - // Kinda ugly way of selecting different radio implementations, but soon this MeshRadio class will be going away - // entirely. At that point we can make things pretty. - RadioInterface &radioIf; - - /** pool is the pool we will alloc our rx packets from - * rxDest is where we will send any rx packets, it becomes receivers responsibility to return packet to the pool - */ - MeshRadio(RadioInterface *rIf); - - bool init(); - - private: - CallbackObserver configChangedObserver = - CallbackObserver(this, &MeshRadio::reloadConfig); - - CallbackObserver preflightSleepObserver = - CallbackObserver(this, &MeshRadio::preflightSleepCb); - - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &MeshRadio::notifyDeepSleepDb); - - /// The radioConfig object just changed, call this to force the hw to change to the new settings - int reloadConfig(void *unused = NULL); - - /// Return 0 if sleep is okay - int preflightSleepCb(void *unused = NULL) { return radioIf.canSleep() ? 0 : 1; } - - int notifyDeepSleepDb(void *unused = NULL) - { - radioIf.sleep(); - return 0; - } - - /** - * Pull our channel settings etc... from protobufs to the dumb interface settings - */ - void applySettings(); -}; diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 7e9fc448..992144b8 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -92,9 +92,9 @@ void MeshService::sendOurOwner(NodeNum dest, bool wantReplies) { MeshPacket *p = allocForSending(); p->to = dest; - p->payload.want_response = wantReplies; - p->payload.has_user = true; - User &u = p->payload.user; + p->decoded.want_response = wantReplies; + p->decoded.has_user = true; + User &u = p->decoded.user; u = owner; DEBUG_MSG("sending owner %s/%s/%s\n", u.id, u.long_name, u.short_name); @@ -108,7 +108,7 @@ const MeshPacket *MeshService::handleFromRadioUser(const MeshPacket *mp) bool isCollision = mp->from == myNodeInfo.my_node_num; // we win if we have a lower macaddr - bool weWin = memcmp(&owner.macaddr, &mp->payload.user.macaddr, sizeof(owner.macaddr)) < 0; + bool weWin = memcmp(&owner.macaddr, &mp->decoded.user.macaddr, sizeof(owner.macaddr)) < 0; if (isCollision) { if (weWin) { @@ -134,7 +134,7 @@ const MeshPacket *MeshService::handleFromRadioUser(const MeshPacket *mp) sendOurOwner(mp->from); - String lcd = String("Joined: ") + mp->payload.user.long_name + "\n"; + String lcd = String("Joined: ") + mp->decoded.user.long_name + "\n"; screen.print(lcd.c_str()); } @@ -143,12 +143,12 @@ const MeshPacket *MeshService::handleFromRadioUser(const MeshPacket *mp) void MeshService::handleIncomingPosition(const MeshPacket *mp) { - if (mp->has_payload && mp->payload.has_position) { - DEBUG_MSG("handled incoming position time=%u\n", mp->payload.position.time); + if (mp->which_payload == MeshPacket_decoded_tag && mp->decoded.has_position) { + DEBUG_MSG("handled incoming position time=%u\n", mp->decoded.position.time); - if (mp->payload.position.time) { + if (mp->decoded.position.time) { struct timeval tv; - uint32_t secs = mp->payload.position.time; + uint32_t secs = mp->decoded.position.time; tv.tv_sec = secs; tv.tv_usec = 0; @@ -171,7 +171,7 @@ int MeshService::handleFromRadio(const MeshPacket *mp) DEBUG_MSG("Ignoring incoming time, because we have a GPS\n"); } - if (mp->has_payload && mp->payload.has_user) { + if (mp->which_payload == MeshPacket_decoded_tag && mp->decoded.has_user) { mp = handleFromRadioUser(mp); } @@ -192,7 +192,7 @@ int MeshService::handleFromRadio(const MeshPacket *mp) MeshPacket *copied = packetPool.allocCopy(*mp); assert(toPhoneQueue.enqueue(copied, 0)); // FIXME, instead of failing for full queue, delete the oldest mssages - if (mp->payload.want_response) + if (mp->decoded.want_response) sendNetworkPing(mp->from); } else { DEBUG_MSG("Not delivering vetoed User message\n"); @@ -257,12 +257,12 @@ void MeshService::sendToMesh(MeshPacket *p) // Strip out any time information before sending packets to other nodes - to keep the wire size small (and because other // nodes shouldn't trust it anyways) Note: for now, we allow a device with a local GPS to include the time, so that gpsless // devices can get time. - if (p->has_payload && p->payload.has_position) { + if (p->which_payload == MeshPacket_decoded_tag && p->decoded.has_position) { if (!gps->isConnected) { - DEBUG_MSG("Stripping time %u from position send\n", p->payload.position.time); - p->payload.position.time = 0; + DEBUG_MSG("Stripping time %u from position send\n", p->decoded.position.time); + p->decoded.position.time = 0; } else - DEBUG_MSG("Providing time to mesh %u\n", p->payload.position.time); + DEBUG_MSG("Providing time to mesh %u\n", p->decoded.position.time); } // If the phone sent a packet just to us, don't send it out into the network @@ -282,7 +282,7 @@ MeshPacket *MeshService::allocForSending() { MeshPacket *p = packetPool.allocZeroed(); - p->has_payload = true; + p->which_payload = MeshPacket_decoded_tag; // Assume payload is decoded at start. p->from = nodeDB.getNodeNum(); p->to = NODENUM_BROADCAST; p->id = generatePacketId(); @@ -312,10 +312,10 @@ void MeshService::sendOurPosition(NodeNum dest, bool wantReplies) // Update our local node info with our position (even if we don't decide to update anyone else) MeshPacket *p = allocForSending(); p->to = dest; - p->payload.has_position = true; - p->payload.position = node->position; - p->payload.want_response = wantReplies; - p->payload.position.time = getValidTime(); // This nodedb timestamp might be stale, so update it if our clock is valid. + p->decoded.has_position = true; + p->decoded.position = node->position; + p->decoded.want_response = wantReplies; + p->decoded.position.time = getValidTime(); // This nodedb timestamp might be stale, so update it if our clock is valid. sendToMesh(p); } @@ -325,9 +325,9 @@ int MeshService::onGPSChanged(void *unused) // Update our local node info with our position (even if we don't decide to update anyone else) MeshPacket *p = allocForSending(); - p->payload.has_position = true; + p->decoded.has_position = true; - Position &pos = p->payload.position; + Position &pos = p->decoded.position; // !zero or !zero lat/long means valid if (gps->latitude != 0 || gps->longitude != 0) { if (gps->altitude != 0) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 73b3391d..8fe82b82 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -5,6 +5,7 @@ #include "FS.h" #include "SPIFFS.h" +#include "CryptoEngine.h" #include "GPS.h" #include "NodeDB.h" #include "PowerFSM.h" @@ -51,7 +52,7 @@ NodeDB::NodeDB() : nodes(devicestate.node_db), numNodes(&devicestate.node_db_cou void NodeDB::resetRadioConfig() { - /// 16 bytes of random PSK for our _public_ default channel that all devices power up on + /// 16 bytes of random PSK for our _public_ default channel that all devices power up on (AES128) static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf}; @@ -75,10 +76,14 @@ void NodeDB::resetRadioConfig() channelSettings.modem_config = ChannelSettings_ModemConfig_Bw125Cr48Sf4096; // slow and long range channelSettings.tx_power = 23; - memcpy(&channelSettings.psk, &defaultpsk, sizeof(channelSettings.psk)); + memcpy(&channelSettings.psk.bytes, &defaultpsk, sizeof(channelSettings.psk)); + channelSettings.psk.size = sizeof(defaultpsk); strcpy(channelSettings.name, "Default"); } + // Tell our crypto engine about the psk + crypto->setKey(channelSettings.psk.size, channelSettings.psk.bytes); + // temp hack for quicker testing /* radioConfig.preferences.screen_on_secs = 30; @@ -274,8 +279,8 @@ size_t NodeDB::getNumOnlineNodes() /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const MeshPacket &mp) { - if (mp.has_payload) { - const SubPacket &p = mp.payload; + if (mp.which_payload == MeshPacket_decoded_tag) { + const SubPacket &p = mp.decoded; DEBUG_MSG("Update DB node 0x%x, rx_time=%u\n", mp.from, mp.rx_time); int oldNumNodes = *numNodes; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index d4a40842..3ad60005 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -1,28 +1,91 @@ #include "RadioInterface.h" +#include "MeshRadio.h" +#include "MeshService.h" #include "NodeDB.h" #include "assert.h" #include "configuration.h" +#include "sleep.h" #include #include #include +/** + * ## LoRaWAN for North America + +LoRaWAN defines 64, 125 kHz channels from 902.3 to 914.9 MHz increments. + +The maximum output power for North America is +30 dBM. + +The band is from 902 to 928 MHz. It mentions channel number and its respective channel frequency. All the 13 channels are +separated by 2.16 MHz with respect to the adjacent channels. Channel zero starts at 903.08 MHz center frequency. +*/ + // 1kb was too small #define RADIO_STACK_SIZE 4096 RadioInterface::RadioInterface() : txQueue(MAX_TX_QUEUE) { assert(sizeof(PacketHeader) == 4); // make sure the compiler did what we expected + + myNodeInfo.num_channels = NUM_CHANNELS; + + // Can't print strings this early - serial not setup yet + // DEBUG_MSG("Set meshradio defaults name=%s\n", channelSettings.name); } bool RadioInterface::init() { - // we want this thread to run at very high priority, because it is effectively running as a user space ISR + DEBUG_MSG("Starting meshradio init...\n"); + + configChangedObserver.observe(&service.configChanged); + preflightSleepObserver.observe(&preflightSleep); + notifyDeepSleepObserver.observe(¬ifyDeepSleep); + + // we now expect interfaces to operate in promiscous mode + // radioIf.setThisAddress(nodeDB.getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at constructor + // time. + + // we want this thread to run at very high priority, because it is effectively running as a user space ISR start("radio", RADIO_STACK_SIZE, configMAX_PRIORITIES - 1); // Start our worker thread return true; } +/** hash a string into an integer + * + * djb2 by Dan Bernstein. + * http://www.cse.yorku.ca/~oz/hash.html + */ +unsigned long hash(char *str) +{ + unsigned long hash = 5381; + int c; + + while ((c = *str++) != 0) + hash = ((hash << 5) + hash) + (unsigned char)c; /* hash * 33 + c */ + + return hash; +} + +/** + * Pull our channel settings etc... from protobufs to the dumb interface settings + */ +void RadioInterface::applyModemConfig() +{ + // Set up default configuration + // No Sync Words in LORA mode. + modemConfig = (ModemConfigChoice)channelSettings.modem_config; + + // Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM + int channel_num = hash(channelSettings.name) % NUM_CHANNELS; + freq = CH0 + CH_SPACING * channel_num; + power = channelSettings.tx_power; + + DEBUG_MSG("Set radio: name=%s, config=%u, ch=%d, power=%d\n", channelSettings.name, channelSettings.modem_config, channel_num, + channelSettings.tx_power); +} + ErrorCode SimRadio::send(MeshPacket *p) { DEBUG_MSG("SimRadio.send\n"); @@ -44,7 +107,7 @@ size_t RadioInterface::beginSending(MeshPacket *p) assert(!sendingPacket); // DEBUG_MSG("sending queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)\n", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); - assert(p->has_payload); + assert(p->which_payload == MeshPacket_encrypted_tag); // It should have already been encoded by now lastTxStart = millis(); @@ -58,11 +121,8 @@ size_t RadioInterface::beginSending(MeshPacket *p) // if the sender nodenum is zero, that means uninitialized assert(h->from); - size_t numbytes = pb_encode_to_bytes(radiobuf + sizeof(PacketHeader), sizeof(radiobuf), SubPacket_fields, &p->payload) + - sizeof(PacketHeader); - - assert(numbytes <= MAX_RHPACKETLEN); + memcpy(radiobuf + sizeof(PacketHeader), p->encrypted.bytes, p->encrypted.size); sendingPacket = p; - return numbytes; + return p->encrypted.size + sizeof(PacketHeader); } \ No newline at end of file diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index cdfae79d..6c7dbd79 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -2,6 +2,7 @@ #include "MemoryPool.h" #include "MeshTypes.h" +#include "Observer.h" #include "PointerQueue.h" #include "WorkerThread.h" #include "mesh.pb.h" @@ -35,6 +36,15 @@ class RadioInterface : protected NotifiedWorkerThread friend class MeshRadio; // for debugging we let that class touch pool PointerQueue *rxDest = NULL; + CallbackObserver configChangedObserver = + CallbackObserver(this, &RadioInterface::reloadConfig); + + CallbackObserver preflightSleepObserver = + CallbackObserver(this, &RadioInterface::preflightSleepCb); + + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &RadioInterface::notifyDeepSleepDb); + protected: MeshPacket *sendingPacket = NULL; // The packet we are currently sending PointerQueue txQueue; @@ -104,6 +114,29 @@ class RadioInterface : protected NotifiedWorkerThread size_t beginSending(MeshPacket *p); virtual void loop() {} // Idle processing + + /** + * Convert our modemConfig enum into wf, sf, etc... + * + * These paramaters will be pull from the channelSettings global + */ + virtual void applyModemConfig(); + + private: + /// Return 0 if sleep is okay + int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; } + + int notifyDeepSleepDb(void *unused = NULL) + { + sleep(); + return 0; + } + + int reloadConfig(void *unused) + { + reconfigure(); + return 0; + } }; class SimRadio : public RadioInterface diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index ada0a337..dee630e0 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -58,6 +58,8 @@ RadioLibInterface *RadioLibInterface::instance; */ void RadioLibInterface::applyModemConfig() { + RadioInterface::applyModemConfig(); + switch (modemConfig) { case Bw125Cr45Sf128: ///< Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Default medium range bw = 125; @@ -287,24 +289,19 @@ void RadioLibInterface::handleReceiveInterrupt() } else { MeshPacket *mp = packetPool.allocZeroed(); - SubPacket *p = &mp->payload; - mp->from = h->from; mp->to = h->to; mp->id = h->id; addReceiveMetadata(mp); - if (!pb_decode_from_bytes(payload, payloadLen, SubPacket_fields, p)) { - DEBUG_MSG("Invalid protobufs in received mesh packet, discarding.\n"); - packetPool.release(mp); - // rxBad++; not really a hw error - } else { - // parsing was successful, queue for our recipient - mp->has_payload = true; - DEBUG_MSG("Lora RX interrupt from=0x%x, id=%u\n", mp->from, mp->id); + mp->which_payload = MeshPacket_encrypted_tag; // Mark that the payload is still encrypted at this point + assert(payloadLen <= sizeof(mp->encrypted.bytes)); + memcpy(mp->encrypted.bytes, payload, payloadLen); + mp->encrypted.size = payloadLen; - deliverToReceiver(mp); - } + DEBUG_MSG("Lora RX interrupt from=0x%x, id=%u\n", mp->from, mp->id); + + deliverToReceiver(mp); } } } diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index e1e0f1a9..a090d132 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -99,8 +99,10 @@ class RadioLibInterface : public RadioInterface protected: /** * Convert our modemConfig enum into wf, sf, etc... + * + * These paramaters will be pull from the channelSettings global */ - void applyModemConfig(); + virtual void applyModemConfig(); /** Could we send right now (i.e. either not actively receiving or transmitting)? */ virtual bool canSendImmediately(); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6e5734e5..3752a2fb 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -1,4 +1,6 @@ #include "Router.h" +#include "CryptoEngine.h" +#include "GPS.h" #include "configuration.h" #include "mesh-pb-constants.h" @@ -44,10 +46,30 @@ void Router::loop() /** * Send a packet on a suitable interface. This routine will * later free() the packet to pool. This routine is not allowed to stall. - * If the txmit queue is full it might return an error. + * If the txmit queue is full it might return an error. */ ErrorCode Router::send(MeshPacket *p) { + // If the packet hasn't yet been encrypted, do so now (it might already be encrypted if we are just forwarding it) + + assert(p->which_payload == MeshPacket_encrypted_tag || + p->which_payload == MeshPacket_decoded_tag); // I _think_ all packets should have a payload by now + + // First convert from protobufs to raw bytes + if (p->which_payload == MeshPacket_decoded_tag) { + static uint8_t bytes[MAX_RHPACKETLEN]; // we have to use a scratch buffer because a union + + size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), SubPacket_fields, &p->decoded); + + assert(numbytes <= MAX_RHPACKETLEN); + crypto->encrypt(p->from, p->id, numbytes, bytes); + + // Copy back into the packet and set the variant type + memcpy(p->encrypted.bytes, bytes, numbytes); + p->encrypted.size = numbytes; + p->which_payload = MeshPacket_encrypted_tag; + } + if (iface) { // DEBUG_MSG("Sending packet via interface fr=0x%x,to=0x%x,id=%d\n", p->from, p->to, p->id); return iface->send(p); @@ -58,8 +80,6 @@ ErrorCode Router::send(MeshPacket *p) } } -#include "GPS.h" - /** * Handle any packet that is received by an interface on this node. * Note: some packets may merely being passed through this node and will be forwarded elsewhere. @@ -70,7 +90,25 @@ void Router::handleReceived(MeshPacket *p) // Also, we should set the time from the ISR and it should have msec level resolution p->rx_time = getValidTime(); // store the arrival timestamp for the phone - DEBUG_MSG("Notifying observers of received packet fr=0x%x,to=0x%x,id=%d\n", p->from, p->to, p->id); - notifyPacketReceived.notifyObservers(p); + assert(p->which_payload == + MeshPacket_encrypted_tag); // I _think_ the only thing that pushes to us is raw devices that just received packets + + // Try to decrypt the packet if we can + static uint8_t bytes[MAX_RHPACKETLEN]; + memcpy(bytes, p->encrypted.bytes, + p->encrypted.size); // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf + crypto->decrypt(p->from, p->id, p->encrypted.size, bytes); + + // Take those raw bytes and convert them back into a well structured protobuf we can understand + if (!pb_decode_from_bytes(bytes, p->encrypted.size, SubPacket_fields, &p->decoded)) { + DEBUG_MSG("Invalid protobufs in received mesh packet, discarding.\n"); + } else { + // parsing was successful, queue for our recipient + p->which_payload = MeshPacket_decoded_tag; + + DEBUG_MSG("Notifying observers of received packet fr=0x%x,to=0x%x,id=%d\n", p->from, p->to, p->id); + notifyPacketReceived.notifyObservers(p); + } + packetPool.release(p); } \ No newline at end of file diff --git a/src/mesh/mesh.pb.h b/src/mesh/mesh.pb.h index ba2293a0..9442bd25 100644 --- a/src/mesh/mesh.pb.h +++ b/src/mesh/mesh.pb.h @@ -36,10 +36,11 @@ typedef struct _RouteDiscovery { pb_callback_t route; } RouteDiscovery; +typedef PB_BYTES_ARRAY_T(32) ChannelSettings_psk_t; typedef struct _ChannelSettings { int32_t tx_power; ChannelSettings_ModemConfig modem_config; - pb_byte_t psk[16]; + ChannelSettings_psk_t psk; char name[12]; } ChannelSettings; @@ -122,11 +123,15 @@ typedef struct _SubPacket { bool want_response; } SubPacket; +typedef PB_BYTES_ARRAY_T(256) MeshPacket_encrypted_t; typedef struct _MeshPacket { int32_t from; int32_t to; - bool has_payload; - SubPacket payload; + pb_size_t which_payload; + union { + SubPacket decoded; + MeshPacket_encrypted_t encrypted; + }; uint32_t rx_time; uint32_t id; float rx_snr; @@ -193,8 +198,8 @@ typedef struct _ToRadio { #define User_init_default {"", "", "", {0}} #define RouteDiscovery_init_default {{{NULL}, NULL}} #define SubPacket_init_default {false, Position_init_default, false, Data_init_default, false, User_init_default, 0} -#define MeshPacket_init_default {0, 0, false, SubPacket_init_default, 0, 0, 0} -#define ChannelSettings_init_default {0, _ChannelSettings_ModemConfig_MIN, {0}, ""} +#define MeshPacket_init_default {0, 0, 0, {SubPacket_init_default}, 0, 0, 0} +#define ChannelSettings_init_default {0, _ChannelSettings_ModemConfig_MIN, {0, {0}}, ""} #define RadioConfig_init_default {false, RadioConfig_UserPreferences_init_default, false, ChannelSettings_init_default} #define RadioConfig_UserPreferences_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define NodeInfo_init_default {0, false, User_init_default, false, Position_init_default, 0} @@ -208,8 +213,8 @@ typedef struct _ToRadio { #define User_init_zero {"", "", "", {0}} #define RouteDiscovery_init_zero {{{NULL}, NULL}} #define SubPacket_init_zero {false, Position_init_zero, false, Data_init_zero, false, User_init_zero, 0} -#define MeshPacket_init_zero {0, 0, false, SubPacket_init_zero, 0, 0, 0} -#define ChannelSettings_init_zero {0, _ChannelSettings_ModemConfig_MIN, {0}, ""} +#define MeshPacket_init_zero {0, 0, 0, {SubPacket_init_zero}, 0, 0, 0} +#define ChannelSettings_init_zero {0, _ChannelSettings_ModemConfig_MIN, {0, {0}}, ""} #define RadioConfig_init_zero {false, RadioConfig_UserPreferences_init_zero, false, ChannelSettings_init_zero} #define RadioConfig_UserPreferences_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define NodeInfo_init_zero {0, false, User_init_zero, false, Position_init_zero, 0} @@ -269,9 +274,10 @@ typedef struct _ToRadio { #define SubPacket_data_tag 3 #define SubPacket_user_tag 4 #define SubPacket_want_response_tag 5 +#define MeshPacket_decoded_tag 3 +#define MeshPacket_encrypted_tag 8 #define MeshPacket_from_tag 1 #define MeshPacket_to_tag 2 -#define MeshPacket_payload_tag 3 #define MeshPacket_rx_time_tag 4 #define MeshPacket_id_tag 6 #define MeshPacket_rx_snr_tag 7 @@ -338,18 +344,19 @@ X(a, STATIC, SINGULAR, BOOL, want_response, 5) #define MeshPacket_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, INT32, from, 1) \ X(a, STATIC, SINGULAR, INT32, to, 2) \ -X(a, STATIC, OPTIONAL, MESSAGE, payload, 3) \ +X(a, STATIC, ONEOF, MESSAGE, (payload,decoded,decoded), 3) \ +X(a, STATIC, ONEOF, BYTES, (payload,encrypted,encrypted), 8) \ X(a, STATIC, SINGULAR, UINT32, rx_time, 4) \ X(a, STATIC, SINGULAR, UINT32, id, 6) \ X(a, STATIC, SINGULAR, FLOAT, rx_snr, 7) #define MeshPacket_CALLBACK NULL #define MeshPacket_DEFAULT NULL -#define MeshPacket_payload_MSGTYPE SubPacket +#define MeshPacket_payload_decoded_MSGTYPE SubPacket #define ChannelSettings_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, INT32, tx_power, 1) \ X(a, STATIC, SINGULAR, UENUM, modem_config, 3) \ -X(a, STATIC, SINGULAR, FIXED_LENGTH_BYTES, psk, 4) \ +X(a, STATIC, SINGULAR, BYTES, psk, 4) \ X(a, STATIC, SINGULAR, STRING, name, 5) #define ChannelSettings_CALLBACK NULL #define ChannelSettings_DEFAULT NULL @@ -492,12 +499,12 @@ extern const pb_msgdesc_t ToRadio_msg; /* RouteDiscovery_size depends on runtime parameters */ #define SubPacket_size 377 #define MeshPacket_size 419 -#define ChannelSettings_size 44 -#define RadioConfig_size 120 +#define ChannelSettings_size 60 +#define RadioConfig_size 136 #define RadioConfig_UserPreferences_size 72 #define NodeInfo_size 132 #define MyNodeInfo_size 85 -#define DeviceState_size 18535 +#define DeviceState_size 18552 #define DebugString_size 258 #define FromRadio_size 428 #define ToRadio_size 422 diff --git a/src/screen.cpp b/src/screen.cpp index 0b3f22fb..c60907aa 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -81,7 +81,7 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state MeshPacket &mp = devicestate.rx_text_message; NodeInfo *node = nodeDB.getNode(mp.from); // DEBUG_MSG("drawing text message from 0x%x: %s\n", mp.from, - // mp.payload.variant.data.payload.bytes); + // mp.decoded.variant.data.decoded.bytes); // Demo for drawStringMaxWidth: // with the third parameter you can define the width after which words will @@ -94,8 +94,8 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state // the max length of this buffer is much longer than we can possibly print static char tempBuf[96]; - assert(mp.payload.has_data); - snprintf(tempBuf, sizeof(tempBuf), " %s", mp.payload.data.payload.bytes); + assert(mp.decoded.has_data); + snprintf(tempBuf, sizeof(tempBuf), " %s", mp.decoded.data.payload.bytes); display->drawStringMaxWidth(4 + x, 10 + y, 128, tempBuf); }