secure_gate/encoding/hex.rs
1// Allow unsafe_code when zeroize is enabled (needed for hex string validation)
2// but forbid it otherwise
3#![cfg_attr(not(feature = "zeroize"), forbid(unsafe_code))]
4
5use alloc::string::String;
6use hex as hex_crate;
7
8use crate::traits::expose_secret::ExposeSecret;
9
10fn zeroize_input(s: &mut String) {
11 #[cfg(feature = "zeroize")]
12 {
13 zeroize::Zeroize::zeroize(s);
14 }
15 #[cfg(not(feature = "zeroize"))]
16 {
17 let _ = s; // Suppress unused variable warning when zeroize is disabled
18 }
19}
20
21/// Validated, lowercase hex string wrapper for secret data.
22///
23/// This struct ensures the contained string is valid hex (even length, valid chars).
24/// Provides methods for decoding back to bytes.
25///
26/// The string is normalized to lowercase during validation.
27///
28/// # Examples
29///
30/// ```
31/// # use secure_gate::{encoding::hex::HexString, ExposeSecret};
32/// let valid = HexString::new("deadbeef".to_string()).unwrap();
33/// assert_eq!(valid.expose_secret(), "deadbeef");
34/// let bytes = valid.decode_into_bytes(); // Vec<u8> of [0xde, 0xad, 0xbe, 0xef]
35/// ```
36pub struct HexString(pub(crate) crate::Dynamic<String>);
37
38impl HexString {
39 /// Create a new `HexString` from a `String`, validating it in-place.
40 ///
41 /// The input `String` is consumed.
42 ///
43 /// # Security Note
44 ///
45 /// **Invalid inputs are only securely zeroized if the `zeroize` feature is enabled.**
46 /// Without `zeroize`, rejected bytes may remain in memory until the `String` is dropped
47 /// normally. Enable the `zeroize` feature for secure wiping of invalid inputs.
48 ///
49 /// Validation rules:
50 /// - Even length
51 /// - Only ASCII hex digits (`0-9`, `a-f`, `A-F`)
52 /// - Uppercase letters are normalized to lowercase
53 ///
54 /// Zero extra allocations are performed – everything happens on the original buffer.
55 ///
56 /// # Errors
57 ///
58 /// Returns `Err("invalid hex string")` if validation fails.
59 ///
60 /// # Example
61 ///
62 /// ```
63 /// use secure_gate::{encoding::hex::HexString, ExposeSecret};
64 /// let valid = HexString::new("deadbeef".to_string()).unwrap();
65 /// assert_eq!(valid.expose_secret(), "deadbeef");
66 /// ```
67 pub fn new(mut s: String) -> Result<Self, &'static str> {
68 // Fast early check – hex strings must have even length
69 if !s.len().is_multiple_of(2) {
70 zeroize_input(&mut s);
71 return Err("invalid hex string");
72 }
73
74 // Work directly on the underlying bytes – no copies
75 let mut bytes = s.into_bytes();
76 let mut valid = true;
77 for b in &mut bytes {
78 match *b {
79 b'A'..=b'F' => *b += 32, // 'A' → 'a'
80 b'a'..=b'f' | b'0'..=b'9' => {}
81 _ => valid = false,
82 }
83 }
84
85 if valid {
86 s = String::from_utf8(bytes).expect("valid UTF-8 after hex normalization");
87 Ok(Self(crate::Dynamic::new(s)))
88 } else {
89 s = String::from_utf8(bytes).unwrap_or_default();
90 zeroize_input(&mut s);
91 Err("invalid hex string")
92 }
93 }
94
95 /// Number of bytes the decoded hex string represents.
96 pub fn byte_len(&self) -> usize {
97 self.0.expose_secret().len() / 2
98 }
99
100 /// decode_to_bytes: borrowing, allocates fresh ` from decoded bytes
101 pub fn decode_to_bytes(&self) -> Vec<u8> {
102 hex_crate::decode(self.expose_secret()).expect("HexString invariant: always valid")
103 }
104
105 /// decode_into_bytes: consuming, decodes then zeroizes the wrapper immediately
106 pub fn decode_into_bytes(self) -> Vec<u8> {
107 hex_crate::decode(self.expose_secret()).expect("HexString invariant: always valid")
108 }
109}
110
111/// Constant-time equality for hex strings — prevents timing attacks when `ct-eq` feature is enabled.
112#[cfg(feature = "ct-eq")]
113impl PartialEq for HexString {
114 fn eq(&self, other: &Self) -> bool {
115 use crate::traits::ConstantTimeEq;
116 self.0
117 .expose_secret()
118 .as_bytes()
119 .ct_eq(other.0.expose_secret().as_bytes())
120 }
121}
122
123#[cfg(not(feature = "ct-eq"))]
124impl PartialEq for HexString {
125 fn eq(&self, other: &Self) -> bool {
126 self.0.expose_secret() == other.0.expose_secret()
127 }
128}
129
130/// Equality implementation for hex strings.
131impl Eq for HexString {}
132
133/// Debug implementation (always redacted).
134impl core::fmt::Debug for HexString {
135 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136 f.write_str("[REDACTED]")
137 }
138}