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}