Skip to main content

bouncycastle_hex/
lib.rs

1//! Good old fashioned hex encoder and decoder.
2//!
3//! This one is implemented using constant-time operations in the conversions
4//! from Strings to byte values, so it is safe to use on cryptographic secret values.
5//!
6//! It should just work as expected: 
7//! encode takes any bytes-like rust type and returns a String, 
8//! decode takes a String (which can be in any bytes-like container) and returns a `Vec<u8>`.
9//!
10//! Moreover, the API of this crate is intended to mirror that of the public `hex` crate,
11//! so you should generally be able to swap `use hex` for `use bouncycastle_hex` and all the function
12//! calls and behaviours should work as expected.
13//! 
14//! ```
15//! use bouncycastle_hex as hex;
16//!
17//! let out = hex::encode(b"\x00\x01\x02\x03"); // "00010203"
18//! let out = hex::encode(&[0x00, 0x01, 0x02, 0x03]); // "00010203"
19//! let out = hex::encode(vec![0x00, 0x01, 0x02, 0x03]); // "00010203"
20//!
21//! let out = hex::decode("00010203").unwrap(); // [0x00, 0x01, 0x02, 0x03]
22//! let out = hex::decode(b"00010203").unwrap(); // [0x00, 0x01, 0x02, 0x03]
23//! ```
24//!
25//! The decoder ignores whitespace and "\x".
26
27#![forbid(unsafe_code)]
28#![forbid(missing_docs)]
29
30use bouncycastle_utils::ct::Condition;
31
32/// Return type for errors relating to Hex encoding and decoding.
33#[derive(Debug)]
34pub enum HexError {
35    /// Invalid hex character encountered at the given index.
36    InvalidHexCharacter(usize),
37    /// Since hex encodes each byte as two characters, the input must have an even length.
38    OddLengthInput,
39    ///
40    InsufficientOutputBufferSize,
41}
42
43/// One-shot encode from bytes to a hex-encoded string using a constant-time implementation.
44pub fn encode<T: AsRef<[u8]>>(input: T) -> String {
45    let mut out = vec![0u8; input.as_ref().len() * 2];
46    encode_out(input.as_ref(), &mut out).unwrap();
47
48    String::from_utf8(out).unwrap()
49}
50
51/// expects an output array which is at least input.len() / 2 in size.
52/// Returns the number of bytes written.
53pub fn encode_out<T: AsRef<[u8]>>(input: T, out: &mut [u8]) -> Result<usize, HexError> {
54    let inref = input.as_ref();
55    if out.len() < inref.len() * 2 {
56        return Err(HexError::InsufficientOutputBufferSize);
57    }
58
59    out.fill(0);
60
61    for i in 0..inref.len() {
62        out[2 * i] = ct_word_to_hex(inref[i] >> 4);
63        out[2 * i + 1] = ct_word_to_hex(inref[i] & 0x0F);
64    }
65    return Ok(inref.len() * 2);
66
67    /// Expects a 4-bit word in the least significant bits.
68    fn ct_word_to_hex(mut c: u8) -> u8 {
69        // Make sure there's nothing in the top bits
70        c &= 0x0F;
71
72        // let in_09 = Condition::<i64>::is_within_range(c as i64, 0, 9);
73        let in_af = Condition::<i64>::is_within_range(c as i64, 10, 15);
74
75        // TODO: redo this once we have ct::u8 implemented 
76        // The i64 is wasteful
77
78        let c_09: i64 = '0' as i64 + (c as i64);
79        let c_az: i64 = 'a' as i64 + (c as i64 - 10);
80
81        let mut ret: i64 = c_09 as i64;
82        ret = in_af.select(c_az as i64, ret);
83        ret as u8
84    }
85}
86
87/// One-shot decode from a hex string to a bytes using a constant-time implementation.
88/// ignores whitespace and \x
89pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, HexError> {
90    let inref = input.as_ref();
91    let mut out: Vec<u8> = vec![0u8; inref.len() / 2];
92    let bytes_written = decode_out(inref, &mut out)?;
93    out.truncate(bytes_written);
94    Ok(out)
95}
96
97/// expects an output array which is at least input.len() / 2 in size.
98/// Returns the number of bytes written.
99pub fn decode_out<T: AsRef<[u8]>>(input: T, out: &mut [u8]) -> Result<usize, HexError> {
100    let inref = input.as_ref();
101    if out.len() < inref.len() / 2 {
102        return Err(HexError::InsufficientOutputBufferSize);
103    }
104
105    out.fill(0);
106
107    let mut b = 0u8;
108    let mut b_i = 0u8;
109    let mut out_i = 0_usize;
110    let mut i = 0_usize;
111    while i < inref.len() {
112        let c = inref[i];
113
114        // first check for whitespace and string null terminators, \x and invalid characters, 
115        // which unfortunately cannot be done fully constant-time.
116        match c {
117            b' ' | b'\t' | b'\n' | b'\r' | 0 => {
118                i += 1;
119                continue;
120            }
121            b'\\' => {
122                if inref[i + 1] == b'x' {
123                    i += 2;
124                    continue;
125                }
126            }
127            _ => {}
128        }
129
130        // parse two hex digits to form one output byte;
131        // the first one is the upper 4 bits.
132        b |= match ct_hex_to_word(c) {
133            0xFF => return Err(HexError::InvalidHexCharacter(i)),
134            c => c,
135        } << (4 * (1 - b_i));
136
137        if b_i == 1 {
138            out[out_i] = b;
139            out_i += 1;
140            b = 0;
141            b_i = 0;
142        } else {
143            b_i = 1;
144        }
145        i += 1;
146    }
147    // if b_i != 0, then we have an un-processed word in the buffer.
148    if b_i != 0 {
149        return Err(HexError::OddLengthInput);
150    }
151
152    return Ok(out_i);
153
154    fn ct_hex_to_word(b: u8) -> u8 {
155        let in_09 = Condition::<i64>::is_within_range(b as i64, 48, 57);
156        let in_af = Condition::<i64>::is_within_range(b as i64, 97, 102);
157        #[allow(non_snake_case)]
158        let in_AF = Condition::<i64>::is_within_range(b as i64, 65, 70);
159
160        // TODO: redo this once we have ct::u8 implemented 
161        // The i64 is wasteful
162
163        let c_09: i64 = b as i64 - ('0' as i64);
164        #[allow(non_snake_case)]
165        let c_AF: i64 = b as i64 - ('A' as i64) + 10;
166        let c_af: i64 = b as i64 - ('a' as i64) + 10;
167
168        let mut ret: i64 = 0xFFi64;
169
170        ret = in_09.select(c_09, ret);
171        ret = in_AF.select(c_AF, ret);
172        ret = in_af.select(c_af, ret);
173
174        ret as u8
175    }
176}