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` / `encoding-bech32m` |
12//! | [`DecodingError`] | Unified wrapper for all above | Always |
13//!
14//! # Design: build-invariant, heap-free, forward-compatible
15//!
16//! - **Identical shapes in debug and release builds.** No variant is gated on
17//!   `cfg(debug_assertions)`, so code that matches on these enums compiles and
18//!   behaves the same under every profile.
19//! - **No heap data.** Errors carry at most `usize` length metadata; they never
20//!   contain payload bytes, HRP strings, or other input-derived text. All error
21//!   types are `Copy` and work without `alloc`.
22//! - **`#[non_exhaustive]`.** Variants (and fields of struct variants) may be
23//!   added in future releases without a semver-major bump; downstream matches
24//!   need a wildcard arm.
25//!
26//! # Security: what errors may reveal
27//!
28//! `InvalidLength` variants carry the expected and actual byte counts in all
29//! builds. Expected lengths are compile-time protocol parameters (key sizes,
30//! nonce sizes) and actual lengths derive from the caller's own input, so
31//! neither is treated as secret — this matches the crate's threat model
32//! (see SECURITY.md). Genuinely input-derived strings (received HRPs, encoding
33//! hints) are **never** captured, in any build. If even coarse error categories
34//! are sensitive in your deployment, redact errors at the logging boundary.
35
36use thiserror::Error;
37
38/// Error returned when a byte slice cannot be converted to a fixed-size array.
39///
40/// Carries the expected and actual lengths in all build profiles. Lengths are
41/// public protocol parameters, not secret material.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
43#[non_exhaustive]
44pub enum FromSliceError {
45    /// The slice length does not match the target array length.
46    #[error("slice length mismatch: expected {expected}, got {got}")]
47    #[non_exhaustive]
48    InvalidLength {
49        /// Number of bytes the target array requires.
50        expected: usize,
51        /// Number of bytes actually provided.
52        got: usize,
53    },
54}
55
56/// Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
57///
58/// *Requires feature `encoding-bech32` or `encoding-bech32m`.*
59///
60/// Variant shapes are identical in debug and release builds. No input-derived
61/// strings (such as the received HRP) are ever captured — the caller already
62/// holds the input and the expected HRP.
63#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
65#[non_exhaustive]
66pub enum Bech32Error {
67    /// The Human-Readable Part (HRP) is invalid.
68    #[error("invalid Human-Readable Part (HRP)")]
69    InvalidHrp,
70    /// Bit conversion during encoding/decoding failed.
71    ///
72    /// **Currently unreachable.** After `CheckedHrpstring::new()` succeeds, the
73    /// `.byte_iter()` iterator is infallible — all bit-conversion happens during
74    /// the `new()` call and any failure surfaces as `OperationFailed` instead.
75    /// This variant is preserved as public API for forward compatibility should a
76    /// fallible conversion path be introduced in a future release of the `bech32` crate.
77    #[error("bit conversion failed")]
78    ConversionFailed,
79    /// General bech32 operation failure (malformed string or checksum mismatch).
80    #[error("bech32 operation failed")]
81    OperationFailed,
82    /// The decoded HRP does not match the HRP the caller required.
83    ///
84    /// The received HRP is deliberately not captured — it is input-derived text.
85    /// The caller passed the expected HRP and holds the input string.
86    #[error("unexpected HRP")]
87    UnexpectedHrp,
88    /// The decoded payload length does not match the target type's length.
89    #[error("decoded length mismatch: expected {expected}, got {got}")]
90    #[non_exhaustive]
91    InvalidLength {
92        /// Number of bytes the target type requires.
93        expected: usize,
94        /// Number of bytes actually decoded.
95        got: usize,
96    },
97}
98
99/// Errors produced when decoding Base32 (RFC 4648 §6, uppercase, unpadded) strings.
100///
101/// *Requires feature `encoding-base32`.*
102///
103/// Variant shapes are identical in debug and release builds; only numeric
104/// length metadata is carried.
105#[cfg(feature = "encoding-base32")]
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
107#[non_exhaustive]
108pub enum Base32Error {
109    /// The string is not valid Base32 (wrong alphabet, wrong case, padding, or
110    /// an impossible length).
111    #[error("invalid base32 string")]
112    InvalidBase32,
113    /// The decoded payload length does not match the target type's length.
114    #[error("decoded length mismatch: expected {expected}, got {got}")]
115    #[non_exhaustive]
116    InvalidLength {
117        /// Number of bytes the target type requires.
118        expected: usize,
119        /// Number of bytes actually decoded.
120        got: usize,
121    },
122}
123
124/// Errors produced when decoding base64url strings.
125///
126/// *Requires feature `encoding-base64`.*
127///
128/// Variant shapes are identical in debug and release builds; only numeric
129/// length metadata is carried.
130#[cfg(feature = "encoding-base64")]
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
132#[non_exhaustive]
133pub enum Base64Error {
134    /// The string is not valid base64url.
135    #[error("invalid base64 string")]
136    InvalidBase64,
137    /// The decoded payload length does not match the target type's length.
138    #[error("decoded length mismatch: expected {expected}, got {got}")]
139    #[non_exhaustive]
140    InvalidLength {
141        /// Number of bytes the target type requires.
142        expected: usize,
143        /// Number of bytes actually decoded.
144        got: usize,
145    },
146}
147
148/// Errors produced when decoding hexadecimal strings.
149///
150/// *Requires feature `encoding-hex`.*
151///
152/// Variant shapes are identical in debug and release builds; only numeric
153/// length metadata is carried.
154#[cfg(feature = "encoding-hex")]
155#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
156#[non_exhaustive]
157pub enum HexError {
158    /// The string is not valid hexadecimal.
159    #[error("invalid hex string")]
160    InvalidHex,
161    /// The decoded payload length does not match the target type's length.
162    #[error("decoded length mismatch: expected {expected}, got {got}")]
163    #[non_exhaustive]
164    InvalidLength {
165        /// Number of bytes the target type requires.
166        expected: usize,
167        /// Number of bytes actually decoded.
168        got: usize,
169    },
170}
171
172/// Unified error type for multi-format decoding operations.
173///
174/// Wraps format-specific errors from hex, base32, base64url, bech32, and bech32m decoders.
175/// Always available; variants depend on enabled features. Like the format-specific
176/// errors it wraps, this type is heap-free, `Copy`, and build-invariant.
177#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
178#[non_exhaustive]
179pub enum DecodingError {
180    /// The input is not valid Bech32.
181    #[cfg(feature = "encoding-bech32")]
182    #[error("invalid bech32 string")]
183    InvalidBech32(#[source] Bech32Error),
184    /// The input is not valid Base32.
185    #[cfg(feature = "encoding-base32")]
186    #[error("invalid base32 string")]
187    InvalidBase32(#[source] Base32Error),
188    /// The input is not valid Base64url.
189    #[cfg(feature = "encoding-base64")]
190    #[error("invalid base64 string")]
191    InvalidBase64(#[source] Base64Error),
192    /// The input is not valid hexadecimal.
193    #[cfg(feature = "encoding-hex")]
194    #[error("invalid hex string")]
195    InvalidHex(#[source] HexError),
196    /// The encoding could not be identified.
197    ///
198    /// Deliberately carries no hint text — free-form diagnostics derived from
199    /// the input would embed input data in the error value.
200    #[error("invalid encoding")]
201    InvalidEncoding,
202}