Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement public/private key encryption for direct messages #1509

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

edinnen
Copy link

@edinnen edinnen commented Jun 13, 2022

Direct message encryption over a shared channel

The goal of this PR is to prevent the interception of direct message payloads by non-recipient nodes over a shared channel on the mesh while simultaneously allowing non-recipient nodes to repeat messages across the mesh.

Public/private key pairs are generated on the Curve25519 elliptic-curve. We then use the elliptic curve Diffie–Hellman key agreement protocol to compute a shared key which we can use to encrypt data in a way that can only be decrypted by the sender or the recipient. This allows us to send secured messages over the "insecure" shared channel.

Direct Message Encryption Steps

  1. During message encryption check to see if the message is a direct message
  2. Compute the shared key between the sender and recipient
  3. Set the CryptoEngine AES key to the shared key
  4. Encrypt the packet's decoded.payload
  5. Carry on with the standard packet encryption function which encrypts the entire packet with the channel AES key

Direct Message Decryption Steps

  1. Decrypt the packet with the channel AES key as normal
  2. Check if the packet is a direct message to the decrypting node
  3. Compute the shared key with the sender
  4. Set the CryptoEngine AES key to the shared key
  5. Decrypt the packet's decoded.payload

Changes

  • Add rweather/Crypto lib
  • Add public_key to User protobuf
  • Add private_key to MyNodeInfo protobuf
  • Generates and stores a public/private key pair on boot. Regenerated on each boot to cycle the keys
  • Public key is sent out over the mesh during device announce broadcasts
  • A shared key is computed and used for any inbound/outbound direct messages and used to encrypt/decrypt just the message payload and not the whole packet

Caveats

In order to properly encrypt/decrypt messages both nodes must store the current public key for the other party. As the public/private keys are cycled every boot other nodes on the network could have outdated public keys for another node. This state will persist until a recently booted node sends an announce message with its new public key to the network. If the keys are out of date messages will be gibberish until the keys are properly announced.

Related Protobufs PR

@garthvh
Copy link
Member

garthvh commented Jun 13, 2022

Looks awesome! Have you seen the ham mode settings? Are we able to easily disable the encryption / use the default psk? We need for encryption to be "off" when the ham mode is on.

@edinnen
Copy link
Author

edinnen commented Jun 13, 2022

Good catch! Does ham mode cause the perhapsDecode & perhapsEncode functions in mesh/Router.cpp to be skipped already? If not I could maybe add a check for !owner.is_licensed in the if statements that trigger the pub key encryption/decryption functions.

@mc-hamster
Copy link
Member

Good catch! Does ham mode cause the perhapsDecode & perhapsEncode functions in mesh/Router.cpp to be skipped already? If not I could maybe add a check for !owner.is_licensed in the if statements that trigger the pub key encryption/decryption functions.

FYI - perhapsDecode and perhapsEncode is also what does the text message compression.

@@ -24,9 +25,21 @@ class CryptoEngine

CryptoKey key = {};

uint8_t private_key[32];
bool keyPairSet;

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please initialize private_key, public_key and keyPairSet

Copy link
Sponsor Member

@caveman99 caveman99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments. cpp_check wants the keys to be initialized and the key in user struct is blowing up memory usage.

@@ -159,7 +159,7 @@ extern const pb_msgdesc_t OEMStore_msg;

/* Maximum encoded size of messages (where known) */
#define ChannelFile_size 624
#define DeviceState_size 23728
#define DeviceState_size 26598
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ouch ... RAM usage...

@GUVWAF
Copy link
Member

GUVWAF commented Jun 13, 2022

Nice work. However, right now direct messages are not rebroadcasted as mentioned in issue #1459. With this PR in, I think direct messages can be handled just like broadcast messages, except that every node can stop flooding once the intended recipient has ACK-ed. I think then two changes are needed as I mentioned in the issue, namely setting the hop limit to HOP_RELIABLE for direct messages as well and letting the FloodingRouter rebroadcast every packet with an address not equal to its own ID. Maybe this is a good time to test this as well?

@@ -239,6 +244,7 @@ ErrorCode Router::send(MeshPacket *p)
#endif

auto encodeResult = perhapsEncode(p);
if (isDirectMessage(p)) p->want_ack = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think isDirectMessage(p) will never be true if p->want_ack is not true already before, so this line will not do anything.

@thebentern
Copy link
Contributor

Closing. It's a great idea, but any future efforts at a similar solution to enhanced privacy need to take into account the limited resources we have to work with on these devices.

@thebentern thebentern closed this Sep 11, 2022
@caveman99
Copy link
Sponsor Member

I'd like to keep this open, as it's a base framework that can be built upon. I see 2 possibilities to make this happen.

  • using a special hardware device as a key vault (ATECC series of chips). This means it's not available on every device but can be added through a simple breakout
  • putting the key store in a separate indexed protobuf or db structure on the Flash FS and just look it up when its needed, not keep it in ram/nodedb all the time.

@caveman99 caveman99 reopened this Sep 27, 2022
@kravietz
Copy link

@caveman99 @thebentern

By the way, I've done some performance comparisons on nRF52 RAK 4631 platform between ChaChaPoly (ChaCha20 stream cipher with Poly1305 authenticator) and various AES keylengths, all from rweather/Crypto @ ^0.4.0 library, and it comes out ChaChaPoly is faster by a factor of 4.5x on encryption and 10x on decryption (!) on that platform, while providing packet integrity protection which AES-CTR is missing if I'm not mistaken. I would guess the performance boost comes with proportional energy consumption savings. I'd be happy to work on a MR to offer an option to select whether ChaChaPoly or AES is to be used, also keeping in mind what is written in https://meshtastic.org/docs/overview/encryption but the public/private key encryption is much broader topic so I'd just focus on replacing

State Size ... 240

Test Vectors:
ChaChaPoly #1 ... Passed
ChaChaPoly #2 ... Passed

Performance Tests:
ChaChaPoly #1 SetKey ... 47.85us per operation, 20898.20 per second
ChaChaPoly #1 Encrypt ... 1.50us per byte, 668735.57 bytes per second
ChaChaPoly #1 Decrypt ... 1.50us per byte, 668735.57 bytes per second
ChaChaPoly #1 AddAuthData ... 0.78us per byte, 1285037.35 bytes per second


State Sizes:
AES128 ... 188
AES192 ... 220
AES256 ... 252

Test Vectors:
AES-128-ECB Encryption ... Passed
AES-128-ECB Decryption ... Passed
AES-192-ECB Encryption ... Passed
AES-192-ECB Decryption ... Passed
AES-256-ECB Encryption ... Passed
AES-256-ECB Decryption ... Passed

Performance Tests:
AES-128-ECB Set Key ... 25.88us per operation, 38641.52 per second
AES-128-ECB Encrypt ... 4.97us per byte, 201277.61 bytes per second
AES-128-ECB Decrypt ... 11.33us per byte, 88275.86 bytes per second

AES-192-ECB Set Key ... 26.86us per operation, 37236.46 per second
AES-192-ECB Encrypt ... 6.01us per byte, 166503.98 bytes per second
AES-192-ECB Decrypt ... 13.79us per byte, 72495.55 bytes per second

AES-256-ECB Set Key ... 34.38us per operation, 29090.91 per second
AES-256-ECB Encrypt ... 7.04us per byte, 141975.63 bytes per second
AES-256-ECB Decrypt ... 16.24us per byte, 61593.94 bytes per second

@hhartzer
Copy link

This is very cool! Has there been any progress on this?

@luzpaz
Copy link
Contributor

luzpaz commented Jul 30, 2023

Wondering as well ?

@kokroo
Copy link
Contributor

kokroo commented Aug 31, 2023

@kravietz Did you use the hardware AES accelerator on the nrf boards? I bet those would be faster and more energy-efficient.

@kravietz
Copy link

@kokroo Interesting, I didn't know the platform has it! We'd need to check the nRF SDK to see how this could be implemented, which shouldn't be that difficult granted that the SD has examples for both AES and ChaCha-Poly

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fcrypto_examples_nrf_crypto.html

RAK4631 is based on nRF52840, which seems to have the following hardware cryptography features:

AES-128 – ECB, CBC, CMAC/CBC-MAC, CTR, CCM/CCM*
Chacha20/Poly1305 AEAD supporting 128- and 256-bit key size
SHA-1, SHA-2 up to 256 bits
Keyed-hash message authentication code (HMAC)
RSA up to 2048-bit key size
SRP up to 3072-bit key size
ECC support for most used curves, including P-256 (secp256r1) and Ed25519/Curve25519

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fstruct_nrf52%2Fstruct%2Fnrf52840.html

These are listed as part of ARM® TrustZone® Cryptocell 310 security subsystem, I'll have a look how these are handled in the SDK.

Relying on hardware crypto creates some portability challenges, but the fallback across all platforms is software implementation which will be faster for ChaCha than for AES in any case.

@kokroo
Copy link
Contributor

kokroo commented Aug 31, 2023

@kravietz We can easily detect platforms and swap out encryption functions across platforms. This is something programmers deal with all the time.

@Woomeico
Copy link

Woomeico commented Jan 20, 2024

I know, this is about PK crypto, but let me first talk about the symmetric encryption, which is important for PK too.

Currently AES in CTR mode is used. This is a stream cipher which is XORed with the data and in a proper CTR mode implementation the counter starts at 0, is then for each block incremented by 1 and most importantly is never reused. This allows to use the full counter range without any reuse happening. If a counter is reused, then two encrypted blocks can be XORed to eliminate the encryption and the result is the XOR of the data of two blocks. When the data has some structure (which is basically always the case) it can be used to get information of the messages, e.g. when a chat message is XORed with a more static node info or telemetry message. The same is the case with OFB and CFB.

On yt is a video why DMR encryption with Hytera is so good. It is because it uses a random IV for OFB. When using a random IV the birthday paradox applies: https://en.wikipedia.org/wiki/Birthday_problem So after about a square of the IV number space IV duplicates can be expected. Because the IV there is only 32 bits long this means after about 64k of frames. (There is also a LFSR involved, which can also create issues, but I don't know any details.) 64k frames is about 6.5 hours talking instead of the 49 years presented in the video. So small mistake and it basically falls apart. This is why I avoid CTR/OFB/CFB if possible and get skeptical when something is using one of these modes. Writing marketing material, that AES is military security, is easy. Getting that security to the road is difficult.

Currently Meshtastic uses a random value as a start value after a boot and then counts from this value. This is a mix between the proper CTR mode and a random value giving us the birthday problem. The more often the node reboots the more birthday problem there is. The counter in the IV is also 32 bits long and is AFAIK not incremented per packet, but per block. So reuse occurs after about 64k to 4G blocks.

What I would do instead is to use the CBC (Edit: typo) mode with ciphertext stealing: https://en.wikipedia.org/wiki/Ciphertext_stealing It is a bit more difficult to implement than CTR, but it has otherwise very similar relevant properties than CTR. It requires only a tiny little bit more processing power and most of all it does also not increase the size of the message. When an IV is reused an attacker gets the info if the same or different data is encrypted, but an XOR of two blocks stays the XOR of two ciphertexts which doesn't help at all.

So when the chance arises to change crypto I do suggest to look into this topic. I am not an expert, just an cryptographically interested user for 30 years. So there might be better solutions than mine and therefore it would be advisable to ask real experts if possible.

Will now think about PK crypto.

@kokroo
Copy link
Contributor

kokroo commented Jan 21, 2024

@Woomeico Wow, i never even thought about this. You've made excellent points there. Do you mind if I discuss a few ideas with you over email? You can mail me at shivkok@gmail.com

@hhartzer
Copy link

hhartzer commented Feb 7, 2024

Should this be added to Meshtastic 3.0 goals?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet