Re-implement PKI from #1509 (#4379)

* Re-implement PKI from #1509
co-authored-by: edinnen <ethanjdinnen@protonmail.com>

* Set the key lengnth to actually make PKI work.

* Remove unused variable and initialize keys to null

* move printBytes() to meshUtils

* Don't reset PKI key son reboot unless needed.

* Remove double encryption for PKI messages

* Cleanup encrypt logic

* Add the MESHTASTIC_EXCLUDE_PKI option, and set it for minimal builds. Required for STM32 targets for now.

* Use SHA-256 for PKI key hashing, and add MESHTASTIC_EXCLUDE_PKI_KEYGEN for STM32

* Fix a crash when node is null

* Don't send PKI encrypted packets while licensed

* use chIndex 8 for PKI

* Don't be so clever, that you corrupt incoming packets

* Pass on channel 8 for now

* Typo

* Lock keys once non-zero

* We in fact need 2 scratch buffers, to store the encrypted bytes, unencrypted bytes, and decoded protobuf.

* Lighter approach to retaining known key

* Attach the public key to PKI decrypted packets in device memory

* Turn PKI back off for STM32 :(

* Don't just memcp over a protobuf

* Don't PKI encrypt nodeinfo packets

* Add a bit more memory logging around nodeDB

* Use the proper macro to refer to NODENUM_BROADCAST

* Typo fix

* Don't PKI encrypt ROUTING (naks and acks)

* Adds SecurityConfig protobuf

* Add admin messages over PKI

* Disable PKI for the WIO-e5

* Add MINIMUM_SAFE_FREE_HEAP macro and set to safe 1.5k

* Add missed "has_security"

* Add the admin_channel_enabled option

* STM32 again

* add missed configuration.h at the top of files

* Add EXCLUDE_TZ and RTC

* Enable PKI build on STM32 once again

* Attempt 1 at moving PKI to aes-ccm

* Fix buffers for encrypt/decrypt

* Eliminate unused aes variable

* Add debugging lines

* Set hash to 0 for PKI

* Fix debug lines so they don't print pointers.

* logic fix and more debug

* Rather important typo

* Check for short packets before attempting decrypt

* Don't forget to give cryptoEngine the keys!

* Use the right scratch buffer

* Cleanup

* moar cleanups

* Minor hardening

* Remove some in-progress stuff

* Turn PKI back off on STM32

* Return false

* 2.5 protos

* Sync up protos

* Add initial cryptography test vector tests

* re-add MINIMUM_SAFE_FREE_HEAP

* Housekeeping and comment fixes

* Add explanatory comment about weak dh25519 keys

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
Jonathan Bennett
2024-08-10 13:45:41 -05:00
parent c451db3a3f
commit b726792efd
19 changed files with 641 additions and 79 deletions

View File

@@ -37,6 +37,7 @@ static MemoryDynamic<meshtastic_MeshPacket> staticPool;
Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
static uint8_t bytes[MAX_RHPACKETLEN];
static uint8_t ScratchEncrypted[MAX_RHPACKETLEN];
/**
* Constructor
@@ -307,70 +308,105 @@ bool perhapsDecode(meshtastic_MeshPacket *p)
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)
return true; // If packet was already decoded just return
// assert(p->which_payloadVariant == MeshPacket_encrypted_tag);
size_t rawSize = p->encrypted.size;
if (rawSize > sizeof(bytes)) {
LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)\n", rawSize);
return false;
}
bool decrypted = false;
ChannelIndex chIndex = 0;
memcpy(bytes, p->encrypted.bytes,
rawSize); // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf
memcpy(ScratchEncrypted, p->encrypted.bytes, rawSize);
#if !(MESHTASTIC_EXCLUDE_PKI)
// Attempt PKI decryption first
if (p->channel == 0 && p->to == nodeDB->getNodeNum() && p->to > 0 && nodeDB->getMeshNode(p->from) != nullptr &&
nodeDB->getMeshNode(p->from)->user.public_key.size > 0 && nodeDB->getMeshNode(p->to)->user.public_key.size > 0 &&
rawSize > 8) {
LOG_DEBUG("Attempting PKI decryption\n");
// Try to find a channel that works with this hash
for (ChannelIndex chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
// Try to use this hash/channel pair
if (channels.decryptForHash(chIndex, p->channel)) {
// Try to decrypt the packet if we can
size_t rawSize = p->encrypted.size;
if (rawSize > sizeof(bytes)) {
LOG_ERROR("Packet too large to attempt decription! (rawSize=%d > 256)\n", rawSize);
return false;
}
memcpy(bytes, p->encrypted.bytes,
rawSize); // we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
if (crypto->decryptCurve25519(p->from, p->id, rawSize, ScratchEncrypted, bytes)) {
LOG_INFO("PKI Decryption worked!\n");
memset(&p->decoded, 0, sizeof(p->decoded));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded)) {
LOG_ERROR("Invalid protobufs in received mesh packet (bad psk?)!\n");
} else if (p->decoded.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_ERROR("Invalid portnum (bad psk?)!\n");
rawSize -= 8;
if (pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded) &&
p->decoded.portnum != meshtastic_PortNum_UNKNOWN_APP) {
decrypted = true;
LOG_INFO("Packet decrypted using PKI!\n");
p->pki_encrypted = true;
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->user.public_key.bytes, 32);
p->public_key.size = 32;
// memcpy(bytes, ScratchEncrypted, rawSize); // TODO: Rename the bytes buffers
// chIndex = 8;
} else {
// parsing was successful
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
p->channel = chIndex; // change to store the index instead of the hash
/* Not actually ever used.
// Decompress if needed. jm
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
// Decompress the payload
char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {};
char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {};
int decompressed_len;
memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size);
decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out);
// LOG_DEBUG("\n\n**\n\nDecompressed length - %d \n", decompressed_len);
memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len);
// Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
} */
printPacket("decoded message", p);
#if ENABLE_JSON_LOGGING
LOG_TRACE("%s\n", MeshPacketSerializer::JsonSerialize(p, false).c_str());
#elif ARCH_PORTDUINO
if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) {
LOG_TRACE("%s\n", MeshPacketSerializer::JsonSerialize(p, false).c_str());
}
#endif
return true;
return false;
}
}
}
#endif
LOG_WARN("No suitable channel found for decoding, hash was 0x%x!\n", p->channel);
return false;
// assert(p->which_payloadVariant == MeshPacket_encrypted_tag);
if (!decrypted) {
// Try to find a channel that works with this hash
for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
// Try to use this hash/channel pair
if (channels.decryptForHash(chIndex, p->channel)) {
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
// printBytes("plaintext", bytes, p->encrypted.size);
// Take those raw bytes and convert them back into a well structured protobuf we can understand
memset(&p->decoded, 0, sizeof(p->decoded));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &p->decoded)) {
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!\n", p->id);
} else if (p->decoded.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_ERROR("Invalid portnum (bad psk?)!\n");
} else {
decrypted = true;
break;
}
}
}
}
if (decrypted) {
// parsing was successful
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
p->channel = chIndex; // change to store the index instead of the hash
/* Not actually ever used.
// Decompress if needed. jm
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP) {
// Decompress the payload
char compressed_in[meshtastic_Constants_DATA_PAYLOAD_LEN] = {};
char decompressed_out[meshtastic_Constants_DATA_PAYLOAD_LEN] = {};
int decompressed_len;
memcpy(compressed_in, p->decoded.payload.bytes, p->decoded.payload.size);
decompressed_len = unishox2_decompress_simple(compressed_in, p->decoded.payload.size, decompressed_out);
// LOG_DEBUG("\n\n**\n\nDecompressed length - %d \n", decompressed_len);
memcpy(p->decoded.payload.bytes, decompressed_out, decompressed_len);
// Switch the port from PortNum_TEXT_MESSAGE_COMPRESSED_APP to PortNum_TEXT_MESSAGE_APP
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
} */
printPacket("decoded message", p);
#if ENABLE_JSON_LOGGING
LOG_TRACE("%s\n", MeshPacketSerializer::JsonSerialize(p, false).c_str());
#elif ARCH_PORTDUINO
if (settingsStrings[traceFilename] != "" || settingsMap[logoutputlevel] == level_trace) {
LOG_TRACE("%s\n", MeshPacketSerializer::JsonSerialize(p, false).c_str());
}
#endif
return true;
} else {
LOG_WARN("No suitable channel found for decoding, hash was 0x%x!\n", p->channel);
return false;
}
}
/** Return 0 for success or a Routing_Errror code for failure
@@ -384,7 +420,6 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
size_t numbytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_Data_msg, &p->decoded);
/* Not actually used, so save the cycles
// Only allow encryption on the text message app.
// TODO: Allow modules to opt into compression.
if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
@@ -432,10 +467,28 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Now that we are encrypting the packet channel should be the hash (no longer the index)
p->channel = hash;
#if !(MESHTASTIC_EXCLUDE_PKI)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
if (!owner.is_licensed && p->to != NODENUM_BROADCAST && node != nullptr && node->user.public_key.size > 0 &&
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP) { // TODO: check for size due to 8 byte tag
LOG_DEBUG("Using PKI!\n");
if (numbytes + 8 > MAX_RHPACKETLEN)
return meshtastic_Routing_Error_TOO_LARGE;
crypto->encryptCurve25519(p->to, getFrom(p), p->id, numbytes, bytes, ScratchEncrypted);
numbytes += 8;
memcpy(p->encrypted.bytes, ScratchEncrypted, numbytes);
p->channel = 0;
} else {
crypto->encrypt(getFrom(p), p->id, numbytes, bytes);
memcpy(p->encrypted.bytes, bytes, numbytes);
}
#else
crypto->encrypt(getFrom(p), p->id, numbytes, bytes);
memcpy(p->encrypted.bytes, bytes, numbytes);
#endif
// Copy back into the packet and set the variant type
memcpy(p->encrypted.bytes, bytes, numbytes);
p->encrypted.size = numbytes;
p->which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
}
@@ -539,4 +592,4 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
handleReceived(p);
packetPool.release(p);
}
}