Skip to main content

secure_gate/traits/decoding/
bech32.rs

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