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}