Skip to main content

bouncycastle_base64/
lib.rs

1//! Good old fashioned base64 encoder and decoder.
2//!
3//! It should just work the way base64 normally works:
4//! [`encode`] takes any bytes-like rust type and returns a String,
5//! while [`decode`] takes a String (which can be in any bytes-like container)
6//! and returns a `Vec<u8>`.
7//!
8//!```
9//! use bouncycastle_base64 as base64;
10//!
11//! let out = base64::encode(b"\x00"); // "AA=="
12//! let out = base64::encode(b"Hello, World!"); // "SGVsbG8sIFdvcmxkIQ=="
13//! let out = base64::encode(b"\x00\x01\x02\x03\x04\x05\x06"); // "AAECAwQFBg=="
14//!
15//! let out = base64::decode("AA==").unwrap(); // b"\x00"
16//! let out = base64::decode("SGVsbG8sIFdvcmxkIQ==").unwrap(); // b"Hello, World!"
17//! let out = base64::decode("AAECAwQFBg==").unwrap(); // b"\x00\x01\x02\x03\x04\x05\x06"
18//!
19//! // note that the decoder automatically ignores whitespace in the b64 input
20//! let out1 = base64::decode("AAEC   Aw QFB\ng==").unwrap(); // b"\x00\x01\x02\x03\x04\x05\x06"
21//! assert_eq!(out, out1);
22//!
23//! // it is also tolerant of missing padding characters
24//! let out = base64::decode("AAECAwQFBg==").unwrap(); // b"\x00\x01\x02\x03\x04\x05\x06"
25//! let out1 = base64::decode("AAECAwQFBg=").unwrap(); // b"\x00\x01\x02\x03\x04\x05\x06"
26//! assert_eq!(out, out1);
27//! let out2 = base64::decode("AAECAwQFBg").unwrap(); // b"\x00\x01\x02\x03\x04\x05\x06"
28//! assert_eq!(out, out2);
29//! ```
30//!
31//! # Streaming
32//! Unlike Hex, Base64 does not align cleanly to byte boundaries.
33//! That means that the above one-shot APIs should only be used if you have the entire content to
34//! process at the same time.
35//! In other words, if data is arbitrarily broken into chunks and handed to the one-shot [`encode`] and [`decode`] APIs,
36//! the results obtained will be incorrect.
37//! Whenever it is necessary to process data in chunks, the streaming API that allows repeated calls to `do_update`
38//! must be used. This produces output as it goes, and correctly holds on to the unprocessed
39//! partial block until either `do_update` or `do_final` is called.
40//!
41//! ```
42//! use bouncycastle_base64 as base64;
43//!
44//! let mut b64_str: String = String::new();
45//! let mut encoder = base64::Base64Encoder::new();
46//! b64_str.push_str( encoder.do_update(b"Hello,").as_str() );
47//! b64_str.push_str( encoder.do_final(b" World!").as_str() );
48//! assert_eq!(b64_str, "SGVsbG8sIFdvcmxkIQ==");
49//!
50//! let mut out_bytes = Vec::<u8>::new();
51//! let mut decoder = base64::Base64Decoder::new(/*skip_whitespace*/ false);
52//! out_bytes.extend( decoder.do_update("SGVs").unwrap() );
53//! out_bytes.extend( decoder.do_final("bG8sIFdvcmxkIQ==").unwrap() );
54//! assert_eq!(out_bytes, b"Hello, World!");
55//! ```
56//!
57//! # Security and constant-time
58//!
59//! The following paper proves that extremely clever attack algorithms exist to recover private keys
60//! if the attacker is allowed to observe closely side-channels of the base64 decode process.
61//!
62//! > [Util::Lookup: Exploiting key decoding in cryptographic libraries (Sieck, 2021)](https://arxiv.org/pdf/2108.04600.pdf),
63//!
64//! As this is a cryptography library, it can be assumed that this base64 implementation will be used to encode
65//! and decode private keys in PEM and JWK formats and so we are only providing a constant-time implementation
66//! in order to remove the temptation to shoot yourself in the foot in the name of a small performance gain.
67//!
68//! During testing, a naïve lookup table-based implementation of base64::decode was 1.7x faster than
69//! a constant-time implementation.
70//! We are quite sure that optimized base64 implementations exist that provide still better performance.
71//! It is necessary to encode gigabytes of non-sensitive data on base64, it is advised to use
72//! one of the good, fast, but non-constant-time base64 implementations available from other projects.
73//!
74//!
75//! # Alphabets:
76//!
77//! At the present time, this base64 implementation only supports the standard alphabet with "+" and "/", specifically:
78//! ```text
79//! ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
80//! ```
81//! but additional alphabets such as the URLSafe alphabet will likely be added in future versions.
82//     /// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_="
83//     URLSafe,
84
85#![forbid(unsafe_code)]
86#![forbid(missing_docs)]
87
88use bouncycastle_utils::ct::Condition;
89
90/// One-shot encode from bytes to a base64-encoded string using a constant-time implementation.
91pub fn encode<T: AsRef<[u8]>>(input: T) -> String {
92    Base64Encoder::new().do_final(input)
93}
94
95/// One-shot decode from a base64-encoded string to bytes using a constant-time implementation.
96pub fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, Base64Error> {
97    Base64Decoder::new(true).do_final(input)
98}
99
100/// Return type for errors relating to Base64 encoding and decoding.
101#[derive(Debug)]
102pub enum Base64Error {
103    /// The [`Base64Decoder::do_update`] method must not be called on a block that contains padding.
104    /// If this error is returned, then the provided input has not been processed and the caller must instead
105    /// pass the same input to [`Base64Decoder::do_final`]. Note that do_final() is tolerant of incomplete padding blocks,
106    /// so even if an additional padding character is contained in the next chunk of input, do_final()
107    /// will still produce the correct output -- ie any additional chunks held by the caller can be discarded.
108    PaddingEncounteredDuringDoUpdate,
109
110    /// Input contained a character that was not in the base64 alphabet. The index of the illegal character is included in the output.
111    InvalidB64Character(usize),
112}
113
114/// The stateful base64 encoder that supports streaming.
115pub struct Base64Encoder {
116    buf: [u8; 3],
117    vals_in_buf: usize,
118}
119
120impl Base64Encoder {
121    /// Create a new instance.
122    pub fn new() -> Self {
123        Self { buf: [0; 3], vals_in_buf: 0 }
124    }
125
126    fn ct_bin_to_b64(c: u8) -> u8 {
127        let in_az = Condition::<i64>::is_within_range(c as i64, 26, 51);
128        let in_09 = Condition::<i64>::is_within_range(c as i64, 52, 61);
129        let eq_plus = Condition::<i64>::is_equal(c as i64, 62);
130        let eq_slash = Condition::<i64>::is_equal(c as i64, 63);
131
132        // TODO: redo this once we have ct::u8 implemented ... the i64 is wasteful
133
134        #[allow(non_snake_case)]
135        let c_AZ: i64 = 'A' as i64 + c as i64;
136        let c_az: i64 = 'a' as i64 + (c as i64 - 26);
137        let c_09: i64 = '0' as i64 + (c as i64 - 2 * 26);
138        let c_plus: i64 = '+' as i64;
139        let c_slash: i64 = '/' as i64;
140
141        let mut ret: i64 = c_AZ as i64;
142        ret = in_az.select(c_az as i64, ret);
143        ret = in_09.select(c_09 as i64, ret);
144        ret = eq_plus.select(c_plus, ret);
145        ret = eq_slash.select(c_slash, ret);
146        ret as u8
147    }
148
149    /// Streaming API that performs Base64 encoding of the provided input, but does not apply
150    /// the final padding and will hold an incomplete block while waiting for more input.
151    pub fn do_update<T: AsRef<[u8]>>(&mut self, input: T) -> String {
152        let inref = input.as_ref();
153        let mut out: Vec<u8> = Vec::with_capacity(inref.len() * 4 / 3 + 4);
154        let mut out_buf: [u8; 4] = [0; 4];
155
156        for i in 0..inref.len() {
157            self.buf[self.vals_in_buf] = inref[i];
158            self.vals_in_buf += 1;
159
160            if self.vals_in_buf == 3 {
161                // process a block
162                Self::encode_block(&self.buf, &mut out_buf);
163                out.append(&mut out_buf.to_vec());
164                self.vals_in_buf = 0;
165            }
166        }
167
168        String::from_utf8(out).unwrap()
169    }
170
171    /// As you would expect, do_final() consumes the object along with a final block.
172    /// do_final may be called with the entire content; ie without any do_update's before it.
173    pub fn do_final<T: AsRef<[u8]>>(mut self, input: T) -> String {
174        let mut out = self.do_update(input);
175
176        // pad the last block.
177        if self.vals_in_buf != 0 {
178            let mut out_buf: [u8; 4] = [0; 4];
179            if self.vals_in_buf == 1 {
180                self.buf[1] = 0;
181            }
182            if self.vals_in_buf <= 2 {
183                self.buf[2] = 0;
184            }
185            Self::encode_block(&self.buf, &mut out_buf);
186            if self.vals_in_buf <= 2 {
187                out_buf[3] = b'=';
188            }
189            if self.vals_in_buf == 1 {
190                out_buf[2] = b'=';
191            }
192            out.push_str(std::str::from_utf8(&out_buf).unwrap());
193        }
194        out
195    }
196
197    fn encode_block<T: AsRef<[u8]>>(input: T, out: &mut [u8]) {
198        let inref = input.as_ref();
199        assert!(inref.len() >= 3);
200        assert!(out.len() >= 4);
201
202        out.fill(0);
203
204        out[0] = Self::ct_bin_to_b64(inref[0] >> 2);
205        out[1] = Self::ct_bin_to_b64(((inref[0] & 0x03) << 4) | inref[1] >> 4);
206        out[2] = Self::ct_bin_to_b64(((inref[1] & 0x0F) << 2) | inref[2] >> 6);
207        out[3] = Self::ct_bin_to_b64(inref[2] & 0x3F);
208    }
209}
210
211/// The stateful base64 decoder that supports streaming.
212pub struct Base64Decoder {
213    buf: [u8; 4],
214    vals_in_buf: usize,
215    skip_whitespace: bool,
216}
217
218impl Base64Decoder {
219    /// Create a new instance.
220    pub fn new(skip_whitespace: bool) -> Self {
221        Base64Decoder { buf: [0; 4], vals_in_buf: 0, skip_whitespace }
222    }
223
224    fn ct_b64_to_bin(b: u8) -> u8 {
225        let in_az = Condition::<i64>::is_within_range(b as i64, 97, 122);
226        #[allow(non_snake_case)]
227        let in_AZ = Condition::<i64>::is_within_range(b as i64, 65, 90);
228        let in_09 = Condition::<i64>::is_within_range(b as i64, 48, 57);
229        let is_plus = Condition::<i64>::is_equal(b as i64, 43);
230        let is_slash = Condition::<i64>::is_equal(b as i64, 47);
231        let is_padding = Condition::<i64>::is_equal(b as i64, 61);
232        let is_whitespace = Condition::<i64>::is_in_list(
233            b as i64,
234            &[' ' as i64, '\t' as i64, '\n' as i64, '\r' as i64],
235        );
236
237        #[allow(non_snake_case)]
238        let c_AZ: i64 = b as i64 - 'A' as i64;
239        let c_az: i64 = b as i64 - 'a' as i64 + 26;
240        let c_09: i64 = b as i64 - '0' as i64 + 2 * 26;
241
242        let mut ret: i64 = 0xFFi64;
243
244        ret = in_AZ.select(c_AZ, ret);
245        ret = in_az.select(c_az, ret);
246        ret = in_09.select(c_09, ret);
247        ret = is_plus.select(62, ret);
248        ret = is_slash.select(63, ret);
249        ret = is_padding.select(0x81, ret);
250        ret = is_whitespace.select(0x80, ret);
251
252        ret as u8
253    }
254
255    /// Streaming API that performs Base64 encoding of the provided input, but does not apply
256    /// the final padding and will hold an incomplete block while waiting for more input.
257    pub fn do_update<T: AsRef<[u8]>>(&mut self, input: T) -> Result<Vec<u8>, Base64Error> {
258        self.decode_internal(input, true)
259    }
260
261    fn decode_internal<T: AsRef<[u8]>>(
262        &mut self,
263        input: T,
264        rollback_if_padding: bool,
265    ) -> Result<Vec<u8>, Base64Error> {
266        // copy the current state so that we can restore it if we encounter a padding character.
267        let starting_state: [u8; 4] = self.buf.clone();
268        let starting_vals_in_block: usize = self.vals_in_buf;
269
270        let inref = input.as_ref();
271        let mut out: Vec<u8> = vec![];
272
273        let mut i: usize = 0;
274        while i < inref.len() {
275            self.buf[self.vals_in_buf] = Self::ct_b64_to_bin(inref[i]);
276            if self.buf[self.vals_in_buf] == 0xFF {
277                return Err(Base64Error::InvalidB64Character(i));
278            }
279            if self.buf[self.vals_in_buf] == 0x80 {
280                if self.skip_whitespace {
281                    i += 1;
282                    continue;
283                } else {
284                    return Err(Base64Error::InvalidB64Character(i));
285                }
286            }
287            if self.buf[self.vals_in_buf] == 0x81 {
288                // Error: we found padding.
289                if rollback_if_padding {
290                    // Roll back and return Base64Error::NonFinalBlockContainsPadding.
291                    self.buf = starting_state.clone();
292                    self.vals_in_buf = starting_vals_in_block;
293                }
294                return Ok(out);
295            }
296
297            i += 1;
298            self.vals_in_buf += 1;
299
300            // here, it can be assumed that the buffer contains no padding.
301            if self.vals_in_buf == 4 {
302                // decode block
303                out.push(self.buf[0] << 2 | self.buf[1] >> 4);
304                out.push(self.buf[1] << 4 | self.buf[2] >> 2);
305                out.push(self.buf[2] << 6 | self.buf[3]);
306                self.vals_in_buf = 0;
307                continue;
308            }
309        }
310
311        Ok(out)
312    }
313
314    /// As can be expected, do_final() consumes the object.
315    pub fn do_final<T: AsRef<[u8]>>(mut self, input: T) -> Result<Vec<u8>, Base64Error> {
316        // process as much as we can the usual way.
317        let mut out = match self.decode_internal(input, false) {
318            Ok(out) => out,
319            Err(Base64Error::PaddingEncounteredDuringDoUpdate) => {
320                panic!(
321                    "rollback_if_padding = false should not produce a Base64Error::PaddingEncounteredDuringDoUpdate"
322                );
323            }
324            Err(e) => return Err(e),
325        };
326
327        // now, a single block containing padding remains to be dealt with.
328        if self.vals_in_buf != 0 {
329            // be tolerant of missing padding
330            // if it is not a complete block at the end, the infer the byte count from the number of leftover symbols
331            let pad_count: u8 = 3 - (self.vals_in_buf as u8 - 1);
332
333            out.push(self.buf[0] << 2 | self.buf[1] >> 4);
334            if pad_count != 2 {
335                out.push(self.buf[1] << 4 | self.buf[2] >> 2);
336            }
337            if pad_count == 0 {
338                out.push(self.buf[2] << 6 | self.buf[3]);
339            }
340        }
341
342        Ok(out)
343    }
344}