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}