Skip to main content

git_simple_encrypt/crypt/
stream.rs

1use std::io::{Read, Write};
2
3use chacha20poly1305_simd::XChaCha20Poly1305;
4use zeroize::Zeroizing;
5
6use crate::{
7    crypt::{
8        header::{CHUNK_SIZE, FILE_ID_LEN, FileHeader, HEADER_LEN, NONCE_LEN},
9        key::{derive_key, derive_nonce, split_keys},
10    },
11    error::{Error, Result},
12};
13
14/// Streaming encryption loop: read plaintext chunks from `reader`, encrypt
15/// each with the cipher, and write `[NONCE | CIPHERTEXT | TAG]` to `writer`.
16fn encrypt_chunks(
17    reader: &mut dyn Read,
18    writer: &mut dyn Write,
19    cipher: &XChaCha20Poly1305,
20    key_mac: &[u8; 32],
21    file_id: &[u8; FILE_ID_LEN],
22    header_bytes: &[u8; HEADER_LEN],
23) -> Result<()> {
24    // Reusable plaintext/ciphertext buffer. `encrypt_in_place` overwrites
25    // the plaintext chunk with its ciphertext and appends the 16 B Poly1305
26    // tag, so the capacity reserves CHUNK_SIZE + TAG_LEN up front — zero
27    // allocation and zero copy per chunk (the old `encrypt()` +
28    // `extend_from_slice` path allocated a fresh Vec and memcopied the whole
29    // chunk every iteration).
30    const TAG_LEN: usize = 16;
31    let mut buffer: Zeroizing<Vec<u8>> = Zeroizing::new(Vec::with_capacity(CHUNK_SIZE + TAG_LEN));
32    let mut aad = {
33        let mut aad = [0u8; HEADER_LEN + 9];
34        aad[..HEADER_LEN].copy_from_slice(header_bytes);
35        aad
36    };
37    let mut chunk_idx = 0u64;
38
39    loop {
40        // Restore a full CHUNK_SIZE window for reading; `encrypt_in_place`
41        // changes the length each iteration, so resize at the top.
42        buffer.resize(CHUNK_SIZE, 0);
43        let mut bytes_read = 0;
44        while bytes_read < CHUNK_SIZE {
45            let n = reader.read(&mut buffer[bytes_read..])?;
46            if n == 0 {
47                break;
48            }
49            bytes_read += n;
50        }
51
52        let is_last_chunk = bytes_read < CHUNK_SIZE;
53        aad[HEADER_LEN..HEADER_LEN + 8].copy_from_slice(&chunk_idx.to_le_bytes());
54        aad[HEADER_LEN + 8] = u8::from(is_last_chunk);
55
56        // Nonce is derived from the *plaintext* chunk, so compute it before
57        // `encrypt_in_place` overwrites the buffer with ciphertext.
58        let nonce = derive_nonce(key_mac, file_id, &buffer[..bytes_read], chunk_idx);
59
60        // Drop the zero-padding tail so the buffer holds exactly the plaintext,
61        // then encrypt in place: buffer becomes ciphertext+tag (len += 16).
62        buffer.truncate(bytes_read);
63        cipher
64            .encrypt_in_place(&nonce, &aad, &mut *buffer)
65            .map_err(|e| Error::EncryptFailed(e.to_string()))?;
66
67        writer.write_all(&nonce)?;
68        writer.write_all(&buffer)?;
69
70        chunk_idx += 1;
71
72        if is_last_chunk {
73            break;
74        }
75    }
76
77    Ok(())
78}
79
80/// Streaming decryption loop: read encrypted chunks from `reader`, decrypt,
81/// and write plaintext to `writer`.
82///
83/// Chunk layout: `[NONCE (24B)] [CIPHERTEXT] [TAG (16B)]`
84fn decrypt_chunks(
85    reader: &mut dyn Read,
86    writer: &mut dyn Write,
87    cipher: &XChaCha20Poly1305,
88    header_bytes: &[u8; HEADER_LEN],
89) -> Result<()> {
90    // Each encrypted chunk is plaintext (<= CHUNK_SIZE) + 16 B tag.
91    // `decrypt_in_place` overwrites it in place with the plaintext and
92    // strips the tag, so this single buffer is reused for every chunk — no
93    // per-chunk Vec allocation.
94    const TAG_LEN: usize = 16;
95    let ct_len = CHUNK_SIZE + TAG_LEN;
96    let mut nonce_buf = [0u8; NONCE_LEN];
97    let mut ct_buffer: Zeroizing<Vec<u8>> = Zeroizing::new(Vec::with_capacity(ct_len));
98    let mut aad = {
99        let mut aad = [0u8; HEADER_LEN + 9];
100        aad[..HEADER_LEN].copy_from_slice(header_bytes);
101        aad
102    };
103    let mut last_chunk_was_final = false;
104    let mut chunk_idx = 0u64;
105
106    loop {
107        match reader.read_exact(&mut nonce_buf) {
108            Ok(()) => {},
109            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
110            Err(e) => return Err(e.into()),
111        }
112
113        ct_buffer.resize(ct_len, 0);
114        let mut bytes_read = 0;
115        while bytes_read < ct_len {
116            let n = reader.read(&mut ct_buffer[bytes_read..])?;
117            if n == 0 {
118                break;
119            }
120            bytes_read += n;
121        }
122
123        if bytes_read < TAG_LEN {
124            return Err(Error::TruncatedChunk);
125        }
126
127        let is_last_chunk = bytes_read < ct_len;
128        ct_buffer.truncate(bytes_read);
129
130        aad[HEADER_LEN..HEADER_LEN + 8].copy_from_slice(&chunk_idx.to_le_bytes());
131        aad[HEADER_LEN + 8] = u8::from(is_last_chunk);
132
133        cipher
134            .decrypt_in_place(&nonce_buf, &aad, &mut *ct_buffer)
135            .map_err(|e| Error::DecryptFailed(e.to_string()))?;
136
137        writer.write_all(&ct_buffer)?;
138
139        chunk_idx += 1;
140
141        if is_last_chunk {
142            last_chunk_was_final = true;
143            break;
144        }
145    }
146
147    if !last_chunk_was_final {
148        return Err(Error::FileTruncated);
149    }
150
151    Ok(())
152}
153
154/// Decrypt the body (with optional Zstd decompression)
155pub(super) fn decrypt_body(
156    reader: &mut dyn Read,
157    writer: &mut dyn Write,
158    cipher: &XChaCha20Poly1305,
159    header: &FileHeader,
160) -> Result<()> {
161    if header.is_compressed() {
162        let mut decoder = zstd::stream::write::Decoder::new(writer)?.auto_flush();
163        decrypt_chunks(reader, &mut decoder, cipher, header.as_bytes())?;
164        decoder.flush()?;
165    } else {
166        decrypt_chunks(reader, writer, cipher, header.as_bytes())?;
167    }
168    Ok(())
169}
170
171/// Encrypt data from `reader` into `writer` using streaming chunked encryption.
172pub fn encrypt_into<R: Read, W: Write>(
173    reader: &mut R,
174    writer: &mut W,
175    derived_key: &[u8; 32],
176    salt: [u8; crate::crypt::header::SALT_LEN],
177    file_id: Option<[u8; FILE_ID_LEN]>,
178    zstd: Option<u8>,
179) -> Result<FileHeader> {
180    let file_id = file_id.unwrap_or_else(FileHeader::generate_file_id);
181    let header = FileHeader::new(zstd.is_some(), salt, file_id);
182    header.write_to(writer)?;
183
184    let (key_enc, key_mac) = split_keys(derived_key);
185    let cipher = XChaCha20Poly1305::new(*key_enc);
186
187    if let Some(level) = zstd {
188        let mut encoder = zstd::stream::read::Encoder::new(reader, i32::from(level))?;
189        encrypt_chunks(
190            &mut encoder,
191            writer,
192            &cipher,
193            &key_mac,
194            &file_id,
195            header.as_bytes(),
196        )?;
197    } else {
198        encrypt_chunks(
199            reader,
200            writer,
201            &cipher,
202            &key_mac,
203            &file_id,
204            header.as_bytes(),
205        )?;
206    }
207
208    Ok(header)
209}
210
211/// Decrypt data from `reader` into `writer`.
212pub fn decrypt_into<R: Read, W: Write>(
213    reader: &mut R,
214    writer: &mut W,
215    master_key: &[u8],
216) -> Result<FileHeader> {
217    let header = FileHeader::read_from(reader)?;
218
219    let derived_key = derive_key(master_key, &header.salt)?;
220    let (key_enc, _) = split_keys(&derived_key);
221    let cipher = XChaCha20Poly1305::new(*key_enc);
222
223    decrypt_body(reader, writer, &cipher, &header)?;
224    Ok(header)
225}