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}