refactor(core/W7): split quic/msgs.rs into handshake/caps/control/pairing
Break the 1302-line quic/msgs.rs into four flat sibling modules behind the quic facade's glob re-exports, so every crate::quic::X path stays byte-stable: handshake.rs (Hello/Welcome/Start + codecs), caps.rs (video-cap bits, codec & chroma negotiation, ColorInfo), control.rs (typed CTL_MAGIC messages + frame), pairing.rs (SPAKE2 ceremony messages). msgs.rs is deleted; quic/mod.rs gains the four `mod`/`pub use` lines and the `pub use crate::reject::*` hoist (moved up from msgs.rs). Pure move; no wire-format or behavior change. Private helpers (truncate_to, put_bytes, get_bytes) stay with their sole callers; no visibility changes. Verified both platforms from clean HEAD snapshots: Linux clippy (quic + no-default, -D warnings) + full cargo test (157 lib + integration); Windows clippy (both) + test --lib (156). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
//! The pairing-ceremony control messages (SPAKE2): PairRequest/Challenge/Proof/Result.
|
||||
|
||||
use super::*;
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Pairing ceremony (typed control messages): instead of a session Hello, a client may open
|
||||
// the control stream with PairRequest. The host shows a short PIN out-of-band (log/UI); the
|
||||
// user types it into the client.
|
||||
//
|
||||
// Trust is established by **SPAKE2** (a balanced PAKE), NOT a hash of the PIN. SPAKE2 turns
|
||||
// the low-entropy PIN into a high-entropy shared key via a Diffie-Hellman exchange; the only
|
||||
// thing an active man-in-the-middle who terminates the (TOFU) ceremony learns is whether a
|
||||
// single PIN guess was right — there is no transcript value that reveals the PIN to an
|
||||
// *offline* dictionary search (the fatal flaw of an HMAC-of-PIN proof over a 4-digit space).
|
||||
// Both peers' certificate fingerprints are bound in as the SPAKE2 identities, so the
|
||||
// established key — and the key-confirmation MACs derived from it — only agree when both
|
||||
// sides saw the same two certificates. After mutual key confirmation the host persists the
|
||||
// client's fingerprint and the client pins the host's.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Type byte of [`PairRequest`].
|
||||
pub const MSG_PAIR_REQUEST: u8 = 0x10;
|
||||
/// Type byte of [`PairChallenge`].
|
||||
pub const MSG_PAIR_CHALLENGE: u8 = 0x11;
|
||||
/// Type byte of [`PairProof`].
|
||||
pub const MSG_PAIR_PROOF: u8 = 0x12;
|
||||
/// Type byte of [`PairResult`].
|
||||
pub const MSG_PAIR_RESULT: u8 = 0x13;
|
||||
|
||||
/// `client → host`: begin pairing. `name` is the human label the host stores (≤64 bytes
|
||||
/// UTF-8); `spake_a` is the client's SPAKE2 message (see [`SpakeRole::start`]).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PairRequest {
|
||||
pub name: String,
|
||||
pub spake_a: Vec<u8>,
|
||||
}
|
||||
|
||||
/// `host → client`: the host's SPAKE2 message + its key-confirmation MAC. The client
|
||||
/// finishes SPAKE2, verifies `confirm` (proving the host derived the same key, i.e. knows
|
||||
/// the PIN and saw the same certs), then sends its own confirmation.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PairChallenge {
|
||||
pub spake_b: Vec<u8>,
|
||||
pub confirm: [u8; 32],
|
||||
}
|
||||
|
||||
/// `client → host`: the client's key-confirmation MAC (its single proof attempt).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PairProof {
|
||||
pub confirm: [u8; 32],
|
||||
}
|
||||
|
||||
/// `host → client`: ceremony outcome.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PairResult {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
/// A length-prefixed (u16 LE) byte field within a control message.
|
||||
fn put_bytes(b: &mut Vec<u8>, x: &[u8]) {
|
||||
b.extend_from_slice(&(x.len() as u16).to_le_bytes());
|
||||
b.extend_from_slice(x);
|
||||
}
|
||||
|
||||
/// Read a length-prefixed field at `off`, returning the bytes and the next offset.
|
||||
fn get_bytes(b: &[u8], off: usize) -> Result<(&[u8], usize)> {
|
||||
if off + 2 > b.len() {
|
||||
return Err(PunktfunkError::InvalidArg("truncated field"));
|
||||
}
|
||||
let n = u16::from_le_bytes([b[off], b[off + 1]]) as usize;
|
||||
let start = off + 2;
|
||||
if start + n > b.len() {
|
||||
return Err(PunktfunkError::InvalidArg("field overruns message"));
|
||||
}
|
||||
Ok((&b[start..start + n], start + n))
|
||||
}
|
||||
|
||||
impl PairRequest {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let name = self.name.as_bytes();
|
||||
let n = name.len().min(64);
|
||||
let mut b = Vec::with_capacity(8 + n + self.spake_a.len());
|
||||
b.extend_from_slice(CTL_MAGIC);
|
||||
b.push(MSG_PAIR_REQUEST);
|
||||
b.push(n as u8);
|
||||
b.extend_from_slice(&name[..n]);
|
||||
put_bytes(&mut b, &self.spake_a);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairRequest> {
|
||||
if b.len() < 6 || &b[0..4] != CTL_MAGIC || b[4] != MSG_PAIR_REQUEST {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairRequest"));
|
||||
}
|
||||
let n = b[5] as usize;
|
||||
if n > 64 || b.len() < 6 + n {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairRequest name"));
|
||||
}
|
||||
let name = String::from_utf8_lossy(&b[6..6 + n]).into_owned();
|
||||
let (spake_a, end) = get_bytes(b, 6 + n)?;
|
||||
if end != b.len() {
|
||||
return Err(PunktfunkError::InvalidArg("trailing bytes"));
|
||||
}
|
||||
Ok(PairRequest {
|
||||
name,
|
||||
spake_a: spake_a.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PairChallenge {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(7 + self.spake_b.len() + 32);
|
||||
b.extend_from_slice(CTL_MAGIC);
|
||||
b.push(MSG_PAIR_CHALLENGE);
|
||||
put_bytes(&mut b, &self.spake_b);
|
||||
b.extend_from_slice(&self.confirm);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairChallenge> {
|
||||
if b.len() < 5 || &b[0..4] != CTL_MAGIC || b[4] != MSG_PAIR_CHALLENGE {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairChallenge"));
|
||||
}
|
||||
let (spake_b, end) = get_bytes(b, 5)?;
|
||||
if end + 32 != b.len() {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairChallenge confirm"));
|
||||
}
|
||||
let mut confirm = [0u8; 32];
|
||||
confirm.copy_from_slice(&b[end..end + 32]);
|
||||
Ok(PairChallenge {
|
||||
spake_b: spake_b.to_vec(),
|
||||
confirm,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PairProof {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(37);
|
||||
b.extend_from_slice(CTL_MAGIC);
|
||||
b.push(MSG_PAIR_PROOF);
|
||||
b.extend_from_slice(&self.confirm);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairProof> {
|
||||
if b.len() != 37 || &b[0..4] != CTL_MAGIC || b[4] != MSG_PAIR_PROOF {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairProof"));
|
||||
}
|
||||
let mut confirm = [0u8; 32];
|
||||
confirm.copy_from_slice(&b[5..37]);
|
||||
Ok(PairProof { confirm })
|
||||
}
|
||||
}
|
||||
|
||||
impl PairResult {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(6);
|
||||
b.extend_from_slice(CTL_MAGIC);
|
||||
b.push(MSG_PAIR_RESULT);
|
||||
b.push(self.ok as u8);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairResult> {
|
||||
if b.len() != 6 || &b[0..4] != CTL_MAGIC || b[4] != MSG_PAIR_RESULT {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairResult"));
|
||||
}
|
||||
Ok(PairResult { ok: b[5] != 0 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user