Files
Matias DendaandThomas Göttgens d05fbec64c Add AEAD (AES-CCM) authenticated encryption for PSK channels (#9749)
* Add AEAD (AES-CCM) authenticated encryption for PSK channels

Extend PSK channel encryption with optional AES-CCM authenticated
encryption (use_aead flag in ChannelSettings). When enabled, messages
include a 12-byte authentication tag that prevents forgery, bit-flipping,
and injection attacks by anyone with the channel PSK.

Changes:
- Add encryptPacketCCM/decryptPacketCCM to CryptoEngine with key
  promotion (16-byte keys zero-padded to 32 for AESSmall256 compat)
- Move AES-CCM primitives (aes-ccm.h/cpp, aesSetKey, aesEncrypt)
  outside PKI guard so they're available unconditionally
- Add isAEADEnabled() to Channels with hash differentiation (XOR 0xAE)
- Add AEAD encrypt/decrypt branches in Router perhapsEncode/perhapsDecode
  with no CTR fallback on AEAD channels
- Add use_aead field to channel.pb.h (bool, tag 8)
- Add MESHTASTIC_AEAD_OVERHEAD constant to RadioInterface.h
- Add comprehensive test suite: round-trip (AES-128/256), tamper
  detection (ciphertext, tag, sweep), wrong PSK, wrong sender,
  packet-too-small, deterministic output verification

Addresses firmware#4030.

* Apply clang-format to match project style

* Guard AEAD path against empty PSK and check encrypt return value

- Add early return in encryptPacketCCM/decryptPacketCCM when
  psk.length == 0, preventing null dereference in aesSetKey
- Check encryptPacketCCM return value in Router::perhapsEncode
  (both PKI and non-PKI paths), returning BAD_REQUEST on failure
  instead of silently transmitting corrupt packets
- Add unit test for empty PSK (encrypt and decrypt must return
  false without crashing)

* Use true AES-128 for 16-byte PSKs instead of promoting to AES-256

aesSetKey now dispatches based on key length: 16 bytes creates
AESSmall128, 32 bytes creates AESSmall256. The aes member type
changes from AESSmall256 to BlockCipher (polymorphic base class).

This removes the unnecessary key promotion that added two extra
AES rounds (14 vs 12) with no security benefit since the entropy
stays at 128 bits for 16-byte keys.

encryptPacketCCM/decryptPacketCCM now pass psk.length directly
to aes_ccm_ae/aes_ccm_ad instead of promoting to 32.

New tests: ECB AES-128 with NIST vectors, AEAD test verifying
AES-128 and AES-256 produce different ciphertexts with same key
material and cross-key decryption fails.

* Reject the invalid-key sentinel in the AEAD paths

CryptoKey documents length == -1 as "invalid key - do not use", but the
AEAD guards only tested for 0. Since length is int8_t and the aes_ccm_*
key length parameter is size_t, a -1 would widen into a huge unsigned
length and be handed to the cipher instead of being rejected.

Both callers in Router.cpp are gated on a non-negative channel hash, and
generateHash() already returns -1 exactly when getKey() yields an invalid
key, so the sentinel cannot reach these functions today. Guard against it
anyway rather than relying on callers to keep that invariant.

* Tie MESHTASTIC_AEAD_OVERHEAD to CryptoEngine::AEAD_TAG_SIZE

The packet-size boundary checks in perhapsEncode/perhapsDecode budget for
MESHTASTIC_AEAD_OVERHEAD, but the tag actually written is AEAD_TAG_SIZE.
Nothing tied the two together, so changing one would have silently produced
oversized packets or truncated payloads. Assert they match instead of
coupling RadioInterface.h to CryptoEngine.

Also trims the sentinel comment to the two-line limit in AGENTS.md.

* Add RFC 3610 known-answer vectors and widen the tamper sweep

Packet Vectors #1, #2 and #7 pin aes_ccm_ae()/aes_ccm_ad() to published data
rather than to their own output, covering M=8 and M=10, a trailing partial block
in every case, and rejection of a modified AAD. Test 1 in test_AES_CCM_AEAD is
relabelled as the smoke test it actually is.

The per-byte tamper loop now walks the whole buffer including the tag, instead of
only the first four ciphertext bytes.

* Cover the second nonce input and tighten the AEAD test buffers

Test 10 only ever varied fromNode, leaving packetId — the other half of the
nonce — unexercised. It now checks each one wrong on its own, both wrong, and
both right, so the negative assertions cannot pass vacuously.

The undersized-packet test wrote into a one-byte buffer and only survived
because decryptPacketCCM() returns before touching it; size it for the whole
input so a regressed length guard fails an assertion instead of the stack.
Also assert makePsk() cannot overrun CryptoKey::bytes.

* Rewrite Unicode dashes to ASCII in AEAD comments

The ascii-dash formatter that landed in develop rewrites U+2014/U+2013 to
an ASCII hyphen. Three files on this branch still carried em dashes in
comments, so Trunk Check went red once develop was merged in. Comments
only, no code change.

* Authenticate sender and destination IDs as AEAD associated data

The nonce binds the sender and the packet id, but nothing bound the
destination, so `to` could be rewritten in flight and the tag would still
validate. Pass `from || to` as associated data to aes_ccm_ae/aes_ccm_ad so
a redirected packet fails authentication.

The hop fields stay out of the AAD on purpose: relays legitimately rewrite
hop_limit, hop_start, relay_node and next_hop.

Adds a sub-test covering redirection to another node and promotion of a
unicast to a broadcast; both must be rejected, and the unmodified
destination must still round-trip.

This changes the on-the-wire format for AEAD packets. Nothing ships with
use_aead yet, so there is no deployed traffic to stay compatible with.

* fix(crypto): repair EXCLUDE_PKI builds and guard AEAD channel config

aes-ccm.cpp is compiled in every build now and calls CryptoEngine::aesSetKey
and CryptoEngine::aesEncrypt, whose definitions were still inside the
!(MESHTASTIC_EXCLUDE_PKI) block in CryptoEngine.cpp, so MESHTASTIC_EXCLUDE_PKI=1
failed at the link step. Move both definitions outside the guard, and move the
pending-public-key declarations back inside it next to the fields they read.

fixupChannel() clears use_aead on a channel that resolves to no key material.
That combination kept a valid-looking channel hash while every encode returned
BAD_REQUEST and every decode dropped, with nothing in the config to show why.

encryptPacketCCM/decryptPacketCCM are virtual, so a platform engine can back
them with hardware CCM the way it already overrides encryptAESCtr.

perhapsEncode() carries one copy of the AEAD/CTR branch instead of an identical
copy in each arm of the MESHTASTIC_EXCLUDE_PKI ifdef.

Tests: three use_aead cases in test_channel_keys covering the hash split, the
no-key clear, and a secondary that borrows the primary's key.

* fix(crypto): move CryptoEngine::hash out of the PKI guard

hash() is plain SHA256, and PortduinoGlue calls it unguarded to derive a MAC address from the CH341 serial, so MESHTASTIC_EXCLUDE_PKI=1 failed to compile. With this and the previous commit that build links clean.

* fix(channels): resolve primaryIndex before hashing in onConfigChanged

A keyless secondary resolves its key through primaryIndex, so fixing up channels in the same pass that finds the primary hashed the early slots against the previous one and cleared their use_aead against a key they do in fact inherit. Split the pass, and re-run the fixups in the no-primary restore path, which moves the primary after the fact. Also splits the thirteen AES-CCM AEAD scenarios into separate test functions so a Unity failure names the one that broke.

* chore(crypto): trim the AEAD maintainer commits

Shortens three comments that outgrew the one-to-two line house rule, drops a truncated sentence and the braces around a single return in perhapsEncode(), and removes a channel test that the moved-primary regression test already covers. No behaviour change.

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
2026-09-14 06:32:35 +00:00

848 lines
36 KiB
C++

// trunk-ignore-all(gitleaks): These are dummy values. Not real secrets.
#include "CryptoEngine.h"
#include "TestUtil.h"
#include "aes-ccm.h"
#include <XEdDSA.h>
#include <cassert>
#include <unity.h>
void HexToBytes(uint8_t *result, const std::string hex, size_t len = 0)
{
if (len) {
memset(result, 0, len);
}
for (unsigned int i = 0; i < hex.length(); i += 2) {
std::string byteString = hex.substr(i, 2);
result[i / 2] = (uint8_t)strtol(byteString.c_str(), NULL, 16);
}
return;
}
void setUp(void)
{
// set stuff up here
}
void tearDown(void)
{
// clean stuff up here
}
void test_SHA256(void)
{
uint8_t expected[32];
uint8_t hash[32] = {0};
HexToBytes(expected, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
crypto->hash(hash, 0);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
HexToBytes(hash, "d3", 32);
HexToBytes(expected, "28969cdfa74a12c82f3bad960b0b000aca2ac329deea5c2328ebc6f2ba9802c1");
crypto->hash(hash, 1);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
HexToBytes(hash, "11af", 32);
HexToBytes(expected, "5ca7133fa735326081558ac312c620eeca9970d1e70a4b95533d956f072d1f98");
crypto->hash(hash, 2);
TEST_ASSERT_EQUAL_MEMORY(hash, expected, 32);
}
void test_SHA256_large_input(void)
{
uint8_t hash[300] = {0};
uint8_t expected[32];
HexToBytes(expected, "d13d4a8b3b8add19b5970157f09d00c12cbda4fed4d74d8493156523f7069b66");
crypto->hash(hash, sizeof(hash));
TEST_ASSERT_EQUAL_MEMORY(hash, expected, sizeof(expected));
}
void test_ECB_AES256(void)
{
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf
uint8_t key[32] = {0};
uint8_t plain[16] = {0};
uint8_t result[16] = {0};
uint8_t expected[16] = {0};
HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A");
HexToBytes(expected, "F3EED1BDB5D2A03C064B5A7E3DB181F8");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51");
HexToBytes(expected, "591CCB10D410ED26DC5BA74A31362870");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
HexToBytes(plain, "30C81C46A35CE411E5FBC1191A0A52EF");
HexToBytes(expected, "B6ED21B99CA6F4F9F153E7B1BEAFED1D");
crypto->aesSetKey(key, 32);
crypto->aesEncrypt(plain, result); // Does 16 bytes at a time
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
}
void test_ECB_AES128(void)
{
// https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/AES_ECB.pdf
uint8_t key[16] = {0};
uint8_t plain[16] = {0};
uint8_t result[16] = {0};
uint8_t expected[16] = {0};
HexToBytes(key, "2B7E151628AED2A6ABF7158809CF4F3C");
HexToBytes(plain, "6BC1BEE22E409F96E93D7E117393172A");
HexToBytes(expected, "3AD77BB40D7A3660A89ECAF32466EF97");
crypto->aesSetKey(key, 16);
crypto->aesEncrypt(plain, result);
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
HexToBytes(plain, "AE2D8A571E03AC9C9EB76FAC45AF8E51");
HexToBytes(expected, "F5D3D58503B9699DE785895A96FDBAAF");
crypto->aesSetKey(key, 16);
crypto->aesEncrypt(plain, result);
TEST_ASSERT_EQUAL_MEMORY(expected, result, 16);
}
void test_DH25519(void)
{
// test vectors from wycheproof x25519
// https://github.com/C2SP/wycheproof/blob/master/testvectors/x25519_test.json
uint8_t private_key[32];
uint8_t public_key[32];
uint8_t expected_shared[32];
HexToBytes(public_key, "504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829");
HexToBytes(private_key, "c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475");
HexToBytes(expected_shared, "436a2c040cf45fea9b29a0cb81b1f41458f863d0d61b453d0a982720d6d61320");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
HexToBytes(public_key, "63aa40c6e38346c5caf23a6df0a5e6c80889a08647e551b3563449befcfc9733");
HexToBytes(private_key, "d85d8c061a50804ac488ad774ac716c3f5ba714b2712e048491379a500211958");
HexToBytes(expected_shared, "279df67a7c4611db4708a0e8282b195e5ac0ed6f4b2f292c6fbd0acac30d1332");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
HexToBytes(public_key, "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f");
HexToBytes(private_key, "18630f93598637c35da623a74559cf944374a559114c7937811041fc8605564a");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(!crypto->setDHPublicKey(public_key)); // Weak public key results in 0 shared key
HexToBytes(public_key, "f7e13a1a067d2f4e1061bf9936fde5be6b0c2494a8f809cbac7f290ef719e91c");
HexToBytes(private_key, "10300724f3bea134eb1575245ef26ff9b8ccd59849cd98ce1a59002fe1d5986c");
HexToBytes(expected_shared, "24becd5dfed9e9289ba2e15b82b0d54f8e9aacb72f5e4248c58d8d74b451ce76");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->setDHPublicKey(public_key));
crypto->hash(crypto->shared_key, 32);
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 32);
}
void test_PKC(void)
{
uint8_t private_key[32];
meshtastic_NodeInfoLite_public_key_t public_key;
uint8_t expected_shared[32];
uint8_t expected_decrypted[32];
uint8_t radioBytes[128] __attribute__((__aligned__));
uint8_t decrypted[128] __attribute__((__aligned__));
uint8_t expected_nonce[16];
uint32_t fromNode = 0x0929;
uint64_t packetNum = 0x13b2d662;
HexToBytes(public_key.bytes, "db18fc50eea47f00251cb784819a3cf5fc361882597f589f0d7ff820e8064457");
public_key.size = 32;
HexToBytes(private_key, "a00330633e63522f8a4d81ec6d9d1e6617f6c8ffd3a4c698229537d44e522277");
HexToBytes(expected_shared, "777b1545c9d6f9a2");
HexToBytes(expected_decrypted, "08011204746573744800");
HexToBytes(radioBytes, "8c646d7a2909000062d6b2136b00000040df24abfcc30a17a3d9046726099e796a1c036a792b");
HexToBytes(expected_nonce, "62d6b213036a792b2909000000");
crypto->setDHPrivateKey(private_key);
TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, radioBytes + 16, decrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13);
TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10);
uint32_t toNode = 0; // Only impacts logging
uint8_t encrypted[128] __attribute__((__aligned__));
TEST_ASSERT(crypto->encryptCurve25519(toNode, fromNode, public_key, packetNum, 10, decrypted, encrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
// The extraNonce is random, so skip checking the nonce and encrypted output here
// Copy the nonce to check it after encryption
memcpy(expected_nonce, crypto->nonce, 16);
// Decrypt the re-encrypted bytes and check they are the same as what we expect
TEST_ASSERT(crypto->decryptCurve25519(fromNode, public_key, packetNum, 22, encrypted, decrypted));
TEST_ASSERT_EQUAL_MEMORY(expected_shared, crypto->shared_key, 8);
TEST_ASSERT_EQUAL_MEMORY(expected_nonce, crypto->nonce, 13);
TEST_ASSERT_EQUAL_MEMORY(expected_decrypted, decrypted, 10);
}
void test_XEdDSA(void)
{
uint8_t private_key[32];
uint8_t x_public_key[32];
uint8_t ed_private_key[32];
uint8_t ed_public_key[32];
uint8_t ed_public_key2[32];
uint8_t message[] = "This is a test!";
uint8_t message2[] = "This is a test.";
uint8_t signature[64];
uint32_t fromNode = 0x1234;
uint32_t packetId = 0xDEADBEEF;
uint32_t portnum = 1;
for (int times = 0; times < 10; times++) {
printf("Start of time %u\n", times);
crypto->generateKeyPair(x_public_key, private_key);
XEdDSA::priv_curve_to_ed_keys(private_key, ed_private_key, ed_public_key);
crypto->curve_to_ed_pub(x_public_key, ed_public_key2);
TEST_ASSERT_EQUAL_MEMORY(ed_public_key, ed_public_key2, 32);
// Sign and verify with metadata
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), signature));
TEST_ASSERT(crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum, message, sizeof(message), signature));
// Different payload fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum, message2, sizeof(message2), signature));
// Different fromNode fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode + 1, packetId, portnum, message, sizeof(message), signature));
// Different packetId fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId + 1, portnum, message, sizeof(message), signature));
// Different portnum fails
TEST_ASSERT_FALSE(
crypto->xeddsa_verify(x_public_key, fromNode, packetId, portnum + 1, message, sizeof(message), signature));
}
}
// A signature only verifies under the signer's own key; a different key (or an all-zero key) fails.
void test_XEdDSA_cross_key_reject(void)
{
uint8_t pubA[32], privA[32];
uint8_t pubB[32], privB[32];
uint8_t signature[64];
uint8_t message[] = "cross-key check";
uint32_t fromNode = 0x4242, packetId = 0xABCD1234, portnum = 7;
crypto->generateKeyPair(pubA, privA); // engine now holds key A
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), signature));
crypto->generateKeyPair(pubB, privB); // unrelated key pair
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), signature));
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pubB, fromNode, packetId, portnum, message, sizeof(message), signature));
uint8_t zeroKey[32] = {0};
TEST_ASSERT_FALSE(crypto->xeddsa_verify(zeroKey, fromNode, packetId, portnum, message, sizeof(message), signature));
}
// Signing with an unset (all-zero) private key must fail rather than emit a bogus signature.
void test_XEdDSA_empty_key_sign_fails(void)
{
CryptoEngine fresh; // freshly constructed: xeddsa_private_key is all zero
uint8_t signature[64];
uint8_t message[] = "no key";
TEST_ASSERT_FALSE(fresh.xeddsa_sign(0x1, 0x2, 0x3, message, sizeof(message), signature));
}
// curve_to_ed_pub caches the last converted key; verifying A, then B, then A must stay correct.
void test_XEdDSA_curve_to_ed_cache(void)
{
uint8_t pubA[32], privA[32], sigA[64];
uint8_t pubB[32], privB[32], sigB[64];
uint8_t message[] = "cache check";
uint32_t fromNode = 0x11, packetId = 0x22, portnum = 3;
crypto->generateKeyPair(pubA, privA);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sigA));
crypto->generateKeyPair(pubB, privB);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sigB));
// Interleave keys to exercise both cache hits and cache invalidation.
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigA));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubB, fromNode, packetId, portnum, message, sizeof(message), sigB));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigA));
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pubA, fromNode, packetId, portnum, message, sizeof(message), sigB));
}
// A payload at the maximum signable size (DATA_PAYLOAD_LEN - signature) round-trips and detects tampering.
void test_XEdDSA_max_payload(void)
{
const size_t len = meshtastic_Constants_DATA_PAYLOAD_LEN - XEDDSA_SIGNATURE_SIZE;
uint8_t payload[meshtastic_Constants_DATA_PAYLOAD_LEN];
for (size_t i = 0; i < len; i++)
payload[i] = (uint8_t)(i * 7 + 1);
uint8_t pub[32], priv[32], signature[64];
crypto->generateKeyPair(pub, priv);
uint32_t fromNode = 0xFEED, packetId = 0xC0DE, portnum = 1;
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, payload, len, signature));
TEST_ASSERT(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, payload, len, signature));
payload[0] ^= 0x01;
TEST_ASSERT_FALSE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, payload, len, signature));
}
// XEdDSA is a randomized (hedged) scheme: the nonce mixes in Z, caller-supplied randomness
// (Signal spec; meshtastic/Crypto#3). CryptoEngine::xeddsa_sign seeds Z from the hardware RNG, so
// signing the same message twice yields *different* signatures that both verify. This pins that
// the randomization is actually wired through end to end - if signing regresses to deterministic
// (Z dropped by the library, or xeddsa_sign stops seeding entropy), the inequality assertion fails.
void test_XEdDSA_repeated_sign_is_randomized(void)
{
uint8_t pub[32], priv[32], sig1[64], sig2[64];
uint8_t message[] = "same message";
uint32_t fromNode = 0x9, packetId = 0x9, portnum = 9;
crypto->generateKeyPair(pub, priv);
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sig1));
TEST_ASSERT(crypto->xeddsa_sign(fromNode, packetId, portnum, message, sizeof(message), sig2));
TEST_ASSERT_TRUE_MESSAGE(memcmp(sig1, sig2, sizeof(sig1)) != 0,
"signatures must differ - XEdDSA Z randomization is not wired through");
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, message, sizeof(message), sig1));
TEST_ASSERT_TRUE(crypto->xeddsa_verify(pub, fromNode, packetId, portnum, message, sizeof(message), sig2));
}
void test_AES_CTR(void)
{
uint8_t expected[32];
uint8_t plain[32];
uint8_t nonce[32];
CryptoKey k;
// vectors from https://www.rfc-editor.org/rfc/rfc3686#section-6
k.length = 32;
HexToBytes(k.bytes, "776BEFF2851DB06F4C8A0542C8696F6C6A81AF1EEC96B4D37FC1D689E6C1C104");
HexToBytes(nonce, "00000060DB5672C97AA8F0B200000001");
HexToBytes(expected, "145AD01DBF824EC7560863DC71E3E0C0");
memcpy(plain, "Single block msg", 16);
crypto->encryptAESCtr(k, nonce, 16, plain);
TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16);
k.length = 16;
memcpy(plain, "Single block msg", 16);
HexToBytes(k.bytes, "AE6852F8121067CC4BF7A5765577F39E");
HexToBytes(nonce, "00000030000000000000000000000001");
HexToBytes(expected, "E4095D4FB7A7B3792D6175A3261311B8");
crypto->encryptAESCtr(k, nonce, 16, plain);
TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16);
}
void test_AES_CCM_partial_block_bounds(void)
{
// aes_ccm_encr() used to write a whole 16-byte AES block at the output before XOR-ing,
// so a trailing partial block scribbled up to 15 bytes past what the caller allocated.
const uint8_t guard = 0xA5;
const size_t guardLen = 16;
const size_t lengths[] = {5, 20}; // pure partial block, and one full block plus a partial one
uint8_t key[32];
uint8_t nonce[13];
uint8_t auth[8];
HexToBytes(key, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
HexToBytes(nonce, "000102030405060708090A0B0C");
for (size_t n = 0; n < sizeof(lengths) / sizeof(lengths[0]); n++) {
const size_t len = lengths[n];
uint8_t plain[32];
uint8_t crypt[32 + guardLen];
uint8_t decrypted[32 + guardLen];
for (size_t i = 0; i < len; i++)
plain[i] = (uint8_t)i;
memset(crypt + len, guard, guardLen);
memset(decrypted + len, guard, guardLen);
TEST_ASSERT_EQUAL(0, aes_ccm_ae(key, sizeof(key), nonce, sizeof(auth), plain, len, nullptr, 0, crypt, auth));
for (size_t i = 0; i < guardLen; i++)
TEST_ASSERT_EQUAL_UINT8(guard, crypt[len + i]);
TEST_ASSERT_TRUE(aes_ccm_ad(key, sizeof(key), nonce, sizeof(auth), crypt, len, nullptr, 0, auth, decrypted));
for (size_t i = 0; i < guardLen; i++)
TEST_ASSERT_EQUAL_UINT8(guard, decrypted[len + i]);
TEST_ASSERT_EQUAL_MEMORY(plain, decrypted, len);
}
}
void test_AES_CCM_rfc3610(void)
{
// Known-answer vectors from RFC 3610 section 8. They all use L=2, which is what
// aes_ccm_ae()/aes_ccm_ad() hardcode, and each ends in a partial block.
struct CcmVector {
const char *key;
const char *nonce;
const char *aad;
const char *plain;
const char *crypt;
const char *tag;
};
const CcmVector vectors[] = {
// Packet Vector #1, M=8
{"C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF", "00000003020100A0A1A2A3A4A5", "0001020304050607",
"08090A0B0C0D0E0F101112131415161718191A1B1C1D1E", "588C979A61C663D2F066D0C2C0F989806D5F6B61DAC384", "17E8D12CFDF926E0"},
// Packet Vector #2, M=8
{"C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF", "00000004030201A0A1A2A3A4A5", "0001020304050607",
"08090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "72C91A36E135F8CF291CA894085C87E3CC15C439C9E43A3B",
"A091D56E10400916"},
// Packet Vector #7, M=10
{"C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF", "00000009080706A0A1A2A3A4A5", "0001020304050607",
"08090A0B0C0D0E0F101112131415161718191A1B1C1D1E", "0135D1B2C95F41D5D1D4FEC185D166B8094E999DFED96C",
"048C56602C97ACBB7490"},
};
for (size_t v = 0; v < sizeof(vectors) / sizeof(vectors[0]); v++) {
const CcmVector &vec = vectors[v];
const size_t plainLen = strlen(vec.plain) / 2;
const size_t aadLen = strlen(vec.aad) / 2;
const size_t tagLen = strlen(vec.tag) / 2;
uint8_t key[16], nonce[13], aad[8];
uint8_t plain[32], expectedCrypt[32], expectedTag[16];
uint8_t crypt[32], tag[16], decrypted[32];
HexToBytes(key, vec.key);
HexToBytes(nonce, vec.nonce);
HexToBytes(aad, vec.aad);
HexToBytes(plain, vec.plain);
HexToBytes(expectedCrypt, vec.crypt);
HexToBytes(expectedTag, vec.tag);
TEST_ASSERT_EQUAL(0, aes_ccm_ae(key, sizeof(key), nonce, tagLen, plain, plainLen, aad, aadLen, crypt, tag));
TEST_ASSERT_EQUAL_MEMORY(expectedCrypt, crypt, plainLen);
TEST_ASSERT_EQUAL_MEMORY(expectedTag, tag, tagLen);
TEST_ASSERT_TRUE(aes_ccm_ad(key, sizeof(key), nonce, tagLen, crypt, plainLen, aad, aadLen, tag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(plain, decrypted, plainLen);
// The AAD is authenticated but not encrypted: corrupting it must fail the tag check
aad[0] ^= 0x01;
TEST_ASSERT_FALSE(aes_ccm_ad(key, sizeof(key), nonce, tagLen, crypt, plainLen, aad, aadLen, tag, decrypted));
}
}
// Helper to create a zero-initialized CryptoKey (matching Channels::getKey() behavior)
static CryptoKey makePsk(const std::string &hex)
{
CryptoKey k;
assert(hex.length() / 2 <= sizeof(k.bytes));
memset(k.bytes, 0, sizeof(k.bytes));
k.length = hex.length() / 2;
HexToBytes(k.bytes, hex);
return k;
}
void test_AES_CCM_AEAD_smoke(void)
{
// Smoke test - encryption changes the payload and produces a tag
// (the known-answer coverage lives in test_AES_CCM_rfc3610)
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0x12345678;
uint32_t toNode = 0x0000AAAA;
uint64_t packetId = 0xAABBCCDD;
uint8_t plaintext[10];
HexToBytes(plaintext, "08011204746573744800");
uint8_t ciphertextWithTag[10 + CryptoEngine::AEAD_TAG_SIZE];
memset(ciphertextWithTag, 0, sizeof(ciphertextWithTag));
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 10, plaintext, ciphertextWithTag));
// Ciphertext should differ from plaintext
TEST_ASSERT_FALSE(memcmp(plaintext, ciphertextWithTag, 10) == 0);
// Tag bytes (last 12) should not all be zero
bool tagAllZero = true;
for (size_t i = 0; i < CryptoEngine::AEAD_TAG_SIZE; i++) {
if (ciphertextWithTag[10 + i] != 0) {
tagAllZero = false;
break;
}
}
TEST_ASSERT_FALSE(tagAllZero);
}
void test_AES_CCM_AEAD_roundtrip_aes256(void)
{
// Round-trip encrypt → decrypt → compare (AES-256)
CryptoKey psk = makePsk("603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
uint32_t fromNode = 0xDEADBEEF;
uint32_t toNode = 0xFFFFFFFF;
uint64_t packetId = 0x0102030405060708;
const char *msg = "Hello Meshtastic AEAD!";
size_t msgLen = strlen(msg);
uint8_t ciphertextWithTag[64];
memset(ciphertextWithTag, 0, sizeof(ciphertextWithTag));
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, msgLen, (const uint8_t *)msg, ciphertextWithTag));
uint8_t decrypted[64];
memset(decrypted, 0, sizeof(decrypted));
size_t totalBytes = msgLen + CryptoEngine::AEAD_TAG_SIZE;
TEST_ASSERT_TRUE(crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, totalBytes, ciphertextWithTag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(msg, decrypted, msgLen);
}
void test_AES_CCM_AEAD_rejects_tampering(void)
{
// Tampered ciphertext - flip a bit, verify rejection
{
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0xABCD1234;
uint32_t toNode = 0x00000001;
uint64_t packetId = 0x11223344;
uint8_t plaintext[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint8_t ciphertextWithTag[8 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 8, plaintext, ciphertextWithTag));
// Flip a bit in the ciphertext portion
ciphertextWithTag[3] ^= 0x01;
uint8_t decrypted[8];
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
}
// Tampered auth tag - modify tag, verify rejection
{
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0xABCD1234;
uint32_t toNode = 0x87654321;
uint64_t packetId = 0x55667788;
uint8_t plaintext[16] = {0};
for (int i = 0; i < 16; i++)
plaintext[i] = (uint8_t)i;
uint8_t ciphertextWithTag[16 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 16, plaintext, ciphertextWithTag));
// Corrupt the auth tag (last byte)
ciphertextWithTag[16 + CryptoEngine::AEAD_TAG_SIZE - 1] ^= 0xFF;
uint8_t decrypted[16];
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 16 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
}
}
void test_AES_CCM_AEAD_rejects_undersized(void)
{
// Packet too small for AEAD - totalBytes <= AEAD_TAG_SIZE
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint8_t dummy[CryptoEngine::AEAD_TAG_SIZE] = {0};
// Sized for the whole input so a regressed length guard fails the assertion below
// instead of corrupting the stack on its way out.
uint8_t out[CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, 0x1234, 0x4321, 0x5678, CryptoEngine::AEAD_TAG_SIZE, dummy, out));
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, 0x1234, 0x4321, 0x5678, 0, dummy, out));
}
void test_AES_CCM_AEAD_rejects_wrong_psk(void)
{
// Wrong PSK - decrypt with different key, verify rejection
CryptoKey pskA = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
CryptoKey pskB = makePsk("00112233445566778899aabbccddeeff");
uint32_t fromNode = 0x99887766;
uint32_t toNode = 0x13579BDF;
uint64_t packetId = 0xDEADFACE;
uint8_t plaintext[12] = "Hello World";
uint8_t ciphertextWithTag[12 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(pskA, fromNode, toNode, packetId, 12, plaintext, ciphertextWithTag));
// Attempt decryption with wrong key
uint8_t decrypted[12];
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(pskB, fromNode, toNode, packetId, 12 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
}
void test_AES_CCM_AEAD_roundtrip_aes128(void)
{
// Round-trip with AES-128 PSK (16-byte key, true AES-128-CCM)
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0x42424242;
uint32_t toNode = 0x2468ACE0;
uint64_t packetId = 0xBEEF1234;
uint8_t plaintext[20] = "AES128 round trip!";
uint8_t ciphertextWithTag[20 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 20, plaintext, ciphertextWithTag));
uint8_t decrypted[20];
TEST_ASSERT_TRUE(crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 20 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(plaintext, decrypted, 20);
}
void test_AES_CCM_AEAD_tamper_sweep(void)
{
// AES-256-CCM round-trip + per-byte tamper detection
CryptoKey psk = makePsk("603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
uint32_t fromNode = 0x01020304;
uint32_t toNode = 0x0BADCAFE;
uint64_t packetId = 0x0A0B0C0D0E0F1011;
uint8_t plaintext[32];
for (int i = 0; i < 32; i++)
plaintext[i] = (uint8_t)(i * 7 + 3);
uint8_t ciphertextWithTag[32 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 32, plaintext, ciphertextWithTag));
// Valid decrypt
uint8_t decrypted[32];
TEST_ASSERT_TRUE(crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 32 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(plaintext, decrypted, 32);
// Flip a bit in every byte in turn, tag included, and verify each one is rejected
for (size_t i = 0; i < 32 + CryptoEngine::AEAD_TAG_SIZE; i++) {
uint8_t tampered[32 + CryptoEngine::AEAD_TAG_SIZE];
memcpy(tampered, ciphertextWithTag, sizeof(tampered));
tampered[i] ^= 0x80;
TEST_ASSERT_FALSE(
crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 32 + CryptoEngine::AEAD_TAG_SIZE, tampered, decrypted));
}
}
void test_AES_CCM_AEAD_is_deterministic(void)
{
// Deterministic - same inputs produce same output
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0xCAFEBABE;
uint32_t toNode = 0x5A5A5A5A;
uint64_t packetId = 0xFEEDFACE;
uint8_t plaintext[5] = {0xDE, 0xAD, 0xBE, 0xEF, 0x42};
uint8_t ct1[5 + CryptoEngine::AEAD_TAG_SIZE];
uint8_t ct2[5 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 5, plaintext, ct1));
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 5, plaintext, ct2));
TEST_ASSERT_EQUAL_MEMORY(ct1, ct2, 5 + CryptoEngine::AEAD_TAG_SIZE);
}
void test_AES_CCM_AEAD_binds_nonce_inputs(void)
{
// Wrong nonce input - the nonce derives from both fromNode and packetId,
// so each one on its own must be enough to make the tag check fail
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNodeA = 0x11111111;
uint32_t fromNodeB = 0x22222222;
uint32_t toNode = 0x77777777;
uint64_t packetIdA = 0xAAAABBBB;
uint64_t packetIdB = 0xCCCCDDDD;
uint8_t plaintext[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
uint8_t ciphertextWithTag[6 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNodeA, toNode, packetIdA, 6, plaintext, ciphertextWithTag));
uint8_t decrypted[6];
// Wrong fromNode, right packetId
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNodeB, toNode, packetIdA, 6 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// Right fromNode, wrong packetId
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNodeA, toNode, packetIdB, 6 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// Both wrong
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNodeB, toNode, packetIdB, 6 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// Both right still succeeds, so the assertions above are not passing for free
TEST_ASSERT_TRUE(crypto->decryptPacketCCM(psk, fromNodeA, toNode, packetIdA, 6 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(plaintext, decrypted, 6);
}
void test_AES_CCM_AEAD_rejects_invalid_psk(void)
{
// Empty PSK - must return false, not crash
CryptoKey emptyPsk;
memset(&emptyPsk, 0, sizeof(emptyPsk));
emptyPsk.length = 0;
uint32_t fromNode = 0xDEADBEEF;
uint32_t toNode = 0x0000BEEF;
uint64_t packetId = 0x12345678;
uint8_t plaintext[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint8_t ciphertextWithTag[8 + CryptoEngine::AEAD_TAG_SIZE];
uint8_t decrypted[8];
// Encrypt with empty PSK must fail gracefully
TEST_ASSERT_FALSE(crypto->encryptPacketCCM(emptyPsk, fromNode, toNode, packetId, 8, plaintext, ciphertextWithTag));
// Decrypt with empty PSK must fail gracefully
// (use dummy ciphertext since encrypt failed)
memset(ciphertextWithTag, 0xAA, sizeof(ciphertextWithTag));
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(emptyPsk, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// CryptoKey uses -1 as its "invalid key - do not use" sentinel, and it would widen
// into a huge unsigned length rather than be rejected. Both directions must refuse it.
CryptoKey invalidPsk;
memset(&invalidPsk, 0, sizeof(invalidPsk));
invalidPsk.length = -1;
TEST_ASSERT_FALSE(crypto->encryptPacketCCM(invalidPsk, fromNode, toNode, packetId, 8, plaintext, ciphertextWithTag));
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(invalidPsk, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
}
void test_AES_CCM_AEAD_key_size_distinction(void)
{
// AES-128 vs AES-256 produce different ciphertexts
// Verifies that 16-byte keys use true AES-128, not AES-256 with padding.
// Same 16 bytes of key material, but one is AES-128 (16 bytes)
// and the other is AES-256 (32 bytes, zero-padded).
CryptoKey psk128 = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
CryptoKey psk256;
memset(psk256.bytes, 0, sizeof(psk256.bytes));
HexToBytes(psk256.bytes, "d4f1bb3a20290759f0bcffabcf4e6901");
psk256.length = 32; // same first 16 bytes, but treated as AES-256
uint32_t fromNode = 0x55AA55AA;
uint32_t toNode = 0x33333333;
uint64_t packetId = 0x1234ABCD;
uint8_t plaintext[8] = {0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80};
uint8_t ct128[8 + CryptoEngine::AEAD_TAG_SIZE];
uint8_t ct256[8 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk128, fromNode, toNode, packetId, 8, plaintext, ct128));
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk256, fromNode, toNode, packetId, 8, plaintext, ct256));
// AES-128 and AES-256 with the same key material must produce different output
TEST_ASSERT_FALSE(memcmp(ct128, ct256, 8 + CryptoEngine::AEAD_TAG_SIZE) == 0);
// Both must still round-trip correctly
uint8_t dec128[8], dec256[8];
TEST_ASSERT_TRUE(
crypto->decryptPacketCCM(psk128, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE, ct128, dec128));
TEST_ASSERT_EQUAL_MEMORY(plaintext, dec128, 8);
TEST_ASSERT_TRUE(
crypto->decryptPacketCCM(psk256, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE, ct256, dec256));
TEST_ASSERT_EQUAL_MEMORY(plaintext, dec256, 8);
// Cross-key decryption must fail
TEST_ASSERT_FALSE(
crypto->decryptPacketCCM(psk256, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE, ct128, dec128));
TEST_ASSERT_FALSE(
crypto->decryptPacketCCM(psk128, fromNode, toNode, packetId, 8 + CryptoEngine::AEAD_TAG_SIZE, ct256, dec256));
}
void test_AES_CCM_AEAD_binds_destination(void)
{
// Rewritten destination - `to` is authenticated as associated data, so changing
// it in flight must fail the tag check even though the nonce is unaffected
CryptoKey psk = makePsk("d4f1bb3a20290759f0bcffabcf4e6901");
uint32_t fromNode = 0x0A0B0C0D;
uint32_t toNode = 0x00000042;
uint32_t otherNode = 0x00000043;
uint32_t broadcast = 0xFFFFFFFF;
uint64_t packetId = 0x99887766;
uint8_t plaintext[9] = {'t', 'o', '-', 'i', 's', '-', 'a', 'a', 'd'};
uint8_t ciphertextWithTag[9 + CryptoEngine::AEAD_TAG_SIZE];
uint8_t decrypted[9];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, toNode, packetId, 9, plaintext, ciphertextWithTag));
// Redirecting the packet to another node must be rejected
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNode, otherNode, packetId, 9 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// Promoting a unicast to a broadcast must be rejected too
TEST_ASSERT_FALSE(crypto->decryptPacketCCM(psk, fromNode, broadcast, packetId, 9 + CryptoEngine::AEAD_TAG_SIZE,
ciphertextWithTag, decrypted));
// The unmodified destination still round-trips, so the rejections above are not vacuous
TEST_ASSERT_TRUE(
crypto->decryptPacketCCM(psk, fromNode, toNode, packetId, 9 + CryptoEngine::AEAD_TAG_SIZE, ciphertextWithTag, decrypted));
TEST_ASSERT_EQUAL_MEMORY(plaintext, decrypted, 9);
// A different destination must also change the tag, not just be rejected on decrypt
uint8_t otherCiphertextWithTag[9 + CryptoEngine::AEAD_TAG_SIZE];
TEST_ASSERT_TRUE(crypto->encryptPacketCCM(psk, fromNode, otherNode, packetId, 9, plaintext, otherCiphertextWithTag));
TEST_ASSERT_FALSE(memcmp(ciphertextWithTag + 9, otherCiphertextWithTag + 9, CryptoEngine::AEAD_TAG_SIZE) == 0);
}
void setup()
{
// NOTE!!! Wait for >2 secs
// if board doesn't support software reset via Serial.DTR/RTS
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN(); // IMPORTANT LINE!
RUN_TEST(test_SHA256);
RUN_TEST(test_SHA256_large_input);
RUN_TEST(test_ECB_AES128);
RUN_TEST(test_ECB_AES256);
RUN_TEST(test_DH25519);
RUN_TEST(test_AES_CTR);
RUN_TEST(test_AES_CCM_partial_block_bounds);
RUN_TEST(test_AES_CCM_rfc3610);
RUN_TEST(test_PKC);
RUN_TEST(test_XEdDSA);
RUN_TEST(test_XEdDSA_cross_key_reject);
RUN_TEST(test_XEdDSA_empty_key_sign_fails);
RUN_TEST(test_XEdDSA_curve_to_ed_cache);
RUN_TEST(test_XEdDSA_max_payload);
RUN_TEST(test_XEdDSA_repeated_sign_is_randomized);
RUN_TEST(test_AES_CCM_AEAD_smoke);
RUN_TEST(test_AES_CCM_AEAD_roundtrip_aes256);
RUN_TEST(test_AES_CCM_AEAD_rejects_tampering);
RUN_TEST(test_AES_CCM_AEAD_rejects_undersized);
RUN_TEST(test_AES_CCM_AEAD_rejects_wrong_psk);
RUN_TEST(test_AES_CCM_AEAD_roundtrip_aes128);
RUN_TEST(test_AES_CCM_AEAD_tamper_sweep);
RUN_TEST(test_AES_CCM_AEAD_is_deterministic);
RUN_TEST(test_AES_CCM_AEAD_binds_nonce_inputs);
RUN_TEST(test_AES_CCM_AEAD_rejects_invalid_psk);
RUN_TEST(test_AES_CCM_AEAD_key_size_distinction);
RUN_TEST(test_AES_CCM_AEAD_binds_destination);
exit(UNITY_END()); // stop unit testing
}
void loop() {}