Skip to main content

shadow_crypt_core/
vault.rs

1//! Version-erased reading of shadow files.
2//!
3//! [`ParsedFile`] is the single entry point for reading a shadow file of any
4//! format version: it sniffs the version byte, parses with the matching
5//! version module, and dispatches every subsequent operation (key derivation,
6//! content decryption, filename decryption) to that version's own
7//! self-contained implementation. Callers never see which version they are
8//! handling unless they ask.
9//!
10//! This module is the one place that knows about all format versions. It
11//! keeps the versions independent of each other: v1 and v2 still share no
12//! code — this module sits above both.
13//!
14//! Key derivation is split out on purpose: [`ParsedFile::kdf_request`]
15//! surfaces the header's (untrusted) KDF parameters so the caller can
16//! validate and budget them *before* running [`ParsedFile::derive_key`].
17
18use crate::{
19    algorithm::Algorithm,
20    errors::{FileError, HeaderError, KeyDerivationError},
21    file::{FileMetadata, PlaintextFile},
22    memory::{SecureBytes, SecureKey, SecureString},
23    report::KeyDerivationReport,
24    v1, v2, v3,
25    version::{Version, read_file_version},
26};
27
28/// Upper bound on the serialized header size of any format version.
29///
30/// Reading this many bytes from the start of a file is always enough to parse
31/// its complete header; [`ParsedFile::parse`] tolerates trailing ciphertext
32/// bytes, so callers need no version-specific length probing.
33pub const MAX_HEADER_LEN: usize = {
34    let v1_len = v1::header::FileHeader::min_length();
35    let v2_len = v2::header::FileHeader::min_length();
36    let v3_len = v3::header::FileHeader::min_length();
37    let max12 = if v1_len > v2_len { v1_len } else { v2_len };
38    (if max12 > v3_len { max12 } else { v3_len }) + u16::MAX as usize
39};
40
41/// The (untrusted) key derivation inputs a file's header asks for,
42/// independent of format version.
43///
44/// These come straight from the file and must be validated against resource
45/// bounds before being handed back to [`ParsedFile::derive_key`].
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct KdfRequest {
48    pub salt: [u8; 16],
49    pub memory_cost: u32,
50    pub time_cost: u32,
51    pub parallelism: u32,
52    pub key_size: u8,
53}
54
55/// A parsed shadow file of any format version.
56pub struct ParsedFile(Inner);
57
58enum Inner {
59    V1(v1::file::EncryptedFile),
60    V2(v2::file::EncryptedFile),
61    V3(v3::file::EncryptedFile),
62}
63
64impl ParsedFile {
65    /// Parses a serialized shadow file, dispatching on its version byte.
66    ///
67    /// Accepts both complete files and header-only prefixes (at least the
68    /// full header must be present); trailing bytes are treated as content
69    /// ciphertext. When parsing a header-only prefix, use
70    /// [`ParsedFile::content_decryptor`] and feed the content bytes
71    /// externally — [`ParsedFile::decrypt`] needs the complete file.
72    pub fn parse(bytes: &[u8]) -> Result<Self, HeaderError> {
73        let inner = match read_file_version(bytes)? {
74            Version::V1 => Inner::V1(v1::file::EncryptedFile::from_bytes(bytes)?),
75            Version::V2 => Inner::V2(v2::file::EncryptedFile::from_bytes(bytes)?),
76            Version::V3 => Inner::V3(v3::file::EncryptedFile::from_bytes(bytes)?),
77        };
78        Ok(Self(inner))
79    }
80
81    pub fn version(&self) -> Version {
82        match &self.0 {
83            Inner::V1(_) => Version::V1,
84            Inner::V2(_) => Version::V2,
85            Inner::V3(_) => Version::V3,
86        }
87    }
88
89    /// The AEAD algorithm this file's format version uses.
90    pub fn algorithm(&self) -> Algorithm {
91        match &self.0 {
92            Inner::V1(_) => v1::ALGORITHM,
93            Inner::V2(_) => v2::ALGORITHM,
94            Inner::V3(_) => v3::ALGORITHM,
95        }
96    }
97
98    /// Total length of the file's serialized header; the content ciphertext
99    /// starts at this offset.
100    pub fn header_length(&self) -> usize {
101        match &self.0 {
102            Inner::V1(f) => f.header().header_length(),
103            Inner::V2(f) => f.header().header_length(),
104            Inner::V3(f) => f.header().header_length(),
105        }
106    }
107
108    /// The key derivation inputs recorded in the file's header. Untrusted:
109    /// validate against resource bounds before calling
110    /// [`ParsedFile::derive_key`].
111    pub fn kdf_request(&self) -> KdfRequest {
112        match &self.0 {
113            Inner::V1(f) => {
114                let p = f.header().kdf_params();
115                KdfRequest {
116                    salt: *f.header().salt(),
117                    memory_cost: p.memory_cost,
118                    time_cost: p.time_cost,
119                    parallelism: p.parallelism,
120                    key_size: p.key_size,
121                }
122            }
123            Inner::V2(f) => {
124                let p = f.header().kdf_params();
125                KdfRequest {
126                    salt: *f.header().salt(),
127                    memory_cost: p.memory_cost,
128                    time_cost: p.time_cost,
129                    parallelism: p.parallelism,
130                    key_size: p.key_size,
131                }
132            }
133            Inner::V3(f) => {
134                let p = f.header().kdf_params();
135                KdfRequest {
136                    salt: *f.header().salt(),
137                    memory_cost: p.memory_cost,
138                    time_cost: p.time_cost,
139                    parallelism: p.parallelism,
140                    key_size: p.key_size,
141                }
142            }
143        }
144    }
145
146    /// Derives the file's encryption key from a password, using the KDF and
147    /// parameters of the file's own format version.
148    pub fn derive_key(
149        &self,
150        password: &[u8],
151    ) -> Result<(SecureKey, KeyDerivationReport), KeyDerivationError> {
152        match &self.0 {
153            Inner::V1(f) => f
154                .header()
155                .kdf_params()
156                .derive_key(password, f.header().salt()),
157            Inner::V2(f) => f
158                .header()
159                .kdf_params()
160                .derive_key(password, f.header().salt()),
161            Inner::V3(f) => f
162                .header()
163                .kdf_params()
164                .derive_key(password, f.header().salt()),
165        }
166    }
167
168    /// Decrypts the file's filename and content. Requires the file to have
169    /// been parsed from its complete bytes; for streamed decryption use
170    /// [`ParsedFile::content_decryptor`].
171    pub fn decrypt(&self, key: &SecureKey) -> Result<PlaintextFile, FileError> {
172        match &self.0 {
173            Inner::V1(f) => f.decrypt(key),
174            Inner::V2(f) => f.decrypt(key),
175            Inner::V3(f) => f.decrypt(key),
176        }
177    }
178
179    /// Decrypts only the original filename stored in the file's header.
180    pub fn decrypt_filename(&self, key: &SecureKey) -> Result<SecureString, FileError> {
181        match &self.0 {
182            Inner::V1(f) => f.header().decrypt_filename(key),
183            Inner::V2(f) => f.header().decrypt_filename(key),
184            Inner::V3(f) => Ok(f.header().decrypt_metadata(key)?.filename().clone()),
185        }
186    }
187
188    /// Decrypts the file's metadata stored in the header. Format versions
189    /// that predate metadata storage (v1, v2) return only the filename.
190    pub fn decrypt_metadata(&self, key: &SecureKey) -> Result<FileMetadata, FileError> {
191        match &self.0 {
192            Inner::V1(f) => Ok(FileMetadata::new(
193                f.header().decrypt_filename(key)?,
194                None,
195                None,
196            )),
197            Inner::V2(f) => Ok(FileMetadata::new(
198                f.header().decrypt_filename(key)?,
199                None,
200                None,
201            )),
202            Inner::V3(f) => f.header().decrypt_metadata(key),
203        }
204    }
205
206    /// Starts decrypting the file's content from externally supplied
207    /// ciphertext bytes (starting at [`ParsedFile::header_length`]), so the
208    /// caller controls I/O and memory. Works uniformly across versions:
209    /// [`ContentDecryptor::chunk_len`] says how to feed the bytes.
210    pub fn content_decryptor(&self, key: &SecureKey) -> ContentDecryptor<'_> {
211        let inner = match &self.0 {
212            Inner::V1(f) => DecryptorInner::V1 {
213                header: f.header(),
214                key: key.clone(),
215                done: false,
216            },
217            Inner::V2(f) => DecryptorInner::V2 {
218                header: f.header(),
219                key: key.clone(),
220                done: false,
221            },
222            Inner::V3(f) => DecryptorInner::V3(v3::stream::StreamOpener::new(f.header(), key)),
223        };
224        ContentDecryptor { inner }
225    }
226}
227
228/// Incremental decryption of one file's content, fed by the caller.
229///
230/// Formats whose content is a single AEAD message (v1, v2) report
231/// [`ContentDecryptor::chunk_len`] `None`: feed the entire content in one
232/// [`ContentDecryptor::decrypt_chunk`] call with `is_last = true`. Streaming
233/// formats report `Some(n)`: feed `n`-byte pieces, the final one shorter or
234/// equal, with `is_last` on the final piece.
235pub struct ContentDecryptor<'a> {
236    inner: DecryptorInner<'a>,
237}
238
239enum DecryptorInner<'a> {
240    V1 {
241        header: &'a v1::header::FileHeader,
242        key: SecureKey,
243        done: bool,
244    },
245    V2 {
246        header: &'a v2::header::FileHeader,
247        key: SecureKey,
248        done: bool,
249    },
250    V3(v3::stream::StreamOpener),
251}
252
253impl ContentDecryptor<'_> {
254    /// Ciphertext bytes to feed per [`ContentDecryptor::decrypt_chunk`]
255    /// call, or `None` when the whole content must be fed at once.
256    pub fn chunk_len(&self) -> Option<usize> {
257        match &self.inner {
258            DecryptorInner::V1 { .. } | DecryptorInner::V2 { .. } => None,
259            DecryptorInner::V3(opener) => Some(opener.chunk_ciphertext_len()),
260        }
261    }
262
263    /// Decrypts the next piece of content ciphertext. `is_last` marks that
264    /// no more bytes follow.
265    pub fn decrypt_chunk(
266        &mut self,
267        ciphertext: &[u8],
268        is_last: bool,
269    ) -> Result<SecureBytes, FileError> {
270        match &mut self.inner {
271            DecryptorInner::V1 { header, key, done } => {
272                whole_content_chunk(done, is_last)?;
273                header.decrypt_content(ciphertext, key)
274            }
275            DecryptorInner::V2 { header, key, done } => {
276                whole_content_chunk(done, is_last)?;
277                header.decrypt_content(ciphertext, key)
278            }
279            DecryptorInner::V3(opener) => opener.open_chunk(ciphertext, is_last),
280        }
281    }
282
283    /// True once the final piece has been decrypted. A content stream that
284    /// ends without this being true was truncated.
285    pub fn finished(&self) -> bool {
286        match &self.inner {
287            DecryptorInner::V1 { done, .. } | DecryptorInner::V2 { done, .. } => *done,
288            DecryptorInner::V3(opener) => opener.finished(),
289        }
290    }
291}
292
293/// Guards the single-chunk contract of the whole-content (v1/v2) decryptors.
294fn whole_content_chunk(done: &mut bool, is_last: bool) -> Result<(), FileError> {
295    if *done || !is_last {
296        return Err(FileError::Crypt(
297            crate::errors::CryptError::DecryptionError(
298                "this format's content must be decrypted as a single final chunk".to_string(),
299            ),
300        ));
301    }
302    *done = true;
303    Ok(())
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    fn seal_v3(password: &[u8], filename: &str, content: &[u8]) -> Vec<u8> {
311        let salt = [1u8; 16];
312        let params = v3::key::KeyDerivationParams::test_defaults();
313        let (key, _) = params.derive_key(password, &salt).unwrap();
314        let metadata =
315            FileMetadata::new(SecureString::new(filename.to_string()), None, Some(0o600));
316        v3::file::EncryptedFile::seal(&metadata, content, &key, params, salt, [2u8; 16], [3u8; 24])
317            .unwrap()
318            .to_bytes()
319    }
320
321    fn seal_v2(password: &[u8], filename: &str, content: &[u8]) -> Vec<u8> {
322        let salt = [1u8; 16];
323        let params = v2::key::KeyDerivationParams::test_defaults();
324        let (key, _) = params.derive_key(password, &salt).unwrap();
325        let plaintext = PlaintextFile::new(
326            SecureString::new(filename.to_string()),
327            SecureBytes::new(content.to_vec()),
328        );
329        v2::file::EncryptedFile::seal(&plaintext, &key, params, salt, [2u8; 24], [3u8; 24])
330            .unwrap()
331            .to_bytes()
332    }
333
334    fn seal_v1(password: &[u8], filename: &str, content: &[u8]) -> Vec<u8> {
335        let salt = [1u8; 16];
336        let params = v1::key::KeyDerivationParams::test_defaults();
337        let content_nonce = [2u8; 24];
338        let filename_nonce = [3u8; 24];
339        let (key, _) = params.derive_key(password, &salt).unwrap();
340        let (filename_ct, _) =
341            v1::crypt::encrypt_bytes(filename.as_bytes(), key.as_bytes(), &filename_nonce).unwrap();
342        let (content_ct, _) =
343            v1::crypt::encrypt_bytes(content, key.as_bytes(), &content_nonce).unwrap();
344        let header =
345            v1::header::FileHeader::new(salt, params, content_nonce, filename_nonce, filename_ct);
346        v1::file::EncryptedFile::new(header, content_ct).to_bytes()
347    }
348
349    #[test]
350    fn parse_dispatches_on_version_byte() {
351        let v1_bytes = seal_v1(b"pw", "a.txt", b"one");
352        let v2_bytes = seal_v2(b"pw", "b.txt", b"two");
353        let v3_bytes = seal_v3(b"pw", "c.txt", b"three");
354
355        assert_eq!(ParsedFile::parse(&v1_bytes).unwrap().version(), Version::V1);
356        assert_eq!(ParsedFile::parse(&v2_bytes).unwrap().version(), Version::V2);
357        assert_eq!(ParsedFile::parse(&v3_bytes).unwrap().version(), Version::V3);
358    }
359
360    #[test]
361    fn parse_rejects_unknown_version() {
362        let mut bytes = seal_v2(b"pw", "a.txt", b"x");
363        bytes[6] = 99;
364        assert!(ParsedFile::parse(&bytes).is_err());
365    }
366
367    #[test]
368    fn decrypt_round_trips_all_versions() {
369        for bytes in [
370            seal_v1(b"pw", "name.txt", b"content"),
371            seal_v2(b"pw", "name.txt", b"content"),
372            seal_v3(b"pw", "name.txt", b"content"),
373        ] {
374            let parsed = ParsedFile::parse(&bytes).unwrap();
375            let (key, _) = parsed.derive_key(b"pw").unwrap();
376            let plaintext = parsed.decrypt(&key).unwrap();
377            assert_eq!(plaintext.filename().as_str(), "name.txt");
378            assert_eq!(plaintext.content().as_slice(), b"content");
379        }
380    }
381
382    #[test]
383    fn decrypt_filename_works_on_header_only_prefix() {
384        for bytes in [
385            seal_v1(b"pw", "name.txt", b"content"),
386            seal_v2(b"pw", "name.txt", b"content"),
387            seal_v3(b"pw", "name.txt", b"content"),
388        ] {
389            // Simulate a bounded header read: any prefix at least as long as
390            // the header (here capped at MAX_HEADER_LEN) must be parseable.
391            let prefix = &bytes[..bytes.len().min(MAX_HEADER_LEN)];
392            let parsed = ParsedFile::parse(prefix).unwrap();
393            let (key, _) = parsed.derive_key(b"pw").unwrap();
394            assert_eq!(parsed.decrypt_filename(&key).unwrap().as_str(), "name.txt");
395        }
396    }
397
398    /// The caller-fed decryptor must reproduce the content for every version
399    /// when fed according to its own chunk_len contract.
400    #[test]
401    fn content_decryptor_round_trips_all_versions() {
402        for bytes in [
403            seal_v1(b"pw", "name.txt", b"content"),
404            seal_v2(b"pw", "name.txt", b"content"),
405            seal_v3(b"pw", "name.txt", b"content"),
406        ] {
407            let parsed = ParsedFile::parse(&bytes).unwrap();
408            let (key, _) = parsed.derive_key(b"pw").unwrap();
409            let content_bytes = &bytes[parsed.header_length()..];
410
411            let mut decryptor = parsed.content_decryptor(&key);
412            let mut out = Vec::new();
413            match decryptor.chunk_len() {
414                None => {
415                    out.extend_from_slice(
416                        decryptor
417                            .decrypt_chunk(content_bytes, true)
418                            .unwrap()
419                            .as_slice(),
420                    );
421                }
422                Some(n) => {
423                    let pieces: Vec<&[u8]> = content_bytes.chunks(n).collect();
424                    for (i, piece) in pieces.iter().enumerate() {
425                        out.extend_from_slice(
426                            decryptor
427                                .decrypt_chunk(piece, i == pieces.len() - 1)
428                                .unwrap()
429                                .as_slice(),
430                        );
431                    }
432                }
433            }
434
435            assert!(decryptor.finished());
436            assert_eq!(out, b"content");
437        }
438    }
439
440    #[test]
441    fn decrypt_metadata_reports_fields_by_version() {
442        let v2_bytes = seal_v2(b"pw", "name.txt", b"content");
443        let parsed = ParsedFile::parse(&v2_bytes).unwrap();
444        let (key, _) = parsed.derive_key(b"pw").unwrap();
445        let meta = parsed.decrypt_metadata(&key).unwrap();
446        assert_eq!(meta.filename().as_str(), "name.txt");
447        assert_eq!(meta.mode(), None);
448
449        let v3_bytes = seal_v3(b"pw", "name.txt", b"content");
450        let parsed = ParsedFile::parse(&v3_bytes).unwrap();
451        let (key, _) = parsed.derive_key(b"pw").unwrap();
452        let meta = parsed.decrypt_metadata(&key).unwrap();
453        assert_eq!(meta.filename().as_str(), "name.txt");
454        assert_eq!(meta.mode(), Some(0o600));
455    }
456
457    #[test]
458    fn kdf_request_reflects_header_params() {
459        let bytes = seal_v2(b"pw", "a.txt", b"x");
460        let parsed = ParsedFile::parse(&bytes).unwrap();
461        let req = parsed.kdf_request();
462
463        let expected = v2::key::KeyDerivationParams::test_defaults();
464        assert_eq!(req.salt, [1u8; 16]);
465        assert_eq!(req.memory_cost, expected.memory_cost);
466        assert_eq!(req.time_cost, expected.time_cost);
467        assert_eq!(req.parallelism, expected.parallelism);
468        assert_eq!(req.key_size, expected.key_size);
469    }
470}