Skip to main content

sendcipher_core/
stream_decryptor.rs

1/* Created on 2025-10-13 */
2/* Copyright (c) 2025-2026 Youcef Lemsafer */
3/* SPDX-License-Identifier: MIT */
4
5use crate::crypto::*;
6
7pub struct CypherChunk {
8    index: u64,
9    blob: Blob,
10}
11
12impl CypherChunk {
13    pub fn new(index: u64, blob: Blob) -> Self {
14        Self {
15            index: index,
16            blob: blob,
17        }
18    }
19
20    pub fn get_index(&self) -> u64 {
21        self.index
22    }
23}
24
25pub struct StreamDecryptor {
26    /// The decryption context
27    decryption_context: CypherContext,
28    /// The manifest associated with the stream being decrypted
29    manifest: Manifest,
30}
31
32impl StreamDecryptor {
33    /// Constructs an instance with a password and an encrypted manifest
34    pub fn with_password(
35        password: &str,
36        manifest_blob: &mut Blob,
37    ) -> Result<Self, crate::error::Error> {
38        let mut decryption_context = setup_file_decryption(manifest_blob, password)?;
39        log::debug!("Decryption context created");
40        let decrypted_blob = crypto::decrypt_blob(manifest_blob, &decryption_context)?;
41        let metadata: &Metadata = decrypted_blob
42            .get_metadata()
43            .as_ref()
44            .ok_or_else(|| crate::error::Error::DecryptionError("Missing metadata".to_string()))?;
45        if metadata.file_type != FileType::Manifest {
46            return Err(crate::error::Error::DecryptionError(
47                "Unexpected file type, a manifest was expected".to_string(),
48            ));
49        }
50        decryption_context.set_file_name(&metadata.file_name);
51        log::debug!("From decrypted data: file_name=`{}'", metadata.file_name);
52        log::debug!(
53            "About to deserialize manifest from {:02x?}",
54            decrypted_blob.get_text()
55        );
56        let manifest = Manifest::from_bytes(decrypted_blob.get_text())?;
57        decryption_context.set_mfp(manifest.mfp().clone());
58
59        Ok(Self {
60            decryption_context: decryption_context,
61            manifest,
62        })
63    }
64
65    /// Returns the decryption context
66    pub(crate) fn get_decryption_context(&self) -> &CypherContext {
67        &self.decryption_context
68    }
69
70    /// Returns the name of the file being decrypted
71    pub fn file_name(&self) -> &String {
72        self.manifest.file_name()
73    }
74
75    /// Returns the size of the file being decrypted
76    pub fn file_size(&self) -> u64 {
77        self.manifest.file_size()
78    }
79
80    /// Decrypts a chunk and returns decrypted text
81    /// Takes ownership of the chunk and does the decryption in place
82    pub fn decrypt_chunk(
83        &self,
84        cypherchunk: &mut CypherChunk,
85    ) -> Result<DecryptedBlob, crate::error::Error> {
86        Self::do_decrypt_chunk(
87            &self.decryption_context,
88            cypherchunk,
89            self.manifest.checksum_algorithm(),
90            self.get_chunk_checksum(cypherchunk.get_index())?,
91        )
92    }
93
94    /// Decrypts a chunk and returns decrypted text.
95    /// Start by verifying the checksum, if the chunk does not have
96    /// the expected checksum an error is returned and the decryption
97    /// is not attempted.
98    /// If the checksum is correct the function takes ownership of the
99    /// chunk and does the decryption in place.
100    pub(crate) fn do_decrypt_chunk(
101        decryption_context: &CypherContext,
102        cypherchunk: &mut CypherChunk,
103        checksum_algorithm: ChecksumAlgorithm,
104        checksum: &Vec<u8>,
105    ) -> Result<DecryptedBlob, crate::error::Error> {
106        let mut hasher = checksum_algorithm.get_checksum_computer();
107        hasher.update(cypherchunk.blob.data());
108        let computed_checksum = hasher.finalize();
109        if checksum != &computed_checksum {
110            return Err(crate::error::Error::ChunkChecksumError(format!(
111                "Chunk {} has wrong checksum",
112                cypherchunk.get_index()
113            )));
114        }
115
116        let mut chunk_decryption_context = decryption_context.clone();
117        chunk_decryption_context.setup_chunk_decryption(cypherchunk.get_index());
118
119        crypto::decrypt_blob(&mut cypherchunk.blob, &chunk_decryption_context)
120    }
121
122    /// Gets the manifest associated with the file being decrypted
123    pub fn get_manifest(&self) -> &Manifest {
124        &self.manifest
125    }
126
127    /// Returns the checksum of the chunk of given index, returns an error in case the index
128    /// has no corresponding chunk
129    fn get_chunk_checksum(&self, chunk_index: u64) -> Result<&Vec<u8>, crate::error::Error> {
130        if chunk_index >= self.manifest.chunks_count() as u64 {
131            return Err(crate::error::Error::DecryptionError(format!(
132                "Invalid chunk index: {}",
133                chunk_index
134            )));
135        }
136        Ok(self.manifest.chunks()[chunk_index as usize].checksum())
137    }
138}
139
140#[cfg(test)]
141mod tests {
142
143    use super::*;
144    use crate::{chunking::*, lcg::*, stream_encryptor::StreamEncryptor, test_utils::*};
145
146    mod utils {
147        use super::*;
148
149        pub(crate) fn create_file_contents(length: usize, lcg: &mut Lcg) -> Vec<u8> {
150            if length == 0 {
151                return Vec::new();
152            }
153            let mut buffer = Vec::<u8>::with_capacity(length);
154            let lcg_value_size = size_of_val(&lcg.clone().scrambled_next());
155            for _ in 0..(length / lcg_value_size) {
156                buffer.extend(lcg.scrambled_next().to_le_bytes());
157            }
158            let remainder = length % lcg_value_size;
159            if remainder != 0 {
160                buffer.extend_from_slice(&lcg.scrambled_next().to_le_bytes()[0..remainder]);
161            }
162            buffer
163        }
164    }
165
166    #[test]
167    /// Basic encryption/decryption test focusing on the manifest
168    fn test_decrypt_manifest() {
169        /*    let _ = env_logger::builder()
170                .filter_level(log::LevelFilter::Debug)
171                .is_test(true)
172                .try_init();
173        */
174        log::debug!("Test test_decrypt_manifest starts");
175        let chunk_generator = RandomChunkGenerator::with_seed(
176            20 * 1024 * 1024,
177            5 * 1024 * 1024,
178            10 * 1024 * 1024,
179            1u128,
180        );
181        let mut encryptor = StreamEncryptor::new("whatever_file_name.txt", chunk_generator, |k| {
182            Ok(AnyKeyWrapper::Argon2id(Argon2idKeyWrapper::new(
183                "password",
184                &create_argon2id_params_for_tests(),
185                k,
186            )?))
187        })
188        .expect("Encryptor creation should succeed");
189
190        let mut lcg = Lcg::new(LCG_PARAMS[0].0, LCG_PARAMS[0].1);
191        let file_contents = utils::create_file_contents(10, &mut lcg);
192        let mut chunks = Vec::new();
193        chunks.extend(encryptor.process_data(&file_contents));
194        chunks.extend(encryptor.on_end_of_data());
195        let mut encrypted_blobs = encryptor.encrypt_chunks(&chunks).unwrap();
196        encrypted_blobs
197            .iter()
198            .try_for_each(|blob| encryptor.register_encrypted_chunk(blob.0, &format!("id{}", blob.0)));
199        let mut manifest_blob = encryptor.finalize().unwrap();
200        // In this test we want exactly one chunk (besides the manifest)
201        assert_eq!(encrypted_blobs.len(), 1);
202
203        let decryptor = StreamDecryptor::with_password("password", &mut manifest_blob).unwrap();
204        let manifest = decryptor.get_manifest();
205
206        // Check decrypted manifest correctness
207        assert_eq!(manifest.file_size(), 10);
208        assert_eq!(manifest.file_name(), "whatever_file_name.txt");
209        assert_eq!(decryptor.file_name(), "whatever_file_name.txt");
210        assert_eq!(manifest.chunks_count(), 1);
211        let chunks = manifest.chunks();
212        assert_eq!(chunks.len(), 1);
213        assert_eq!(chunks[0].id(), &"id0".to_string());
214
215        // Decrypt and check the unique chunk
216        let (chunk_index, blob) = encrypted_blobs.first_mut().unwrap();
217        let decrypted_blob = decryptor
218            .decrypt_chunk(&mut CypherChunk::new(*chunk_index, std::mem::take(blob)))
219            .unwrap();
220
221        assert_eq!(decrypted_blob.get_text(), &file_contents);
222    }
223
224    #[test]
225    /// Basic encryption/decryption test focusing on the manifest
226    fn test_decrypt_after_parallel_encrypt() {
227        let chunk_generator = RandomChunkGenerator::with_seed(
228            20 * 1024 * 1024,
229            5 * 1024 * 1024,
230            10 * 1024 * 1024,
231            1u128,
232        );
233        let mut encryptor = StreamEncryptor::new("whatever_file_name.txt", chunk_generator, |k| {
234            Ok(AnyKeyWrapper::Argon2id(Argon2idKeyWrapper::new(
235                "password",
236                &create_argon2id_params_for_tests(),
237                k,
238            )?))
239        })
240        .expect("Encryptor creation should succeed");
241
242        let mut lcg = Lcg::new(LCG_PARAMS[0].0, LCG_PARAMS[0].1);
243        let file_contents = utils::create_file_contents(10, &mut lcg);
244        let mut chunks = Vec::new();
245        chunks.extend(encryptor.process_data(&file_contents));
246        chunks.extend(encryptor.on_end_of_data());
247        let mut encrypted_blobs = encryptor.parallel_encrypt_chunks(2, &chunks).unwrap();
248        encrypted_blobs
249            .iter()
250            .try_for_each(|blob| encryptor.register_encrypted_chunk(blob.0, &format!("id{}", blob.0)));
251        let mut manifest_blob = encryptor.finalize().unwrap();
252        // In this test we want exactly one chunk (besides the manifest)
253        assert_eq!(encrypted_blobs.len(), 1);
254
255        let decryptor = StreamDecryptor::with_password("password", &mut manifest_blob).unwrap();
256        let manifest = decryptor.get_manifest();
257
258        // Check decrypted manifest correctness
259        assert_eq!(manifest.file_size(), 10);
260        assert_eq!(manifest.file_name(), "whatever_file_name.txt");
261        assert_eq!(decryptor.file_name(), "whatever_file_name.txt");
262        assert_eq!(manifest.chunks_count(), 1);
263        let chunks = manifest.chunks();
264        assert_eq!(chunks.len(), 1);
265        assert_eq!(chunks[0].id(), &"id0".to_string());
266
267        // Decrypt and check the unique chunk
268        let (chunk_index, blob) = encrypted_blobs.first_mut().unwrap();
269        let decrypted_blob = decryptor
270            .decrypt_chunk(&mut CypherChunk::new(*chunk_index, std::mem::take(blob)))
271            .unwrap();
272
273        assert_eq!(decrypted_blob.get_text(), &file_contents);
274    }
275}