Skip to main content

xenia_wire/
error.rs

1// Copyright (c) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Error taxonomy for the Xenia wire.
5//!
6//! All fallible operations return [`WireError`]. The variants are coarse on
7//! purpose: a deployed receiver should react the same way to any failure
8//! (drop the envelope, keep the session alive) without leaking the specific
9//! reason to an attacker. Finer-grained diagnosis is available in debug
10//! builds via the inner error strings.
11
12use thiserror::Error;
13
14/// Errors returned by the wire codec.
15#[derive(Debug, Error)]
16#[non_exhaustive]
17pub enum WireError {
18    /// Bincode encode or decode failure. Wraps the inner reason for logs.
19    #[error("xenia wire codec: {0}")]
20    Codec(String),
21
22    /// Session has no current or previous key. The caller must install a key
23    /// via [`crate::Session::install_key`] before sealing or opening.
24    #[error("xenia wire: session key not established")]
25    NoSessionKey,
26
27    /// AEAD seal failed. In practice this means the key material was
28    /// rejected by the underlying ChaCha20-Poly1305 implementation — should
29    /// not happen with a valid 32-byte key and under-capacity plaintext.
30    #[error("xenia wire: AEAD seal failed")]
31    SealFailed,
32
33    /// AEAD open failed. Either:
34    /// - the envelope is under the minimum length (12 nonce + 16 tag),
35    /// - the ciphertext fails Poly1305 verification (wrong key, corruption,
36    ///   or tampering), or
37    /// - the replay window rejected a valid-ciphertext envelope as a
38    ///   duplicate or too-old sequence.
39    ///
40    /// Callers SHOULD NOT distinguish these sub-cases in production — doing
41    /// so leaks timing or structure to an attacker. Drop the envelope and
42    /// keep the session alive.
43    #[error("xenia wire: AEAD open failed")]
44    OpenFailed,
45
46    /// The 32-bit nonce sequence space is exhausted. The caller must
47    /// rekey before sealing more envelopes; continuing would wrap the
48    /// sequence counter and cause nonce reuse, which catastrophically
49    /// breaks ChaCha20-Poly1305 confidentiality and integrity.
50    ///
51    /// Reached after `2^32` seals on a single installed key — approximately
52    /// 4.5 years at 30 fps, or ~40 hours at 30 kHz. Any production deployment
53    /// should rekey on a much shorter cadence (the reference default is 30
54    /// minutes), so this variant surfaces a programming error: the caller
55    /// disabled or failed to trigger rekey.
56    #[error("xenia wire: sequence space exhausted — rekey required")]
57    SequenceExhausted,
58
59    /// Seal or open of an application `FRAME` (or `FRAME_LZ4`) payload
60    /// was attempted on a session whose consent state is not `Approved`.
61    /// The peer must complete the consent ceremony — seal a
62    /// `ConsentRequest`, receive an approving `ConsentResponse` — before
63    /// application data can flow.
64    ///
65    /// Only surfaced when the `consent` feature is enabled. Sessions
66    /// compiled without the feature behave as draft-01 (no enforcement).
67    #[cfg(feature = "consent")]
68    #[error("xenia wire: consent ceremony not completed — FRAME refused")]
69    NoConsent,
70
71    /// Seal or open of an application `FRAME` (or `FRAME_LZ4`) payload
72    /// was attempted on a session that entered the `Revoked` terminal
73    /// state. The session is finished; the caller must tear it down
74    /// and — if appropriate — start a new one with a new consent
75    /// ceremony.
76    #[cfg(feature = "consent")]
77    #[error("xenia wire: consent revoked — session terminated")]
78    ConsentRevoked,
79
80    /// Consent state machine transition is a protocol violation
81    /// (SPEC draft-03 §12.6). Surfaced by
82    /// [`crate::Session::observe_consent`] when the peer emits a
83    /// consent event that cannot legally follow the current state —
84    /// for example a `Revocation` from `Requested` (revoking something
85    /// that was never approved), or a `Denied` that contradicts a
86    /// prior `Approved` for the same `request_id`.
87    ///
88    /// This variant signals "the peer's state machine is broken or
89    /// compromised." The wire layer does NOT own the transport, so it
90    /// cannot tear down the connection itself. Callers SHOULD treat
91    /// this as a hard fault and terminate the session.
92    #[cfg(feature = "consent")]
93    #[error("xenia wire: consent protocol violation: {0}")]
94    ConsentProtocolViolation(crate::consent::ConsentViolation),
95}
96
97impl WireError {
98    /// Construct a `Codec(...)` from any `Display` encode error. Primarily
99    /// used by `Sealable::to_bin` implementations.
100    pub fn encode<E: core::fmt::Display>(e: E) -> Self {
101        Self::Codec(format!("encode: {e}"))
102    }
103
104    /// Construct a `Codec(...)` from any `Display` decode error. Primarily
105    /// used by `Sealable::from_bin` implementations.
106    pub fn decode<E: core::fmt::Display>(e: E) -> Self {
107        Self::Codec(format!("decode: {e}"))
108    }
109}