use rayon::prelude::*;
use shadow_crypt_core::{
memory::SecureString,
progress::ProgressCounter,
report::DecryptionReport,
vault::{MAX_HEADER_LEN, ParsedFile},
};
use crate::{
decryption::{
file::{DecryptionInput, DecryptionInputFile},
file_ops::stream_decrypt_file,
},
errors::{WorkflowError, WorkflowResult},
kdf::derive_untrusted_key,
ui::{display_decryption_success, display_error, display_progress},
utils::read_n_bytes_from_file,
};
pub fn run_workflow(input: DecryptionInput) -> WorkflowResult<()> {
let total = input.files.len();
let counter = ProgressCounter::new(total as u64);
let failures: Vec<WorkflowError> = input
.files
.par_iter()
.filter_map(|input_file| {
let result = process_file_decryption(
input_file.to_owned(),
&input.password,
&input.output_dir,
input.force,
)
.map_err(|e| WorkflowError::per_file(&input_file.filename, e));
counter.increment();
if !input.quiet {
display_progress(&counter);
}
match result {
Ok(report) => {
if !input.quiet {
display_decryption_success(&report);
}
None
}
Err(e) => {
display_error(&e);
Some(e)
}
}
})
.collect();
if !failures.is_empty() {
let message = format!("{} of {} file(s) failed to decrypt", failures.len(), total);
return Err(if failures.iter().all(|e| e.is_authentication_failure()) {
WorkflowError::Authentication(message)
} else {
WorkflowError::Decryption(message)
});
}
Ok(())
}
fn process_file_decryption(
file: DecryptionInputFile,
password: &SecureString,
output_dir: &std::path::Path,
force: bool,
) -> WorkflowResult<DecryptionReport> {
let start_time = std::time::Instant::now();
let header_bytes = read_n_bytes_from_file(&file.path, MAX_HEADER_LEN)?;
let parsed = ParsedFile::parse(header_bytes.as_slice())?;
let key = derive_untrusted_key(&parsed, password)?;
let metadata = parsed.decrypt_metadata(&key)?;
let output_file = stream_decrypt_file(&file, &parsed, &key, &metadata, output_dir, force)?;
let duration = start_time.elapsed();
Ok(DecryptionReport::new(
file.filename,
output_file.filename,
duration,
parsed.algorithm(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kdf::MAX_KDF_MEMORY_KIB;
use shadow_crypt_core::{
file::PlaintextFile,
memory::{SecureBytes, SecureString},
profile::SecurityProfile,
v1, v2,
};
fn decrypt(bytes: &[u8], password: &SecureString) -> WorkflowResult<PlaintextFile> {
let parsed = ParsedFile::parse(bytes)?;
let key = derive_untrusted_key(&parsed, password)?;
Ok(parsed.decrypt(&key)?)
}
#[test]
fn test_v1_oversized_kdf_params_rejected_before_derivation() {
let kdf_params = v1::key::KeyDerivationParams::new(MAX_KDF_MEMORY_KIB + 1, 1, 1, 32);
let header =
v1::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3]);
let mut bytes = header.serialize();
bytes.extend_from_slice(b"ciphertext");
let password = SecureString::new("pw".to_string());
assert!(decrypt(&bytes, &password).is_err());
}
#[test]
fn test_v2_oversized_kdf_params_rejected_before_derivation() {
let kdf_params = v2::key::KeyDerivationParams::new(u32::MAX, 1, 1, 32);
let header =
v2::header::FileHeader::new([0u8; 16], kdf_params, [0u8; 24], [0u8; 24], vec![1, 2, 3])
.unwrap();
let mut bytes = header.serialize();
bytes.extend_from_slice(b"ciphertext");
let password = SecureString::new("pw".to_string());
assert!(decrypt(&bytes, &password).is_err());
}
#[test]
fn test_v2_swapped_ciphertext_pairs_fail_decryption() {
let password = SecureString::new("testpassword".to_string());
let salt = [1u8; 16];
let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
let content_nonce = [2u8; 24];
let filename_nonce = [3u8; 24];
let (key, _) = kdf_params
.derive_key(password.as_str().as_bytes(), &salt)
.unwrap();
let binding =
v2::header::HeaderBinding::new(&salt, &kdf_params, &content_nonce, &filename_nonce);
let (filename_ct, _) = v2::crypt::encrypt_bytes(
b"name.txt",
key.as_bytes(),
&filename_nonce,
&binding.aad(v2::header::AadPurpose::Filename),
)
.unwrap();
let (content_ct, _) = v2::crypt::encrypt_bytes(
b"content",
key.as_bytes(),
&content_nonce,
&binding.aad(v2::header::AadPurpose::Content),
)
.unwrap();
let swapped_header = v2::header::FileHeader::new(
salt,
kdf_params,
filename_nonce, content_nonce, content_ct, )
.unwrap();
let mut bytes = swapped_header.serialize();
bytes.extend_from_slice(&filename_ct);
assert!(decrypt(&bytes, &password).is_err());
}
#[test]
fn test_v2_round_trip_via_raw_bytes() {
let password = SecureString::new("testpassword".to_string());
let salt = [1u8; 16];
let kdf_params = v2::key::KeyDerivationParams::from(SecurityProfile::Test);
let (key, _) = kdf_params
.derive_key(password.as_str().as_bytes(), &salt)
.unwrap();
let plaintext_file = PlaintextFile::new(
SecureString::new("name.txt".to_string()),
SecureBytes::new(b"content".to_vec()),
);
let sealed = v2::file::EncryptedFile::seal(
&plaintext_file,
&key,
kdf_params,
salt,
[2u8; 24],
[3u8; 24],
)
.unwrap();
let decrypted = decrypt(&sealed.to_bytes(), &password).unwrap();
assert_eq!(decrypted.filename().as_str(), "name.txt");
assert_eq!(decrypted.content().as_slice(), b"content");
}
}