Skip to main content

base64_ng_tokio/
lib.rs

1#![deny(unsafe_code)]
2#![deny(missing_docs)]
3#![deny(clippy::all)]
4#![deny(clippy::pedantic)]
5
6//! Optional Tokio async helpers for `base64-ng`.
7//!
8//! The crate provides two API tiers:
9//!
10//! - read-all/write-all convenience functions, with `*_limited` variants for
11//!   peer-controlled request or frame boundaries.
12//! - manual [`AsyncRead`] and [`AsyncWrite`] streaming adapters with fixed
13//!   internal buffers and explicit drop cleanup.
14//!
15//! The streaming adapters are implemented as explicit state machines. They do
16//! not use `async fn` internally, so cancellation can only leave data in each
17//! adapter's fixed pending/output buffers; those buffers are cleared on drop.
18//!
19//! # Security
20//!
21//! The read-all helpers use RAII-guarded temporary `Vec<u8>` allocations and
22//! the normal strict decode path. The guards wipe initialized bytes and spare
23//! capacity on ordinary return, I/O error, or future cancellation. They are
24//! not constant-time-oriented token validators or high-assurance secret
25//! decoders. For secret-bearing async frames, collect a bounded frame under
26//! the application's approved memory policy and decode through
27//! `base64_ng::ct`, staged CT decode, `base64-ng-derive`, or
28//! `base64-ng-sanitization`.
29
30mod decoder_writer;
31mod encoder_writer;
32mod queue;
33mod readers;
34
35pub use decoder_writer::DecoderWriter;
36pub use encoder_writer::EncoderWriter;
37pub use readers::{DecoderReader, EncoderReader};
38
39use base64_ng::{Alphabet, Engine};
40use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
41
42const READ_ALL_EAGER_CAP: usize = 8192;
43
44/// Reads all bytes from `reader`, encodes them, and writes the encoded output.
45///
46/// # Errors
47///
48/// Returns I/O errors from the reader or writer, and wraps Base64 encoding
49/// errors as [`io::ErrorKind::InvalidInput`].
50pub async fn encode_reader_to_writer<A, const PAD: bool, R, W>(
51    engine: &Engine<A, PAD>,
52    reader: &mut R,
53    writer: &mut W,
54) -> io::Result<u64>
55where
56    A: Alphabet,
57    R: AsyncRead + Unpin + ?Sized,
58    W: AsyncWrite + Unpin + ?Sized,
59{
60    let input = read_to_end_guarded(reader).await?;
61    let output = WipingVec::from_vec(
62        engine
63            .encode_vec(input.as_slice())
64            .map_err(encode_io_error)?,
65    );
66    let written = output.len() as u64;
67    writer.write_all(output.as_slice()).await?;
68    Ok(written)
69}
70
71/// Reads at most `max_input_len` bytes from `reader`, encodes them, and writes
72/// the encoded output.
73///
74/// If the input exceeds `max_input_len`, this returns
75/// [`io::ErrorKind::InvalidData`] and does not write to `writer`.
76///
77/// # Errors
78///
79/// Returns I/O errors from the reader or writer, reports oversized input as
80/// [`io::ErrorKind::InvalidData`], and wraps Base64 encoding errors as
81/// [`io::ErrorKind::InvalidInput`].
82pub async fn encode_reader_to_writer_limited<A, const PAD: bool, R, W>(
83    engine: &Engine<A, PAD>,
84    reader: &mut R,
85    writer: &mut W,
86    max_input_len: usize,
87) -> io::Result<u64>
88where
89    A: Alphabet,
90    R: AsyncRead + Unpin + ?Sized,
91    W: AsyncWrite + Unpin + ?Sized,
92{
93    let input = read_to_end_limited(reader, max_input_len).await?;
94    let output = WipingVec::from_vec(
95        engine
96            .encode_vec(input.as_slice())
97            .map_err(encode_io_error)?,
98    );
99    let written = output.len() as u64;
100    writer.write_all(output.as_slice()).await?;
101    Ok(written)
102}
103
104/// Reads all Base64 bytes from `reader`, decodes them, and writes decoded bytes.
105///
106/// Decoding happens before any output is written. If input is malformed, the
107/// writer is untouched by this helper.
108///
109/// # Errors
110///
111/// Returns I/O errors from the reader or writer, and wraps Base64 decoding
112/// errors as [`io::ErrorKind::InvalidData`].
113pub async fn decode_reader_to_writer<A, const PAD: bool, R, W>(
114    engine: &Engine<A, PAD>,
115    reader: &mut R,
116    writer: &mut W,
117) -> io::Result<u64>
118where
119    A: Alphabet,
120    R: AsyncRead + Unpin + ?Sized,
121    W: AsyncWrite + Unpin + ?Sized,
122{
123    let input = read_to_end_guarded(reader).await?;
124    let output = WipingVec::from_vec(
125        engine
126            .decode_vec(input.as_slice())
127            .map_err(decode_io_error)?,
128    );
129    let written = output.len() as u64;
130    writer.write_all(output.as_slice()).await?;
131    Ok(written)
132}
133
134/// Reads at most `max_input_len` Base64 bytes from `reader`, decodes them, and
135/// writes decoded bytes.
136///
137/// If the input exceeds `max_input_len` or is malformed, this returns before
138/// writing to `writer`.
139///
140/// # Errors
141///
142/// Returns I/O errors from the reader or writer, reports oversized or malformed
143/// input as [`io::ErrorKind::InvalidData`], and writes no decoded output on
144/// either condition.
145pub async fn decode_reader_to_writer_limited<A, const PAD: bool, R, W>(
146    engine: &Engine<A, PAD>,
147    reader: &mut R,
148    writer: &mut W,
149    max_input_len: usize,
150) -> io::Result<u64>
151where
152    A: Alphabet,
153    R: AsyncRead + Unpin + ?Sized,
154    W: AsyncWrite + Unpin + ?Sized,
155{
156    let input = read_to_end_limited(reader, max_input_len).await?;
157    let output = WipingVec::from_vec(
158        engine
159            .decode_vec(input.as_slice())
160            .map_err(decode_io_error)?,
161    );
162    let written = output.len() as u64;
163    writer.write_all(output.as_slice()).await?;
164    Ok(written)
165}
166
167/// Encodes `input` into an owned byte vector.
168///
169/// # Errors
170///
171/// Returns an I/O error if Base64 encoding fails.
172pub fn encode_to_vec<A, const PAD: bool>(
173    engine: &Engine<A, PAD>,
174    input: impl AsRef<[u8]>,
175) -> io::Result<Vec<u8>>
176where
177    A: Alphabet,
178{
179    engine.encode_vec(input.as_ref()).map_err(encode_io_error)
180}
181
182/// Decodes `input` into an owned byte vector.
183///
184/// # Errors
185///
186/// Returns an I/O error if Base64 decoding fails.
187pub fn decode_to_vec<A, const PAD: bool>(
188    engine: &Engine<A, PAD>,
189    input: impl AsRef<[u8]>,
190) -> io::Result<Vec<u8>>
191where
192    A: Alphabet,
193{
194    engine.decode_vec(input.as_ref()).map_err(decode_io_error)
195}
196
197fn encode_io_error(error: base64_ng::EncodeError) -> io::Error {
198    io::Error::new(io::ErrorKind::InvalidInput, error)
199}
200
201fn decode_io_error(error: base64_ng::DecodeError) -> io::Error {
202    io::Error::new(io::ErrorKind::InvalidData, error.kind().as_str())
203}
204
205fn wipe_bytes(bytes: &mut [u8]) {
206    base64_ng::secure_wipe(bytes);
207}
208
209struct WipingVec(Vec<u8>);
210
211impl WipingVec {
212    fn new() -> Self {
213        Self(Vec::new())
214    }
215
216    fn with_capacity(capacity: usize) -> Self {
217        Self(Vec::with_capacity(capacity))
218    }
219
220    fn from_vec(bytes: Vec<u8>) -> Self {
221        Self(bytes)
222    }
223
224    fn as_slice(&self) -> &[u8] {
225        &self.0
226    }
227
228    fn len(&self) -> usize {
229        self.0.len()
230    }
231
232    fn extend_from_slice_wiping_old(
233        &mut self,
234        bytes: &[u8],
235        capacity_limit: usize,
236    ) -> io::Result<()> {
237        let required = self.len().checked_add(bytes.len()).ok_or_else(|| {
238            io::Error::new(
239                io::ErrorKind::InvalidData,
240                "base64-ng-tokio input is too large",
241            )
242        })?;
243        if required > capacity_limit {
244            return Err(io::Error::new(
245                io::ErrorKind::InvalidData,
246                "base64-ng-tokio input exceeds configured limit",
247            ));
248        }
249
250        if required <= self.0.capacity() {
251            self.0.extend_from_slice(bytes);
252            return Ok(());
253        }
254
255        let grown_capacity = self.0.capacity().saturating_mul(2).max(required);
256        let replacement_capacity = grown_capacity.min(capacity_limit);
257        let mut replacement = Self::with_capacity(replacement_capacity);
258        replacement.0.extend_from_slice(&self.0);
259        replacement.0.extend_from_slice(bytes);
260        core::mem::swap(self, &mut replacement);
261        drop(replacement);
262        Ok(())
263    }
264}
265
266impl Drop for WipingVec {
267    fn drop(&mut self) {
268        // Initialize the existing spare capacity without reallocating so the
269        // reviewed wipe primitive covers the complete allocation.
270        self.0.resize(self.0.capacity(), 0);
271        wipe_bytes(&mut self.0);
272        self.0.clear();
273    }
274}
275
276struct WipingArray<const N: usize>([u8; N]);
277
278impl<const N: usize> WipingArray<N> {
279    const fn new() -> Self {
280        Self([0; N])
281    }
282}
283
284impl<const N: usize> Drop for WipingArray<N> {
285    fn drop(&mut self) {
286        wipe_bytes(&mut self.0);
287    }
288}
289
290async fn read_to_end_guarded<R>(reader: &mut R) -> io::Result<WipingVec>
291where
292    R: AsyncRead + Unpin + ?Sized,
293{
294    let mut input = WipingVec::new();
295    let mut chunk = WipingArray::<8192>::new();
296
297    loop {
298        let read = reader.read(&mut chunk.0).await?;
299        if read == 0 {
300            return Ok(input);
301        }
302
303        input.extend_from_slice_wiping_old(&chunk.0[..read], usize::MAX)?;
304        wipe_bytes(&mut chunk.0[..read]);
305    }
306}
307
308async fn read_to_end_limited<R>(reader: &mut R, max_input_len: usize) -> io::Result<WipingVec>
309where
310    R: AsyncRead + Unpin + ?Sized,
311{
312    let mut input = WipingVec::with_capacity(max_input_len.min(READ_ALL_EAGER_CAP));
313    let mut chunk = WipingArray::<8192>::new();
314
315    loop {
316        let remaining = max_input_len - input.len();
317        let read_cap = if remaining < chunk.0.len() {
318            remaining + 1
319        } else {
320            chunk.0.len()
321        };
322        let read = reader.read(&mut chunk.0[..read_cap]).await?;
323        if read == 0 {
324            return Ok(input);
325        }
326
327        if read > remaining {
328            return Err(io::Error::new(
329                io::ErrorKind::InvalidData,
330                "base64-ng-tokio input exceeds configured limit",
331            ));
332        }
333
334        input.extend_from_slice_wiping_old(&chunk.0[..read], max_input_len)?;
335        wipe_bytes(&mut chunk.0[..read]);
336    }
337}