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
29/// The encoding variant used for Bech32 strings.
30///
31/// Bech32 and Bech32m are two similar but incompatible encoding variants.
32/// Bech32m provides stronger error detection and is preferred for new applications.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum EncodingVariant {
35    /// Original Bech32 encoding variant.
36    Bech32,
37    /// Improved Bech32m encoding variant with stronger error detection.
38    Bech32m,
39}
40
41/// Validated Bech32/Bech32m string wrapper for secret data.
42pub struct Bech32String {
43    pub(crate) inner: crate::Dynamic<String>,
44    pub(crate) variant: EncodingVariant,
45}
46
47impl Bech32String {
48    /// Create a new `Bech32String` from a `String`, validating and normalizing it.
49    ///
50    /// Accepts mixed-case input (normalized to lowercase for storage).
51    ///
52    /// # Security Note
53    ///
54    /// **Invalid inputs are only securely zeroized if the `zeroize` feature is enabled.**
55    /// Without `zeroize`, rejected bytes may remain in memory until the `String` is dropped
56    /// normally. Enable the `zeroize` feature for secure wiping of invalid inputs.
57    pub fn new(mut s: String) -> Result<Self, &'static str> {
58        let unchecked = UncheckedHrpstring::new(&s).map_err(|_| "invalid bech32 string")?;
59        let variant = if unchecked.validate_checksum::<Bech32>().is_ok() {
60            EncodingVariant::Bech32
61        } else if unchecked.validate_checksum::<Bech32m>().is_ok() {
62            EncodingVariant::Bech32m
63        } else {
64            return Err("invalid bech32 string");
65        };
66
67        // Normalize to lowercase
68        s.make_ascii_lowercase();
69
70        Ok(Self {
71            inner: crate::Dynamic::new(s),
72            variant,
73        })
74    }
75
76    /// Create a new `Bech32String` from a validated string, bypassing checks.
77    ///
78    /// # Safety
79    ///
80    /// The input string must be a valid, canonical lowercase Bech32 or Bech32m string.
81    /// Incorrect use can lead to invalid encodings or security issues.
82    pub(crate) fn new_unchecked(s: String, variant: EncodingVariant) -> Self {
83        Self {
84            inner: crate::Dynamic::new(s),
85            variant,
86        }
87    }
88
89    /// Check if this is a Bech32 encoding.
90    #[inline(always)]
91    pub fn is_bech32(&self) -> bool {
92        self.variant == EncodingVariant::Bech32
93    }
94
95    /// Check if this is a Bech32m encoding.
96    #[inline(always)]
97    pub fn is_bech32m(&self) -> bool {
98        self.variant == EncodingVariant::Bech32m
99    }
100
101    /// Get the Human-Readable Part (HRP) of the string.
102    pub fn hrp(&self) -> Hrp {
103        let (hrp, _) =
104            decode(self.inner.expose_secret().as_str()).expect("Bech32String is always valid");
105        hrp
106    }
107
108    /// Exact number of bytes the decoded payload represents (allocation-free).
109    pub fn byte_len(&self) -> usize {
110        let s = self.inner.expose_secret().as_str();
111        let sep_pos = s.find('1').expect("valid bech32 has '1' separator");
112        let data_part_len = s.len() - sep_pos - 1;
113        let data_chars = data_part_len - 6; // subtract checksum
114        (data_chars * 5) / 8
115    }
116
117    /// Length of the encoded string (in characters).
118    #[inline(always)]
119    pub const fn len(&self) -> usize {
120        self.inner.len()
121    }
122
123    /// Whether the encoded string is empty.
124    #[inline(always)]
125    pub const fn is_empty(&self) -> bool {
126        self.inner.is_empty()
127    }
128
129    /// Get the detected encoding variant.
130    pub fn variant(&self) -> EncodingVariant {
131        self.variant
132    }
133
134    /// Decode the validated Bech32/Bech32m string into raw bytes, consuming the wrapper.
135    pub fn into_bytes(self) -> Vec<u8> {
136        let (_, data) =
137            decode(self.inner.expose_secret().as_str()).expect("Bech32String is always valid");
138        data
139    }
140}
141
142/// Constant-time equality (prevents timing attacks when comparing encoded secrets).
143#[cfg(feature = "ct-eq")]
144impl PartialEq for Bech32String {
145    fn eq(&self, other: &Self) -> bool {
146        use crate::ct_eq::ConstantTimeEq;
147        self.inner
148            .expose_secret()
149            .as_bytes()
150            .ct_eq(other.inner.expose_secret().as_bytes())
151    }
152}
153
154/// Regular equality (fallback when `ct-eq` feature is not enabled).
155#[cfg(not(feature = "ct-eq"))]
156impl PartialEq for Bech32String {
157    fn eq(&self, other: &Self) -> bool {
158        self.inner.expose_secret() == other.inner.expose_secret()
159    }
160}
161
162/// Equality implementation.
163impl Eq for Bech32String {}
164
165/// Debug implementation (always redacted).
166impl core::fmt::Debug for Bech32String {
167    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
168        f.write_str("[REDACTED]")
169    }
170}