-
Notifications
You must be signed in to change notification settings - Fork 513
Add ChaChaPoly AEAD-4 decryption support (Phase 1) #1677
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
Open
weebl2000
wants to merge
4
commits into
meshcore-dev:dev
Choose a base branch
from
weebl2000:feature/aead-4-encryption
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+271
−28
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
26bdb41
Add ChaChaPoly AEAD-4 decryption support (Phase 1)
weebl2000 e224e5c
Enable AEAD-4 sending to peers that advertise support
weebl2000 6526793
Fix AEAD-4 payload size check and nonce wrap-around
weebl2000 7637e64
Fix AEAD-4 assoc data mismatch — route type bits set after encryption
weebl2000 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||
| #include "Utils.h" | ||||||||
| #include <AES.h> | ||||||||
| #include <SHA256.h> | ||||||||
| #include <ChaChaPoly.h> | ||||||||
|
|
||||||||
| #ifdef ARDUINO | ||||||||
| #include <Arduino.h> | ||||||||
|
|
@@ -87,6 +88,120 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin | |||||||
| return 0; // invalid HMAC | ||||||||
| } | ||||||||
|
|
||||||||
| /* | ||||||||
| * AEAD-4: ChaCha20-Poly1305 authenticated encryption with 4-byte tag. | ||||||||
| * | ||||||||
| * Wire format (replaces ECB's [HMAC:2][ciphertext:N*16]): | ||||||||
| * [nonce:2] [ciphertext:M] [tag:4] (M = exact plaintext length) | ||||||||
| * | ||||||||
| * Key derivation (per-message, eliminates nonce-reuse catastrophe): | ||||||||
| * msg_key[32] = HMAC-SHA256(shared_secret[32], nonce_hi || nonce_lo || dest_hash || src_hash) | ||||||||
| * Including hashes makes keys direction-dependent: Alice->Bob and Bob->Alice derive | ||||||||
| * different keys even with the same nonce (for 255/256 peer pairs; the 1/256 where | ||||||||
| * dest_hash == src_hash remains a residual risk inherent to 1-byte hashes). | ||||||||
| * | ||||||||
| * IV construction (12 bytes, from on-wire fields): | ||||||||
| * iv[12] = { nonce_hi, nonce_lo, dest_hash, src_hash, 0, 0, 0, 0, 0, 0, 0, 0 } | ||||||||
| * | ||||||||
| * Associated data (authenticated but not encrypted): | ||||||||
| * Peer msgs: header || dest_hash || src_hash | ||||||||
| * Anon reqs: header || dest_hash | ||||||||
| * Group msgs: header || channel_hash | ||||||||
| * | ||||||||
| * Nonce: 16-bit counter per peer, seeded from HW RNG on boot. With per-message | ||||||||
| * key derivation, even a nonce collision (across reboots) only leaks P1 XOR P2 | ||||||||
| * for that message pair — no key recovery, no impact on other messages. | ||||||||
| * | ||||||||
| * Group channels: all members share the same key, so cross-sender nonce | ||||||||
| * collisions are possible (~300 msgs for 50% chance with random nonces). | ||||||||
| * Damage is bounded (message pair leak, no key recovery). | ||||||||
| */ | ||||||||
| int Utils::aeadEncrypt(const uint8_t* shared_secret, | ||||||||
| uint8_t* dest, | ||||||||
| const uint8_t* src, int src_len, | ||||||||
| const uint8_t* assoc_data, int assoc_len, | ||||||||
| uint16_t nonce_counter, | ||||||||
| uint8_t dest_hash, uint8_t src_hash) { | ||||||||
| if (src_len <= 0) return 0; | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider a bit more defense here?
Suggested change
|
||||||||
|
|
||||||||
| // Write nonce to output | ||||||||
| dest[0] = (uint8_t)(nonce_counter >> 8); | ||||||||
| dest[1] = (uint8_t)(nonce_counter & 0xFF); | ||||||||
|
|
||||||||
| // Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash) | ||||||||
| // Including hashes makes the key direction-dependent, preventing keystream reuse | ||||||||
| // when Alice->Bob and Bob->Alice use the same nonce (255/256 peer pairs). | ||||||||
| uint8_t msg_key[32]; | ||||||||
| { | ||||||||
| uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { dest[0], dest[1], dest_hash, src_hash }; | ||||||||
| SHA256 sha; | ||||||||
| sha.resetHMAC(shared_secret, PUB_KEY_SIZE); | ||||||||
| sha.update(kdf_input, sizeof(kdf_input)); | ||||||||
| sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32); | ||||||||
| } | ||||||||
|
|
||||||||
| // Build 12-byte IV from on-wire fields | ||||||||
| uint8_t iv[12]; | ||||||||
| iv[0] = dest[0]; // nonce_hi | ||||||||
| iv[1] = dest[1]; // nonce_lo | ||||||||
| iv[2] = dest_hash; | ||||||||
| iv[3] = src_hash; | ||||||||
| memset(&iv[4], 0, 8); | ||||||||
|
|
||||||||
| ChaChaPoly cipher; | ||||||||
| cipher.setKey(msg_key, 32); | ||||||||
| cipher.setIV(iv, 12); | ||||||||
| cipher.addAuthData(assoc_data, assoc_len); | ||||||||
| cipher.encrypt(dest + AEAD_NONCE_SIZE, src, src_len); | ||||||||
| cipher.computeTag(dest + AEAD_NONCE_SIZE + src_len, AEAD_TAG_SIZE); | ||||||||
| cipher.clear(); | ||||||||
| memset(msg_key, 0, 32); | ||||||||
|
|
||||||||
| return AEAD_NONCE_SIZE + src_len + AEAD_TAG_SIZE; | ||||||||
| } | ||||||||
|
|
||||||||
| int Utils::aeadDecrypt(const uint8_t* shared_secret, | ||||||||
| uint8_t* dest, | ||||||||
| const uint8_t* src, int src_len, | ||||||||
| const uint8_t* assoc_data, int assoc_len, | ||||||||
| uint8_t dest_hash, uint8_t src_hash) { | ||||||||
| // Minimum: nonce(2) + at least 1 byte ciphertext + tag(4) | ||||||||
| if (src_len < AEAD_NONCE_SIZE + 1 + AEAD_TAG_SIZE) return 0; | ||||||||
|
|
||||||||
| int ct_len = src_len - AEAD_NONCE_SIZE - AEAD_TAG_SIZE; | ||||||||
|
|
||||||||
| // Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash) | ||||||||
| uint8_t msg_key[32]; | ||||||||
| { | ||||||||
| uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { src[0], src[1], dest_hash, src_hash }; | ||||||||
| SHA256 sha; | ||||||||
| sha.resetHMAC(shared_secret, PUB_KEY_SIZE); | ||||||||
| sha.update(kdf_input, sizeof(kdf_input)); | ||||||||
| sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32); | ||||||||
| } | ||||||||
|
|
||||||||
| // Build 12-byte IV from on-wire fields | ||||||||
| uint8_t iv[12]; | ||||||||
| iv[0] = src[0]; // nonce_hi | ||||||||
| iv[1] = src[1]; // nonce_lo | ||||||||
| iv[2] = dest_hash; | ||||||||
| iv[3] = src_hash; | ||||||||
| memset(&iv[4], 0, 8); | ||||||||
|
|
||||||||
| ChaChaPoly cipher; | ||||||||
| cipher.setKey(msg_key, 32); | ||||||||
| cipher.setIV(iv, 12); | ||||||||
| cipher.addAuthData(assoc_data, assoc_len); | ||||||||
| cipher.decrypt(dest, src + AEAD_NONCE_SIZE, ct_len); | ||||||||
|
|
||||||||
| bool valid = cipher.checkTag(src + AEAD_NONCE_SIZE + ct_len, AEAD_TAG_SIZE); | ||||||||
| cipher.clear(); | ||||||||
| memset(msg_key, 0, 32); | ||||||||
| if (!valid) memset(dest, 0, ct_len); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ECB path doesn't do this. Maybe it should? |
||||||||
|
|
||||||||
| return valid ? ct_len : 0; | ||||||||
| } | ||||||||
|
|
||||||||
| static const char hex_chars[] = "0123456789ABCDEF"; | ||||||||
|
|
||||||||
| void Utils::toHex(char* dest, const uint8_t* src, size_t len) { | ||||||||
|
|
||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're working in unsigned and
data_lenis controlled by the caller, lengths close to max will cause this check's additions to wrap and potentially bypass the check.Consider something like calculating: