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}