Skip to main content

secure_gate/
error.rs

1//! Centralized error types for the secure-gate crate.
2//!
3//! # Error types
4//!
5//! | Type | Produced by | Feature |
6//! |------|------------|---------|
7//! | [`FromSliceError`] | [`Fixed::try_from(&[u8])`](crate::Fixed) | Always |
8//! | [`HexError`] | [`Fixed::try_from_hex`](crate::Fixed::try_from_hex), [`FromHexStr`](crate::FromHexStr) | `encoding-hex` |
9//! | [`Base32Error`] | [`Fixed::try_from_base32`](crate::Fixed::try_from_base32), [`FromBase32Str`](crate::FromBase32Str) | `encoding-base32` |
10//! | [`Base64Error`] | [`Fixed::try_from_base64url`](crate::Fixed::try_from_base64url), [`FromBase64UrlStr`](crate::FromBase64UrlStr) | `encoding-base64` |
11//! | [`Bech32Error`] | `try_from_bech32*`, [`FromBech32Str`](crate::FromBech32Str), [`FromBech32mStr`](crate::FromBech32mStr) | `encoding-bech32` |
12//!
13//! Every type in this module is produced by a function in this crate. There is no
14//! unified wrapper enum: a caller decoding several formats owns the union it needs,
15//! and nothing here would help it build one.
16//!
17//! # Design: build-invariant, heap-free, forward-compatible
18//!
19//! - **Identical shapes in debug and release builds.** No variant is gated on
20//!   `cfg(debug_assertions)`, so code that matches on these enums compiles and
21//!   behaves the same under every profile.
22//! - **No heap data.** Errors carry at most `usize` length metadata; they never
23//!   contain payload bytes, HRP strings, or other input-derived text. All error
24//!   types are `Copy` and work without `alloc`.
25//! - **`#[non_exhaustive]`.** Variants (and fields of struct variants) may be
26//!   added in future releases without a semver-major bump; downstream matches
27//!   need a wildcard arm. This is also what lets the crate ship a variant only
28//!   once something produces it, rather than reserving one in advance.
29//! - **No derive macros.** [`Display`](core::fmt::Display) and
30//!   [`Error`](core::error::Error) are written out by hand below. The messages are
31//!   fixed strings and two `usize` fields, which is not enough work to justify
32//!   putting a proc-macro (and `syn`, `quote`, `proc-macro2`) in the dependency
33//!   graph of every downstream build.
34//!
35//! # Security: what errors may reveal
36//!
37//! `InvalidLength` variants carry the expected and actual byte counts in all
38//! builds. Expected lengths are compile-time protocol parameters (key sizes,
39//! nonce sizes) and actual lengths derive from the caller's own input, so
40//! neither is treated as secret — this matches the crate's threat model
41//! (see SECURITY.md). Genuinely input-derived strings (received HRPs, encoding
42//! hints) are **never** captured, in any build. If even coarse error categories
43//! are sensitive in your deployment, redact errors at the logging boundary.
44
45use core::fmt;
46
47/// Error returned when a byte slice cannot be converted to a fixed-size array.
48///
49/// Carries the expected and actual lengths in all build profiles. Lengths are
50/// public protocol parameters, not secret material.
51///
52/// Implements [`core::error::Error`] unconditionally — no `std` feature is
53/// required, and the impl is present in `no_std` builds. (On the 0.8 line
54/// this impl is `std`-gated, because `core::error::Error` needs Rust 1.81 and
55/// that branch targets 1.70. Code that must build against both should not
56/// assume the trait is available without `std`.)
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum FromSliceError {
60    /// The slice length does not match the target array length.
61    #[non_exhaustive]
62    InvalidLength {
63        /// Number of bytes the target array requires.
64        expected: usize,
65        /// Number of bytes actually provided.
66        got: usize,
67    },
68}
69
70impl fmt::Display for FromSliceError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match *self {
73            Self::InvalidLength { expected, got } => {
74                write!(f, "slice length mismatch: expected {expected}, got {got}")
75            }
76        }
77    }
78}
79
80impl core::error::Error for FromSliceError {}
81
82/// Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
83///
84/// *Requires feature `encoding-bech32`.*
85///
86/// Variant shapes are identical in debug and release builds. No input-derived
87/// strings (such as the received HRP) are ever captured — the caller already
88/// holds the input and the expected HRP.
89///
90/// Implements [`core::error::Error`] unconditionally — no `std` feature is
91/// required, and the impl is present in `no_std` builds. (On the 0.8 line
92/// this impl is `std`-gated, because `core::error::Error` needs Rust 1.81 and
93/// that branch targets 1.70. Code that must build against both should not
94/// assume the trait is available without `std`.)
95#[cfg(feature = "encoding-bech32")]
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum Bech32Error {
99    /// The Human-Readable Part (HRP) is invalid.
100    InvalidHrp,
101    /// General bech32 operation failure (malformed string or checksum mismatch).
102    ///
103    /// This is where bit-conversion failures surface too: all bit conversion
104    /// happens inside `CheckedHrpstring::new()`, and the `.byte_iter()` that
105    /// follows a successful `new()` is infallible.
106    OperationFailed,
107    /// The decoded HRP does not match the HRP the caller required.
108    ///
109    /// The received HRP is deliberately not captured — it is input-derived text.
110    /// The caller passed the expected HRP and holds the input string.
111    UnexpectedHrp,
112    /// The decoded payload length does not match the target type's length.
113    #[non_exhaustive]
114    InvalidLength {
115        /// Number of bytes the target type requires.
116        expected: usize,
117        /// Number of bytes actually decoded.
118        got: usize,
119    },
120}
121
122#[cfg(feature = "encoding-bech32")]
123impl fmt::Display for Bech32Error {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        match *self {
126            Self::InvalidHrp => f.write_str("invalid Human-Readable Part (HRP)"),
127            Self::OperationFailed => f.write_str("bech32 operation failed"),
128            Self::UnexpectedHrp => f.write_str("unexpected HRP"),
129            Self::InvalidLength { expected, got } => {
130                write!(f, "decoded length mismatch: expected {expected}, got {got}")
131            }
132        }
133    }
134}
135
136#[cfg(feature = "encoding-bech32")]
137impl core::error::Error for Bech32Error {}
138
139/// Errors produced when decoding Base32 (RFC 4648 §6, uppercase, unpadded) strings.
140///
141/// *Requires feature `encoding-base32`.*
142///
143/// Variant shapes are identical in debug and release builds; only numeric
144/// length metadata is carried.
145///
146/// Implements [`core::error::Error`] unconditionally — no `std` feature is
147/// required, and the impl is present in `no_std` builds. (On the 0.8 line
148/// this impl is `std`-gated, because `core::error::Error` needs Rust 1.81 and
149/// that branch targets 1.70. Code that must build against both should not
150/// assume the trait is available without `std`.)
151#[cfg(feature = "encoding-base32")]
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153#[non_exhaustive]
154pub enum Base32Error {
155    /// The string is not valid Base32 (wrong alphabet, wrong case, padding, or
156    /// an impossible length).
157    InvalidBase32,
158    /// The decoded payload length does not match the target type's length.
159    #[non_exhaustive]
160    InvalidLength {
161        /// Number of bytes the target type requires.
162        expected: usize,
163        /// Number of bytes actually decoded.
164        got: usize,
165    },
166}
167
168#[cfg(feature = "encoding-base32")]
169impl fmt::Display for Base32Error {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match *self {
172            Self::InvalidBase32 => f.write_str("invalid base32 string"),
173            Self::InvalidLength { expected, got } => {
174                write!(f, "decoded length mismatch: expected {expected}, got {got}")
175            }
176        }
177    }
178}
179
180#[cfg(feature = "encoding-base32")]
181impl core::error::Error for Base32Error {}
182
183/// Errors produced when decoding base64url strings.
184///
185/// *Requires feature `encoding-base64`.*
186///
187/// Variant shapes are identical in debug and release builds; only numeric
188/// length metadata is carried.
189///
190/// Implements [`core::error::Error`] unconditionally — no `std` feature is
191/// required, and the impl is present in `no_std` builds. (On the 0.8 line
192/// this impl is `std`-gated, because `core::error::Error` needs Rust 1.81 and
193/// that branch targets 1.70. Code that must build against both should not
194/// assume the trait is available without `std`.)
195#[cfg(feature = "encoding-base64")]
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197#[non_exhaustive]
198pub enum Base64Error {
199    /// The string is not valid base64url.
200    InvalidBase64,
201    /// The decoded payload length does not match the target type's length.
202    #[non_exhaustive]
203    InvalidLength {
204        /// Number of bytes the target type requires.
205        expected: usize,
206        /// Number of bytes actually decoded.
207        got: usize,
208    },
209}
210
211#[cfg(feature = "encoding-base64")]
212impl fmt::Display for Base64Error {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match *self {
215            Self::InvalidBase64 => f.write_str("invalid base64 string"),
216            Self::InvalidLength { expected, got } => {
217                write!(f, "decoded length mismatch: expected {expected}, got {got}")
218            }
219        }
220    }
221}
222
223#[cfg(feature = "encoding-base64")]
224impl core::error::Error for Base64Error {}
225
226/// Errors produced when decoding hexadecimal strings.
227///
228/// *Requires feature `encoding-hex`.*
229///
230/// Variant shapes are identical in debug and release builds; only numeric
231/// length metadata is carried.
232///
233/// Implements [`core::error::Error`] unconditionally — no `std` feature is
234/// required, and the impl is present in `no_std` builds. (On the 0.8 line
235/// this impl is `std`-gated, because `core::error::Error` needs Rust 1.81 and
236/// that branch targets 1.70. Code that must build against both should not
237/// assume the trait is available without `std`.)
238#[cfg(feature = "encoding-hex")]
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
240#[non_exhaustive]
241pub enum HexError {
242    /// The string is not valid hexadecimal.
243    InvalidHex,
244    /// The decoded payload length does not match the target type's length.
245    #[non_exhaustive]
246    InvalidLength {
247        /// Number of bytes the target type requires.
248        expected: usize,
249        /// Number of bytes actually decoded.
250        got: usize,
251    },
252}
253
254#[cfg(feature = "encoding-hex")]
255impl fmt::Display for HexError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match *self {
258            Self::InvalidHex => f.write_str("invalid hex string"),
259            Self::InvalidLength { expected, got } => {
260                write!(f, "decoded length mismatch: expected {expected}, got {got}")
261            }
262        }
263    }
264}
265
266#[cfg(feature = "encoding-hex")]
267impl core::error::Error for HexError {}