Skip to main content

git_simple_encrypt/crypt/
stream.rs

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