|
1 | 1 | #include "Utils.h" |
2 | 2 | #include <AES.h> |
3 | 3 | #include <SHA256.h> |
| 4 | +#include <ChaChaPoly.h> |
4 | 5 |
|
5 | 6 | #ifdef ARDUINO |
6 | 7 | #include <Arduino.h> |
@@ -87,6 +88,120 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin |
87 | 88 | return 0; // invalid HMAC |
88 | 89 | } |
89 | 90 |
|
| 91 | +/* |
| 92 | + * AEAD-4: ChaCha20-Poly1305 authenticated encryption with 4-byte tag. |
| 93 | + * |
| 94 | + * Wire format (replaces ECB's [HMAC:2][ciphertext:N*16]): |
| 95 | + * [nonce:2] [ciphertext:M] [tag:4] (M = exact plaintext length) |
| 96 | + * |
| 97 | + * Key derivation (per-message, eliminates nonce-reuse catastrophe): |
| 98 | + * msg_key[32] = HMAC-SHA256(shared_secret[32], nonce_hi || nonce_lo || dest_hash || src_hash) |
| 99 | + * Including hashes makes keys direction-dependent: Alice->Bob and Bob->Alice derive |
| 100 | + * different keys even with the same nonce (for 255/256 peer pairs; the 1/256 where |
| 101 | + * dest_hash == src_hash remains a residual risk inherent to 1-byte hashes). |
| 102 | + * |
| 103 | + * IV construction (12 bytes, from on-wire fields): |
| 104 | + * iv[12] = { nonce_hi, nonce_lo, dest_hash, src_hash, 0, 0, 0, 0, 0, 0, 0, 0 } |
| 105 | + * |
| 106 | + * Associated data (authenticated but not encrypted): |
| 107 | + * Peer msgs: header || dest_hash || src_hash |
| 108 | + * Anon reqs: header || dest_hash |
| 109 | + * Group msgs: header || channel_hash |
| 110 | + * |
| 111 | + * Nonce: 16-bit counter per peer, seeded from HW RNG on boot. With per-message |
| 112 | + * key derivation, even a nonce collision (across reboots) only leaks P1 XOR P2 |
| 113 | + * for that message pair — no key recovery, no impact on other messages. |
| 114 | + * |
| 115 | + * Group channels: all members share the same key, so cross-sender nonce |
| 116 | + * collisions are possible (~300 msgs for 50% chance with random nonces). |
| 117 | + * Damage is bounded (message pair leak, no key recovery). |
| 118 | + */ |
| 119 | +int Utils::aeadEncrypt(const uint8_t* shared_secret, |
| 120 | + uint8_t* dest, |
| 121 | + const uint8_t* src, int src_len, |
| 122 | + const uint8_t* assoc_data, int assoc_len, |
| 123 | + uint16_t nonce_counter, |
| 124 | + uint8_t dest_hash, uint8_t src_hash) { |
| 125 | + if (src_len <= 0) return 0; |
| 126 | + |
| 127 | + // Write nonce to output |
| 128 | + dest[0] = (uint8_t)(nonce_counter >> 8); |
| 129 | + dest[1] = (uint8_t)(nonce_counter & 0xFF); |
| 130 | + |
| 131 | + // Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash) |
| 132 | + // Including hashes makes the key direction-dependent, preventing keystream reuse |
| 133 | + // when Alice->Bob and Bob->Alice use the same nonce (255/256 peer pairs). |
| 134 | + uint8_t msg_key[32]; |
| 135 | + { |
| 136 | + uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { dest[0], dest[1], dest_hash, src_hash }; |
| 137 | + SHA256 sha; |
| 138 | + sha.resetHMAC(shared_secret, PUB_KEY_SIZE); |
| 139 | + sha.update(kdf_input, sizeof(kdf_input)); |
| 140 | + sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32); |
| 141 | + } |
| 142 | + |
| 143 | + // Build 12-byte IV from on-wire fields |
| 144 | + uint8_t iv[12]; |
| 145 | + iv[0] = dest[0]; // nonce_hi |
| 146 | + iv[1] = dest[1]; // nonce_lo |
| 147 | + iv[2] = dest_hash; |
| 148 | + iv[3] = src_hash; |
| 149 | + memset(&iv[4], 0, 8); |
| 150 | + |
| 151 | + ChaChaPoly cipher; |
| 152 | + cipher.setKey(msg_key, 32); |
| 153 | + cipher.setIV(iv, 12); |
| 154 | + cipher.addAuthData(assoc_data, assoc_len); |
| 155 | + cipher.encrypt(dest + AEAD_NONCE_SIZE, src, src_len); |
| 156 | + cipher.computeTag(dest + AEAD_NONCE_SIZE + src_len, AEAD_TAG_SIZE); |
| 157 | + cipher.clear(); |
| 158 | + memset(msg_key, 0, 32); |
| 159 | + |
| 160 | + return AEAD_NONCE_SIZE + src_len + AEAD_TAG_SIZE; |
| 161 | +} |
| 162 | + |
| 163 | +int Utils::aeadDecrypt(const uint8_t* shared_secret, |
| 164 | + uint8_t* dest, |
| 165 | + const uint8_t* src, int src_len, |
| 166 | + const uint8_t* assoc_data, int assoc_len, |
| 167 | + uint8_t dest_hash, uint8_t src_hash) { |
| 168 | + // Minimum: nonce(2) + at least 1 byte ciphertext + tag(4) |
| 169 | + if (src_len < AEAD_NONCE_SIZE + 1 + AEAD_TAG_SIZE) return 0; |
| 170 | + |
| 171 | + int ct_len = src_len - AEAD_NONCE_SIZE - AEAD_TAG_SIZE; |
| 172 | + |
| 173 | + // Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash) |
| 174 | + uint8_t msg_key[32]; |
| 175 | + { |
| 176 | + uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { src[0], src[1], dest_hash, src_hash }; |
| 177 | + SHA256 sha; |
| 178 | + sha.resetHMAC(shared_secret, PUB_KEY_SIZE); |
| 179 | + sha.update(kdf_input, sizeof(kdf_input)); |
| 180 | + sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32); |
| 181 | + } |
| 182 | + |
| 183 | + // Build 12-byte IV from on-wire fields |
| 184 | + uint8_t iv[12]; |
| 185 | + iv[0] = src[0]; // nonce_hi |
| 186 | + iv[1] = src[1]; // nonce_lo |
| 187 | + iv[2] = dest_hash; |
| 188 | + iv[3] = src_hash; |
| 189 | + memset(&iv[4], 0, 8); |
| 190 | + |
| 191 | + ChaChaPoly cipher; |
| 192 | + cipher.setKey(msg_key, 32); |
| 193 | + cipher.setIV(iv, 12); |
| 194 | + cipher.addAuthData(assoc_data, assoc_len); |
| 195 | + cipher.decrypt(dest, src + AEAD_NONCE_SIZE, ct_len); |
| 196 | + |
| 197 | + bool valid = cipher.checkTag(src + AEAD_NONCE_SIZE + ct_len, AEAD_TAG_SIZE); |
| 198 | + cipher.clear(); |
| 199 | + memset(msg_key, 0, 32); |
| 200 | + if (!valid) memset(dest, 0, ct_len); |
| 201 | + |
| 202 | + return valid ? ct_len : 0; |
| 203 | +} |
| 204 | + |
90 | 205 | static const char hex_chars[] = "0123456789ABCDEF"; |
91 | 206 |
|
92 | 207 | void Utils::toHex(char* dest, const uint8_t* src, size_t len) { |
|
0 commit comments