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}