fix(host): fresh random per-session nonce salt instead of the static "pkf1"

Every session sealed with the literal salt b"pkf1", so GCM nonce uniqueness
(nonce = salt || sequence) rested ENTIRELY on the per-session key being fresh —
correct today, but a single key-reuse bug anywhere in the handshake path would
have meant immediate catastrophic nonce reuse instead of merely a wrong key.
Random salt per session keeps the documented second line of defense real. The
salt is negotiated via Welcome, so every deployed client just follows — no wire
or compat change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:55:51 +02:00
parent 6fbab53d56
commit dd73ae2469
+8 -1
View File
@@ -987,6 +987,13 @@ async fn serve_session(
let mut key = [0u8; 16]; let mut key = [0u8; 16];
rand::thread_rng().fill_bytes(&mut key); rand::thread_rng().fill_bytes(&mut key);
// Fresh per-session salt alongside the fresh key. GCM nonce uniqueness only *requires* one
// of the two to be unique per session (the nonce is salt || sequence under the session
// key), but a constant salt would make a key-reuse bug catastrophic instead of merely
// wrong — this keeps the second line of defense real. Negotiated via Welcome, so clients
// just follow.
let mut salt = [0u8; 4];
rand::thread_rng().fill_bytes(&mut salt);
let welcome = Welcome { let welcome = Welcome {
abi_version: punktfunk_core::WIRE_VERSION, abi_version: punktfunk_core::WIRE_VERSION,
udp_port, udp_port,
@@ -1012,7 +1019,7 @@ async fn serve_session(
shard_payload: mtu1500_shard_payload_for(peer.ip()) as u16, shard_payload: mtu1500_shard_payload_for(peer.ip()) as u16,
encrypt: true, encrypt: true,
key, key,
salt: *b"pkf1", salt,
frames: match source { frames: match source {
Punktfunk1Source::Synthetic => frames, Punktfunk1Source::Synthetic => frames,
Punktfunk1Source::Virtual => 0, // unbounded — client streams until we close Punktfunk1Source::Virtual => 0, // unbounded — client streams until we close