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//! | [`Base64Error`] | [`Fixed::try_from_base64url`](crate::Fixed::try_from_base64url), [`FromBase64UrlStr`](crate::FromBase64UrlStr) | `encoding-base64` |
10//! | [`Bech32Error`] | `try_from_bech32*`, [`FromBech32Str`](crate::FromBech32Str), [`FromBech32mStr`](crate::FromBech32mStr) | `encoding-bech32` / `encoding-bech32m` |
11//! | [`DecodingError`] | Unified wrapper for all above | Always |
12//!
13//! # Design: build-invariant, heap-free, forward-compatible
14//!
15//! - **Identical shapes in debug and release builds.** No variant is gated on
16//!   `cfg(debug_assertions)`, so code that matches on these enums compiles and
17//!   behaves the same under every profile.
18//! - **No heap data.** Errors carry at most `usize` length metadata; they never
19//!   contain payload bytes, HRP strings, or other input-derived text. All error
20//!   types are `Copy` and work without `alloc`.
21//! - **`#[non_exhaustive]`.** Variants (and fields of struct variants) may be
22//!   added in future releases without a semver-major bump; downstream matches
23//!   need a wildcard arm.
24//!
25//! # Security: what errors may reveal
26//!
27//! `InvalidLength` variants carry the expected and actual byte counts in all
28//! builds. Expected lengths are compile-time protocol parameters (key sizes,
29//! nonce sizes) and actual lengths derive from the caller's own input, so
30//! neither is treated as secret — this matches the crate's threat model
31//! (see SECURITY.md). Genuinely input-derived strings (received HRPs, encoding
32//! hints) are **never** captured, in any build. If even coarse error categories
33//! are sensitive in your deployment, redact errors at the logging boundary.
34//!
35//! # 0.8 LTS note: hand-written impls instead of `thiserror`
36//!
37//! Unlike the 0.9 line, this branch does not use `thiserror`: its `no_std`
38//! support requires `core::error::Error` (Rust 1.81+), above this branch's
39//! MSRV of 1.70. `Display` is implemented manually for every build, and
40//! `std::error::Error` (including `source()` chaining on [`DecodingError`])
41//! is provided behind the `std` feature.
42
43use core::fmt;
44
45/// Error returned when a byte slice cannot be converted to a fixed-size array.
46///
47/// Carries the expected and actual lengths in all build profiles. Lengths are
48/// public protocol parameters, not secret material.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum FromSliceError {
52    /// The slice length does not match the target array length.
53    #[non_exhaustive]
54    InvalidLength {
55        /// Number of bytes the target array requires.
56        expected: usize,
57        /// Number of bytes actually provided.
58        got: usize,
59    },
60}
61
62impl fmt::Display for FromSliceError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::InvalidLength { expected, got } => {
66                write!(f, "slice length mismatch: expected {expected}, got {got}")
67            }
68        }
69    }
70}
71
72#[cfg(feature = "std")]
73impl std::error::Error for FromSliceError {}
74
75/// Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
76///
77/// *Requires feature `encoding-bech32` or `encoding-bech32m`.*
78///
79/// Variant shapes are identical in debug and release builds. No input-derived
80/// strings (such as the received HRP) are ever captured — the caller already
81/// holds the input and the expected HRP.
82#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84#[non_exhaustive]
85pub enum Bech32Error {
86    /// The Human-Readable Part (HRP) is invalid.
87    InvalidHrp,
88    /// Bit conversion during encoding/decoding failed.
89    ///
90    /// **Currently unreachable.** After `CheckedHrpstring::new()` succeeds, the
91    /// `.byte_iter()` iterator is infallible — all bit-conversion happens during
92    /// the `new()` call and any failure surfaces as `OperationFailed` instead.
93    /// This variant is preserved as public API for forward compatibility should a
94    /// fallible conversion path be introduced in a future release of the `bech32` crate.
95    ConversionFailed,
96    /// General bech32 operation failure (malformed string or checksum mismatch).
97    OperationFailed,
98    /// The decoded HRP does not match the HRP the caller required.
99    ///
100    /// The received HRP is deliberately not captured — it is input-derived text.
101    /// The caller passed the expected HRP and holds the input string.
102    UnexpectedHrp,
103    /// The decoded payload length does not match the target type's length.
104    #[non_exhaustive]
105    InvalidLength {
106        /// Number of bytes the target type requires.
107        expected: usize,
108        /// Number of bytes actually decoded.
109        got: usize,
110    },
111}
112
113#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
114impl fmt::Display for Bech32Error {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::InvalidHrp => f.write_str("invalid Human-Readable Part (HRP)"),
118            Self::ConversionFailed => f.write_str("bit conversion failed"),
119            Self::OperationFailed => f.write_str("bech32 operation failed"),
120            Self::UnexpectedHrp => f.write_str("unexpected HRP"),
121            Self::InvalidLength { expected, got } => {
122                write!(f, "decoded length mismatch: expected {expected}, got {got}")
123            }
124        }
125    }
126}
127
128#[cfg(all(
129    feature = "std",
130    any(feature = "encoding-bech32", feature = "encoding-bech32m")
131))]
132impl std::error::Error for Bech32Error {}
133
134/// Errors produced when decoding base64url strings.
135///
136/// *Requires feature `encoding-base64`.*
137///
138/// Variant shapes are identical in debug and release builds; only numeric
139/// length metadata is carried.
140#[cfg(feature = "encoding-base64")]
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum Base64Error {
144    /// The string is not valid base64url.
145    InvalidBase64,
146    /// The decoded payload length does not match the target type's length.
147    #[non_exhaustive]
148    InvalidLength {
149        /// Number of bytes the target type requires.
150        expected: usize,
151        /// Number of bytes actually decoded.
152        got: usize,
153    },
154}
155
156#[cfg(feature = "encoding-base64")]
157impl fmt::Display for Base64Error {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::InvalidBase64 => f.write_str("invalid base64 string"),
161            Self::InvalidLength { expected, got } => {
162                write!(f, "decoded length mismatch: expected {expected}, got {got}")
163            }
164        }
165    }
166}
167
168#[cfg(all(feature = "std", feature = "encoding-base64"))]
169impl std::error::Error for Base64Error {}
170
171/// Errors produced when decoding hexadecimal strings.
172///
173/// *Requires feature `encoding-hex`.*
174///
175/// Variant shapes are identical in debug and release builds; only numeric
176/// length metadata is carried.
177#[cfg(feature = "encoding-hex")]
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
179#[non_exhaustive]
180pub enum HexError {
181    /// The string is not valid hexadecimal.
182    InvalidHex,
183    /// The decoded payload length does not match the target type's length.
184    #[non_exhaustive]
185    InvalidLength {
186        /// Number of bytes the target type requires.
187        expected: usize,
188        /// Number of bytes actually decoded.
189        got: usize,
190    },
191}
192
193#[cfg(feature = "encoding-hex")]
194impl fmt::Display for HexError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            Self::InvalidHex => f.write_str("invalid hex string"),
198            Self::InvalidLength { expected, got } => {
199                write!(f, "decoded length mismatch: expected {expected}, got {got}")
200            }
201        }
202    }
203}
204
205#[cfg(all(feature = "std", feature = "encoding-hex"))]
206impl std::error::Error for HexError {}
207
208/// Unified error type for multi-format decoding operations.
209///
210/// Wraps format-specific errors from hex, base64url, bech32, and bech32m decoders.
211/// Always available; variants depend on enabled features. Like the format-specific
212/// errors it wraps, this type is heap-free, `Copy`, and build-invariant.
213#[derive(Clone, Copy, Debug, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum DecodingError {
216    /// The input is not valid Bech32.
217    #[cfg(feature = "encoding-bech32")]
218    InvalidBech32(Bech32Error),
219    /// The input is not valid Base64url.
220    #[cfg(feature = "encoding-base64")]
221    InvalidBase64(Base64Error),
222    /// The input is not valid hexadecimal.
223    #[cfg(feature = "encoding-hex")]
224    InvalidHex(HexError),
225    /// The encoding could not be identified.
226    ///
227    /// Deliberately carries no hint text — free-form diagnostics derived from
228    /// the input would embed input data in the error value.
229    InvalidEncoding,
230}
231
232impl fmt::Display for DecodingError {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            #[cfg(feature = "encoding-bech32")]
236            Self::InvalidBech32(_) => f.write_str("invalid bech32 string"),
237            #[cfg(feature = "encoding-base64")]
238            Self::InvalidBase64(_) => f.write_str("invalid base64 string"),
239            #[cfg(feature = "encoding-hex")]
240            Self::InvalidHex(_) => f.write_str("invalid hex string"),
241            Self::InvalidEncoding => f.write_str("invalid encoding"),
242        }
243    }
244}
245
246#[cfg(feature = "std")]
247impl std::error::Error for DecodingError {
248    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
249        match self {
250            #[cfg(feature = "encoding-bech32")]
251            Self::InvalidBech32(inner) => Some(inner),
252            #[cfg(feature = "encoding-base64")]
253            Self::InvalidBase64(inner) => Some(inner),
254            #[cfg(feature = "encoding-hex")]
255            Self::InvalidHex(inner) => Some(inner),
256            Self::InvalidEncoding => None,
257        }
258    }
259}