shadow_crypt_shell/encryption/
workflow.rs1use 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 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 ¶ms,
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 if delete_original {
130 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 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}