Skip to main content

shadow_crypt_shell/encryption/
workflow.rs

1use rayon::prelude::*;
2use shadow_crypt_core::{
3    memory::SecureString,
4    progress::ProgressCounter,
5    report::EncryptionReport,
6    v3::{self, key::KeyDerivationParams, stream::StreamSealer},
7};
8
9use crate::{
10    encryption::{
11        file::{EncryptionInput, EncryptionInputFile, InputKind},
12        file_ops::{
13            gather_metadata, stream_encrypt_directory, stream_encrypt_file, walk_directory,
14        },
15        nonce::{generate_nonce, generate_nonce_prefix},
16        salt::generate_salt,
17    },
18    errors::{WorkflowError, WorkflowResult},
19    kdf::with_kdf_memory_permit,
20    ui::{display_encryption_success, display_error, display_progress, display_warning},
21};
22
23pub fn run_workflow(input: EncryptionInput) -> WorkflowResult<()> {
24    let params = KeyDerivationParams::from(input.security_profile);
25    let total = input.files.len();
26    let counter = ProgressCounter::new(total as u64);
27
28    // Each file gets its own salt and derived key so that files encrypted in the
29    // same session cannot be correlated by comparing header salts.
30    let failures: usize = input
31        .files
32        .par_iter()
33        .map(|input_file| {
34            let result = process_file_encryption(
35                input_file.to_owned(),
36                &input.password,
37                &params,
38                &input.output_dir,
39                input.delete,
40            )
41            .map_err(|e| WorkflowError::per_file(&input_file.filename, e));
42            counter.increment();
43            if !input.quiet {
44                display_progress(&counter);
45            }
46            match result {
47                Ok(report) => {
48                    if !input.quiet {
49                        display_encryption_success(&report);
50                    }
51                    0
52                }
53                Err(e) => {
54                    display_error(&e);
55                    1
56                }
57            }
58        })
59        .sum();
60
61    if failures > 0 {
62        return Err(WorkflowError::Encryption(format!(
63            "{} of {} file(s) failed to encrypt",
64            failures, total
65        )));
66    }
67
68    Ok(())
69}
70
71fn process_file_encryption(
72    file: EncryptionInputFile,
73    password: &SecureString,
74    kdf_params: &KeyDerivationParams,
75    output_dir: &std::path::Path,
76    delete_original: bool,
77) -> WorkflowResult<EncryptionReport> {
78    let start_time = std::time::Instant::now();
79
80    let salt: [u8; 16] = generate_salt()?;
81    let (key, _) = with_kdf_memory_permit(kdf_params.memory_cost, || {
82        kdf_params.derive_key(password.as_str().as_bytes(), salt.as_ref())
83    })?;
84
85    let nonce_prefix: [u8; 16] = generate_nonce_prefix()?;
86    let metadata_nonce: [u8; 24] = generate_nonce()?;
87
88    let mut skipped_entries = 0;
89    let output_file = match file.kind {
90        InputKind::File => {
91            let metadata = gather_metadata(&file);
92            let (header, sealer) = StreamSealer::begin(
93                &metadata,
94                &key,
95                kdf_params.clone(),
96                salt,
97                nonce_prefix,
98                metadata_nonce,
99            )?;
100            stream_encrypt_file(&file, &header, sealer, output_dir)?
101        }
102        InputKind::Directory => {
103            let (entries, skipped) = walk_directory(&file.path)?;
104            skipped_entries = skipped;
105            if skipped > 0 {
106                display_warning(&format!(
107                    "Skipped {} unsupported entr{} (symlinks, special files, unsupported names) in '{}'",
108                    skipped,
109                    if skipped == 1 { "y" } else { "ies" },
110                    file.filename
111                ));
112            }
113            let metadata = gather_metadata(&file).into_archive();
114            let (header, sealer) = StreamSealer::begin(
115                &metadata,
116                &key,
117                kdf_params.clone(),
118                salt,
119                nonce_prefix,
120                metadata_nonce,
121            )?;
122            stream_encrypt_directory(&entries, &header, sealer, output_dir)?
123        }
124    };
125
126    // Delete only after the output is fully committed to disk. A failed
127    // deletion is an error (scripts relying on --delete must notice), but
128    // the encrypted output itself is complete and valid at this point.
129    if delete_original {
130        // Skipped entries exist only in the original tree — deleting it
131        // would destroy them with no encrypted copy anywhere.
132        if skipped_entries > 0 {
133            return Err(WorkflowError::File(format!(
134                "encrypted successfully to '{}', but {} skipped entr{} exist only in the original; refusing to delete it",
135                output_file.filename,
136                skipped_entries,
137                if skipped_entries == 1 { "y" } else { "ies" },
138            )));
139        }
140        let removal = match file.kind {
141            InputKind::File => std::fs::remove_file(&file.path),
142            InputKind::Directory => {
143                // If the output landed inside the input tree (e.g.
144                // --output-dir mydir/out for input mydir), deleting the
145                // tree would destroy the ciphertext along with the
146                // originals.
147                let output_path = std::fs::canonicalize(&output_file.path)?;
148                let input_path = std::fs::canonicalize(&file.path)?;
149                if output_path.starts_with(&input_path) {
150                    return Err(WorkflowError::File(format!(
151                        "encrypted successfully to '{}', but the output is inside the input directory; refusing to delete it",
152                        output_file.filename,
153                    )));
154                }
155                std::fs::remove_dir_all(&file.path)
156            }
157        };
158        removal.map_err(|e| {
159            WorkflowError::File(format!(
160                "encrypted successfully to '{}', but failed to delete the original: {}",
161                output_file.filename, e
162            ))
163        })?;
164    }
165
166    let duration = start_time.elapsed();
167
168    Ok(EncryptionReport::new(
169        file.filename,
170        output_file.filename,
171        duration,
172        v3::ALGORITHM,
173        delete_original,
174    ))
175}