fix(punktfunk/1): adversarial-review fixes — SPAKE2 pairing, renegotiation hardening, +more
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
Triaged the multi-agent review of the renegotiation + pairing + Sway + AV1/surround batch
(1 critical, 11 major/minor confirmed). Fixes:
CRITICAL — PIN pairing was offline-brute-forceable. The HMAC-of-PIN proof let an active
MITM who terminates the TOFU ceremony recover the 4-digit PIN by offline dictionary search
(all other inputs observable) and forge a correctly-bound proof. Replaced with **SPAKE2**
(balanced PAKE, `spake2` crate) + key-confirmation MACs, binding both cert fingerprints as
the SPAKE2 identities: an attacker gets exactly ONE online guess, no offline search, and
mismatched cert views (a real MITM) never reach a shared key. Also reworked the UX to an
"arming PIN" — one PIN per arming window shown at host startup (the SPAKE2 client needs the
PIN to build its first message, so it can't be minted per-connection). Validated live:
wrong PIN rejected in 0.1s, right PIN pairs + persists + the paired identity streams.
Pairing hardening: `--allow-pairing`/`--require-pairing` must arm pairing (default rejects
unsolicited ceremonies); per-host cooldown bounds online guessing; the client flushes its
CONNECTION_CLOSE so a refused ceremony can't wedge the sequential host for the full timeout;
atomic (temp+rename) paired-store writes.
Protocol: control/pairing messages use a distinct CTL_MAGIC (PKFc) — fully disjoint from
the positional Hello namespace (a future abi_version can't be misparsed as a control
message); all typed decodes are length-exact. ABI_VERSION → 2 (punktfunk_connect signature
gained the identity params; header regenerated).
Renegotiation: drain the reconfig channel to the NEWEST mode (one rebuild, not one per
stale step); validate refresh_hz; build the new pipeline BEFORE dropping the old so a
rebuild failure keeps the session on its current mode instead of killing it.
GameStream: packetDuration snaps to {5,10} (an in-between value isn't a legal Opus frame
size and would kill audio). Sway: chooser file moved to $XDG_RUNTIME_DIR (was a fixed
world-writable /tmp path — DoS / capture-misdirection by another local user).
Swift: fixed two compile breakers in the new pairing/identity APIs (Int32 status .rawValue,
UInt cap cast). New SPAKE2 + namespace-disjointness + pairing-roundtrip unit tests; the
in-process pairing test now also exercises the arming PIN + cooldown. 114 tests green,
clippy -D warnings clean (both feature sets), fmt, C-ABI harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -466,7 +466,13 @@ fn audio_body(
|
||||
let mut enc = SessionEncoder::new(layout)?;
|
||||
// Opus frame duration; Moonlight negotiates 5 ms (default) or 10 ms via
|
||||
// `x-nv-aqos.packetDuration` and sizes its decoder at `48 * duration` samples.
|
||||
let frame_ms = params.packet_duration_ms.clamp(5, 10) as usize;
|
||||
// Already snapped to {5, 10} at parse time; guard here too so only legal Opus frame
|
||||
// sizes (48 kHz × {5,10} ms = 240/480 samples) ever reach the encoder.
|
||||
let frame_ms = if params.packet_duration_ms >= 10 {
|
||||
10
|
||||
} else {
|
||||
5
|
||||
} as usize;
|
||||
let samples_per_channel = SAMPLE_RATE as usize * frame_ms / 1000;
|
||||
let frame_len = samples_per_channel * layout.channels as usize; // interleaved samples
|
||||
let mut acc: Vec<f32> = Vec::with_capacity(frame_len * 4);
|
||||
|
||||
@@ -321,10 +321,13 @@ fn audio_params(map: &HashMap<String, String>) -> audio::AudioParams {
|
||||
}
|
||||
};
|
||||
let high_quality = parse_u("x-nv-audio.surround.AudioQuality") == Some(1);
|
||||
// Moonlight uses 5 ms (default) or 10 ms (slow decoder / low-bitrate links).
|
||||
let packet_duration_ms = parse_u("x-nv-aqos.packetDuration")
|
||||
.map(|d| d.clamp(5, 10) as u8)
|
||||
.unwrap_or(5);
|
||||
// Moonlight uses 5 ms (default) or 10 ms (slow decoder / low-bitrate links). Snap to
|
||||
// those two — an in-between value like 7 isn't a legal Opus frame size and would make
|
||||
// every encode fail; clamping (not snapping) would let it through.
|
||||
let packet_duration_ms = match parse_u("x-nv-aqos.packetDuration") {
|
||||
Some(d) if d >= 10 => 10,
|
||||
_ => 5,
|
||||
};
|
||||
audio::AudioParams {
|
||||
channels,
|
||||
high_quality,
|
||||
|
||||
Reference in New Issue
Block a user