Compare commits

..
Author SHA1 Message Date
vidplace7 db2b334ee8 Short circuit invalid nodenum check in ham mode.
Until signing is implemented, this will prevent the nodenum from being regenerated upon each reboot.
2026-06-09 19:53:08 -04:00
15 changed files with 71 additions and 283 deletions
@@ -87,12 +87,6 @@
</screenshots>
<releases>
<release version="2.7.26" date="2026-06-10">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.26</url>
</release>
<release version="2.7.25" date="2026-05-23">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.25</url>
</release>
<release version="2.7.24" date="2026-05-08">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
</release>
-12
View File
@@ -1,15 +1,3 @@
meshtasticd (2.7.26.0) unstable; urgency=medium
* Version 2.7.26
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 10 Jun 2026 00:19:23 +0000
meshtasticd (2.7.25.0) unstable; urgency=medium
* Version 2.7.25
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 23 May 2026 01:16:20 +0000
meshtasticd (2.7.24.0) unstable; urgency=medium
* Version 2.7.24
-5
View File
@@ -14,7 +14,6 @@
* For more information, see: https://meshtastic.org/
*/
#include "power.h"
#include "BluetoothCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
@@ -963,10 +962,6 @@ void Power::readPowerStatus()
lastLogTime = millis();
}
newStatus.notifyObservers(&powerStatus2);
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
if (hasBattery == OptTrue)
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
#ifdef DEBUG_HEAP
if (lastheap != memGet.getFreeHeap()) {
// Use stack-allocated buffer to avoid heap allocations in monitoring code
+2 -5
View File
@@ -271,7 +271,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
}
// Called to trigger a banner with custom message and duration
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
std::function<void(uint32_t)> bannerCallback)
{
#ifdef USE_EINK
@@ -284,10 +284,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
NotificationRenderer::alertBannerCallback = bannerCallback;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::curSelected = 0;
if (useBase16)
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
else
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
NotificationRenderer::numDigits = digits;
NotificationRenderer::currentNumber = 0;
+1 -2
View File
@@ -311,8 +311,7 @@ class Screen : public concurrency::OSThread
void showOverlayBanner(BannerOverlayOptions);
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback);
+4 -5
View File
@@ -2289,10 +2289,8 @@ void menuHandler::testMenu()
void menuHandler::numberTest()
{
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](int number_picked) -> void {
LOG_WARN("Nodenum: 0x%08x", number_picked);
keyVerificationModule->sendInitialRequest(number_picked);
});
screen->showNumberPicker("Pick a number\n ", 30000, 4,
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
}
void menuHandler::wifiBaseMenu()
@@ -2476,7 +2474,8 @@ void menuHandler::keyVerificationFinalPrompt()
options.notificationType = graphics::notificationTypeEnum::selection_picker;
options.bannerCallback = [=](int selected) {
if (selected == 1) {
keyVerificationModule->commitVerifiedRemoteNode();
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
}
};
screen->showOverlayBanner(options);
-23
View File
@@ -233,29 +233,6 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
return true;
}
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
{
pendingKeyVerificationNode = node;
memcpy(pendingKeyVerificationPublicKey, key, 32);
hasPendingKeyVerificationKey = true;
}
void CryptoEngine::clearPendingPublicKey()
{
pendingKeyVerificationNode = 0;
memset(pendingKeyVerificationPublicKey, 0, 32);
hasPendingKeyVerificationKey = false;
}
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
{
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
return false;
out.size = 32;
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
return true;
}
#endif
concurrency::Lock *cryptLock;
-12
View File
@@ -50,15 +50,6 @@ class CryptoEngine
virtual bool setDHPublicKey(uint8_t *publicKey);
virtual void hash(uint8_t *bytes, size_t numBytes);
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
void setPendingPublicKey(uint32_t node, const uint8_t *key);
void clearPendingPublicKey();
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
virtual void aesSetKey(const uint8_t *key, size_t key_len);
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
@@ -94,9 +85,6 @@ class CryptoEngine
#if !(MESHTASTIC_EXCLUDE_PKI)
uint8_t shared_key[32] = {0};
uint8_t private_key[32] = {0};
uint32_t pendingKeyVerificationNode = 0;
uint8_t pendingKeyVerificationPublicKey[32] = {0};
bool hasPendingKeyVerificationKey = false;
#endif
/**
* Init our 128 bit nonce for a new packet
+12 -7
View File
@@ -1542,6 +1542,8 @@ void NodeDB::pickNewNodeNum()
// Identity check via public key (or "empty slot?" when no keys yet);
// macaddr no longer lives on the slim header.
// This check does not work when is_licensed=true since we don't store a public key.
// Revisit with XEdDSA signing.
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
if (!n)
return false;
@@ -1550,13 +1552,16 @@ void NodeDB::pickNewNodeNum()
return !nodeInfoLiteHasUser(n);
};
meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
nodeNum = candidate;
// Short circuit the check for licensed devices since they do not have public keys to compare against the nodeDB.
if (!owner.is_licensed) {
meshtastic_NodeInfoLite *found;
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
if (found)
LOG_WARN("NOTE! Our desired nodenum 0x%x is invalid or in use, picking 0x%x", nodeNum, candidate);
nodeNum = candidate;
}
}
LOG_DEBUG("Use nodenum 0x%x ", nodeNum);
+11 -41
View File
@@ -485,25 +485,14 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
bool decrypted = false;
ChannelIndex chIndex = 0;
#if !(MESHTASTIC_EXCLUDE_PKI)
// Resolve the sender's public key: prefer the one stored in NodeDB, otherwise fall back to a
// not-yet-verified key held during an in-progress key-verification handshake. The latter lets us
// DH-decode the follow-on PKI packet before the peer's key has been committed to NodeDB.
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
bool haveRemoteKey = false;
auto *fromNode = nodeDB->getMeshNode(p->from);
if (fromNode != nullptr && fromNode->public_key.size > 0) {
remotePublic = fromNode->public_key;
haveRemoteKey = true;
} else if (crypto->getPendingPublicKey(p->from, remotePublic)) {
haveRemoteKey = true;
}
// Attempt PKI decryption first
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && haveRemoteKey &&
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
rawSize > MESHTASTIC_PKC_OVERHEAD) {
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
LOG_DEBUG("Attempt PKI decryption");
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
bytes)) {
LOG_INFO("PKI Decryption worked!");
meshtastic_Data decodedtmp;
@@ -514,7 +503,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
decrypted = true;
LOG_INFO("Packet decrypted using PKI!");
p->pki_encrypted = true;
memcpy(&p->public_key.bytes, remotePublic.bytes, 32);
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
p->public_key.size = 32;
p->decoded = decodedtmp;
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
@@ -688,19 +677,6 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
#if !(MESHTASTIC_EXCLUDE_PKI)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
// Resolve the destination's public key: prefer NodeDB, otherwise (for a key-verification
// follow-on packet that explicitly requested PKI) fall back to the not-yet-verified key held
// during an in-progress handshake. This lets us DH-encode the follow-on packet before the
// peer's key has been committed to NodeDB.
meshtastic_NodeInfoLite_public_key_t destPublic = {0, {0}};
bool haveDestKey = false;
if (node != nullptr && node->public_key.size == 32) {
destPublic = node->public_key;
haveDestKey = true;
} else if (p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
crypto->getPendingPublicKey(p->to, destPublic)) {
haveDestKey = true;
}
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
// is not in the local nodedb
// First, only PKC encrypt packets we are originating
@@ -718,29 +694,23 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
// Some portnums either make no sense to send with PKC
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
// uses the pending key resolved into haveDestKey/destPublic above.
// Though possible the first packet each direction should go non-pkc
// to handle the case where the remote node has our key, but we don't have theirs.
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
LOG_DEBUG("Use PKI!");
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE;
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
if (!haveDestKey) {
// Check for a known public key for the destination
if (node == nullptr || node->public_key.size != 32) {
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
p->decoded.portnum);
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
}
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && node != nullptr &&
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) {
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
*node->public_key.bytes);
return meshtastic_Routing_Error_PKI_FAILED;
}
crypto->encryptCurve25519(p->to, getFrom(p), destPublic, p->id, numbytes, bytes, p->encrypted.bytes);
crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
numbytes += MESHTASTIC_PKC_OVERHEAD;
p->channel = 0;
p->pki_encrypted = true;
+32 -94
View File
@@ -1,13 +1,11 @@
#if !MESHTASTIC_EXCLUDE_PKI
#include "KeyVerificationModule.h"
#include "CryptoEngine.h"
#include "MeshService.h"
#include "RTC.h"
#include "graphics/draw/MenuHandler.h"
#include "main.h"
#include "meshUtils.h"
#include "modules/AdminModule.h"
#include "modules/NodeInfoModule.h"
#include <SHA256.h>
KeyVerificationModule *keyVerificationModule;
@@ -50,7 +48,9 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
request->key_verification.nonce == currentNonce) {
commitVerifiedRemoteNode();
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr)
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
resetToIdle();
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
resetToIdle();
@@ -63,8 +63,9 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
{
updateState();
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
// the bootstrap case; the follow-on hash1 packet (M3) is required to be PKI in its branch below.
if (mp.pki_encrypted == false) {
return false;
}
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
return false;
}
@@ -73,14 +74,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
}
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
r->hash1.size == 32) {
r->hash1.size == 0) {
memcpy(hash2, r->hash2.bytes, 32);
// The response carries the responder's public key in hash1. If we don't already hold it, stash it
// as a pending key so the Router can PKI-encrypt our follow-on packet (committed to NodeDB on accept).
auto *responderNode = nodeDB->getMeshNode(currentRemoteNode);
if (responderNode == nullptr || responderNode->public_key.size != 32)
crypto->setPendingPublicKey(currentRemoteNode, r->hash1.bytes);
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, false, [](int number_picked) -> void {
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
keyVerificationModule->processSecurityNumber(number_picked);
});)
@@ -95,10 +91,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
service->sendClientNotification(cn);
LOG_INFO("Received hash2");
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
return false;
return true;
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
r->nonce == currentNonce) {
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
memset(message, 0, sizeof(message));
sprintf(message, "Verification: \n");
@@ -111,9 +106,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
options.notificationType = graphics::notificationTypeEnum::selection_picker;
options.bannerCallback =
[=](int selected) {
LOG_WARN("User selected %d for key verification", selected);
if (selected == 1) {
keyVerificationModule->commitVerifiedRemoteNode();
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (remoteNodePtr)
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
}
};
screen->showOverlayBanner(options);)
@@ -139,29 +135,22 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
{
LOG_DEBUG("keyVerification start");
// generate nonce
updateState(false);
updateState();
if (currentState != KEY_VERIFICATION_IDLE) {
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
return false;
}
updateState(true);
currentNonce = random();
currentNonceTimestamp = getTime();
currentRemoteNode = remoteNode;
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
KeyVerification.nonce = currentNonce;
KeyVerification.hash2.size = 0;
// Carry our public key in the otherwise-unused hash1 field so a peer that does not yet hold our
// key can learn it from this first message (bootstrap / onboarding).
KeyVerification.hash1.size = 32;
memcpy(KeyVerification.hash1.bytes, owner.public_key.bytes, 32);
KeyVerification.hash1.size = 0;
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
p->to = remoteNode;
p->channel = 0;
// Only request PKI when we already hold the destination's key. Otherwise this first message goes out
// channel-encrypted (the Router falls back) so the peer can bootstrap from the key carried in hash1.
auto *remoteNodePtr = nodeDB->getMeshNode(remoteNode);
p->pki_encrypted = (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32);
p->pki_encrypted = true;
p->decoded.want_response = true;
p->priority = meshtastic_MeshPacket_Priority_HIGH;
service->sendToMesh(p, RX_SRC_LOCAL, true);
@@ -178,6 +167,9 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
LOG_WARN("Key Verification requested, but already in a request");
return nullptr;
} else if (!currentRequest->pki_encrypted) {
LOG_WARN("Key Verification requested, but not in a PKI packet");
return nullptr;
}
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
@@ -192,35 +184,15 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
response.nonce = scratch.nonce;
currentRemoteNode = req.from;
currentNonceTimestamp = getTime();
currentSecurityNumber = random(1, 999999); // fixme, use better random
currentSecurityNumber = random(1, 999999);
// Resolve the requester's public key. When the request arrived PKI-encrypted the Router populated
// currentRequest->public_key; otherwise (channel-encrypted bootstrap) it rides in the hash1 field.
// If we don't already hold the key in NodeDB, stash it as a pending key so the Router can DH-decode
// the follow-on PKI packet. The pending key is only committed to NodeDB once verification is accepted.
const uint8_t *senderKey = nullptr;
if (currentRequest->pki_encrypted && currentRequest->public_key.size == 32) {
senderKey = currentRequest->public_key.bytes; // this is bizarre, fixme
} else if (scratch.hash1.size == 32) {
senderKey = scratch.hash1.bytes;
}
if (senderKey == nullptr) {
LOG_WARN("Key Verification request without a usable public key");
resetToIdle();
return nullptr;
}
auto *senderNode = nodeDB->getMeshNode(currentRemoteNode);
bool senderKeyInNodeDB = (senderNode != nullptr && senderNode->public_key.size == 32);
if (!senderKeyInNodeDB)
crypto->setPendingPublicKey(currentRemoteNode, senderKey);
// generate local hash1
// generate hash1
hash.reset();
hash.update(&currentSecurityNumber, sizeof(currentSecurityNumber));
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(&ourNodeNum, sizeof(ourNodeNum));
hash.update(senderKey, 32);
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
hash.update(owner.public_key.bytes, owner.public_key.size);
hash.finalize(hash1, 32);
@@ -229,17 +201,13 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
hash.update(&currentNonce, sizeof(currentNonce));
hash.update(hash1, 32);
hash.finalize(hash2, 32);
// Carry our public key in hash1 of the response so the requester can bootstrap our key as well.
response.hash1.size = 32;
memcpy(response.hash1.bytes, owner.public_key.bytes, 32);
response.hash1.size = 0;
response.hash2.size = 32;
memcpy(response.hash2.bytes, hash2, 32);
responsePacket = allocDataProtobuf(response);
// PKI-encrypt the response only if we already held the requester's key. In the bootstrap case it goes
// out channel-encrypted so the requester (who lacks our key) can decode it and read hash1.
responsePacket->pki_encrypted = senderKeyInNodeDB;
responsePacket->pki_encrypted = true;
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
@@ -263,16 +231,11 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
NodeNum ourNodeNum = nodeDB->getNodeNum();
uint8_t scratch_hash[32] = {0};
LOG_WARN("received security number: %u", incomingNumber);
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
// handshake (bootstrap case).
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
if (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32) {
remotePublic = remoteNodePtr->public_key;
} else if (!crypto->getPendingPublicKey(currentRemoteNode, remotePublic)) {
LOG_WARN("No public key available for remote node, aborting key verification");
resetToIdle();
return;
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
currentState = KEY_VERIFICATION_IDLE;
return; // should we throw an error here?
}
LOG_WARN("hashing ");
// calculate hash1
@@ -283,7 +246,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
hash.update(&currentRemoteNode, sizeof(currentRemoteNode));
hash.update(owner.public_key.bytes, owner.public_key.size);
hash.update(remotePublic.bytes, 32);
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
hash.finalize(hash1, 32);
hash.reset();
@@ -326,13 +289,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
return;
}
void KeyVerificationModule::updateState(bool resetTimer)
void KeyVerificationModule::updateState()
{
if (currentState != KEY_VERIFICATION_IDLE) {
// check for the 60 second timeout
if (currentNonceTimestamp < getTime() - 60) {
resetToIdle();
} else if (resetTimer) {
} else {
currentNonceTimestamp = getTime();
}
}
@@ -347,31 +310,6 @@ void KeyVerificationModule::resetToIdle()
currentSecurityNumber = 0;
currentRemoteNode = 0;
currentState = KEY_VERIFICATION_IDLE;
// Discard any not-yet-verified key learned during this handshake; on reject/timeout it is never trusted.
crypto->clearPendingPublicKey();
}
void KeyVerificationModule::commitVerifiedRemoteNode()
{
// The remote node already has a NodeDB entry by this point (packets were exchanged during the
// handshake), so getMeshNode is sufficient; bail defensively if it is somehow absent.
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentRemoteNode);
if (!node) {
LOG_WARN("Attempted to commit key, but unknown node");
return;
}
// If we only held the peer's key as a pending (unverified) key during the handshake, commit it to
// NodeDB now that the user has confirmed the verification, so future PKI traffic can use it.
meshtastic_NodeInfoLite_public_key_t pending = {0, {0}};
if (node->public_key.size != 32 && crypto->getPendingPublicKey(currentRemoteNode, pending))
node->public_key = pending;
node->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
LOG_WARN("Node %u manually verified with security number %u", currentRemoteNode, currentSecurityNumber);
if (nodeInfoModule)
nodeInfoModule->sendOurNodeInfo(currentRemoteNode, false, node->channel, true);
// todo: initiate save
crypto->clearPendingPublicKey();
currentState = KEY_VERIFICATION_IDLE;
}
void KeyVerificationModule::generateVerificationCode(char *readableCode)
+2 -36
View File
@@ -12,39 +12,6 @@ enum KeyVerificationState {
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
};
// KeyVerification Module overview
// This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy
// connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so
// it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the
// initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new
// node even if they don't have that node in the local nodeDB at all.
// The handshake process is as follows (NodeA = initiator, NodeB = responder):
// 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the
// hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already
// holds NodeB's key, otherwise channel-encrypted (the bootstrap case).
//
// 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a
// random 6-digit security number and stashes NodeA's public key (as a pending key if not already in
// the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B),
// then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's
// key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1.
//
// 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key,
// and prompts the user to enter the security number. The security number is never sent over the mesh
// and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from
// the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends
// its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and
// shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1.
//
// 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it
// matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation.
//
// The final on-screen code comparison is the actual manual verification: the user confirms the codes
// match on both devices, proving the two nodes agree on the same public keys (no MitM substitution).
// PKI-encrypting the follow-on packet additionally proves each node holds the private key for the
// agreed public key.
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
{
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
@@ -62,7 +29,6 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
bool sendInitialRequest(NodeNum remoteNode);
void generateVerificationCode(char *); // fills char with the user readable verification code
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
protected:
/* Called to handle a particular incoming message
@@ -92,8 +58,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
char message[40] = {0};
void processSecurityNumber(uint32_t);
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
void resetToIdle(); // Zero out module state
void updateState(); // check the timeouts and maybe reset the state to idle
void resetToIdle(); // Zero out module state
};
extern KeyVerificationModule *keyVerificationModule;
+3 -21
View File
@@ -40,7 +40,6 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
BLECharacteristic *fromNumCharacteristic;
BLECharacteristic *BatteryCharacteristic;
static int lastBatteryLevel = -1; // last value written to 0x2A19, to skip redundant writes/notifies
BLECharacteristic *logRadioCharacteristic;
BLEServer *bleServer;
@@ -719,8 +718,6 @@ void NimbleBluetooth::deinit()
#endif
BLEDevice::deinit(true);
BatteryCharacteristic = nullptr; // freed by deinit; clear so updateBatteryLevel() won't touch it
lastBatteryLevel = -1;
#endif
}
@@ -859,31 +856,16 @@ void NimbleBluetooth::setupService()
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
// Seed an initial 0-100 level so an early read of 0x2A19 returns a valid value.
uint8_t initialLevel = (powerStatus && powerStatus->getHasBattery()) ? powerStatus->getBatteryChargePercent() : 0;
if (initialLevel > 100)
initialLevel = 100;
BatteryCharacteristic->setValue(&initialLevel, 1);
lastBatteryLevel = initialLevel;
batteryService->start();
}
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if (!config.bluetooth.enabled || !BatteryCharacteristic)
return;
if (level > 100) // 0x2A19 must stay within the BAS 0-100 range
level = 100;
if (level == lastBatteryLevel)
return;
lastBatteryLevel = level;
// Cache the value so a READ works without a subscriber; notify only when connected.
BatteryCharacteristic->setValue(&level, 1);
if (nimbleBluetooth && nimbleBluetooth->isConnected())
if ((config.bluetooth.enabled == true) && nimbleBluetooth && nimbleBluetooth->isConnected()) {
BatteryCharacteristic->setValue(&level, 1);
BatteryCharacteristic->notify();
}
}
void NimbleBluetooth::clearBonds()
+2 -12
View File
@@ -15,9 +15,8 @@ static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16
static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16));
static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16));
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
static int lastBatteryLevel = -1; // last value written to BAS, to skip redundant writes/notifies
static BLEDis bledis; // DIS (Device Information Service) helper class instance
static BLEBas blebas; // BAS (Battery Service) helper class instance
#ifndef BLE_DFU_SECURE
static BLEDfu bledfu; // DFU software update helper service
#else
@@ -337,7 +336,6 @@ void NRF52Bluetooth::setup()
LOG_INFO("Init the Battery Service");
blebas.begin();
blebas.write(0); // Unknown battery level for now
lastBatteryLevel = 0;
// Setup the Heart Rate Monitor service using
// BLEService and BLECharacteristic classes
LOG_INFO("Init the Mesh bluetooth service");
@@ -357,14 +355,6 @@ void NRF52Bluetooth::resumeAdvertising()
/// Given a level between 0-100, update the BLE attribute
void updateBatteryLevel(uint8_t level)
{
if (!nrf52Bluetooth) // skip until the Battery Service has been begun in setup()
return;
if (level > 100) // BAS battery level must stay within 0-100
level = 100;
if (level == lastBatteryLevel)
return;
lastBatteryLevel = level;
blebas.write(level);
}
void NRF52Bluetooth::clearBonds()
+2 -2
View File
@@ -1,4 +1,4 @@
[VERSION]
major = 2
minor = 7
build = 26
minor = 8
build = 0