secure_gate/traits/encoding/mod.rs
1//! Encoding traits for explicit secret-to-string conversion.
2//!
3//! > **Import paths:** `use secure_gate::ToHex;` etc. (not `secure_gate::traits::encoding::hex::ToHex`)
4//!
5//! All encoding traits require `alloc`. Every encoder returns
6//! [`EncodedSecret`](crate::EncodedSecret): the encoded form is a second full copy of
7//! the secret, and it stays wiped until it drops.
8//! See the [`decoding`](super::decoding) module for the reverse direction.
9//!
10//! Each encoding trait has its own feature gate:
11//!
12//! | Trait | Feature |
13//! |------------------|---------------------|
14//! | [`ToHex`] | `encoding-hex` |
15//! | [`ToBase32`] | `encoding-base32` |
16//! | [`ToBase64Url`] | `encoding-base64` |
17//! | [`ToBech32`] | `encoding-bech32` |
18//! | [`ToBech32m`] | `encoding-bech32` |
19pub mod base32;
20pub mod base64_url;
21pub mod bech32;
22#[cfg(feature = "encoding-bech32")]
23pub mod bech32m;
24pub mod hex;
25
26/// Which case an encoder should emit.
27///
28/// This parameter appears only on encoders where both cases are canonical and decoding
29/// accepts either — currently bech32 and bech32m, where BIP-173 forbids mixed case and
30/// accepts either pure case, with the checksum defined over the lowercase form.
31///
32/// It is deliberately absent elsewhere, and the absence is the safety property:
33///
34/// - `to_base64url` takes no case, because base64url's alphabet gives `a`–`z` and
35/// `A`–`Z` distinct meanings. Converting the case of a base64url string destroys it.
36/// - `to_base32` takes no case, because RFC 4648 §6 output is uppercase by definition
37/// and the decoder rejects anything else.
38/// - `to_hex` / `to_hex_upper` stay two methods, because hex decoding is case-insensitive
39/// either way and the pair predates this type.
40///
41/// A caller therefore cannot ask for a case conversion that would corrupt the value —
42/// there is no parameter to pass.
43#[cfg(feature = "encoding-bech32")]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum Case {
47 /// Lowercase output. The canonical form for bech32 and bech32m.
48 Lower,
49 /// Uppercase output. Valid per BIP-173, and the form used by key formats such as
50 /// age's `AGE-SECRET-KEY-…`.
51 Upper,
52}
53
54/// Marker for types that may be **encoded**: byte-shaped sources.
55///
56/// *Available whenever any encoding feature is on (`encoding-hex`, `encoding-base32`,
57/// `encoding-base64`, or `encoding-bech32`).*
58///
59/// Every encoding trait in this module is blanket-implemented for
60/// `T: AsRef<[u8]> + EncodableBytes`. The second bound is what keeps string-shaped
61/// types out, and it is load-bearing rather than decorative — removing it changes
62/// which calls compile.
63///
64/// # Why the extra bound exists
65///
66/// `str` and `String` implement `AsRef<[u8]>`, so an `AsRef<[u8]>`-only blanket made
67/// every string an encoding *input* — even though strings are this crate's decoding
68/// input. Two silent wrong answers followed from that:
69///
70/// - `encoded.to_hex()` compiled for an [`EncodedSecret`](crate::EncodedSecret), which
71/// derefs to `str`. It hex-encoded the *encoded text*: a 32-byte key came back as 124
72/// hex characters, with no signature or name to suggest anything was wrong.
73/// - `"text".to_hex()` encoded a string's UTF-8 bytes, which is occasionally what
74/// someone means and usually not.
75///
76/// Both are now compile errors. Write `.as_bytes()` when you did mean the UTF-8.
77///
78/// # Implementing it
79///
80/// Implemented here for `[u8]`, `[u8; N]` and `Vec<u8>`. It is an **opt-in marker** in
81/// the same family as [`CloneableSecret`](crate::CloneableSecret) and
82/// [`SerializableSecret`](crate::SerializableSecret): implement it for your own
83/// byte-shaped newtype to make it encodable.
84///
85/// ```rust
86/// # #[cfg(all(feature = "encoding-hex", feature = "alloc"))] {
87/// use secure_gate::{EncodableBytes, ToHex};
88///
89/// struct Nonce([u8; 12]);
90/// impl AsRef<[u8]> for Nonce {
91/// fn as_ref(&self) -> &[u8] { &self.0 }
92/// }
93/// impl EncodableBytes for Nonce {}
94///
95/// assert_eq!(Nonce([0xAB; 12]).to_hex().len(), 24);
96/// # }
97/// ```
98///
99/// The orphan rule means no downstream crate can implement it for `str` or `String`,
100/// so the hole cannot be reopened from outside.
101#[cfg(any(
102 feature = "encoding-hex",
103 feature = "encoding-base32",
104 feature = "encoding-base64",
105 feature = "encoding-bech32",
106))]
107pub trait EncodableBytes {}
108
109#[cfg(any(
110 feature = "encoding-hex",
111 feature = "encoding-base32",
112 feature = "encoding-base64",
113 feature = "encoding-bech32",
114))]
115impl EncodableBytes for [u8] {}
116
117#[cfg(any(
118 feature = "encoding-hex",
119 feature = "encoding-base32",
120 feature = "encoding-base64",
121 feature = "encoding-bech32",
122))]
123impl<const N: usize> EncodableBytes for [u8; N] {}
124
125#[cfg(all(
126 feature = "alloc",
127 any(
128 feature = "encoding-hex",
129 feature = "encoding-base32",
130 feature = "encoding-base64",
131 feature = "encoding-bech32",
132 )
133))]
134impl EncodableBytes for alloc::vec::Vec<u8> {}
135
136// Every encoding trait produces EncodedSecret — all require alloc
137#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
138pub use base32::ToBase32;
139#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
140pub use base64_url::ToBase64Url;
141#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
142pub use bech32::ToBech32;
143#[cfg(feature = "encoding-bech32")]
144pub use bech32::{BECH32_CODE_LENGTH, Bech32Sized, Bech32Standard, bech32_code_length};
145#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
146pub use bech32m::ToBech32m;
147#[cfg(feature = "encoding-bech32")]
148pub use bech32m::{Bech32mSized, Bech32mStandard};
149#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
150pub use hex::ToHex;