Skip to main content

crypt_io/stream/
file.rs

1//! File-level helpers built on top of [`super::StreamEncryptor`] /
2//! [`super::StreamDecryptor`].
3//!
4//! These functions exist for the common "encrypt this file into that
5//! file" workflow. For finer control (custom chunk size, hooking into
6//! a different I/O type, processing bytes from a network socket),
7//! drive the streaming types directly.
8
9use std::fs::File;
10use std::io::{BufReader, BufWriter, Read, Write};
11use std::path::Path;
12
13use crate::aead::Algorithm;
14use crate::error::{Error, Result};
15
16use super::frame::HEADER_LEN;
17use super::{StreamDecryptor, StreamEncryptor};
18
19/// I/O read buffer for `encrypt_file` / `decrypt_file`. Sized at 64 KiB
20/// to match the default chunk size — minimises syscall overhead.
21const IO_BUFFER_LEN: usize = 64 * 1024;
22
23/// Encrypt the file at `input_path` into `output_path` using `key` and
24/// the given AEAD `algorithm`. Uses the default chunk size (64 KiB).
25///
26/// The output file is overwritten if it already exists. On any failure
27/// after the output file has been opened, callers should treat the
28/// output file as junk and remove it.
29///
30/// # Errors
31///
32/// - [`Error::InvalidKey`] if `key` is not 32 bytes.
33/// - [`Error::RandomFailure`] if the OS RNG cannot produce a nonce.
34/// - [`Error::Mac`] for I/O failures (file open, read, write) — the
35///   variant carries a `&'static str` reason; the underlying
36///   `std::io::Error` is not surfaced (would risk leaking path
37///   fragments through error rendering).
38/// - [`Error::AuthenticationFailed`] for the (unreachable in
39///   practice) AEAD failure path.
40///
41/// # Example
42///
43/// ```no_run
44/// # #[cfg(all(feature = "stream", feature = "aead-chacha20"))] {
45/// use crypt_io::Algorithm;
46/// use crypt_io::stream;
47///
48/// let key = [0u8; 32];
49/// stream::encrypt_file("input.bin", "output.enc", &key, Algorithm::ChaCha20Poly1305)?;
50/// # }
51/// # Ok::<(), crypt_io::Error>(())
52/// ```
53pub fn encrypt_file(
54    input_path: impl AsRef<Path>,
55    output_path: impl AsRef<Path>,
56    key: &[u8],
57    algorithm: Algorithm,
58) -> Result<()> {
59    let input = File::open(input_path.as_ref()).map_err(|_| Error::Mac("stream: open input"))?;
60    let output =
61        File::create(output_path.as_ref()).map_err(|_| Error::Mac("stream: create output"))?;
62    let mut reader = BufReader::with_capacity(IO_BUFFER_LEN, input);
63    let mut writer = BufWriter::with_capacity(IO_BUFFER_LEN, output);
64
65    let (mut enc, header) = StreamEncryptor::new(key, algorithm)?;
66    writer
67        .write_all(&header)
68        .map_err(|_| Error::Mac("stream: write header"))?;
69
70    let mut io_buf = alloc::vec![0u8; IO_BUFFER_LEN];
71    loop {
72        let n = reader
73            .read(&mut io_buf)
74            .map_err(|_| Error::Mac("stream: read input"))?;
75        if n == 0 {
76            break;
77        }
78        let encrypted = enc.update(&io_buf[..n])?;
79        writer
80            .write_all(&encrypted)
81            .map_err(|_| Error::Mac("stream: write chunk"))?;
82    }
83
84    let tail = enc.finalize()?;
85    writer
86        .write_all(&tail)
87        .map_err(|_| Error::Mac("stream: write final chunk"))?;
88    writer
89        .flush()
90        .map_err(|_| Error::Mac("stream: flush output"))?;
91    Ok(())
92}
93
94/// Decrypt the file at `input_path` into `output_path` using `key`. The
95/// algorithm is read from the stream's header.
96///
97/// On authentication failure the output file may contain partially-
98/// decrypted plaintext from earlier chunks. **Callers must delete the
99/// output file when this function returns an error** — otherwise an
100/// attacker who can flip later chunks could leak earlier plaintext to
101/// disk.
102///
103/// # Errors
104///
105/// - [`Error::InvalidKey`] if `key` is not 32 bytes.
106/// - [`Error::InvalidCiphertext`] if the header is malformed or the
107///   stream is truncated below the minimum frame (header + tag).
108/// - [`Error::Mac`] for I/O failures.
109/// - [`Error::AuthenticationFailed`] for any cryptographic failure.
110///
111/// # Example
112///
113/// ```no_run
114/// # #[cfg(all(feature = "stream", feature = "aead-chacha20"))] {
115/// use crypt_io::stream;
116///
117/// let key = [0u8; 32];
118/// stream::decrypt_file("input.enc", "output.bin", &key)?;
119/// # }
120/// # Ok::<(), crypt_io::Error>(())
121/// ```
122pub fn decrypt_file(
123    input_path: impl AsRef<Path>,
124    output_path: impl AsRef<Path>,
125    key: &[u8],
126) -> Result<()> {
127    let input = File::open(input_path.as_ref()).map_err(|_| Error::Mac("stream: open input"))?;
128    let output =
129        File::create(output_path.as_ref()).map_err(|_| Error::Mac("stream: create output"))?;
130    let mut reader = BufReader::with_capacity(IO_BUFFER_LEN, input);
131    let mut writer = BufWriter::with_capacity(IO_BUFFER_LEN, output);
132
133    let mut header = [0u8; HEADER_LEN];
134    reader
135        .read_exact(&mut header)
136        .map_err(|_| Error::Mac("stream: read header"))?;
137
138    let mut dec = StreamDecryptor::new(key, &header)?;
139    let mut io_buf = alloc::vec![0u8; IO_BUFFER_LEN];
140    loop {
141        let n = reader
142            .read(&mut io_buf)
143            .map_err(|_| Error::Mac("stream: read input"))?;
144        if n == 0 {
145            break;
146        }
147        let plaintext = dec.update(&io_buf[..n])?;
148        writer
149            .write_all(&plaintext)
150            .map_err(|_| Error::Mac("stream: write plaintext"))?;
151    }
152
153    let tail = dec.finalize()?;
154    writer
155        .write_all(&tail)
156        .map_err(|_| Error::Mac("stream: write final plaintext"))?;
157    writer
158        .flush()
159        .map_err(|_| Error::Mac("stream: flush output"))?;
160    Ok(())
161}