Skip to main content

lexe_hex/
hex.rs

1//! Utilities for encoding, decoding, and displaying lowercase hex/base16
2//! formatted data.
3//!
4//! Encoding and decoding is also designed to be (likely) constant time
5//! (as much as we can without diving into manual assembly), which allows us to
6//! safely hex::encode and hex::decode secrets without potentially leaking bits
7//! via timing side channels.
8
9use std::{
10    borrow::Cow,
11    fmt::{self, Write},
12};
13
14/// Errors which can be produced while decoding a hex string.
15#[derive(Copy, Clone, Debug)]
16pub enum DecodeError {
17    BadOutputLength,
18    InvalidCharacter,
19    OddInputLength,
20}
21
22impl std::error::Error for DecodeError {}
23
24impl fmt::Display for DecodeError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        let s = match self {
27            Self::BadOutputLength =>
28                "output buffer length != half input length",
29            Self::InvalidCharacter => "input contains non-hex character",
30            Self::OddInputLength => "input string length must be even",
31        };
32        write!(f, "hex decode error: {s}")
33    }
34}
35
36// --- Public functions --- //
37
38/// Convert a byte slice to an owned hex string. If you simply need to display a
39/// byte slice as hex, use [`display`] instead, which avoids the allocation.
40pub fn encode(bytes: &[u8]) -> String {
41    let mut out = vec![0u8; bytes.len() * 2];
42
43    for (src, dst) in bytes.iter().zip(out.chunks_exact_mut(2)) {
44        dst[0] = encode_nibble(src >> 4);
45        dst[1] = encode_nibble(src & 0x0f);
46    }
47
48    // SAFETY: hex characters ([0-9a-f]*) are always valid UTF-8.
49    unsafe { String::from_utf8_unchecked(out) }
50}
51
52/// Try to decode a hex string to owned bytes (`Vec<u8>`).
53pub fn decode(hex: &str) -> Result<Vec<u8>, DecodeError> {
54    let hex_chunks = hex_str_to_chunks(hex)?;
55    let mut out = vec![0u8; hex_chunks.len()];
56    decode_to_slice_inner(hex_chunks, &mut out).map(|()| out)
57}
58
59/// A `const fn` to decode a hex string to a fixed-length array at compile time.
60/// Panics if the input was not a valid hex string.
61///
62/// To decode to a fixed-length array without panicking on invalid inputs, use
63/// the [`FromHex`] trait instead, e.g. `<[u8; 32]>::from_hex(&s)`.
64pub const fn decode_const<const N: usize>(hex: &[u8]) -> [u8; N] {
65    if hex.len() != N * 2 {
66        panic!("hex input is the wrong length");
67    }
68
69    let mut bytes = [0u8; N];
70    let mut idx = 0;
71    let mut err = 0;
72
73    while idx < N {
74        let b_hi = decode_nibble(hex[2 * idx]);
75        let b_lo = decode_nibble(hex[(2 * idx) + 1]);
76        let byte = (b_hi << 4) | b_lo;
77        err |= byte >> 8;
78        bytes[idx] = byte as u8;
79        idx += 1;
80    }
81
82    match err {
83        0 => bytes,
84        _ => panic!("invalid hex char"),
85    }
86}
87
88/// Decodes a hex string into an output buffer. This is also designed to be
89/// (likely) constant time.
90pub fn decode_to_slice(hex: &str, out: &mut [u8]) -> Result<(), DecodeError> {
91    let hex_chunks = hex_str_to_chunks(hex)?;
92    decode_to_slice_inner(hex_chunks, out)
93}
94
95/// Get a [`HexDisplay`] which provides a `Debug` and `Display` impl for the
96/// given byte slice. Useful for displaying a hex value without allocating.
97///
98/// Example:
99///
100/// ```
101/// use lexe_hex::hex;
102/// let bytes = [69u8; 32];
103/// println!("Bytes as hex: {}", hex::display(&bytes));
104/// ```
105#[inline]
106pub fn display(bytes: &[u8]) -> HexDisplay<'_> {
107    HexDisplay(bytes)
108}
109
110// --- FromHex trait --- //
111
112/// A trait to deserialize something from a hex-encoded string slice.
113///
114/// Examples:
115///
116/// ```
117/// # use std::borrow::Cow;
118/// use lexe_hex::hex::FromHex;
119/// let s = String::from("e7f51d925349a26f742e6eef3670f489aaf14fbbb5b5c3f209892f2f1baae1c9");
120///
121/// <Vec<u8>>::from_hex(&s).unwrap();
122/// <Cow<'_, [u8]>>::from_hex(&s).unwrap();
123/// <[u8; 32]>::from_hex(&s).unwrap();
124/// ```
125pub trait FromHex: Sized {
126    fn from_hex(s: &str) -> Result<Self, DecodeError>;
127}
128
129impl FromHex for Vec<u8> {
130    fn from_hex(s: &str) -> Result<Self, DecodeError> {
131        decode(s)
132    }
133}
134
135impl FromHex for Cow<'_, [u8]> {
136    fn from_hex(s: &str) -> Result<Self, DecodeError> {
137        decode(s).map(Cow::Owned)
138    }
139}
140
141impl<const N: usize> FromHex for [u8; N] {
142    fn from_hex(s: &str) -> Result<Self, DecodeError> {
143        let mut out = [0u8; N];
144        decode_to_slice(s, out.as_mut_slice())?;
145        Ok(out)
146    }
147}
148
149// --- HexDisplay implementation --- //
150
151/// Provides `Debug` and `Display` impls for a byte slice.
152/// Useful for displaying hex value without allocating via [`encode`].
153pub struct HexDisplay<'a>(&'a [u8]);
154
155impl fmt::Display for HexDisplay<'_> {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        for byte in self.0 {
158            f.write_char(encode_nibble(byte >> 4) as char)?;
159            f.write_char(encode_nibble(byte & 0x0f) as char)?;
160        }
161        Ok(())
162    }
163}
164
165impl fmt::Debug for HexDisplay<'_> {
166    #[inline]
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "\"{self}\"")
169    }
170}
171
172// --- Internal helpers --- //
173
174fn hex_str_to_chunks(hex: &str) -> Result<&[[u8; 2]], DecodeError> {
175    let (hex_chunks, extra) = hex.as_bytes().as_chunks::<2>();
176    if extra.is_empty() {
177        Ok(hex_chunks)
178    } else {
179        Err(DecodeError::OddInputLength)
180    }
181}
182
183fn decode_to_slice_inner(
184    hex_chunks: &[[u8; 2]],
185    out: &mut [u8],
186) -> Result<(), DecodeError> {
187    if hex_chunks.len() != out.len() {
188        return Err(DecodeError::BadOutputLength);
189    }
190
191    let mut err = 0;
192    for (&[c_hi, c_lo], out_i) in hex_chunks.iter().zip(out) {
193        let byte = (decode_nibble(c_hi) << 4) | decode_nibble(c_lo);
194        err |= byte >> 8;
195        *out_i = byte as u8;
196    }
197
198    match err {
199        0 => Ok(()),
200        _ => Err(DecodeError::InvalidCharacter),
201    }
202}
203
204/// Encode a single nibble to hex. This encode fn is also designed to be
205/// (likely) constant time (as far as we can guarantee w/o dropping into
206/// assembly).
207#[inline(always)]
208#[allow(non_upper_case_globals)]
209const fn encode_nibble(nib: u8) -> u8 {
210    // nib ∈ [0, 15]
211    //
212    //                     nib >= 10
213    //                         |
214    //                         v
215    // [         ] -- gap9a -- [         ]
216    // 0 1 2 ... 9 : ; ... _ ` a b ... e f
217
218    const b_0: i16 = b'0' as i16;
219    const b_9: i16 = b'9' as i16;
220    const b_a: i16 = b'a' as i16;
221
222    let nib = nib as i16;
223    let base = nib + b_0;
224    // `hex::encode` is used to encode secrets. Don't branch on secrets.
225    // Though, this branch version doesn't codegen to a branch on
226    // x86_64 + opt-level=3.
227    //
228    // equiv: let gap_9a = if nib >= 10 { b'a' - b'9' - 1 } else { 0 };
229    let gap_9a = ((b_9 - b_0 - nib) >> 8) & (b_a - b_9 - 1);
230    (base + gap_9a) as u8
231}
232
233/// Decode a single nibble of lower hex
234#[inline(always)]
235const fn decode_nibble(src: u8) -> u16 {
236    // 0-9  0x30-0x39
237    // a-f  0x61-0x66
238    let byte = src as i16;
239    let mut ret: i16 = -1;
240
241    // 0-9  0x30-0x39
242    // if (byte > 0x2f && byte < 0x3a) ret += byte - 0x30 + 1; // -47
243    ret += (((0x2fi16 - byte) & (byte - 0x3a)) >> 8) & (byte - 47);
244    // a-f  0x61-0x66
245    // if (byte > 0x60 && byte < 0x67) ret += byte - 0x61 + 10 + 1; // -86
246    ret += (((0x60i16 - byte) & (byte - 0x67)) >> 8) & (byte - 86);
247
248    ret as u16
249}
250
251#[cfg(test)]
252mod test {
253    use proptest::{
254        arbitrary::any, char, collection::vec, prop_assert_eq, proptest,
255        strategy::Strategy,
256    };
257
258    use super::*;
259
260    #[inline]
261    fn is_even(x: usize) -> bool {
262        x & 1 == 0
263    }
264
265    #[test]
266    fn test_encode() {
267        assert_eq!("", encode(&[]));
268        assert_eq!(
269            "01348900abff",
270            encode(&[0x01, 0x34, 0x89, 0x00, 0xab, 0xff])
271        );
272    }
273
274    #[test]
275    fn test_decode_const() {
276        const FOO: [u8; 6] = decode_const(b"01348900abff");
277        assert_eq!(&FOO, &[0x01, 0x34, 0x89, 0x00, 0xab, 0xff]);
278    }
279
280    #[test]
281    fn test_roundtrip_b2s2b() {
282        let bytes = &[0x01, 0x34, 0x89, 0x00, 0xab, 0xff];
283        assert_eq!(bytes.as_slice(), decode(&encode(bytes)).unwrap());
284
285        proptest!(|(bytes in vec(any::<u8>(), 0..10))| {
286            assert_eq!(bytes.as_slice(), decode(&encode(&bytes)).unwrap());
287        })
288    }
289
290    #[test]
291    fn test_roundtrip_s2b2s() {
292        let hex = "01348900abff";
293        assert_eq!(hex, encode(&decode(hex).unwrap()));
294
295        let hex_char = char::ranges(['0'..='9', 'a'..='f'].as_slice().into());
296        let hex_chars = vec(hex_char, 0..10);
297        let hex_strs =
298            hex_chars.prop_filter_map("no odd length hex strings", |chars| {
299                if is_even(chars.len()) {
300                    Some(String::from_iter(chars))
301                } else {
302                    None
303                }
304            });
305
306        proptest!(|(hex in hex_strs)| {
307            assert_eq!(hex.to_ascii_lowercase(), encode(&decode(&hex).unwrap()));
308        })
309    }
310
311    #[test]
312    fn test_encode_display_equiv() {
313        proptest!(|(bytes: Vec<u8>)| {
314            let out1 = encode(&bytes);
315            let out2 = display(&bytes).to_string();
316            prop_assert_eq!(out1, out2);
317        });
318    }
319}