git_simple_encrypt/crypt/
stream.rs1use 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
17fn 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
77fn 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
152pub(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
169pub 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
209pub 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}