fix: address 16 architecture design flaws across all crates
Phase 1 — Foundation: - Constant-time token comparison via subtle::ConstantTimeEq (Fix 11) - Structured error codes E001–E020 in new error_codes.rs (Fix 15) - Remove dead envelope.capnp code and related types (Fix 16) Phase 2 — Auth Hardening: - Registration collision check via has_user_record() (Fix 5) - Auth required on uploadHybridKey/fetchHybridKey RPCs (Fix 1) - Identity-token binding at registration and login (Fix 2) - Session token expiry with 24h TTL and background reaper (Fix 3) - Bounded pending logins with 5-minute timeout (Fix 4) Phase 3 — Resource Limits: - Rate limiting: 100 enqueues/60s per token (Fix 6) - Queue depth cap at 1000 + 7-day message TTL/GC (Fix 7) - Partial queue drain via limit param on fetch/fetchWait (Fix 8) Phase 4 — Crypto Fixes: - OPAQUE KSF switched from Identity to Argon2id (Fix 10) - Random AEAD nonce in hybrid KEM instead of HKDF-derived (Fix 12) - Zeroize secret fields in HybridKeypairBytes (Fix 13) - Encrypted client state files via QPCE format (Fix 9) Phase 5 — Protocol: - Commit fan-out to all existing members on invite (Fix 14) - Add member_identities() to GroupMember Breaking: existing OPAQUE registrations invalidated (Argon2 KSF). Schema: added auth to hybrid key ops, identityKey to OPAQUE finish RPCs, limit to fetch/fetchWait. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,22 +11,9 @@
|
||||
//!
|
||||
//! `build.rs` invokes `capnpc` at compile time and writes generated Rust source
|
||||
//! into `$OUT_DIR`. The `include!` macros below splice that code in as a module.
|
||||
//!
|
||||
//! # Canonical serialisation (M2+)
|
||||
//!
|
||||
//! `build_envelope` uses standard Cap'n Proto wire format. Canonical serialisation
|
||||
//! (deterministic byte representation for cryptographic signing of KeyPackages and
|
||||
//! Commits) is added in M2 once the Authentication Service is introduced.
|
||||
|
||||
// ── Generated types ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/envelope.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
pub mod envelope_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/envelope_capnp.rs"));
|
||||
}
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/auth.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
@@ -48,95 +35,6 @@ pub mod node_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/node_capnp.rs"));
|
||||
}
|
||||
|
||||
// ── Re-exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The message-type discriminant from the `Envelope` schema.
|
||||
///
|
||||
/// Re-exported here so callers can `use quicnprotochat_proto::MsgType` without
|
||||
/// spelling out the full generated module path.
|
||||
pub use envelope_capnp::envelope::MsgType;
|
||||
|
||||
// ── Owned envelope type ───────────────────────────────────────────────────────
|
||||
|
||||
/// An owned, decoded `Envelope` with no Cap'n Proto reader lifetimes.
|
||||
///
|
||||
/// All byte fields are eagerly copied out of the Cap'n Proto reader so that
|
||||
/// this type is `Send + 'static` and can cross async task boundaries freely.
|
||||
///
|
||||
/// # Invariants
|
||||
///
|
||||
/// - `group_id` and `sender_id` are either empty (for control messages such as
|
||||
/// `Ping`/`Pong`) or exactly 32 bytes (SHA-256 digest).
|
||||
/// - `payload` is empty for `Ping` and `Pong`; non-empty for all MLS variants.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedEnvelope {
|
||||
pub msg_type: MsgType,
|
||||
/// SHA-256 of the group name, or empty for point-to-point control messages.
|
||||
pub group_id: Vec<u8>,
|
||||
/// SHA-256 of the sender's Ed25519 identity public key, or empty.
|
||||
pub sender_id: Vec<u8>,
|
||||
/// Opaque payload — interpretation is determined by `msg_type`.
|
||||
pub payload: Vec<u8>,
|
||||
/// Unix timestamp in milliseconds.
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
// ── Serialisation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
/// Serialise a [`ParsedEnvelope`] to unpacked Cap'n Proto wire bytes.
|
||||
///
|
||||
/// The returned bytes include the Cap'n Proto segment table header followed by
|
||||
/// the message data. They are suitable for use as the body of a length-prefixed
|
||||
/// quicnprotochat frame (the frame codec in `quicnprotochat-core` prepends the 4-byte length).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`capnp::Error`] if the underlying allocator fails (out of memory).
|
||||
/// This is not expected under normal operation.
|
||||
pub fn build_envelope(env: &ParsedEnvelope) -> Result<Vec<u8>, capnp::Error> {
|
||||
use capnp::message;
|
||||
|
||||
let mut message = message::Builder::new_default();
|
||||
{
|
||||
let mut root = message.init_root::<envelope_capnp::envelope::Builder>();
|
||||
root.set_msg_type(env.msg_type);
|
||||
root.set_group_id(&env.group_id);
|
||||
root.set_sender_id(&env.sender_id);
|
||||
root.set_payload(&env.payload);
|
||||
root.set_timestamp_ms(env.timestamp_ms);
|
||||
}
|
||||
to_bytes(&message)
|
||||
}
|
||||
|
||||
/// Deserialise unpacked Cap'n Proto wire bytes into a [`ParsedEnvelope`].
|
||||
///
|
||||
/// All data is copied out of the Cap'n Proto reader before returning, so the
|
||||
/// input slice is not retained.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`capnp::Error`] if the bytes are not valid Cap'n Proto wire format.
|
||||
/// - [`capnp::Error`] if `msgType` contains a discriminant not present in the
|
||||
/// current schema (forward-compatibility guard).
|
||||
pub fn parse_envelope(bytes: &[u8]) -> Result<ParsedEnvelope, capnp::Error> {
|
||||
let reader = from_bytes(bytes)?;
|
||||
let root = reader.get_root::<envelope_capnp::envelope::Reader>()?;
|
||||
|
||||
let msg_type = root.get_msg_type().map_err(|nis| {
|
||||
capnp::Error::failed(format!(
|
||||
"Envelope.msgType contains unknown discriminant: {nis}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ParsedEnvelope {
|
||||
msg_type,
|
||||
group_id: root.get_group_id()?.to_vec(),
|
||||
sender_id: root.get_sender_id()?.to_vec(),
|
||||
payload: root.get_payload()?.to_vec(),
|
||||
timestamp_ms: root.get_timestamp_ms(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Low-level byte ↔ message conversions ──────────────────────────────────────
|
||||
|
||||
/// Serialise a Cap'n Proto message builder to unpacked wire bytes.
|
||||
@@ -162,57 +60,3 @@ pub fn from_bytes(
|
||||
let mut cursor = std::io::Cursor::new(bytes);
|
||||
capnp::serialize::read_message(&mut cursor, capnp::message::ReaderOptions::new())
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Round-trip a Ping envelope through build → parse and verify all fields.
|
||||
#[test]
|
||||
fn ping_round_trip() {
|
||||
let original = ParsedEnvelope {
|
||||
msg_type: MsgType::Ping,
|
||||
group_id: vec![],
|
||||
sender_id: vec![0xAB; 32],
|
||||
payload: vec![],
|
||||
timestamp_ms: 1_700_000_000_000,
|
||||
};
|
||||
|
||||
let bytes = build_envelope(&original).expect("build_envelope failed");
|
||||
let parsed = parse_envelope(&bytes).expect("parse_envelope failed");
|
||||
|
||||
assert!(matches!(parsed.msg_type, MsgType::Ping));
|
||||
assert_eq!(parsed.group_id, original.group_id);
|
||||
assert_eq!(parsed.sender_id, original.sender_id);
|
||||
assert_eq!(parsed.payload, original.payload);
|
||||
assert_eq!(parsed.timestamp_ms, original.timestamp_ms);
|
||||
}
|
||||
|
||||
/// Round-trip a Pong envelope.
|
||||
#[test]
|
||||
fn pong_round_trip() {
|
||||
let original = ParsedEnvelope {
|
||||
msg_type: MsgType::Pong,
|
||||
group_id: vec![],
|
||||
sender_id: vec![0xCD; 32],
|
||||
payload: vec![],
|
||||
timestamp_ms: 1_700_000_001_000,
|
||||
};
|
||||
|
||||
let bytes = build_envelope(&original).expect("build_envelope failed");
|
||||
let parsed = parse_envelope(&bytes).expect("parse_envelope failed");
|
||||
|
||||
assert!(matches!(parsed.msg_type, MsgType::Pong));
|
||||
assert_eq!(parsed.sender_id, original.sender_id);
|
||||
assert_eq!(parsed.timestamp_ms, original.timestamp_ms);
|
||||
}
|
||||
|
||||
/// Corrupted bytes must produce an error, not a panic.
|
||||
#[test]
|
||||
fn corrupted_bytes_error() {
|
||||
let result = parse_envelope(&[0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
assert!(result.is_err(), "expected error for corrupted input");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user