Skip to main content

secure_gate/traits/decoding/
base32.rs

1//! Base32 decoding trait (RFC 4648 §6, uppercase, unpadded).
2//!
3//! > **Import path:** `use secure_gate::FromBase32Str;`
4//!
5//! This trait provides secure, explicit decoding of Base32-encoded strings
6//! (RFC 4648 §6 alphabet, uppercase, no padding) to byte vectors. It is designed for
7//! handling untrusted input in cryptographic contexts, such as decoding `otpauth://`
8//! TOTP/HOTP shared secrets or encoded keys.
9//!
10//! Requires the `encoding-base32` feature.
11//!
12//! # Security Notes
13//!
14//! - **Treat all input as untrusted**: validate Base32 strings upstream before
15//!   wrapping in secrets. Invalid input may indicate tampering or injection attempts.
16//! - **Heap allocation**: Returns `Vec<u8>` — wrap in [`Fixed`](crate::Fixed) or
17//!   [`Dynamic`](crate::Dynamic) to store as a secret.
18//! - **Strict validation**: uppercase RFC 4648 §6 alphabet (`A`–`Z`, `2`–`7`) only,
19//!   no `=` padding, no whitespace, no non-ASCII, and the length must be ≡ 0, 2, 4, 5
20//!   or 7 (mod 8) — the only lengths an unpadded Base32 string can have. Lowercase and
21//!   mixed-case input is rejected; normalise upstream (`to_ascii_uppercase()`, ideally
22//!   inside `zeroize::Zeroizing`, since it copies the encoded secret).
23//! - **Lenient trailing bits**: non-zero bits in the final partial group are ignored
24//!   rather than rejected (`"MZ"` decodes to the same byte as `"MY"`). Decoding is
25//!   therefore not injective: two distinct strings can yield the same bytes. Only
26//!   encoder-produced strings are canonical, so round-tripping
27//!   `encode(decode(s)) == s` holds only for canonical `s`.
28//!
29//! # Example
30//!
31//! ```rust
32//! # #[cfg(feature = "encoding-base32")]
33//! use secure_gate::{FromBase32Str, Fixed};
34//! # #[cfg(feature = "encoding-base32")]
35//! {
36//! // "NBSWY3DP" decodes to b"hello"
37//! let bytes = "NBSWY3DP".try_from_base32().unwrap();
38//! assert_eq!(bytes, b"hello");
39//!
40//! // Wrap result in a secret immediately
41//! let secret: Fixed<[u8; 4]> = Fixed::try_from_base32("32W353Y").unwrap();
42//!
43//! // Error on invalid input
44//! assert!("!!!!".try_from_base32().is_err());
45//! }
46//! ```
47#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
48use crate::error::Base32Error;
49
50/// Extension trait for decoding uppercase, unpadded Base32 strings into byte vectors.
51///
52/// *Requires features `encoding-base32` and `alloc`.*
53///
54/// Blanket-implemented for all `AsRef<str>` types. Returns `Vec<u8>` — requires heap
55/// allocation. For no-alloc targets, use `Fixed::try_from_base32` instead, which
56/// decodes directly into a stack-allocated `[u8; N]` buffer.
57///
58/// Uses the RFC 4648 §6 alphabet (`A`–`Z`, `2`–`7`) without `=` padding.
59///
60/// **The returned `Vec<u8>` is plain heap memory and is not zeroized on drop.** Wrap
61/// the result in [`Fixed`](crate::Fixed) or [`Dynamic`](crate::Dynamic) immediately
62/// (or in [`zeroize::Zeroizing`]) if the decoded bytes are sensitive. Prefer
63/// `Fixed::try_from_base32` / `Dynamic::try_from_base32`, which perform the
64/// wrapping for you and zeroize their internal temporaries.
65///
66/// Treat all input as untrusted; validate lengths and content upstream before wrapping
67/// decoded bytes in secrets.
68#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
69pub trait FromBase32Str {
70    /// Decodes an uppercase, unpadded Base32 string into a byte vector.
71    ///
72    /// # Errors
73    ///
74    /// - [`Base32Error::InvalidBase32`] — wrong alphabet or case, `=` padding,
75    ///   whitespace, or a length that is impossible for unpadded Base32.
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use secure_gate::FromBase32Str;
81    ///
82    /// // "NBSWY3DP" decodes to b"hello"
83    /// let bytes = "NBSWY3DP".try_from_base32()?;
84    /// assert_eq!(bytes, b"hello");
85    ///
86    /// assert!("nbswy3dp".try_from_base32().is_err()); // lowercase
87    /// assert!("MY======".try_from_base32().is_err()); // padding
88    /// assert!("!!!!".try_from_base32().is_err()); // invalid chars
89    /// # Ok::<(), secure_gate::Base32Error>(())
90    /// ```
91    fn try_from_base32(&self) -> Result<alloc::vec::Vec<u8>, Base32Error>;
92}
93
94// Blanket impl to cover any AsRef<str> (e.g., &str, String, etc.)
95// Returns Vec<u8> — alloc required.
96#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
97impl<T: AsRef<str> + ?Sized> FromBase32Str for T {
98    fn try_from_base32(&self) -> Result<alloc::vec::Vec<u8>, Base32Error> {
99        use base32ct::{Base32UpperUnpadded, Encoding};
100        Base32UpperUnpadded::decode_vec(self.as_ref()).map_err(|_| Base32Error::InvalidBase32)
101    }
102}