Implement public/private key encryption for direct messages
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
- During message encryption check to see if the message is a direct message
- Compute the shared key between the sender and recipient
- Set the
CryptoEngineAES key to the shared key - Encrypt the packet's
decoded.payload - Carry on with the standard packet encryption function which encrypts the entire packet with the channel AES key
Direct Message Decryption Steps
- Decrypt the packet with the channel AES key as normal
- Check if the packet is a direct message to the decrypting node
- Compute the shared key with the sender
- Set the
CryptoEngineAES key to the shared key - Decrypt the packet's
decoded.payload
Changes
- Add
rweather/Cryptolib - Add
public_keytoUserprotobuf - Add
private_keytoMyNodeInfoprotobuf - 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.
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.
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.
Good catch! Does ham mode cause the
perhapsDecode&perhapsEncodefunctions in mesh/Router.cpp to be skipped already? If not I could maybe add a check for!owner.is_licensedin the if statements that trigger the pub key encryption/decryption functions.
FYI - perhapsDecode and perhapsEncode is also what does the text message compression.
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?
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.
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 @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
This is very cool! Has there been any progress on this?
Wondering as well ?
@kravietz Did you use the hardware AES accelerator on the nrf boards? I bet those would be faster and more energy-efficient.
@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.
@kravietz We can easily detect platforms and swap out encryption functions across platforms. This is something programmers deal with all the time.
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.
@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 [email protected]
Should this be added to Meshtastic 3.0 goals?