Skip to main content

secure_gate/traits/decoding/
bech32.rs

1//! Bech32 decoding trait.
2//!
3//! This trait provides secure, explicit decoding of Bech32 strings (BIP-173 checksum)
4//! to byte vectors, with HRP validation as the primary path. It is designed for handling
5//! untrusted input in cryptographic contexts, such as decoding encoded addresses or keys.
6//!
7//! **Requires the `encoding-bech32` feature** (distinct from Bech32m).
8//!
9//! # Security Notes
10//!
11//! - **Treat all input as untrusted**: validate Bech32 strings upstream before wrapping
12//!   in secrets. HRP validation prevents cross-protocol confusion attacks.
13//! - **HRP validation**: use [`try_from_bech32`](FromBech32Str::try_from_bech32) as the
14//!   default; use [`try_from_bech32_unchecked`](FromBech32Str::try_from_bech32_unchecked)
15//!   only when you intentionally need the decoded HRP. Test empty and invalid HRP inputs
16//!   in security-critical code.
17//! - **Heap allocation**: Returns `Vec<u8>` — wrap in [`Fixed`](crate::Fixed) or
18//!   [`Dynamic`](crate::Dynamic) to store as a secret.
19//!
20//! # Example
21//!
22//! ```rust
23//! # #[cfg(feature = "encoding-bech32")]
24//! use secure_gate::FromBech32Str;
25//! # #[cfg(feature = "encoding-bech32")]
26//! {
27//! // BIP-173 minimal valid Bech32 test vector
28//! let bech32 = "A12UEL5L";
29//!
30//! let data = bech32.try_from_bech32("A").expect("HRP matches");
31//! assert!(data.is_empty());
32//!
33//! let (hrp, data) = bech32.try_from_bech32_unchecked().expect("valid bech32");
34//! assert_eq!(hrp.to_ascii_lowercase(), "a");
35//! assert!(data.is_empty());
36//!
37//! // Error on invalid input
38//! assert!("not-bech32".try_from_bech32("a").is_err());
39//! }
40//! ```
41#[cfg(feature = "encoding-bech32")]
42use super::super::encoding::bech32::Bech32Large;
43#[cfg(feature = "encoding-bech32")]
44use bech32::primitives::decode::CheckedHrpstring;
45#[cfg(feature = "encoding-bech32")]
46use crate::error::Bech32Error;
47
48/// Extension trait for decoding Bech32 (BIP-173) strings into byte vectors.
49///
50/// *Requires feature `encoding-bech32`.*
51///
52/// Blanket-implemented for all `AsRef<str>` types. Treat all input as untrusted;
53/// HRP validation prevents injection attacks and cross-protocol confusion.
54///
55/// **Extended payload capacity**: Uses the custom `Bech32Large` variant (8191 Fe32
56/// values, ~5 KB (5,115 bytes maximum payload)) — significantly larger than Bech32m's standard 90-byte
57/// limit. Strings encoded via [`ToBech32`](crate::ToBech32) round-trip correctly here
58/// but will fail with [`FromBech32mStr`](crate::FromBech32mStr) when they exceed ~90 bytes.
59#[cfg(feature = "encoding-bech32")]
60pub trait FromBech32Str {
61    /// Decodes a Bech32 (BIP-173) string, validating that the HRP matches `expected_hrp`.
62    ///
63    /// The HRP comparison is case-insensitive. Returns only the data bytes — the HRP
64    /// is validated and discarded.
65    ///
66    /// Validates the BIP-173 checksum using the extended `Bech32Large`
67    /// variant (8191 Fe32 limit) for large-payload compatibility.
68    ///
69    /// # Errors
70    ///
71    /// - [`Bech32Error::OperationFailed`] — invalid checksum or malformed string.
72    /// - [`Bech32Error::UnexpectedHrp`] — decoded HRP does not match `expected_hrp`.
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use secure_gate::FromBech32Str;
78    ///
79    /// // BIP-173 minimal valid test vector
80    /// let data = "A12UEL5L".try_from_bech32("A")?;
81    /// assert!(data.is_empty());
82    ///
83    /// // HRP mismatch returns an error
84    /// assert!("A12UEL5L".try_from_bech32("bc").is_err());
85    /// # Ok::<(), secure_gate::Bech32Error>(())
86    /// ```
87    fn try_from_bech32(&self, expected_hrp: &str) -> Result<Vec<u8>, Bech32Error>;
88
89    /// Decodes a Bech32 (BIP-173) string into `(HRP, data_bytes)` without validating the HRP.
90    ///
91    /// Validates the BIP-173 checksum using the extended `Bech32Large`
92    /// variant (8191 Fe32 limit) for large-payload compatibility.
93    ///
94    /// # Errors
95    ///
96    /// - [`Bech32Error::OperationFailed`] — invalid checksum or malformed string.
97    /// - [`Bech32Error::ConversionFailed`] — bit-conversion failure.
98    ///
99    /// # Examples
100    ///
101    /// ```rust
102    /// use secure_gate::FromBech32Str;
103    ///
104    /// // BIP-173 minimal valid test vector
105    /// let (hrp, data) = "A12UEL5L".try_from_bech32_unchecked()?;
106    /// assert_eq!(hrp.to_ascii_lowercase(), "a");
107    /// assert!(data.is_empty());
108    /// # Ok::<(), secure_gate::Bech32Error>(())
109    /// ```
110    fn try_from_bech32_unchecked(&self) -> Result<(String, Vec<u8>), Bech32Error>;
111}
112
113// Blanket impl to cover any AsRef<str> (e.g., &str, String, etc.)
114#[cfg(feature = "encoding-bech32")]
115impl<T: AsRef<str> + ?Sized> FromBech32Str for T {
116    fn try_from_bech32_unchecked(&self) -> Result<(String, Vec<u8>), Bech32Error> {
117        let s = self.as_ref();
118        // Use CheckedHrpstring to validate Bech32 checksum (supports large via custom Bech32Large)
119        let checked =
120            CheckedHrpstring::new::<Bech32Large>(s).map_err(|_| Bech32Error::OperationFailed)?;
121
122        // Get HRP (lowercase)
123        let hrp = checked.hrp().to_string();
124
125        // Collect data as 8-bit bytes (handles empty)
126        let data: Vec<u8> = checked.byte_iter().collect();
127
128        Ok((hrp, data))
129    }
130
131    fn try_from_bech32(&self, expected_hrp: &str) -> Result<Vec<u8>, Bech32Error> {
132        let (got_hrp, data) = self.try_from_bech32_unchecked()?;
133        if !got_hrp.eq_ignore_ascii_case(expected_hrp) {
134            #[cfg(debug_assertions)]
135            return Err(Bech32Error::UnexpectedHrp {
136                expected: expected_hrp.to_string(),
137                got: got_hrp,
138            });
139            #[cfg(not(debug_assertions))]
140            return Err(Bech32Error::UnexpectedHrp);
141        }
142        Ok(data)
143    }
144}