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}