Skip to main content

shadow_crypt_core/v3/
file.rs

1use crate::{
2    errors::{CryptError, FileError, HeaderError},
3    file::{FileMetadata, PlaintextFile},
4    memory::{SecureBytes, SecureKey},
5    v3::{
6        key::KeyDerivationParams,
7        stream::{StreamOpener, StreamSealer},
8    },
9};
10
11use super::header::FileHeader;
12
13/// Represents a complete encrypted file with header and content.
14///
15/// This is the whole-bytes convenience over the streaming API in
16/// [`crate::v3::stream`]; callers that cannot hold the content in memory
17/// should drive [`StreamSealer`]/[`StreamOpener`] directly.
18#[derive(Debug)]
19pub struct EncryptedFile {
20    header: FileHeader,
21    ciphertext: Vec<u8>,
22}
23
24impl EncryptedFile {
25    pub fn new(header: FileHeader, ciphertext: Vec<u8>) -> Self {
26        Self { header, ciphertext }
27    }
28
29    /// Encrypts metadata and content into a complete v3 encrypted file.
30    ///
31    /// The caller supplies the key (derived from `kdf_params` and `salt`) and
32    /// fresh random salt/nonces, keeping this function deterministic.
33    pub fn seal(
34        metadata: &FileMetadata,
35        content: &[u8],
36        key: &SecureKey,
37        kdf_params: KeyDerivationParams,
38        salt: [u8; 16],
39        nonce_prefix: [u8; 16],
40        metadata_nonce: [u8; 24],
41    ) -> Result<Self, FileError> {
42        let (header, mut sealer) = StreamSealer::begin(
43            metadata,
44            key,
45            kdf_params,
46            salt,
47            nonce_prefix,
48            metadata_nonce,
49        )?;
50
51        let chunk_size = sealer.chunk_plaintext_len();
52        let pieces: Vec<&[u8]> = if content.is_empty() {
53            vec![&[][..]]
54        } else {
55            content.chunks(chunk_size).collect()
56        };
57
58        let mut ciphertext = Vec::with_capacity(content.len() + pieces.len() * 16);
59        for (i, piece) in pieces.iter().enumerate() {
60            ciphertext.extend_from_slice(&sealer.seal_chunk(piece, i == pieces.len() - 1)?);
61        }
62
63        Ok(Self::new(header, ciphertext))
64    }
65
66    /// Parses a serialized v3 file into its header and content ciphertext.
67    pub fn from_bytes(bytes: &[u8]) -> Result<Self, HeaderError> {
68        let header = FileHeader::try_deserialize(bytes)?;
69        let ciphertext = bytes[header.header_length()..].to_vec();
70
71        Ok(Self::new(header, ciphertext))
72    }
73
74    /// Serializes the complete file (header followed by content ciphertext).
75    pub fn to_bytes(&self) -> Vec<u8> {
76        let mut bytes = self.header.serialize();
77        bytes.extend_from_slice(&self.ciphertext);
78        bytes
79    }
80
81    /// Decrypts the metadata and the full content stream. The inverse of
82    /// [`EncryptedFile::seal`].
83    pub fn decrypt(&self, key: &SecureKey) -> Result<PlaintextFile, FileError> {
84        let metadata = self.header.decrypt_metadata(key)?;
85
86        let mut opener = StreamOpener::new(&self.header, key);
87        let piece_len = opener.chunk_ciphertext_len();
88        if self.ciphertext.is_empty() {
89            return Err(FileError::Crypt(CryptError::DecryptionError(
90                "invalid content stream: missing final chunk".to_string(),
91            )));
92        }
93
94        let pieces: Vec<&[u8]> = self.ciphertext.chunks(piece_len).collect();
95        let mut content = Vec::with_capacity(self.ciphertext.len());
96        for (i, piece) in pieces.iter().enumerate() {
97            let plaintext = opener.open_chunk(piece, i == pieces.len() - 1)?;
98            content.extend_from_slice(plaintext.as_slice());
99        }
100
101        Ok(PlaintextFile::new(
102            metadata.filename().clone(),
103            SecureBytes::new(content),
104        ))
105    }
106
107    pub fn header(&self) -> &FileHeader {
108        &self.header
109    }
110    pub fn ciphertext(&self) -> &[u8] {
111        &self.ciphertext
112    }
113}