secure_gate/encoding/
bech32.rs

1//! Bech32 encoding utilities, supporting both Bech32 and Bech32m variants.
2//!
3//! Provides `Bech32String` for secure handling of Bech32/Bech32m-encoded secrets.
4//! The type stores the encoding variant and offers methods to query it,
5//! decode to bytes, and access the HRP.
6//!
7//! Input strings may be mixed-case (as permitted by the spec). The stored string
8//! is always canonical lowercase.
9//!
10//! # Examples
11//!
12//! ```
13//! # #[cfg(feature = "encoding-bech32")]
14//! # {
15//! use secure_gate::encoding::bech32::Bech32String;
16//!
17//! let bech32 = Bech32String::new("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq".to_string()).unwrap();
18//! assert!(bech32.is_bech32());
19//! # }
20//! ```
21
22#![cfg_attr(not(feature = "zeroize"), forbid(unsafe_code))]
23
24use alloc::string::String;
25
26use bech32::primitives::decode::UncheckedHrpstring;
27use bech32::{decode, primitives::hrp::Hrp, Bech32, Bech32m};
28
29use crate::traits::expose_secret::ExposeSecret;
30
31fn zeroize_input(s: &mut String) {
32    #[cfg(feature = "zeroize")]
33    {
34        zeroize::Zeroize::zeroize(s);
35    }
36    #[cfg(not(feature = "zeroize"))]
37    {
38        let _ = s; // Suppress unused variable warning when zeroize is disabled
39    }
40}
41
42/// The encoding variant used for Bech32 strings.
43///
44/// Bech32 and Bech32m are two similar but incompatible encoding variants.
45/// Bech32m provides stronger error detection and is preferred for new applications.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum EncodingVariant {
48    /// Original Bech32 encoding variant.
49    Bech32,
50    /// Improved Bech32m encoding variant with stronger error detection.
51    Bech32m,
52}
53
54/// Validated Bech32/Bech32m string wrapper for secret data.
55pub struct Bech32String {
56    pub(crate) inner: crate::Dynamic<String>,
57    pub(crate) variant: EncodingVariant,
58}
59
60impl Bech32String {
61    /// Create a new `Bech32String` from a `String`, validating and normalizing it.
62    ///
63    /// Accepts mixed-case input (normalized to lowercase for storage).
64    ///
65    /// # Security Note
66    ///
67    /// **Invalid inputs are only securely zeroized if the `zeroize` feature is enabled.**
68    /// Without `zeroize`, rejected bytes may remain in memory until the `String` is dropped
69    /// normally. Enable the `zeroize` feature for secure wiping of invalid inputs.
70    pub fn new(mut s: String) -> Result<Self, &'static str> {
71        let unchecked = UncheckedHrpstring::new(&s).map_err(|_| "invalid bech32 string")?;
72        let variant = if unchecked.validate_checksum::<Bech32>().is_ok() {
73            EncodingVariant::Bech32
74        } else if unchecked.validate_checksum::<Bech32m>().is_ok() {
75            EncodingVariant::Bech32m
76        } else {
77            zeroize_input(&mut s);
78            return Err("invalid bech32 string");
79        };
80
81        // Normalize to lowercase
82        s.make_ascii_lowercase();
83
84        Ok(Self {
85            inner: crate::Dynamic::new(s),
86            variant,
87        })
88    }
89
90    /// Check if this is a Bech32 encoding.
91    #[inline(always)]
92    pub fn is_bech32(&self) -> bool {
93        self.variant == EncodingVariant::Bech32
94    }
95
96    /// Check if this is a Bech32m encoding.
97    #[inline(always)]
98    pub fn is_bech32m(&self) -> bool {
99        self.variant == EncodingVariant::Bech32m
100    }
101
102    /// Get the Human-Readable Part (HRP) of the string.
103    pub fn hrp(&self) -> Hrp {
104        let (hrp, _) =
105            decode(self.inner.expose_secret().as_str()).expect("Bech32String is always valid");
106        hrp
107    }
108
109    /// Exact number of bytes the decoded payload represents (allocation-free).
110    pub fn byte_len(&self) -> usize {
111        let s = self.inner.expose_secret().as_str();
112        let sep_pos = s.rfind('1').expect("valid bech32 has '1' separator");
113        let data_part_len = s.len() - sep_pos - 1;
114        let data_chars = data_part_len - 6; // subtract checksum
115        (data_chars * 5) / 8
116    }
117
118    /// decode_to_bytes: borrowing, allocates fresh `Vec<u8>` from decoded bytes
119    pub fn decode_to_bytes(&self) -> Vec<u8> {
120        let (_, data) = decode(self.inner.expose_secret().as_str())
121            .expect("Bech32String invariant: always valid");
122        data
123    }
124
125    /// Get the detected encoding variant.
126    pub fn variant(&self) -> EncodingVariant {
127        self.variant
128    }
129
130    /// decode_into_bytes: consuming, decodes then zeroizes the wrapper immediately
131    pub fn decode_into_bytes(self) -> Vec<u8> {
132        let (_, data) =
133            decode(self.inner.expose_secret().as_str()).expect("Bech32String is always valid");
134        data
135    }
136}
137
138/// Constant-time equality (prevents timing attacks when comparing encoded secrets).
139#[cfg(feature = "ct-eq")]
140impl PartialEq for Bech32String {
141    fn eq(&self, other: &Self) -> bool {
142        use crate::traits::ConstantTimeEq;
143        self.inner
144            .expose_secret()
145            .as_bytes()
146            .ct_eq(other.inner.expose_secret().as_bytes())
147    }
148}
149
150/// Regular equality (fallback when `ct-eq` feature is not enabled).
151#[cfg(not(feature = "ct-eq"))]
152impl PartialEq for Bech32String {
153    fn eq(&self, other: &Self) -> bool {
154        self.inner.expose_secret() == other.inner.expose_secret()
155    }
156}
157
158/// Equality implementation.
159impl Eq for Bech32String {}
160
161/// Debug implementation (always redacted).
162impl core::fmt::Debug for Bech32String {
163    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
164        f.write_str("[REDACTED]")
165    }
166}