Skip to main content

secure_gate/traits/encoding/
bech32m.rs

1//! Bech32m encoding trait.
2//!
3//! > **Import path:** `use secure_gate::ToBech32m;`
4//!
5//! This trait provides secure, explicit encoding of byte data to Bech32m strings
6//! (BIP-350 checksum) with a specified HRP. Designed for intentional export.
7//!
8//! Requires the `encoding-bech32m` feature.
9//!
10//! # Security Notes
11//!
12//! - **BIP-350 variant**: Enhanced checksum vs. BIP-173 Bech32 — use Bech32m
13//!   for Taproot, SegWit v1+, and modern address formats.
14//! - **Full secret exposure**: The resulting string contains the **entire** secret.
15//!   Always treat output as sensitive.
16//! - **Zeroizing variants**: Prefer `try_to_bech32m_zeroizing`, which returns [`EncodedSecret`]
17//!   (wrapping `Zeroizing<String>` with redacted `Debug`) when the encoded form remains sensitive.
18//! - **Audit visibility**: Direct wrapper calls (`key.try_to_bech32m(...)`) do **not** appear in
19//!   `grep expose_secret` / `grep with_secret` audit sweeps. For audit-first teams or
20//!   multi-step operations, prefer `with_secret(|b| b.try_to_bech32m(...))` — the borrow
21//!   checker enforces the reference cannot escape the closure.
22//! - **HRP**: pass the intended human-readable part to `try_to_bech32m`; test empty and
23//!   invalid HRP inputs in security-critical code.
24//! - **Standard BIP-350 payload limit (~90 bytes)**: intentionally kept at spec
25//!   compliance for interoperability with Bitcoin Taproot/SegWit v1+ tooling.
26//!   For non-address use cases with large payloads (age-style encryption recipients,
27//!   ciphertexts), use [`ToBech32`](crate::ToBech32) / [`FromBech32Str`](crate::FromBech32Str)
28//!   which use the extended `Bech32Large` variant (~5 KB (5,115 bytes maximum payload)).
29//! - **Treat all input as untrusted**: validate data upstream before wrapping.
30//!
31//! # Example
32//!
33//! ```rust
34//! use secure_gate::{Fixed, ToBech32m, RevealSecret};
35//!
36//! let secret = Fixed::new([0x00u8, 0x01]);
37//!
38//! // Use try_to_bech32m — the sole encoding API:
39//! let encoded = secret.with_secret(|s| s.try_to_bech32m("key")).unwrap();
40//! assert!(encoded.starts_with("key1"));
41//!
42//! // Zeroizing variant for sensitive encoded output:
43//! let encoded_z = secret.try_to_bech32m_zeroizing("key")?;
44//! assert!(encoded_z.starts_with("key1"));
45//! // encoded_z is EncodedSecret — zeroized on drop, redacted Debug
46//! # Ok::<(), secure_gate::Bech32Error>(())
47//! ```
48#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
49use bech32::encode_lower;
50#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
51use bech32::{Bech32m, Hrp};
52
53#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
54use crate::error::Bech32Error;
55
56/// Extension trait for encoding byte data as Bech32m (BIP-350) strings.
57///
58/// *Requires feature `encoding-bech32m`.*
59///
60/// Blanket-implemented for all `AsRef<[u8]>` types. Use [`try_to_bech32m`](Self::try_to_bech32m)
61/// with the protocol's HRP. Test empty and invalid HRP inputs in security-critical code.
62///
63/// **Design note — intentional size asymmetry**: `ToBech32m` targets BIP-350
64/// (Bitcoin Taproot/SegWit v1+ addresses, typically 20–40 bytes). The 90-byte spec
65/// limit is deliberate; oversized Bech32m strings break interoperability with wallets
66/// and address parsers. For large secrets (encryption recipients, ciphertexts,
67/// arbitrary keys ≥ ~50 bytes), use [`ToBech32`](crate::ToBech32) / `Bech32Large`.
68#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
69pub trait ToBech32m {
70    /// Fallibly encodes bytes as a Bech32m (BIP-350) string with the given HRP.
71    ///
72    /// # Errors
73    ///
74    /// - [`Bech32Error::InvalidHrp`] — `hrp` contains invalid characters.
75    /// - [`Bech32Error::OperationFailed`] — encoding failure (e.g., data too large).
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use secure_gate::ToBech32m;
81    ///
82    /// let encoded = b"hello".try_to_bech32m("key")?;
83    /// assert!(encoded.starts_with("key1"));
84    /// # Ok::<(), secure_gate::Bech32Error>(())
85    /// ```
86    fn try_to_bech32m(&self, hrp: &str) -> Result<alloc::string::String, Bech32Error>;
87
88    /// Fallibly encodes bytes as Bech32m and wraps the result in [`crate::EncodedSecret`].
89    fn try_to_bech32m_zeroizing(&self, hrp: &str) -> Result<crate::EncodedSecret, Bech32Error>;
90}
91
92// Blanket impl to cover any AsRef<[u8]> (e.g., &[u8], Vec<u8>, [u8; N], etc.)
93// encode_lower returns String — requires alloc.
94#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
95impl<T: AsRef<[u8]> + ?Sized> ToBech32m for T {
96    #[inline(always)]
97    fn try_to_bech32m(&self, hrp: &str) -> Result<alloc::string::String, Bech32Error> {
98        let hrp_parsed = Hrp::parse(hrp).map_err(|_| Bech32Error::InvalidHrp)?;
99        encode_lower::<Bech32m>(hrp_parsed, self.as_ref()).map_err(|_| Bech32Error::OperationFailed)
100    }
101
102    #[inline(always)]
103    fn try_to_bech32m_zeroizing(&self, hrp: &str) -> Result<crate::EncodedSecret, Bech32Error> {
104        self.try_to_bech32m(hrp).map(crate::EncodedSecret::new)
105    }
106}
107
108#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
109#[cfg(test)]
110mod tests {
111    use bech32::{encode_lower, Bech32m, Hrp};
112
113    #[test]
114    #[should_panic(expected = "TooLong")]
115    fn test_capped_overflow_bech32m() {
116        let large_data = vec![0u8; 800];
117        let hrp = Hrp::parse("test").unwrap();
118        let _ = encode_lower::<Bech32m>(hrp, &large_data).unwrap();
119    }
120}