Skip to main content

secure_gate/traits/encoding/
bech32.rs

1//! Bech32 encoding trait.
2//!
3//! > **Import path:** `use secure_gate::ToBech32;`
4//!
5//! This trait provides secure, explicit encoding of byte data to Bech32 strings
6//! (BIP-173 checksum) with a specified Human-Readable Part (HRP). Designed for
7//! intentional export (addresses, QR codes, audited logs).
8//!
9//! Requires the `encoding-bech32` feature.
10//!
11//! # Security Notes
12//!
13//! - **Full secret exposure**: The resulting string contains the **entire** secret.
14//!   Always treat output as sensitive.
15//! - **Zeroizing variants**: Prefer `try_to_bech32_zeroizing`, which returns [`EncodedSecret`]
16//!   (wrapping `Zeroizing<String>` with redacted `Debug`) when the encoded form remains sensitive.
17//! - **Audit visibility**: Direct wrapper calls (`key.try_to_bech32(...)`) do **not** appear in
18//!   `grep expose_secret` / `grep with_secret` audit sweeps. For audit-first teams or
19//!   multi-step operations, prefer `with_secret(|b| b.try_to_bech32(...))` — the borrow
20//!   checker enforces the reference cannot escape the closure.
21//! - **HRP**: pass the intended human-readable part to `try_to_bech32`; test empty and
22//!   invalid HRP inputs in security-critical code.
23//! - **Extended limit**: Uses [`Bech32Large`] (8191 Fe32 values, ~5 KB (5,115 bytes maximum payload)) instead
24//!   of the 90-character standard limit — suitable for large secrets such as
25//!   age-style encryption recipients, ciphertexts, and arbitrary binary payloads.
26//!   For Bitcoin address formats, use [`ToBech32m`](crate::ToBech32m) (BIP-350).
27//! - **Treat all input as untrusted**: validate data upstream before wrapping.
28//!
29//! # Example
30//!
31//! ```rust
32//! # #[cfg(feature = "encoding-bech32")]
33//! use secure_gate::{Fixed, ToBech32, RevealSecret};
34//! # #[cfg(feature = "encoding-bech32")]
35//! {
36//! let secret = Fixed::new([0x42u8; 4]);
37//!
38//! // Use try_to_bech32 — the sole encoding API:
39//! let encoded = secret.with_secret(|s| s.try_to_bech32("test")).unwrap();
40//! assert!(encoded.starts_with("test1"));
41//! }
42//! ```
43#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
44use bech32::encode_lower;
45#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
46use bech32::Hrp;
47
48#[cfg(feature = "encoding-bech32")]
49use bech32::primitives::checksum::Checksum;
50
51/// Custom Bech32 (BIP-173) checksum variant with an extended payload capacity.
52///
53/// Matches classic Bech32 checksum behavior but raises the `CODE_LENGTH` limit to
54/// 8191 Fe32 values (~5 KB (5,115 bytes maximum payload)), well above the standard 90-character limit.
55/// Used by the [`ToBech32`] trait to support large secrets while preserving full
56/// checksum validation.
57///
58/// Most users interact with this type indirectly via [`ToBech32`]. It is `pub`
59/// for use in `impl Checksum` and for advanced callers who construct their own
60/// `encode_lower::<Bech32Large>(...)` calls.
61#[cfg(feature = "encoding-bech32")]
62#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub enum Bech32Large {}
64
65#[cfg(feature = "encoding-bech32")]
66impl Checksum for Bech32Large {
67    type MidstateRepr = u32;
68
69    const CODE_LENGTH: usize = 8191;
70    const CHECKSUM_LENGTH: usize = 6;
71
72    const GENERATOR_SH: [u32; 5] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
73    const TARGET_RESIDUE: u32 = 1;
74}
75
76#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
77use crate::error::Bech32Error;
78
79/// Extension trait for encoding byte data as Bech32 (BIP-173) strings.
80///
81/// *Requires feature `encoding-bech32`.*
82///
83/// Blanket-implemented for all `AsRef<[u8]>` types. Use [`try_to_bech32`](Self::try_to_bech32)
84/// with the protocol's HRP. Test empty and invalid HRP inputs in security-critical code.
85#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
86pub trait ToBech32 {
87    /// Fallibly encodes bytes as a Bech32 (BIP-173) string with the given HRP.
88    ///
89    /// # Errors
90    ///
91    /// - [`Bech32Error::InvalidHrp`] — `hrp` contains invalid characters.
92    /// - [`Bech32Error::OperationFailed`] — encoding failure.
93    ///
94    /// # Examples
95    ///
96    /// ```rust
97    /// use secure_gate::ToBech32;
98    ///
99    /// let encoded = b"hello".try_to_bech32("test")?;
100    /// assert!(encoded.starts_with("test1"));
101    /// # Ok::<(), secure_gate::Bech32Error>(())
102    /// ```
103    fn try_to_bech32(&self, hrp: &str) -> Result<alloc::string::String, Bech32Error>;
104
105    /// Fallibly encodes bytes as Bech32 and wraps the result in [`crate::EncodedSecret`].
106    fn try_to_bech32_zeroizing(&self, hrp: &str) -> Result<crate::EncodedSecret, Bech32Error>;
107}
108
109// Blanket impl to cover any AsRef<[u8]> (e.g., &[u8], Vec<u8>, [u8; N], etc.)
110// encode_lower returns String — requires alloc.
111#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
112impl<T: AsRef<[u8]> + ?Sized> ToBech32 for T {
113    #[inline(always)]
114    fn try_to_bech32(&self, hrp: &str) -> Result<alloc::string::String, Bech32Error> {
115        let hrp_parsed = Hrp::parse(hrp).map_err(|_| Bech32Error::InvalidHrp)?;
116        encode_lower::<Bech32Large>(hrp_parsed, self.as_ref())
117            .map_err(|_| Bech32Error::OperationFailed)
118    }
119
120    #[inline(always)]
121    fn try_to_bech32_zeroizing(&self, hrp: &str) -> Result<crate::EncodedSecret, Bech32Error> {
122        self.try_to_bech32(hrp).map(crate::EncodedSecret::new)
123    }
124}
125
126#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use bech32::primitives::iter::ByteIterExt;
131    use bech32::{decode, encode_lower, Bech32, Fe32, Fe32IterExt, NoChecksum};
132
133    #[test]
134    fn test_bech32_large_with_checksum() {
135        let large_data = vec![0u8; 1000];
136        let hrp = Hrp::parse("test").unwrap();
137        let encoded = encode_lower::<Bech32Large>(hrp, &large_data).unwrap();
138
139        let pos = encoded.rfind('1').unwrap();
140        let hrp_str = &encoded[..pos];
141        let data_str = &encoded[pos + 1..];
142        let decoded_hrp = Hrp::parse(hrp_str).unwrap();
143        let data_part = &data_str[..data_str.len() - 6];
144        let mut fe32s = Vec::new();
145        for c in data_part.chars() {
146            fe32s.push(Fe32::from_char(c).unwrap());
147        }
148        let decoded_data: Vec<u8> = fe32s.iter().copied().fes_to_bytes().collect();
149
150        assert_eq!(decoded_hrp, hrp);
151        assert_eq!(decoded_data, large_data);
152
153        let re_encoded = encode_lower::<Bech32Large>(decoded_hrp, &decoded_data).unwrap();
154        assert_eq!(re_encoded, encoded);
155    }
156
157    #[test]
158    fn test_bit_conversion_large_uncapped() {
159        let large_data = vec![0u8; 4096];
160        let fes: Vec<Fe32> = large_data.iter().copied().bytes_to_fes().collect();
161        assert_eq!(fes.len(), (large_data.len() * 8).div_ceil(5));
162
163        let bytes_back: Vec<u8> = fes.iter().copied().fes_to_bytes().collect();
164        assert_eq!(bytes_back, large_data);
165    }
166
167    #[test]
168    fn test_full_encode_decode_uncapped() {
169        let large_data = vec![0u8; 1000];
170        let hrp = Hrp::parse("test").unwrap();
171        let encoded = encode_lower::<NoChecksum>(hrp, &large_data).unwrap();
172        assert!(encoded.len() > 1000 * 8 / 5);
173
174        let s = &encoded;
175        let pos = s.rfind('1').unwrap();
176        let hrp_str = &s[..pos];
177        let data_str = &s[pos + 1..];
178        let decoded_hrp = Hrp::parse(hrp_str).unwrap();
179        let mut fe32s = Vec::new();
180        for c in data_str.chars() {
181            fe32s.push(Fe32::from_char(c).unwrap());
182        }
183        let decoded_data: Vec<u8> = fe32s.iter().copied().fes_to_bytes().collect();
184        assert_eq!(decoded_hrp, hrp);
185        assert_eq!(decoded_data, large_data);
186    }
187
188    #[test]
189    fn test_bip173_roundtrip() {
190        let data = b"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"[4..].to_vec();
191        let hrp = Hrp::parse("bc").unwrap();
192        let encoded = encode_lower::<Bech32>(hrp, &data).unwrap();
193        let (decoded_hrp, decoded_data) = decode(&encoded).unwrap();
194        assert_eq!(decoded_hrp, hrp);
195        assert_eq!(decoded_data, data);
196    }
197}