mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-14 06:16:23 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8336a54b9 | |||
| b39fbd4ed3 |
@@ -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,
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
@@ -284,7 +284,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
if (useBase16)
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||
else
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
NotificationRenderer::numDigits = digits;
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
|
||||
@@ -311,7 +311,8 @@ 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, 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 showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
|
||||
@@ -2289,8 +2289,10 @@ void menuHandler::testMenu()
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](int number_picked) -> void {
|
||||
LOG_WARN("Nodenum: 0x%08x", number_picked);
|
||||
keyVerificationModule->sendInitialRequest(number_picked);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
@@ -2474,8 +2476,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback = [=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);
|
||||
|
||||
@@ -233,6 +233,29 @@ 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;
|
||||
|
||||
|
||||
@@ -50,6 +50,15 @@ 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);
|
||||
@@ -85,6 +94,9 @@ 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
|
||||
|
||||
+41
-11
@@ -485,14 +485,25 @@ 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) && 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) {
|
||||
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) {
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
|
||||
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
|
||||
bytes)) {
|
||||
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
@@ -503,7 +514,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
decrypted = true;
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
|
||||
memcpy(&p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
@@ -677,6 +688,19 @@ 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
|
||||
@@ -694,23 +718,29 @@ 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) {
|
||||
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)) {
|
||||
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 known public key for the destination
|
||||
if (node == nullptr || node->public_key.size != 32) {
|
||||
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||
if (!haveDestKey) {
|
||||
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) &&
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && node != nullptr &&
|
||||
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), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), destPublic, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#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;
|
||||
@@ -48,9 +50,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
commitVerifiedRemoteNode();
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -63,9 +63,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
}
|
||||
// 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.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||
return false;
|
||||
}
|
||||
@@ -74,9 +73,14 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
}
|
||||
|
||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||
r->hash1.size == 0) {
|
||||
r->hash1.size == 32) {
|
||||
memcpy(hash2, r->hash2.bytes, 32);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
||||
// 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 {
|
||||
keyVerificationModule->processSecurityNumber(number_picked);
|
||||
});)
|
||||
|
||||
@@ -91,9 +95,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
service->sendClientNotification(cn);
|
||||
LOG_INFO("Received hash2");
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
return true;
|
||||
return false;
|
||||
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
|
||||
r->nonce == currentNonce) {
|
||||
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
@@ -106,10 +111,9 @@ 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) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -135,22 +139,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
{
|
||||
LOG_DEBUG("keyVerification start");
|
||||
// generate nonce
|
||||
updateState();
|
||||
updateState(false);
|
||||
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;
|
||||
KeyVerification.hash1.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);
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
||||
p->to = remoteNode;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
// 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->decoded.want_response = true;
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -167,9 +178,6 @@ 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;
|
||||
|
||||
@@ -184,15 +192,35 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
response.nonce = scratch.nonce;
|
||||
currentRemoteNode = req.from;
|
||||
currentNonceTimestamp = getTime();
|
||||
currentSecurityNumber = random(1, 999999);
|
||||
currentSecurityNumber = random(1, 999999); // fixme, use better random
|
||||
|
||||
// generate hash1
|
||||
// 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
|
||||
hash.reset();
|
||||
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
||||
hash.update(senderKey, 32);
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
@@ -201,13 +229,17 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(hash1, 32);
|
||||
hash.finalize(hash2, 32);
|
||||
response.hash1.size = 0;
|
||||
// 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.hash2.size = 32;
|
||||
memcpy(response.hash2.bytes, hash2, 32);
|
||||
|
||||
responsePacket = allocDataProtobuf(response);
|
||||
|
||||
responsePacket->pki_encrypted = true;
|
||||
// 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;
|
||||
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();
|
||||
@@ -231,11 +263,16 @@ 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 = 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?
|
||||
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;
|
||||
}
|
||||
LOG_WARN("hashing ");
|
||||
// calculate hash1
|
||||
@@ -246,7 +283,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
|
||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
||||
hash.update(remotePublic.bytes, 32);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
hash.reset();
|
||||
@@ -289,13 +326,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
return;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::updateState()
|
||||
void KeyVerificationModule::updateState(bool resetTimer)
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
} else if (resetTimer) {
|
||||
currentNonceTimestamp = getTime();
|
||||
}
|
||||
}
|
||||
@@ -310,6 +347,31 @@ 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)
|
||||
|
||||
@@ -12,6 +12,39 @@ 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 =
|
||||
@@ -29,6 +62,7 @@ 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
|
||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
char message[40] = {0};
|
||||
|
||||
void processSecurityNumber(uint32_t);
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
Reference in New Issue
Block a user