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}