Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions components/microlink/src/ml_wg_mgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -672,8 +672,10 @@ static void disco_build_ping(microlink_t *ml, int peer_idx,

/* Encrypt with NaCl box: our disco private key -> peer's disco public key */
uint8_t ciphertext[46 + NACL_BOX_MACBYTES];
nacl_box(ciphertext, plaintext, sizeof(plaintext), nonce,
p->disco_key, ml->disco_private_key);
uint8_t disco_shared[32];
nacl_box_shared_get(disco_shared, p->disco_key, ml->disco_private_key);
nacl_box_afternm(ciphertext, plaintext, sizeof(plaintext), nonce,
disco_shared);

/* Build packet: magic(6) + our_disco_pubkey(32) + nonce(24) + ciphertext(62) = 124 bytes */
size_t pos = 0;
Expand Down Expand Up @@ -733,8 +735,10 @@ static void disco_build_pong(microlink_t *ml, int peer_idx,

/* Encrypt */
uint8_t ciphertext[32 + NACL_BOX_MACBYTES];
nacl_box(ciphertext, plaintext, sizeof(plaintext), nonce,
p->disco_key, ml->disco_private_key);
uint8_t disco_shared[32];
nacl_box_shared_get(disco_shared, p->disco_key, ml->disco_private_key);
nacl_box_afternm(ciphertext, plaintext, sizeof(plaintext), nonce,
disco_shared);

/* Build packet */
size_t pos = 0;
Expand Down Expand Up @@ -1038,8 +1042,13 @@ static void process_disco_packet(microlink_t *ml, const ml_rx_packet_t *pkt) {
uint8_t *plaintext = malloc(plaintext_len);
if (!plaintext) return;

if (nacl_box_open(plaintext, ciphertext, ciphertext_len, nonce,
sender_disco_key, ml->disco_private_key) != 0) {
/* Cached shared key: the X25519 inside nacl_box_open() costs ~30 ms and
* would otherwise run on every DISCO packet received. */
uint8_t disco_shared[32];
nacl_box_shared_get(disco_shared, sender_disco_key, ml->disco_private_key);

if (nacl_box_open_afternm(plaintext, ciphertext, ciphertext_len, nonce,
disco_shared) != 0) {
ESP_LOGW(TAG, "DISCO decrypt failed");
free(plaintext);
return;
Expand Down Expand Up @@ -1271,8 +1280,9 @@ static void disco_send_call_me_maybe(microlink_t *ml, int peer_idx) {
esp_fill_random(nonce, DISCO_NONCE_LEN);

uint8_t ciphertext[sizeof(plaintext) + NACL_BOX_MACBYTES];
nacl_box(ciphertext, plaintext, pt_len, nonce,
p->disco_key, ml->disco_private_key);
uint8_t disco_shared[32];
nacl_box_shared_get(disco_shared, p->disco_key, ml->disco_private_key);
nacl_box_afternm(ciphertext, plaintext, pt_len, nonce, disco_shared);

/* Build packet: magic(6) + disco_pubkey(32) + nonce(24) + ciphertext */
uint8_t pkt[256];
Expand Down
42 changes: 42 additions & 0 deletions components/microlink/src/nacl_box.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "nacl_box.h"
#include "x25519.h"
#include <string.h>
#include <stdbool.h>

/* Debug logging - set to 1 to enable */
#define NACL_BOX_DEBUG 0
Expand Down Expand Up @@ -540,6 +541,47 @@ int nacl_box_beforenm(uint8_t *shared_key,
return 0;
}

/* ---------------------------------------------------------------------------
* Shared-key cache
*
* Keyed on the peer public key; our secret key never changes at runtime, so a
* hit avoids the X25519 that would otherwise run on every DISCO packet.
* Entries are replaced round-robin - a tailnet has far fewer simultaneously
* active peers than slots, and a miss costs exactly what the old path did.
* ------------------------------------------------------------------------- */

#define NACL_SHARED_CACHE_SLOTS 24

static struct {
uint8_t peer_pk[32];
uint8_t shared_key[32];
bool valid;
} s_shared_cache[NACL_SHARED_CACHE_SLOTS];

static int s_shared_cache_next;

int nacl_box_shared_get(uint8_t *shared_key,
const uint8_t *peer_pk,
const uint8_t *our_sk) {
for (int i = 0; i < NACL_SHARED_CACHE_SLOTS; i++) {
if (s_shared_cache[i].valid &&
memcmp(s_shared_cache[i].peer_pk, peer_pk, 32) == 0) {
memcpy(shared_key, s_shared_cache[i].shared_key, 32);
return 0;
}
}

int ret = nacl_box_beforenm(shared_key, peer_pk, our_sk);
if (ret != 0) return ret;

int slot = s_shared_cache_next;
s_shared_cache_next = (s_shared_cache_next + 1) % NACL_SHARED_CACHE_SLOTS;
memcpy(s_shared_cache[slot].peer_pk, peer_pk, 32);
memcpy(s_shared_cache[slot].shared_key, shared_key, 32);
s_shared_cache[slot].valid = true;
return 0;
}

int nacl_box_afternm(uint8_t *ciphertext,
const uint8_t *plaintext, size_t plaintext_len,
const uint8_t *nonce,
Expand Down
19 changes: 19 additions & 0 deletions components/microlink/src/nacl_box.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,23 @@ int nacl_box_open_afternm(uint8_t *plaintext,
const uint8_t *nonce,
const uint8_t *shared_key);

/**
* @brief Shared key for (peer_pk, our_sk), memoised across calls.
*
* nacl_box_beforenm() is an X25519 scalar multiplication, which costs roughly
* 30 ms on an ESP32-S3. DISCO performs one per packet in each direction, so on
* a tailnet of any size the WireGuard manager task spends most of its time
* recomputing the same handful of shared secrets. Our own secret key is fixed
* for the node's lifetime, so the result depends only on the peer's public key
* and can be cached.
*
* @param shared_key Output 32-byte shared key
* @param peer_pk Peer's public key (32 bytes) - the cache key
* @param our_sk Our secret key (32 bytes)
* @return 0 on success, -1 on failure
*/
int nacl_box_shared_get(uint8_t *shared_key,
const uint8_t *peer_pk,
const uint8_t *our_sk);

#endif /* _NACL_BOX_H_ */