1use chacha20poly1305::{
15 KeyInit, XChaCha20Poly1305,
16 aead::{Aead, Payload},
17};
18
19use crate::{
20 errors::{CryptError, FileError},
21 file::FileMetadata,
22 memory::{SecureBytes, SecureKey},
23 v3::{
24 crypt,
25 header::{AadPurpose, FileHeader, HeaderBinding},
26 key::KeyDerivationParams,
27 },
28};
29
30pub const CHUNK_SIZE: u32 = 1024 * 1024; pub const TAG_SIZE: usize = 16;
36
37const MAX_COUNTER: u64 = (1 << 56) - 1;
39
40fn chunk_nonce(prefix: &[u8; 16], counter: u64, is_last: bool) -> [u8; 24] {
41 let mut nonce = [0u8; 24];
42 nonce[..16].copy_from_slice(prefix);
43 nonce[16..23].copy_from_slice(&counter.to_le_bytes()[..7]);
44 nonce[23] = u8::from(is_last);
45 nonce
46}
47
48pub struct StreamSealer {
50 cipher: XChaCha20Poly1305,
51 nonce_prefix: [u8; 16],
52 chunk_size: usize,
53 aad: Vec<u8>,
54 counter: u64,
55 finished: bool,
56}
57
58impl StreamSealer {
59 pub fn begin(
64 metadata: &FileMetadata,
65 key: &SecureKey,
66 kdf_params: KeyDerivationParams,
67 salt: [u8; 16],
68 nonce_prefix: [u8; 16],
69 metadata_nonce: [u8; 24],
70 ) -> Result<(FileHeader, StreamSealer), FileError> {
71 Self::begin_with_chunk_size(
72 metadata,
73 key,
74 kdf_params,
75 salt,
76 nonce_prefix,
77 metadata_nonce,
78 CHUNK_SIZE,
79 )
80 }
81
82 pub fn begin_with_chunk_size(
86 metadata: &FileMetadata,
87 key: &SecureKey,
88 kdf_params: KeyDerivationParams,
89 salt: [u8; 16],
90 nonce_prefix: [u8; 16],
91 metadata_nonce: [u8; 24],
92 chunk_size: u32,
93 ) -> Result<(FileHeader, StreamSealer), FileError> {
94 let envelope = crate::v3::metadata::serialize(metadata)?;
95
96 let binding = HeaderBinding::new(
97 &salt,
98 &kdf_params,
99 &nonce_prefix,
100 chunk_size,
101 &metadata_nonce,
102 );
103 let (metadata_ciphertext, _) = crypt::encrypt_bytes(
104 envelope.as_slice(),
105 key.as_bytes(),
106 &metadata_nonce,
107 &binding.aad(AadPurpose::Metadata),
108 )?;
109 let content_aad = binding.aad(AadPurpose::Content);
110
111 let header = FileHeader::new(
112 salt,
113 kdf_params,
114 nonce_prefix,
115 chunk_size,
116 metadata_nonce,
117 metadata_ciphertext,
118 )?;
119
120 let sealer = StreamSealer {
121 cipher: XChaCha20Poly1305::new(key.as_bytes().into()),
122 nonce_prefix,
123 chunk_size: chunk_size as usize,
124 aad: content_aad,
125 counter: 0,
126 finished: false,
127 };
128 Ok((header, sealer))
129 }
130
131 pub fn chunk_plaintext_len(&self) -> usize {
134 self.chunk_size
135 }
136
137 pub fn seal_chunk(&mut self, plaintext: &[u8], is_last: bool) -> Result<Vec<u8>, FileError> {
140 if self.finished {
141 return Err(stream_error("chunk sealed after the final chunk"));
142 }
143 if !is_last && plaintext.len() != self.chunk_size {
144 return Err(stream_error("non-final chunk must be exactly chunk-sized"));
145 }
146 if plaintext.len() > self.chunk_size {
147 return Err(stream_error("chunk larger than the declared chunk size"));
148 }
149 if self.counter > MAX_COUNTER {
150 return Err(stream_error("chunk counter overflow"));
151 }
152
153 let nonce = chunk_nonce(&self.nonce_prefix, self.counter, is_last);
154 let ciphertext = self
155 .cipher
156 .encrypt(
157 (&nonce).into(),
158 Payload {
159 msg: plaintext,
160 aad: &self.aad,
161 },
162 )
163 .map_err(|e| {
164 FileError::Crypt(CryptError::EncryptionError(format!(
165 "Encryption failed: {}",
166 e
167 )))
168 })?;
169
170 self.counter += 1;
171 self.finished = is_last;
172 Ok(ciphertext)
173 }
174
175 pub fn finished(&self) -> bool {
177 self.finished
178 }
179}
180
181pub struct StreamOpener {
183 cipher: XChaCha20Poly1305,
184 nonce_prefix: [u8; 16],
185 chunk_size: usize,
186 aad: Vec<u8>,
187 counter: u64,
188 finished: bool,
189}
190
191impl StreamOpener {
192 pub fn new(header: &FileHeader, key: &SecureKey) -> StreamOpener {
193 StreamOpener {
194 cipher: XChaCha20Poly1305::new(key.as_bytes().into()),
195 nonce_prefix: *header.nonce_prefix(),
196 chunk_size: header.chunk_size() as usize,
197 aad: header.binding().aad(AadPurpose::Content),
198 counter: 0,
199 finished: false,
200 }
201 }
202
203 pub fn chunk_ciphertext_len(&self) -> usize {
206 self.chunk_size + TAG_SIZE
207 }
208
209 pub fn open_chunk(
213 &mut self,
214 ciphertext: &[u8],
215 is_last: bool,
216 ) -> Result<SecureBytes, FileError> {
217 if self.finished {
218 return Err(stream_error("data present after the final chunk"));
219 }
220 if !is_last && ciphertext.len() != self.chunk_ciphertext_len() {
221 return Err(stream_error("non-final chunk has the wrong length"));
222 }
223 if ciphertext.len() < TAG_SIZE || ciphertext.len() > self.chunk_ciphertext_len() {
224 return Err(stream_error("chunk has an impossible length"));
225 }
226 if self.counter > MAX_COUNTER {
227 return Err(stream_error("chunk counter overflow"));
228 }
229
230 let nonce = chunk_nonce(&self.nonce_prefix, self.counter, is_last);
231 let plaintext = self
232 .cipher
233 .decrypt(
234 (&nonce).into(),
235 Payload {
236 msg: ciphertext,
237 aad: &self.aad,
238 },
239 )
240 .map_err(|_| {
241 FileError::Crypt(CryptError::DecryptionError(
242 "authentication failed (wrong password, or the file is corrupted)".to_string(),
243 ))
244 })?;
245
246 self.counter += 1;
247 self.finished = is_last;
248 Ok(SecureBytes::new(plaintext))
249 }
250
251 pub fn finished(&self) -> bool {
254 self.finished
255 }
256}
257
258fn stream_error(msg: &str) -> FileError {
259 FileError::Crypt(CryptError::DecryptionError(format!(
260 "invalid content stream: {}",
261 msg
262 )))
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::memory::SecureString;
269
270 fn test_setup(chunk_size: u32) -> (FileHeader, StreamSealer, SecureKey) {
271 let key = SecureKey::new([9u8; 32]);
272 let metadata = FileMetadata::new(SecureString::new("a.txt".to_string()), None, None);
273 let (header, sealer) = StreamSealer::begin_with_chunk_size(
274 &metadata,
275 &key,
276 KeyDerivationParams::test_defaults(),
277 [1u8; 16],
278 [2u8; 16],
279 [3u8; 24],
280 chunk_size,
281 )
282 .unwrap();
283 (header, sealer, key)
284 }
285
286 fn seal_all(sealer: &mut StreamSealer, content: &[u8], chunk_size: usize) -> Vec<Vec<u8>> {
290 let pieces: Vec<&[u8]> = if content.is_empty() {
291 vec![&[][..]]
292 } else {
293 content.chunks(chunk_size).collect()
294 };
295 pieces
296 .iter()
297 .enumerate()
298 .map(|(i, piece)| sealer.seal_chunk(piece, i == pieces.len() - 1).unwrap())
299 .collect()
300 }
301
302 fn open_all(
303 header: &FileHeader,
304 key: &SecureKey,
305 chunks: &[Vec<u8>],
306 ) -> Result<Vec<u8>, FileError> {
307 let mut opener = StreamOpener::new(header, key);
308 let mut out = Vec::new();
309 for (i, chunk) in chunks.iter().enumerate() {
310 let is_last = i == chunks.len() - 1;
311 out.extend_from_slice(opener.open_chunk(chunk, is_last)?.as_slice());
312 }
313 assert!(opener.finished());
314 Ok(out)
315 }
316
317 #[test]
318 fn multi_chunk_round_trip() {
319 let (header, mut sealer, key) = test_setup(8);
320 let content = b"this content spans multiple chunks".to_vec();
321
322 let chunks = seal_all(&mut sealer, &content, 8);
323 assert!(chunks.len() > 2);
324 assert_eq!(open_all(&header, &key, &chunks).unwrap(), content);
325 }
326
327 #[test]
328 fn empty_content_round_trip() {
329 let (header, mut sealer, key) = test_setup(8);
330 let chunks = seal_all(&mut sealer, b"", 8);
331 assert_eq!(chunks.len(), 1);
332 assert_eq!(open_all(&header, &key, &chunks).unwrap(), b"");
333 }
334
335 #[test]
336 fn exact_multiple_round_trip() {
337 let (header, mut sealer, key) = test_setup(8);
338 let content = b"0123456789abcdef".to_vec(); let chunks = seal_all(&mut sealer, &content, 8);
341 assert_eq!(chunks.len(), 2); assert_eq!(open_all(&header, &key, &chunks).unwrap(), content);
343 }
344
345 #[test]
346 fn truncation_is_detected() {
347 let (header, mut sealer, key) = test_setup(8);
348 let mut chunks = seal_all(&mut sealer, b"0123456789abcdefgh", 8);
349
350 chunks.pop();
352 assert!(open_all(&header, &key, &chunks).is_err());
353 }
354
355 #[test]
356 fn reordering_is_detected() {
357 let (header, mut sealer, key) = test_setup(8);
358 let mut chunks = seal_all(&mut sealer, b"0123456789abcdefgh", 8);
359 chunks.swap(0, 1);
360 assert!(open_all(&header, &key, &chunks).is_err());
361 }
362
363 #[test]
364 fn corruption_is_detected() {
365 let (header, mut sealer, key) = test_setup(8);
366 let mut chunks = seal_all(&mut sealer, b"0123456789abcdefgh", 8);
367 chunks[1][0] ^= 1;
368 assert!(open_all(&header, &key, &chunks).is_err());
369 }
370
371 #[test]
372 fn sealing_after_final_chunk_fails() {
373 let (_, mut sealer, _) = test_setup(8);
374 sealer.seal_chunk(b"tail", true).unwrap();
375 assert!(sealer.seal_chunk(b"more", true).is_err());
376 }
377
378 #[test]
379 fn short_non_final_chunk_fails() {
380 let (_, mut sealer, _) = test_setup(8);
381 assert!(sealer.seal_chunk(b"tiny", false).is_err());
382 }
383
384 #[test]
385 fn wrong_key_fails() {
386 let (header, mut sealer, key) = test_setup(8);
387 let chunks = seal_all(&mut sealer, b"content", 8);
388
389 let wrong = SecureKey::new([1u8; 32]);
390 assert!(open_all(&header, &wrong, &chunks).is_err());
391 assert!(open_all(&header, &key, &chunks).is_ok());
392 }
393}