From 3308de49adfdb2a55c646e6b675da998f2c36900 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 19:42:27 +0100 Subject: Remember each sender's secret beside the node's key: no X25519 at restart A listener lets a quiet sender's secret go with its active state, so a node started again made one X25519 for every sender that came back (169 us on this Pi, most of a wake's CPU): asymmetric work in proportion to the links, which design 011 counts as a reason to design again. The secret is now remembered in a file beside the key (KEY.links, 0600, begun again for another node's key): a place of 64 bytes a sender in the order allowed, mapped, so a sender at rest takes no memory for it. A place is the first 28 bytes of the sender's key, a check (FNV-1a of the key and the secret) and the secret, written secret first and key last; a place whose check fails is made again. The UDP link checks see a receiver started again make no X25519. --- serve/link.c | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) (limited to 'serve/link.c') diff --git a/serve/link.c b/serve/link.c index ed1b695..e8c8a7a 100644 --- a/serve/link.c +++ b/serve/link.c @@ -13,8 +13,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -783,6 +785,8 @@ struct PureLinkListener { unsigned port; uint64_t run, idle, swept, trimmed, untrimmed; PureAead *aead; /* keyed by each packet's run */ + unsigned char *memo; /* the secrets remembered (pure_link_remember), mapped */ + size_t memo_size; int fd, wake; pthread_t thread; bool started; @@ -926,12 +930,71 @@ static void drop_held(Stream *st, size_t k) { --st->held_count; } -/* A peer's active state, made at its first packet: the secret its key shares with ours. */ +/* The memo of secrets (pure_link_remember): a header, then a place a peer + * in the order allowed, each the first 28 bytes of its key, a check (FNV-1a + * of its whole key and the secret) and the secret. A place whose check + * fails (never made, another key, cut short by a crash) is made again. */ +enum { MEMO_HEAD = 64, MEMO_PLACE = 64 }; +static const unsigned char memo_magic[8] = {'P', 'L', 'M', '1', 0, 0, 0, 0}; + +static uint32_t memo_check(const unsigned char key[PURE_SEAL_KEY], const unsigned char secret[32]) { + return fnv(fnv(2166136261u, key, PURE_SEAL_KEY), secret, 32); +} + +bool pure_link_remember(PureLinkListener *s, const char *path, char *error, size_t size) { + int fd = open(path, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC, 0600); + struct stat info; + unsigned char head[MEMO_HEAD]; + size_t needed = MEMO_HEAD + (s->peer_count ? s->peer_count : 1) * MEMO_PLACE; + bool ok = fd >= 0 && !fstat(fd, &info) && S_ISREG(info.st_mode) && !(info.st_mode & 077); + /* Another node's key, or none yet: begun again. */ + bool ours = ok && pread(fd, head, MEMO_HEAD, 0) == MEMO_HEAD && !memcmp(head, memo_magic, 8) && + !memcmp(head + 8, s->own_public, PURE_SEAL_KEY); + if (ok && !ours) { + memset(head, 0, MEMO_HEAD); + memcpy(head, memo_magic, 8); + memcpy(head + 8, s->own_public, PURE_SEAL_KEY); + ok = !ftruncate(fd, 0) && pwrite(fd, head, MEMO_HEAD, 0) == MEMO_HEAD; + } + ok = ok && (ours && (size_t)info.st_size >= needed ? true : !ftruncate(fd, (off_t)needed)); + void *map = ok ? mmap(NULL, needed, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0) : MAP_FAILED; + if (fd >= 0) close(fd); + if (map == MAP_FAILED) { + snprintf(error, size, "%s: cannot keep secrets there (a file only its owner may read)", path); + return false; + } + s->memo = map; + s->memo_size = needed; + return true; +} + +/* A peer's active state, made at its first packet: the secret its key + * shares with ours, remembered or made (one X25519, then remembered: the + * secret first, the key that makes the place its own last). */ static Active *activate(PureLinkListener *s, Peer *peer, uint64_t now) { Active *a = calloc(1, sizeof(*a)), **actives = room(s->actives, &s->active_capacity, s->active_count, sizeof(*actives), 64); if (actives) s->actives = actives; - if (!a || !actives || !pure_link_secret(s->own, peer->key, a->secret)) { - if (a) explicit_bzero(a->secret, sizeof(a->secret)); + if (!a || !actives) { + free(a); + return NULL; + } + size_t index = (size_t)(peer - s->peers); + unsigned char *place = s->memo && MEMO_HEAD + (index + 1) * MEMO_PLACE <= s->memo_size ? + s->memo + MEMO_HEAD + index * MEMO_PLACE : NULL; + if (place && !memcmp(place, peer->key, 28) && get32(place + 28) == memo_check(peer->key, place + 32)) { + memcpy(a->secret, place + 32, 32); + ++s->counts.remembered; + } else if (pure_link_secret(s->own, peer->key, a->secret)) { + if (place) { + memset(place, 0, 28); + __atomic_thread_fence(__ATOMIC_RELEASE); + memcpy(place + 32, a->secret, 32); + put32(place + 28, memo_check(peer->key, a->secret)); + __atomic_thread_fence(__ATOMIC_RELEASE); + memcpy(place, peer->key, 28); + } + } else { + explicit_bzero(a, sizeof(*a)); free(a); return NULL; } @@ -1409,6 +1472,7 @@ void pure_link_listener_close(PureLinkListener *s, PureLinkCounts *counts) { } while (s->active_count) let_go(s, s->actives[0]); pure_aead_free(s->aead); + if (s->memo) munmap(s->memo, s->memo_size); free(s->actives); free(s->peers); free(s->index); -- cgit v1.2.3