1use crate::crypto::{CipherId, SecretKey, TAG_LEN, chunk_aad, chunk_nonce};
18use crate::error::{Error, Result};
19use crate::header::{COMPRESS_ZSTD, FixedHeader, flags};
20use crate::tlv::CompressionIndexEntry;
21
22pub type ProgressFn<'a> = &'a mut dyn FnMut(u64, u64);
39
40pub fn encrypt_chunk(
48 payload_key: &SecretKey,
49 cipher: CipherId,
50 base_nonce: &[u8; 7],
51 file_uuid: &[u8; 16],
52 index: u32,
53 is_final: bool,
54 plaintext: &[u8],
55) -> Result<Vec<u8>> {
56 let nonce = chunk_nonce(base_nonce, index, is_final);
57 let aad = chunk_aad(file_uuid, index);
58 cipher.encrypt(payload_key, &nonce, plaintext, &aad)
59}
60
61pub fn decrypt_chunk(
68 payload_key: &SecretKey,
69 cipher: CipherId,
70 base_nonce: &[u8; 7],
71 file_uuid: &[u8; 16],
72 index: u32,
73 is_final: bool,
74 ciphertext: &[u8],
75) -> Result<Vec<u8>> {
76 let nonce = chunk_nonce(base_nonce, index, is_final);
77 let aad = chunk_aad(file_uuid, index);
78 cipher
79 .decrypt(payload_key, &nonce, ciphertext, &aad)?
80 .ok_or(Error::ChunkAuthFailed { index: u64::from(index) })
81}
82
83fn decompress_chunk(body: &[u8], plain_len: u32, chunk_size: u32) -> Result<Vec<u8>> {
92 if plain_len > chunk_size {
93 return Err(Error::MalformedTlv {
94 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
95 reason: "index plain_len exceeds chunk_size",
96 });
97 }
98 let out = zstd::bulk::decompress(body, plain_len as usize)
99 .map_err(|err| Error::Compression { reason: err.to_string() })?;
100 if out.len() != plain_len as usize {
102 return Err(Error::Compression {
103 reason: format!(
104 "decompressed length {} does not match index plain_len {plain_len}",
105 out.len()
106 ),
107 });
108 }
109 Ok(out)
110}
111
112pub fn encrypt_payload(
122 header: &FixedHeader,
123 payload_key: &SecretKey,
124 plaintext: &[u8],
125 zstd_level: i32,
126) -> Result<(Vec<u8>, Vec<CompressionIndexEntry>)> {
127 encrypt_payload_with_progress(header, payload_key, plaintext, zstd_level, None)
128}
129
130pub fn encrypt_payload_with_progress(
136 header: &FixedHeader,
137 payload_key: &SecretKey,
138 plaintext: &[u8],
139 zstd_level: i32,
140 mut progress: Option<ProgressFn<'_>>,
141) -> Result<(Vec<u8>, Vec<CompressionIndexEntry>)> {
142 let compressed = header.compress_id == COMPRESS_ZSTD;
143 let chunk_size = header.chunk_size as usize;
144 let n_chunks = header.n_chunks();
145
146 let n_chunks_u32 = u32::try_from(n_chunks)
147 .map_err(|_| Error::MalformedHeader { reason: "chunk count exceeds u32" })?;
148
149 let mut out = Vec::new();
150 let mut index = Vec::new();
151
152 for i in 0..n_chunks_u32 {
153 let start = (i as usize).saturating_mul(chunk_size);
154 let end = start.saturating_add(chunk_size).min(plaintext.len());
155 let raw = plaintext.get(start..end).unwrap_or(&[]);
157 let is_final = i.saturating_add(1) == n_chunks_u32;
158
159 let body = if compressed {
160 zstd::bulk::compress(raw, zstd_level)
161 .map_err(|e| Error::Compression { reason: e.to_string() })?
162 } else {
163 raw.to_vec()
164 };
165
166 let ct = encrypt_chunk(
167 payload_key,
168 header.cipher_id,
169 &header.base_nonce,
170 &header.file_uuid,
171 i,
172 is_final,
173 &body,
174 )?;
175
176 if compressed {
177 index.push(CompressionIndexEntry {
178 ct_offset: out.len() as u64,
179 ct_len: u32::try_from(ct.len()).map_err(|_| Error::MalformedHeader {
180 reason: "chunk ciphertext exceeds u32",
181 })?,
182 plain_len: u32::try_from(raw.len()).map_err(|_| Error::MalformedHeader {
183 reason: "chunk plaintext exceeds u32",
184 })?,
185 });
186 }
187 out.extend_from_slice(&ct);
188
189 if let Some(cb) = progress.as_deref_mut() {
193 cb(end as u64, plaintext.len() as u64);
194 }
195 }
196
197 Ok((out, index))
198}
199
200pub fn encrypt_payload_stream<R: std::io::Read>(
213 header: &FixedHeader,
214 payload_key: &SecretKey,
215 input: &mut R,
216 zstd_level: i32,
217 sink: &mut dyn FnMut(&[u8]) -> Result<()>,
218 mut progress: Option<ProgressFn<'_>>,
219) -> Result<([u8; 32], Vec<CompressionIndexEntry>)> {
220 use blake2::digest::{Digest, consts::U32};
221 use std::io::Read as _;
222
223 let compressed = header.compress_id == COMPRESS_ZSTD;
224 let chunk_size = u64::from(header.chunk_size);
225 let total = header.plaintext_size;
226 let n_chunks_u32 = u32::try_from(header.n_chunks())
227 .map_err(|_| Error::MalformedHeader { reason: "chunk count exceeds u32" })?;
228
229 let mut hasher = blake2::Blake2b::<U32>::new();
230 let mut index = Vec::new();
231 let mut ct_offset = 0u64;
232 let mut done = 0u64;
233 let mut raw = Vec::with_capacity(usize::try_from(chunk_size).unwrap_or(0));
234
235 for i in 0..n_chunks_u32 {
236 let want = total.saturating_sub(done).min(chunk_size);
237 raw.clear();
238 let got = (&mut *input).take(want).read_to_end(&mut raw).map_err(Error::Io)?;
239 if got as u64 != want {
240 return Err(Error::Io(std::io::Error::new(
241 std::io::ErrorKind::UnexpectedEof,
242 "source shrank while encrypting",
243 )));
244 }
245 hasher.update(&raw);
246 let is_final = i.saturating_add(1) == n_chunks_u32;
247
248 let body = if compressed {
249 zstd::bulk::compress(&raw, zstd_level)
250 .map_err(|e| Error::Compression { reason: e.to_string() })?
251 } else {
252 raw.clone()
253 };
254 let ct = encrypt_chunk(
255 payload_key,
256 header.cipher_id,
257 &header.base_nonce,
258 &header.file_uuid,
259 i,
260 is_final,
261 &body,
262 )?;
263 if compressed {
264 index.push(CompressionIndexEntry {
265 ct_offset,
266 ct_len: u32::try_from(ct.len()).map_err(|_| Error::MalformedHeader {
267 reason: "chunk ciphertext exceeds u32",
268 })?,
269 plain_len: u32::try_from(raw.len()).map_err(|_| Error::MalformedHeader {
270 reason: "chunk plaintext exceeds u32",
271 })?,
272 });
273 }
274 sink(&ct)?;
275 ct_offset = ct_offset.saturating_add(ct.len() as u64);
276 done = done.saturating_add(want);
277 if let Some(cb) = progress.as_deref_mut() {
278 cb(done, total);
279 }
280 }
281
282 let mut probe = [0u8; 1];
284 if input.read(&mut probe).map_err(Error::Io)? != 0 {
285 return Err(Error::Io(std::io::Error::new(
286 std::io::ErrorKind::InvalidData,
287 "source grew while encrypting",
288 )));
289 }
290
291 Ok((hasher.finalize().into(), index))
292}
293
294pub fn decrypt_payload(
305 header: &FixedHeader,
306 payload_key: &SecretKey,
307 ciphertext: &[u8],
308 index: Option<&[CompressionIndexEntry]>,
309) -> Result<Vec<u8>> {
310 decrypt_payload_with_progress(header, payload_key, ciphertext, index, None)
311}
312
313pub fn decrypt_payload_with_progress(
319 header: &FixedHeader,
320 payload_key: &SecretKey,
321 ciphertext: &[u8],
322 index: Option<&[CompressionIndexEntry]>,
323 mut progress: Option<ProgressFn<'_>>,
324) -> Result<Vec<u8>> {
325 let compressed = header.has_flag(flags::COMPRESSED);
326 let n_chunks = header.n_chunks();
327 let n_chunks_u32 = u32::try_from(n_chunks)
328 .map_err(|_| Error::MalformedHeader { reason: "chunk count exceeds u32" })?;
329
330 let mut out = usize::try_from(header.plaintext_size)
333 .map_or_else(|_| Vec::new(), Vec::with_capacity);
334
335 if compressed {
336 let idx = index.ok_or(Error::MissingTlv {
337 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
338 })?;
339 if idx.len() as u64 != n_chunks {
340 return Err(Error::MalformedTlv {
341 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
342 reason: "index entry count does not match chunk count",
343 });
344 }
345 for (i, e) in idx.iter().enumerate() {
346 let i_u32 = u32::try_from(i)
347 .map_err(|_| Error::MalformedHeader { reason: "chunk index exceeds u32" })?;
348 let start = usize::try_from(e.ct_offset)
349 .map_err(|_| Error::MalformedTlv {
350 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
351 reason: "ct_offset exceeds addressable range",
352 })?;
353 let end = start.saturating_add(e.ct_len as usize);
354 let ct = ciphertext.get(start..end).ok_or(Error::Truncated {
355 context: "compressed chunk",
356 need: end,
357 got: ciphertext.len(),
358 })?;
359 let is_final = i_u32.saturating_add(1) == n_chunks_u32;
360
361 let body = decrypt_chunk(
362 payload_key,
363 header.cipher_id,
364 &header.base_nonce,
365 &header.file_uuid,
366 i_u32,
367 is_final,
368 ct,
369 )?;
370 let plain = decompress_chunk(&body, e.plain_len, header.chunk_size)?;
371 out.extend_from_slice(&plain);
372 if let Some(cb) = progress.as_deref_mut() {
373 cb(out.len() as u64, header.plaintext_size);
374 }
375 }
376 } else {
377 for i in 0..n_chunks_u32 {
378 let (off, len) = header.chunk_ct_range(u64::from(i))?;
379 let rel = off
381 .checked_sub(u64::from(header.header_len))
382 .ok_or(Error::MalformedHeader { reason: "chunk offset precedes payload" })?;
383 let start = usize::try_from(rel).map_err(|_| Error::MalformedHeader {
384 reason: "chunk offset exceeds addressable range",
385 })?;
386 let end = start.saturating_add(usize::try_from(len).map_err(|_| {
387 Error::MalformedHeader { reason: "chunk length exceeds addressable range" }
388 })?);
389 let ct = ciphertext.get(start..end).ok_or(Error::Truncated {
390 context: "payload chunk",
391 need: end,
392 got: ciphertext.len(),
393 })?;
394 let is_final = i.saturating_add(1) == n_chunks_u32;
395
396 let plain = decrypt_chunk(
397 payload_key,
398 header.cipher_id,
399 &header.base_nonce,
400 &header.file_uuid,
401 i,
402 is_final,
403 ct,
404 )?;
405 out.extend_from_slice(&plain);
406 if let Some(cb) = progress.as_deref_mut() {
407 cb(out.len() as u64, header.plaintext_size);
408 }
409 }
410 }
411
412 if out.len() as u64 != header.plaintext_size {
413 return Err(Error::MalformedHeader {
414 reason: "decrypted length does not match plaintext_size",
415 });
416 }
417 Ok(out)
418}
419
420pub fn read_range<F>(
452 header: &FixedHeader,
453 payload_key: &SecretKey,
454 index: Option<&[CompressionIndexEntry]>,
455 offset: u64,
456 length: u64,
457 mut fetch_ct: F,
458) -> Result<Vec<u8>>
459where
460 F: FnMut(u64, u64) -> Result<Vec<u8>>,
461{
462 if length == 0 {
463 return Ok(Vec::new());
464 }
465 let end = offset
466 .checked_add(length)
467 .ok_or(Error::MalformedHeader { reason: "range end overflows" })?
468 .min(header.plaintext_size);
469 if offset >= header.plaintext_size {
470 return Ok(Vec::new());
471 }
472
473 let n_chunks = header.n_chunks();
474 let compressed = header.has_flag(flags::COMPRESSED);
475 let cs = u64::from(header.chunk_size);
476
477 let first = header.chunk_index_of(offset);
478 let last = header.chunk_index_of(end.saturating_sub(1));
479
480 let mut acc = Vec::with_capacity(usize::try_from(end.saturating_sub(offset)).unwrap_or(0));
481
482 for i in first..=last {
483 if i >= n_chunks {
484 return Err(Error::ChunkOutOfRange { index: i, total: n_chunks });
485 }
486 let i_u32 =
487 u32::try_from(i).map_err(|_| Error::MalformedHeader { reason: "chunk index exceeds u32" })?;
488 let is_final = i.saturating_add(1) == n_chunks;
489
490 let plain = if compressed {
491 let idx = index.ok_or(Error::MissingTlv {
492 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
493 })?;
494 let e = idx.get(usize::try_from(i).unwrap_or(usize::MAX)).ok_or(
495 Error::MalformedTlv {
496 tlv_type: crate::tlv::types::COMPRESSION_INDEX,
497 reason: "index shorter than chunk count",
498 },
499 )?;
500 let ct = fetch_ct(e.ct_offset, u64::from(e.ct_len))?;
501 let body = decrypt_chunk(
502 payload_key,
503 header.cipher_id,
504 &header.base_nonce,
505 &header.file_uuid,
506 i_u32,
507 is_final,
508 &ct,
509 )?;
510 decompress_chunk(&body, e.plain_len, header.chunk_size)?
511 } else {
512 let (abs_off, ct_len) = header.chunk_ct_range(i)?;
513 let rel = abs_off
514 .checked_sub(u64::from(header.header_len))
515 .ok_or(Error::MalformedHeader { reason: "chunk offset precedes payload" })?;
516 let ct = fetch_ct(rel, ct_len)?;
517 decrypt_chunk(
518 payload_key,
519 header.cipher_id,
520 &header.base_nonce,
521 &header.file_uuid,
522 i_u32,
523 is_final,
524 &ct,
525 )?
526 };
527
528 let chunk_start = i.saturating_mul(cs);
530 let want_from = offset.saturating_sub(chunk_start);
531 let want_to = end.saturating_sub(chunk_start).min(plain.len() as u64);
532 if want_from < want_to {
533 let a = usize::try_from(want_from).unwrap_or(0);
534 let b = usize::try_from(want_to).unwrap_or(plain.len());
535 if let Some(s) = plain.get(a..b) {
536 acc.extend_from_slice(s);
537 }
538 }
539 }
540 Ok(acc)
541}
542
543#[must_use]
547pub fn payload_ct_size(header: &FixedHeader) -> u64 {
548 let n = header.n_chunks();
549 header
550 .plaintext_size
551 .saturating_add(n.saturating_mul(TAG_LEN as u64))
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::crypto::{Argon2Params, Fek, KEY_LEN, Kek, SecretKey};
558 use crate::file::{EncryptOptions, RandomMaterial, encrypt, open};
559 use crate::header::{MIN_CHUNK_SIZE, SLOT_AREA_LEN, SLOT_LEN};
560
561 fn make(plain: &[u8]) -> (Vec<u8>, Kek) {
563 let salt = [0x31u8; 16];
564 let params = Argon2Params::TEST_WEAK;
565 let kek = Kek::from_password(b"payload-test", &salt, params).unwrap();
566 let kek2 = Kek::from_password(b"payload-test", &salt, params).unwrap();
567 let opts = EncryptOptions {
568 chunk_size: MIN_CHUNK_SIZE,
569 argon2: params,
570 ..EncryptOptions::default()
571 };
572 let enc = encrypt(plain, &[kek], &salt, &opts, &RandomMaterial::generate()).unwrap();
573 (enc.bytes, kek2)
574 }
575
576 #[test]
584 fn fetch_ct_offset_is_payload_relative() {
585 let plain: Vec<u8> = (0..150_000u32).map(|i| (i % 253) as u8).collect();
586 let (bytes, kek) = make(&plain);
587 let opened = open(&bytes, &[kek]).unwrap();
588 let header_len = u64::from(opened.header.header_len);
589 assert!(header_len > 0, "头部长度应为正");
590
591 let seen: std::cell::RefCell<Vec<(u64, u64)>> = std::cell::RefCell::new(Vec::new());
593 let got = read_range(
594 &opened.header,
595 opened.payload_key(),
596 None,
597 0,
598 plain.len() as u64,
599 |off, len| {
600 seen.borrow_mut().push((off, len));
601 let a = usize::try_from(off + header_len).unwrap();
602 let b = usize::try_from(off + header_len + len).unwrap();
603 Ok(bytes[a..b].to_vec())
604 },
605 )
606 .unwrap();
607 assert_eq!(got, plain, "正确加回 header_len 后应完整还原");
608
609 let calls = seen.borrow();
611 assert_eq!(calls[0].0, 0, "首个 fetch_ct 偏移应为 0,即相对载荷起点");
612
613 let wrong = read_range(
615 &opened.header,
616 opened.payload_key(),
617 None,
618 0,
619 1000,
620 |off, len| {
621 let a = usize::try_from(off).unwrap();
623 let b = usize::try_from(off + len).unwrap();
624 Ok(bytes[a..b].to_vec())
625 },
626 );
627 assert!(
628 matches!(wrong, Err(Error::ChunkAuthFailed { .. })),
629 "把偏移当绝对值使用必须触发认证失败,实际: {wrong:?}"
630 );
631 }
632
633 #[test]
635 fn read_range_only_fetches_needed_chunks() {
636 let cs = u64::from(MIN_CHUNK_SIZE);
637 let plain: Vec<u8> = (0..(cs * 5) as u32).map(|i| (i % 251) as u8).collect();
638 let (bytes, kek) = make(&plain);
639 let opened = open(&bytes, &[kek]).unwrap();
640 let header_len = u64::from(opened.header.header_len);
641 assert_eq!(opened.header.n_chunks(), 5, "应为 5 块");
642
643 let n_calls = std::cell::Cell::new(0u32);
645 let total_ct = std::cell::Cell::new(0u64);
646 let off = cs * 3 + 500;
647 let got = read_range(
648 &opened.header,
649 opened.payload_key(),
650 None,
651 off,
652 100,
653 |o, l| {
654 n_calls.set(n_calls.get() + 1);
655 total_ct.set(total_ct.get() + l);
656 let a = usize::try_from(o + header_len).unwrap();
657 let b = usize::try_from(o + header_len + l).unwrap();
658 Ok(bytes[a..b].to_vec())
659 },
660 )
661 .unwrap();
662
663 assert_eq!(got, &plain[off as usize..(off + 100) as usize]);
664 assert_eq!(n_calls.get(), 1, "只跨 1 块就只该取 1 次");
665 assert!(
667 total_ct.get() <= cs + TAG_LEN as u64,
668 "实读密文 {} 超过单块上限,说明退化成了多块读取",
669 total_ct.get()
670 );
671 assert!(
672 total_ct.get() * 2 < plain.len() as u64,
673 "实读密文不应接近整个文件"
674 );
675 }
676
677 #[test]
679 fn read_range_across_chunk_boundary() {
680 let cs = u64::from(MIN_CHUNK_SIZE);
681 let plain: Vec<u8> = (0..(cs * 3) as u32).map(|i| (i % 249) as u8).collect();
682 let (bytes, kek) = make(&plain);
683 let opened = open(&bytes, &[kek]).unwrap();
684 let header_len = u64::from(opened.header.header_len);
685
686 let fetch = |o: u64, l: u64| -> Result<Vec<u8>> {
687 let a = usize::try_from(o + header_len).unwrap();
688 let b = usize::try_from(o + header_len + l).unwrap();
689 Ok(bytes[a..b].to_vec())
690 };
691
692 let off = cs - 50;
694 let got = read_range(&opened.header, opened.payload_key(), None, off, 100, fetch).unwrap();
695 assert_eq!(got, &plain[off as usize..(off + 100) as usize]);
696
697 let off2 = cs * 3 - 30;
699 let got2 = read_range(&opened.header, opened.payload_key(), None, off2, 30, fetch).unwrap();
700 assert_eq!(got2, &plain[off2 as usize..]);
701
702 let got3 = read_range(&opened.header, opened.payload_key(), None, off2, 9999, fetch).unwrap();
704 assert_eq!(got3, &plain[off2 as usize..], "超出部分应被裁剪");
705
706 let got4 = read_range(
708 &opened.header,
709 opened.payload_key(),
710 None,
711 plain.len() as u64,
712 10,
713 fetch,
714 )
715 .unwrap();
716 assert!(got4.is_empty(), "起点超界应返回空而非报错");
717 }
718
719 #[test]
724 fn progress_is_monotonic_and_reaches_total() {
725 let cs = u64::from(MIN_CHUNK_SIZE);
726 let plain: Vec<u8> = (0..(cs * 3 + 123) as u32).map(|i| (i % 251) as u8).collect();
728
729 let salt = [7u8; 16];
730 let params = Argon2Params::TEST_WEAK;
731 let kek = Kek::from_password(b"progress-test", &salt, params).unwrap();
732 let kek2 = Kek::from_password(b"progress-test", &salt, params).unwrap();
733 let opts = EncryptOptions {
734 chunk_size: MIN_CHUNK_SIZE,
735 argon2: params,
736 ..EncryptOptions::default()
737 };
738
739 let mut seen: Vec<(u64, u64)> = Vec::new();
740 let enc = {
741 let mut cb = |done: u64, total: u64| seen.push((done, total));
742 crate::file::encrypt_with_progress(
743 &plain,
744 &[kek],
745 &salt,
746 &opts,
747 &RandomMaterial::generate(),
748 Some(&mut cb),
749 )
750 .unwrap()
751 };
752
753 assert_eq!(seen.len(), 4, "3 个整块 + 1 个尾块,应回调 4 次");
754 assert!(
755 seen.windows(2).all(|w| w[1].0 >= w[0].0),
756 "进度必须单调不减,实际: {seen:?}"
757 );
758 assert!(
759 seen.iter().all(|&(_, total)| total == plain.len() as u64),
760 "总长必须始终是明文长度"
761 );
762 assert_eq!(
763 seen.last().map(|&(d, _)| d),
764 Some(plain.len() as u64),
765 "最后一次必须报满,否则界面停在 99%"
766 );
767
768 let opened = open(&enc.bytes, &[kek2]).unwrap();
770 let mut dseen: Vec<u64> = Vec::new();
771 let out = {
772 let mut dcb = |done: u64, _total: u64| dseen.push(done);
773 opened
774 .decrypt_all_with_progress(&enc.bytes, Some(&mut dcb))
775 .unwrap()
776 };
777 assert_eq!(out, plain, "带进度回调不能改变解密结果");
778 assert_eq!(
779 dseen.last().copied(),
780 Some(plain.len() as u64),
781 "解密进度也必须报满"
782 );
783 }
784
785 #[test]
790 fn progress_reports_plaintext_bytes_when_compressed() {
791 let cs = u64::from(MIN_CHUNK_SIZE);
792 let plain = vec![0u8; (cs * 3) as usize];
794
795 let salt = [9u8; 16];
796 let params = Argon2Params::TEST_WEAK;
797 let kek = Kek::from_password(b"zip-progress", &salt, params).unwrap();
798 let opts = EncryptOptions {
799 chunk_size: MIN_CHUNK_SIZE,
800 argon2: params,
801 compress: true,
802 ..EncryptOptions::default()
803 };
804
805 let mut last = 0u64;
806 let enc = {
807 let mut cb = |done: u64, _total: u64| last = done;
808 crate::file::encrypt_with_progress(
809 &plain,
810 &[kek],
811 &salt,
812 &opts,
813 &RandomMaterial::generate(),
814 Some(&mut cb),
815 )
816 .unwrap()
817 };
818
819 assert_eq!(last, plain.len() as u64, "压缩时进度仍应按明文计");
820 assert!(
821 enc.bytes.len() < plain.len(),
822 "前提校验:全零数据应当被显著压缩,否则这个测试没测到东西"
823 );
824 }
825
826 #[test]
836 fn progress_does_not_change_output() {
837 let plain: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
838 let salt = [3u8; 16];
839 let params = Argon2Params::TEST_WEAK;
840 let kek = Kek::from_password(b"same-out", &salt, params).unwrap();
841 let opts = EncryptOptions {
842 chunk_size: MIN_CHUNK_SIZE,
843 argon2: params,
844 ..EncryptOptions::default()
845 };
846
847 let key_bytes: [u8; KEY_LEN] = core::array::from_fn(|i| (i as u8).wrapping_mul(7) ^ 0x3D);
855 let fek = Fek::from_key(SecretKey::from_bytes(key_bytes));
856 let fek2 = Fek::from_key(SecretKey::from_bytes(key_bytes));
857 let rnd = RandomMaterial {
858 file_uuid: core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(2)),
859 base_nonce: core::array::from_fn(|i| (i as u8).wrapping_mul(13).wrapping_add(5)),
860 slot_padding: Some(
861 (0..SLOT_AREA_LEN - SLOT_LEN).map(|i| (i % 251) as u8).collect(),
862 ),
863 };
864
865 let a = crate::file::encrypt_with_fek(
866 &plain,
867 &[kek.duplicate()],
868 &salt,
869 &opts,
870 &rnd,
871 &fek,
872 )
873 .unwrap();
874
875 let mut hits = 0u32;
876 let b = {
877 let mut cb = |_d: u64, _t: u64| hits += 1;
878 crate::file::encrypt_with_fek_and_progress(
879 &plain,
880 &[kek],
881 &salt,
882 &opts,
883 &rnd,
884 &fek2,
885 Some(&mut cb),
886 )
887 .unwrap()
888 };
889
890 assert_eq!(a.bytes, b.bytes, "带不带进度回调,产出必须逐字节相同");
891 assert!(hits > 0, "前提校验:回调确实被调用过,否则上面的相等是白比的");
892 }
893}