ct_codecs/
lib.rs

1//! # CT-Codecs
2//!
3//! A Rust implementation of constant-time Base64 and Hexadecimal codecs,
4//! reimplemented from libsodium and libhydrogen.
5//!
6//! ## Features
7//!
8//! - **Constant-time implementation** for cryptographic applications where timing attacks are a concern
9//! - **Strict validation** ensuring Base64 strings are not malleable
10//! - **Multiple variants** of Base64: standard, URL-safe, with and without padding
11//! - **Character filtering** for ignoring specific characters during decoding (like whitespace)
12//! - **Zero dependencies** and **`no_std` compatible**
13//! - **Memory safety** with `#![forbid(unsafe_code)]`
14//!
15//! ## Usage Examples
16//!
17//! ### Base64 Encoding
18//!
19//! ```
20//! use ct_codecs::{Base64, Encoder};
21//!
22//! fn example() -> Result<(), ct_codecs::Error> {
23//!     let data = b"Hello, world!";
24//!     let encoded = Base64::encode_to_string(data)?;
25//!     assert_eq!(encoded, "SGVsbG8sIHdvcmxkIQ==");
26//!     Ok(())
27//! }
28//! # example().unwrap();
29//! ```
30//!
31//! ### Base64 Decoding
32//!
33//! ```
34//! use ct_codecs::{Base64, Decoder};
35//!
36//! fn example() -> Result<(), ct_codecs::Error> {
37//!     let encoded = "SGVsbG8sIHdvcmxkIQ==";
38//!     let decoded = Base64::decode_to_vec(encoded, None)?;
39//!     assert_eq!(decoded, b"Hello, world!");
40//!     Ok(())
41//! }
42//! # example().unwrap();
43//! ```
44//!
45//! ### Hexadecimal Encoding/Decoding
46//!
47//! ```
48//! use ct_codecs::{Hex, Encoder, Decoder};
49//!
50//! fn example() -> Result<(), ct_codecs::Error> {
51//!     let data = b"Hello, world!";
52//!     let encoded = Hex::encode_to_string(data)?;
53//!     let decoded = Hex::decode_to_vec(&encoded, None)?;
54//!     assert_eq!(decoded, data);
55//!     Ok(())
56//! }
57//! # example().unwrap();
58//! ```
59//!
60//! ### No-std Usage with Pre-allocated Buffers
61//!
62//! ```
63//! use ct_codecs::{Base64, Encoder, Decoder};
64//!
65//! fn example() -> Result<(), ct_codecs::Error> {
66//!     let data = b"Hello, world!";
67//!     let mut encoded_buf = [0u8; 20]; // Must be large enough
68//!     let encoded = Base64::encode(&mut encoded_buf, data)?;
69//!     
70//!     let mut decoded_buf = [0u8; 13]; // Must be large enough
71//!     let decoded = Base64::decode(&mut decoded_buf, encoded, None)?;
72//!     assert_eq!(decoded, data);
73//!     Ok(())
74//! }
75//! # example().unwrap();
76//! ```
77
78#![cfg_attr(not(feature = "std"), no_std)]
79#![forbid(unsafe_code)]
80
81mod base64;
82mod error;
83mod hex;
84
85pub use base64::*;
86pub use error::*;
87pub use hex::*;
88
89/// Trait for encoding binary data into text representations.
90///
91/// Implementors of this trait provide constant-time encoding operations
92/// for a specific encoding format (Base64, Hex, etc.).
93pub trait Encoder {
94    /// Calculates the length of the encoded output for a given binary input length.
95    ///
96    /// # Arguments
97    ///
98    /// * `bin_len` - The length of the binary input in bytes
99    ///
100    /// # Returns
101    ///
102    /// * `Ok(usize)` - The required length for the encoded output
103    /// * `Err(Error::Overflow)` - If the calculation would overflow
104    fn encoded_len(bin_len: usize) -> Result<usize, Error>;
105
106    /// Encodes binary data into a text representation.
107    ///
108    /// # Arguments
109    ///
110    /// * `encoded` - Mutable buffer to store the encoded output
111    /// * `bin` - Binary input data to encode
112    ///
113    /// # Returns
114    ///
115    /// * `Ok(&[u8])` - A slice of the encoded buffer containing the encoded data
116    /// * `Err(Error::Overflow)` - If the output buffer is too small
117    fn encode<IN: AsRef<[u8]>>(encoded: &mut [u8], bin: IN) -> Result<&[u8], Error>;
118
119    /// Encodes binary data and returns the result as a string slice.
120    ///
121    /// # Arguments
122    ///
123    /// * `encoded` - Mutable buffer to store the encoded output
124    /// * `bin` - Binary input data to encode
125    ///
126    /// # Returns
127    ///
128    /// * `Ok(&str)` - A string slice containing the encoded data
129    /// * `Err(Error::Overflow)` - If the output buffer is too small
130    fn encode_to_str<IN: AsRef<[u8]>>(encoded: &mut [u8], bin: IN) -> Result<&str, Error> {
131        Ok(core::str::from_utf8(Self::encode(encoded, bin)?).unwrap())
132    }
133
134    /// Encodes binary data and returns the result as a String.
135    ///
136    /// This method is only available when the `std` feature is enabled.
137    ///
138    /// # Arguments
139    ///
140    /// * `bin` - Binary input data to encode
141    ///
142    /// # Returns
143    ///
144    /// * `Ok(String)` - A String containing the encoded data
145    /// * `Err(Error::Overflow)` - If the calculation would overflow
146    #[cfg(feature = "std")]
147    fn encode_to_string<IN: AsRef<[u8]>>(bin: IN) -> Result<String, Error> {
148        let mut encoded = vec![0u8; Self::encoded_len(bin.as_ref().len())?];
149        let encoded_len = Self::encode(&mut encoded, bin)?.len();
150        encoded.truncate(encoded_len);
151        Ok(String::from_utf8(encoded).unwrap())
152    }
153}
154
155/// Trait for decoding text representations back into binary data.
156///
157/// Implementors of this trait provide constant-time decoding operations
158/// for a specific encoding format (Base64, Hex, etc.).
159pub trait Decoder {
160    /// Decodes text data back into its binary representation.
161    ///
162    /// # Arguments
163    ///
164    /// * `bin` - Mutable buffer to store the decoded output
165    /// * `encoded` - Text input data to decode
166    /// * `ignore` - Optional set of characters to ignore during decoding (e.g., whitespace)
167    ///
168    /// # Returns
169    ///
170    /// * `Ok(&[u8])` - A slice of the binary buffer containing the decoded data
171    /// * `Err(Error::Overflow)` - If the output buffer is too small
172    /// * `Err(Error::InvalidInput)` - If the input contains invalid characters
173    fn decode<'t, IN: AsRef<[u8]>>(
174        bin: &'t mut [u8],
175        encoded: IN,
176        ignore: Option<&[u8]>,
177    ) -> Result<&'t [u8], Error>;
178
179    /// Decodes text data and returns the result as a Vec<u8>.
180    ///
181    /// This method is only available when the `std` feature is enabled.
182    ///
183    /// # Arguments
184    ///
185    /// * `encoded` - Text input data to decode
186    /// * `ignore` - Optional set of characters to ignore during decoding (e.g., whitespace)
187    ///
188    /// # Returns
189    ///
190    /// * `Ok(Vec<u8>)` - A Vec containing the decoded binary data
191    /// * `Err(Error::InvalidInput)` - If the input contains invalid characters
192    #[cfg(feature = "std")]
193    fn decode_to_vec<IN: AsRef<[u8]>>(
194        encoded: IN,
195        ignore: Option<&[u8]>,
196    ) -> Result<Vec<u8>, Error> {
197        let mut bin = vec![0u8; encoded.as_ref().len()];
198        let bin_len = Self::decode(&mut bin, encoded, ignore)?.len();
199        bin.truncate(bin_len);
200        Ok(bin)
201    }
202}
203
204/// Constant-time equality check for two byte slices.
205///
206/// # Arguments
207///
208/// * `x` - First byte slice
209/// * `y` - Second byte slice
210///
211/// # Returns
212///
213/// * `bool` - `true` if the slices are equal, `false` otherwise
214pub fn verify(x: &[u8], y: &[u8]) -> bool {
215    if x.len() != y.len() {
216        return false;
217    }
218    let mut v: u32 = 0;
219    // Old Rust versions, don't have black_box(), using volatile is unsafe,
220    // and WebAssembly doesn't support volatile and ignores black_box() anyway.
221    // So, add an extra layer of compiler confusion.
222    let (mut h1, mut h2) = (0u32, 0u32);
223    for (b1, b2) in x.iter().zip(y.iter()) {
224        h1 ^= (h1 << 5).wrapping_add((h1 >> 2) ^ *b1 as u32);
225        h2 ^= (h2 << 5).wrapping_add((h2 >> 2) ^ *b2 as u32);
226    }
227    v |= h1 ^ h2;
228    for (a, b) in x.iter().zip(y.iter()) {
229        v |= (a ^ b) as u32;
230    }
231    v == 0
232}