1use crate::crypto::{Argon2Params, CipherId, Fek, Kek, SecretKey, content_hash, header_mac, verify_header_mac};
17use crate::error::{Error, Result};
18use crate::payload;
19use crate::header::{
20 COMPRESS_NONE, COMPRESS_ZSTD, DEFAULT_CHUNK_SIZE, FIXED_HEADER_LEN, FixedHeader,
21 HEADER_MAC_LEN, KDF_ARGON2ID, SLOT_AREA_LEN, SLOT_AREA_OFFSET, SLOT_COUNT, SLOT_COUNT_U8,
22 TLV_AREA_OFFSET, VERSION_MAJOR, VERSION_MINOR, flags, validate_chunk_size,
23};
24use crate::slot::{build_slot_area, unwrap_fek};
25use crate::tlv::{
26 CompressionIndexEntry, TlvEntry, TlvSet, decode_compression_index, encode_compression_index,
27 encrypt_entry, pad_filename, tlv_flags, types, unpad_filename,
28};
29use crate::util::{Writer, random_16};
30
31#[derive(Debug, Clone)]
33pub struct EncryptOptions {
34 pub filename: Option<String>,
36 pub preserve_extension: bool,
38 pub compress: bool,
40 pub zstd_level: i32,
42 pub chunk_size: u32,
44 pub cipher: CipherId,
46 pub argon2: Argon2Params,
48 pub write_content_hash: bool,
50 pub thumbnail: Option<Vec<u8>>,
52 pub media_meta: Option<Vec<u8>>,
54 pub moov_cache: Option<Vec<u8>>,
65 pub folder_index: Option<Vec<u8>>,
71 pub slot_directory: Option<Vec<u8>>,
80}
81
82impl Default for EncryptOptions {
83 fn default() -> Self {
84 Self {
85 filename: None,
86 preserve_extension: false,
87 compress: false,
88 zstd_level: 3,
89 chunk_size: DEFAULT_CHUNK_SIZE,
90 cipher: CipherId::ChaCha20Poly1305,
91 argon2: Argon2Params::default(),
92 write_content_hash: true,
93 thumbnail: None,
94 media_meta: None,
95 moov_cache: None,
96 folder_index: None,
97 slot_directory: None,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
107pub struct RandomMaterial {
108 pub file_uuid: [u8; 16],
110 pub base_nonce: [u8; 7],
112 pub slot_padding: Option<Vec<u8>>,
114}
115
116impl RandomMaterial {
117 #[must_use]
119 pub fn generate() -> Self {
120 let mut bn = [0u8; 7];
121 crate::util::fill_random(&mut bn);
122 Self { file_uuid: random_16(), base_nonce: bn, slot_padding: None }
123 }
124}
125
126#[derive(Debug)]
128pub struct EncryptedFile {
129 pub bytes: Vec<u8>,
131 pub header: FixedHeader,
133}
134
135pub fn encrypt(
145 plaintext: &[u8],
146 keks: &[Kek],
147 vault_salt: &[u8; 16],
148 opts: &EncryptOptions,
149 rnd: &RandomMaterial,
150) -> Result<EncryptedFile> {
151 encrypt_with_progress(plaintext, keks, vault_salt, opts, rnd, None)
152}
153
154pub fn encrypt_with_progress(
163 plaintext: &[u8],
164 keks: &[Kek],
165 vault_salt: &[u8; 16],
166 opts: &EncryptOptions,
167 rnd: &RandomMaterial,
168 progress: Option<payload::ProgressFn<'_>>,
169) -> Result<EncryptedFile> {
170 if keks.is_empty() {
171 return Err(Error::TooManySlots { got: 0, max: SLOT_COUNT });
172 }
173 validate_chunk_size(opts.chunk_size)?;
174
175 let fek = Fek::random();
176 encrypt_with_fek_and_progress(plaintext, keks, vault_salt, opts, rnd, &fek, progress)
177}
178
179pub fn encrypt_with_fek(
190 plaintext: &[u8],
191 keks: &[Kek],
192 vault_salt: &[u8; 16],
193 opts: &EncryptOptions,
194 rnd: &RandomMaterial,
195 fek: &Fek,
196) -> Result<EncryptedFile> {
197 encrypt_with_fek_and_progress(plaintext, keks, vault_salt, opts, rnd, fek, None)
198}
199
200pub fn encrypt_with_fek_and_progress(
206 plaintext: &[u8],
207 keks: &[Kek],
208 vault_salt: &[u8; 16],
209 opts: &EncryptOptions,
210 rnd: &RandomMaterial,
211 fek: &Fek,
212 progress: Option<payload::ProgressFn<'_>>,
213) -> Result<EncryptedFile> {
214 if opts.chunk_size == 0 {
215 return Err(Error::MalformedHeader { reason: "chunk_size must not be zero" });
216 }
217
218 let mut header = new_header(opts, rnd, vault_salt, plaintext.len() as u64);
219 let payload_key = fek.derive_payload_key(&header.file_uuid);
220 let (payload, index) = payload::encrypt_payload_with_progress(
221 &header,
222 &payload_key,
223 plaintext,
224 opts.zstd_level,
225 progress,
226 )?;
227
228 let hash = opts.write_content_hash.then(|| content_hash(plaintext));
229 let tlvs = build_tlvs(opts, fek, hash.as_ref(), &index, opts.folder_index.as_deref())?;
230 let head = seal_header(&mut header, &tlvs, keks, fek, rnd, opts.cipher)?;
231
232 let mut out = Writer::with_capacity(head.len().saturating_add(payload.len()));
233 out.bytes(&head).bytes(&payload);
234
235 let bytes = out.into_vec();
236 debug_assert_eq!(
237 bytes.len(),
238 (header.header_len as usize).saturating_add(payload.len()),
239 "输出长度与 header_len + payload 不符"
240 );
241
242 Ok(EncryptedFile { bytes, header })
243}
244
245fn file_flags(opts: &EncryptOptions) -> u32 {
247 let mut file_flags = 0u32;
248 if opts.filename.is_some() {
249 file_flags |= flags::FILENAME_ENCRYPTED;
250 }
251 if opts.preserve_extension {
252 file_flags |= flags::EXT_PRESERVED;
253 }
254 if opts.compress {
255 file_flags |= flags::COMPRESSED;
256 }
257 if opts.thumbnail.is_some() {
258 file_flags |= flags::HAS_THUMBNAIL;
259 }
260 if opts.slot_directory.is_some() {
261 file_flags |= flags::SLOT_DIRECTORY;
262 }
263 if opts.folder_index.is_some() {
264 file_flags |= flags::CONTAINER;
265 }
266 file_flags
267}
268
269fn new_header(
271 opts: &EncryptOptions,
272 rnd: &RandomMaterial,
273 vault_salt: &[u8; 16],
274 plaintext_size: u64,
275) -> FixedHeader {
276 FixedHeader {
277 version_major: VERSION_MAJOR,
278 version_minor: VERSION_MINOR,
279 header_len: 0,
280 file_uuid: rnd.file_uuid,
281 vault_salt: *vault_salt,
282 flags: file_flags(opts),
283 cipher_id: opts.cipher,
284 kdf_id: KDF_ARGON2ID,
285 compress_id: if opts.compress { COMPRESS_ZSTD } else { COMPRESS_NONE },
286 slot_count: SLOT_COUNT_U8,
287 argon2_m_kib: opts.argon2.m_kib,
288 argon2_t: opts.argon2.t,
289 argon2_p: opts.argon2.p,
290 chunk_size: opts.chunk_size,
291 plaintext_size,
292 base_nonce: rnd.base_nonce,
293 chunk_version: 0,
294 tlv_len: 0,
295 }
296}
297
298fn build_tlvs(
304 opts: &EncryptOptions,
305 fek: &Fek,
306 hash: Option<&[u8; 32]>,
307 index: &[CompressionIndexEntry],
308 folder_index: Option<&[u8]>,
309) -> Result<TlvSet> {
310 let mut tlvs = TlvSet::new();
311
312 if let Some(name) = &opts.filename {
313 let padded = pad_filename(name)?;
314 tlvs.push(encrypt_entry(
315 types::FILENAME,
316 tlv_flags::CRITICAL,
317 &padded,
318 fek,
319 opts.cipher,
320 )?);
321 }
322 if opts.preserve_extension {
323 let ext = opts
324 .filename
325 .as_deref()
326 .and_then(|n| n.rsplit_once('.').map(|(_, e)| e))
327 .unwrap_or("");
328 tlvs.push(TlvEntry::new(types::PLAIN_EXT, 0, ext.as_bytes().to_vec()));
329 }
330 if let Some(t) = &opts.thumbnail {
331 tlvs.push(encrypt_entry(types::THUMBNAIL, 0, t, fek, opts.cipher)?);
332 }
333 if let Some(m) = &opts.media_meta {
334 tlvs.push(encrypt_entry(types::MEDIA_META, 0, m, fek, opts.cipher)?);
335 }
336 if let Some(mv) = &opts.moov_cache {
337 tlvs.push(encrypt_entry(types::MOOV_CACHE, 0, mv, fek, opts.cipher)?);
340 }
341 if let Some(idx) = folder_index {
342 tlvs.push(encrypt_entry(
344 types::FOLDER_INDEX,
345 tlv_flags::CRITICAL,
346 idx,
347 fek,
348 opts.cipher,
349 )?);
350 }
351 if let Some(dir) = &opts.slot_directory {
352 tlvs.push(encrypt_entry(
360 types::SLOT_DIRECTORY,
361 tlv_flags::CRITICAL,
362 dir,
363 fek,
364 opts.cipher,
365 )?);
366 }
367 if let Some(h) = hash {
368 tlvs.push(encrypt_entry(types::CONTENT_HASH, 0, h, fek, opts.cipher)?);
369 }
370 if opts.compress {
373 let enc = encode_compression_index(index);
374 tlvs.push(encrypt_entry(
375 types::COMPRESSION_INDEX,
376 tlv_flags::CRITICAL,
377 &enc,
378 fek,
379 opts.cipher,
380 )?);
381 }
382 Ok(tlvs)
383}
384
385fn seal_header(
387 header: &mut FixedHeader,
388 tlvs: &TlvSet,
389 keks: &[Kek],
390 fek: &Fek,
391 rnd: &RandomMaterial,
392 cipher: CipherId,
393) -> Result<Vec<u8>> {
394 let tlv_blob = tlvs.to_bytes();
395 header.tlv_len = u32::try_from(tlv_blob.len())
396 .map_err(|_| Error::MalformedHeader { reason: "tlv area exceeds u32" })?;
397 header.header_len = u32::try_from(
398 TLV_AREA_OFFSET
399 .saturating_add(tlv_blob.len())
400 .saturating_add(HEADER_MAC_LEN),
401 )
402 .map_err(|_| Error::MalformedHeader { reason: "header_len exceeds u32" })?;
403
404 let fixed = header.to_bytes()?;
405 let slot_area = if let Some(pad) = &rnd.slot_padding {
406 build_slot_area_with_padding(keks, fek, &header.file_uuid, cipher, pad)?
407 } else {
408 build_slot_area(keks, fek, &header.file_uuid, cipher)?
409 };
410
411 let mut covered = Writer::with_capacity(header.header_len as usize);
413 covered.bytes(&fixed).bytes(&slot_area).bytes(&tlv_blob);
414 let mac_key = fek.derive_header_mac_key(&header.file_uuid);
415 let mac = header_mac(&mac_key, covered.as_slice());
416 let mut out = covered.into_vec();
417 out.extend_from_slice(&mac);
418 Ok(out)
419}
420
421pub type FinalizeFolderIndex<'a> = &'a mut dyn FnMut() -> Result<Vec<u8>>;
428
429#[derive(Debug, Clone)]
431pub struct StreamedFile {
432 pub header: FixedHeader,
434 pub head: Vec<u8>,
437 pub len: u64,
439}
440
441#[allow(clippy::too_many_arguments)]
459pub fn encrypt_stream<R: std::io::Read, W: std::io::Write + std::io::Seek>(
460 input: &mut R,
461 plaintext_len: u64,
462 keks: &[Kek],
463 vault_salt: &[u8; 16],
464 opts: &EncryptOptions,
465 rnd: &RandomMaterial,
466 out: &mut W,
467 progress: Option<payload::ProgressFn<'_>>,
468 finalize_folder_index: Option<FinalizeFolderIndex<'_>>,
469) -> Result<StreamedFile> {
470 if keks.is_empty() {
471 return Err(Error::TooManySlots { got: 0, max: SLOT_COUNT });
472 }
473 validate_chunk_size(opts.chunk_size)?;
474 let fek = Fek::random();
475 encrypt_stream_with_fek(
476 input,
477 plaintext_len,
478 keks,
479 vault_salt,
480 opts,
481 rnd,
482 &fek,
483 out,
484 progress,
485 finalize_folder_index,
486 )
487}
488
489#[allow(clippy::too_many_arguments)]
495pub fn encrypt_stream_with_fek<R: std::io::Read, W: std::io::Write + std::io::Seek>(
496 input: &mut R,
497 plaintext_len: u64,
498 keks: &[Kek],
499 vault_salt: &[u8; 16],
500 opts: &EncryptOptions,
501 rnd: &RandomMaterial,
502 fek: &Fek,
503 out: &mut W,
504 progress: Option<payload::ProgressFn<'_>>,
505 finalize_folder_index: Option<FinalizeFolderIndex<'_>>,
506) -> Result<StreamedFile> {
507 use std::io::SeekFrom;
508 if opts.chunk_size == 0 {
509 return Err(Error::MalformedHeader { reason: "chunk_size must not be zero" });
510 }
511 let mut header = new_header(opts, rnd, vault_salt, plaintext_len);
512 let n_chunks = usize::try_from(header.n_chunks())
513 .map_err(|_| Error::MalformedHeader { reason: "chunk count exceeds usize" })?;
514
515 let zero_hash = [0u8; 32];
518 let placeholder_index = vec![
519 CompressionIndexEntry { ct_offset: 0, ct_len: 0, plain_len: 0 };
520 if opts.compress { n_chunks } else { 0 }
521 ];
522 let placeholder = build_tlvs(
523 opts,
524 fek,
525 opts.write_content_hash.then_some(&zero_hash),
526 &placeholder_index,
527 opts.folder_index.as_deref(),
528 )?;
529 let reserved = TLV_AREA_OFFSET
530 .saturating_add(placeholder.to_bytes().len())
531 .saturating_add(HEADER_MAC_LEN);
532 let base = out.stream_position().map_err(Error::Io)?;
533 out.write_all(&vec![0u8; reserved]).map_err(Error::Io)?;
534
535 let payload_key = fek.derive_payload_key(&header.file_uuid);
536 let (hash, index) = payload::encrypt_payload_stream(
537 &header,
538 &payload_key,
539 input,
540 opts.zstd_level,
541 &mut |ct| out.write_all(ct).map_err(Error::Io),
542 progress,
543 )?;
544
545 let folder_index = match (finalize_folder_index, opts.folder_index.as_deref()) {
546 (Some(f), Some(placeholder_idx)) => {
547 let real = f()?;
548 if real.len() != placeholder_idx.len() {
549 return Err(Error::MalformedHeader {
550 reason: "finalized folder index length differs from placeholder",
551 });
552 }
553 Some(real)
554 }
555 (_, other) => other.map(<[u8]>::to_vec),
556 };
557 let tlvs = build_tlvs(
558 opts,
559 fek,
560 opts.write_content_hash.then_some(&hash),
561 &index,
562 folder_index.as_deref(),
563 )?;
564 let head = seal_header(&mut header, &tlvs, keks, fek, rnd, opts.cipher)?;
565 if head.len() != reserved {
566 return Err(Error::MalformedHeader { reason: "streamed header length changed" });
567 }
568 let end = out.stream_position().map_err(Error::Io)?;
569 out.seek(SeekFrom::Start(base)).map_err(Error::Io)?;
570 out.write_all(&head).map_err(Error::Io)?;
571 out.seek(SeekFrom::Start(end)).map_err(Error::Io)?;
572 out.flush().map_err(Error::Io)?;
573 Ok(StreamedFile { header, head, len: end.saturating_sub(base) })
574}
575
576fn build_slot_area_with_padding(
578 keks: &[Kek],
579 fek: &Fek,
580 file_uuid: &[u8; 16],
581 cipher: CipherId,
582 padding: &[u8],
583) -> Result<Vec<u8>> {
584 use crate::crypto::ZERO_NONCE;
585 use crate::header::SLOT_LEN;
586
587 if keks.len() > SLOT_COUNT {
588 return Err(Error::TooManySlots { got: keks.len(), max: SLOT_COUNT });
589 }
590 let mut w = Writer::with_capacity(SLOT_AREA_LEN);
591 for (i, kek) in keks.iter().enumerate() {
592 let idx = u16::try_from(i)
593 .map_err(|_| Error::TooManySlots { got: keks.len(), max: SLOT_COUNT })?;
594 let wk = kek.derive_slot_key(file_uuid, idx);
595 w.bytes(&cipher.encrypt(&wk, &ZERO_NONCE, fek.as_key().as_bytes(), &[])?);
596 }
597 let need = SLOT_AREA_LEN.saturating_sub(keks.len().saturating_mul(SLOT_LEN));
598 let pad = padding.get(..need).ok_or(Error::MalformedHeader {
599 reason: "provided slot padding shorter than required",
600 })?;
601 w.bytes(pad);
602 Ok(w.into_vec())
603}
604
605#[derive(Debug)]
609pub struct OpenedFile {
610 pub header: FixedHeader,
612 pub tlvs: TlvSet,
614 pub kek_index: usize,
616 pub slot_index: u16,
618 fek: Fek,
619 payload_key: SecretKey,
620}
621
622impl OpenedFile {
623 #[must_use]
625 pub const fn payload_key(&self) -> &SecretKey {
626 &self.payload_key
627 }
628
629 #[must_use]
631 pub const fn fek(&self) -> &Fek {
632 &self.fek
633 }
634
635 pub fn filename(&self) -> Result<String> {
642 let padded = self.tlvs.decrypt_value(types::FILENAME, &self.fek, self.header.cipher_id)?;
643 unpad_filename(&padded)
644 }
645
646 #[must_use]
648 pub fn plain_extension(&self) -> Option<String> {
649 self.tlvs
650 .find(types::PLAIN_EXT)
651 .map(|e| String::from_utf8_lossy(&e.value).into_owned())
652 }
653
654 pub fn thumbnail(&self) -> Result<Vec<u8>> {
660 self.tlvs.decrypt_value(types::THUMBNAIL, &self.fek, self.header.cipher_id)
661 }
662
663 pub fn media_meta(&self) -> Result<Vec<u8>> {
669 self.tlvs.decrypt_value(types::MEDIA_META, &self.fek, self.header.cipher_id)
670 }
671
672 pub fn moov_cache(&self) -> Result<Vec<u8>> {
681 self.tlvs.decrypt_value(types::MOOV_CACHE, &self.fek, self.header.cipher_id)
682 }
683
684 #[must_use]
689 pub fn has_moov_cache(&self) -> bool {
690 self.tlvs.find(types::MOOV_CACHE).is_some()
691 }
692
693 #[must_use]
695 pub fn has_media_meta(&self) -> bool {
696 self.tlvs.find(types::MEDIA_META).is_some()
697 }
698
699 #[must_use]
704 pub fn has_thumbnail(&self) -> bool {
705 self.tlvs.find(types::THUMBNAIL).is_some()
706 }
707
708 pub fn stored_content_hash(&self) -> Result<[u8; 32]> {
714 let v = self.tlvs.decrypt_value(types::CONTENT_HASH, &self.fek, self.header.cipher_id)?;
715 let s = v.get(..32).ok_or(Error::MalformedTlv {
716 tlv_type: types::CONTENT_HASH,
717 reason: "content hash must be 32 bytes",
718 })?;
719 let mut out = [0u8; 32];
720 out.copy_from_slice(s);
721 Ok(out)
722 }
723
724 pub fn compression_index(&self) -> Result<Vec<CompressionIndexEntry>> {
730 if !self.header.has_flag(flags::COMPRESSED) {
731 return Ok(Vec::new());
732 }
733 let raw =
734 self.tlvs.decrypt_value(types::COMPRESSION_INDEX, &self.fek, self.header.cipher_id)?;
735 decode_compression_index(&raw)
736 }
737
738 pub fn folder_index(&self) -> Result<crate::container::ContainerIndex> {
747 if !self.header.has_flag(flags::CONTAINER) {
748 return Err(Error::MissingTlv { tlv_type: types::FOLDER_INDEX });
749 }
750 let raw = self
751 .tlvs
752 .decrypt_value(types::FOLDER_INDEX, &self.fek, self.header.cipher_id)?;
753 crate::container::ContainerIndex::parse(&raw)
754 }
755
756 #[must_use]
758 pub const fn is_container(&self) -> bool {
759 self.header.has_flag(flags::CONTAINER)
760 }
761
762 #[must_use]
764 pub const fn is_slot_managed(&self) -> bool {
765 self.header.has_flag(flags::SLOT_DIRECTORY)
766 }
767
768 pub fn slot_directory(&self) -> Result<crate::slotdir::SlotDirectory> {
778 if !self.is_slot_managed() {
779 return Err(Error::MissingTlv { tlv_type: types::SLOT_DIRECTORY });
780 }
781 let raw = self
782 .tlvs
783 .decrypt_value(types::SLOT_DIRECTORY, &self.fek, self.header.cipher_id)?;
784 crate::slotdir::SlotDirectory::decode(&raw)
785 }
786
787 pub fn raw_slot_directory(&self) -> Result<Vec<u8>> {
796 if !self.is_slot_managed() {
797 return Err(Error::MissingTlv { tlv_type: types::SLOT_DIRECTORY });
798 }
799 self.tlvs
800 .decrypt_value(types::SLOT_DIRECTORY, &self.fek, self.header.cipher_id)
801 }
802
803 pub fn raw_folder_index(&self) -> Result<Vec<u8>> {
814 if !self.header.has_flag(flags::CONTAINER) {
815 return Err(Error::MissingTlv { tlv_type: types::FOLDER_INDEX });
816 }
817 self.tlvs.decrypt_value(types::FOLDER_INDEX, &self.fek, self.header.cipher_id)
818 }
819
820 pub fn decrypt_all(&self, file_bytes: &[u8]) -> Result<Vec<u8>> {
829 self.decrypt_all_with_progress(file_bytes, None)
830 }
831
832 pub fn decrypt_all_with_progress(
838 &self,
839 file_bytes: &[u8],
840 progress: Option<payload::ProgressFn<'_>>,
841 ) -> Result<Vec<u8>> {
842 let start = self.header.header_len as usize;
843 let payload = file_bytes.get(start..).ok_or(Error::Truncated {
844 context: "payload",
845 need: start,
846 got: file_bytes.len(),
847 })?;
848
849 let idx = self.compression_index()?;
850 let idx_ref = if idx.is_empty() { None } else { Some(idx.as_slice()) };
851 let plain = payload::decrypt_payload_with_progress(
852 &self.header,
853 &self.payload_key,
854 payload,
855 idx_ref,
856 progress,
857 )?;
858
859 if let Ok(expected) = self.stored_content_hash() {
861 use subtle::ConstantTimeEq;
862 let actual = content_hash(&plain);
863 if !bool::from(actual.ct_eq(&expected)) {
864 return Err(Error::ContentHashMismatch);
865 }
866 }
867 Ok(plain)
868 }
869}
870
871pub fn peek_header(data: &[u8]) -> Result<FixedHeader> {
881 FixedHeader::parse(data)
882}
883
884#[must_use]
888pub fn is_omy_file(data: &[u8]) -> bool {
889 data.get(..8).is_some_and(|m| m == crate::header::MAGIC_FILE)
890}
891
892pub fn open(data: &[u8], keks: &[Kek]) -> Result<OpenedFile> {
906 let header = FixedHeader::parse(data)?;
907
908 let hlen = header.header_len as usize;
909 if data.len() < hlen {
910 return Err(Error::Truncated { context: "header", need: hlen, got: data.len() });
911 }
912
913 let slot_end = SLOT_AREA_OFFSET.saturating_add(SLOT_AREA_LEN);
914 let slot_area = data.get(SLOT_AREA_OFFSET..slot_end).ok_or(Error::Truncated {
915 context: "key slot area",
916 need: slot_end,
917 got: data.len(),
918 })?;
919
920 let tlv_end = TLV_AREA_OFFSET.saturating_add(header.tlv_len as usize);
921 let tlv_blob = data.get(TLV_AREA_OFFSET..tlv_end).ok_or(Error::Truncated {
922 context: "tlv area",
923 need: tlv_end,
924 got: data.len(),
925 })?;
926
927 let mac_end = tlv_end.saturating_add(HEADER_MAC_LEN);
928 let stored_mac = data.get(tlv_end..mac_end).ok_or(Error::Truncated {
929 context: "header mac",
930 need: mac_end,
931 got: data.len(),
932 })?;
933
934 let (fek, kek_index, slot_index) =
936 unwrap_fek(slot_area, keks, &header.file_uuid, header.cipher_id)?;
937
938 let covered = data.get(..tlv_end).ok_or(Error::Truncated {
940 context: "mac-covered region",
941 need: tlv_end,
942 got: data.len(),
943 })?;
944 let mac_key = fek.derive_header_mac_key(&header.file_uuid);
945 let mut expected = [0u8; HEADER_MAC_LEN];
946 let src = stored_mac.get(..HEADER_MAC_LEN).ok_or(Error::Truncated {
947 context: "header mac",
948 need: HEADER_MAC_LEN,
949 got: stored_mac.len(),
950 })?;
951 expected.copy_from_slice(src);
952 if !verify_header_mac(&mac_key, covered, &expected) {
953 return Err(Error::HeaderMacMismatch);
954 }
955
956 let tlvs = TlvSet::parse(tlv_blob)?;
958 tlvs.check_critical()?;
959
960 if header.has_flag(flags::COMPRESSED) && tlvs.find(types::COMPRESSION_INDEX).is_none() {
962 return Err(Error::MissingTlv { tlv_type: types::COMPRESSION_INDEX });
963 }
964
965 let payload_key = fek.derive_payload_key(&header.file_uuid);
966 Ok(OpenedFile { header, tlvs, kek_index, slot_index, fek, payload_key })
967}
968
969pub fn open_with_password(data: &[u8], password: &[u8]) -> Result<OpenedFile> {
980 let header = FixedHeader::parse(data)?;
981 let params = Argon2Params {
982 m_kib: header.argon2_m_kib,
983 t: header.argon2_t,
984 p: header.argon2_p,
985 };
986 let kek = Kek::from_password(password, &header.vault_salt, params)?;
987 open(data, &[kek])
988}
989
990pub const PEEK_SIZE: usize = FIXED_HEADER_LEN;