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