use rayon::prelude::*;
use shadow_crypt_core::{
algorithm::Algorithm,
memory::{SecureBytes, SecureString},
progress::ProgressCounter,
report::DecryptionReport,
v1, v2,
version::{Version, read_file_version},
};
use crate::{
decryption::{
file::{DecryptionInput, DecryptionInputFile, DecryptionOutputFile},
file_ops::{load_file_bytes, store_plaintext_file},
},
errors::{WorkflowError, WorkflowResult},
kdf::derive_key_from_untrusted_params,
ui::{display_decryption_report, display_progress},
utils::parse_string_from_bytes,
};
pub fn run_workflow(input: DecryptionInput) -> WorkflowResult<()> {
let total = input.files.len();
let counter = ProgressCounter::new(total as u64);
let failures: usize = input
.files
.par_iter()
.map(|input_file| {
let result =
process_file_decryption(input_file.to_owned(), &input.password, &input.output_dir);
counter.increment();
display_progress(&counter);
let failed = result.is_err();
display_decryption_report(result);
usize::from(failed)
})
.sum();
if failures > 0 {
return Err(WorkflowError::Decryption(format!(
"{} of {} file(s) failed to decrypt",
failures, total
)));
}
Ok(())
}
fn process_file_decryption(
file: DecryptionInputFile,
password: &SecureString,
output_dir: &std::path::Path,
) -> WorkflowResult<DecryptionReport> {
let start_time = std::time::Instant::now();
let bytes = load_file_bytes(&file)?;
let (filename, content, algorithm) = match read_file_version(&bytes)? {
Version::V1 => decrypt_v1(&bytes, password)?,
Version::V2 => decrypt_v2(&bytes, password)?,
};
let output_file: DecryptionOutputFile = store_plaintext_file(&filename, &content, output_dir)?;
let duration = start_time.elapsed();
Ok(DecryptionReport::new(
file.filename,
output_file.filename,
duration,
algorithm,
))
}
fn decrypt_v1(
bytes: &[u8],
password: &SecureString,
) -> WorkflowResult<(SecureString, SecureBytes, Algorithm)> {
let encrypted_file = v1::file_ops::get_encrypted_file_from_bytes(bytes)?;
let header = encrypted_file.header();
let kdf_params = v1::header_ops::get_kdf_params(header);
let key = derive_key_from_untrusted_params(
kdf_params.memory_cost,
kdf_params.time_cost,
kdf_params.parallelism,
kdf_params.key_size,
|| v1::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
)?;
let (filename_bytes, algorithm) = v1::crypt::decrypt_bytes(
&header.filename_ciphertext,
key.as_bytes(),
&header.filename_nonce,
)?;
let filename = parse_string_from_bytes(&filename_bytes)?;
let (content, _) = v1::crypt::decrypt_bytes(
encrypted_file.ciphertext(),
key.as_bytes(),
&header.content_nonce,
)?;
Ok((filename, content, algorithm))
}
fn decrypt_v2(
bytes: &[u8],
password: &SecureString,
) -> WorkflowResult<(SecureString, SecureBytes, Algorithm)> {
let encrypted_file = v2::file_ops::get_encrypted_file_from_bytes(bytes)?;
let header = encrypted_file.header();
let kdf_params = v2::header_ops::get_kdf_params(header);
let key = derive_key_from_untrusted_params(
kdf_params.memory_cost,
kdf_params.time_cost,
kdf_params.parallelism,
kdf_params.key_size,
|| v2::key_ops::derive_key(password.as_str().as_bytes(), &header.salt, &kdf_params),
)?;
let binding = header.binding();
let (filename_bytes, algorithm) = v2::crypt::decrypt_bytes(
&header.filename_ciphertext,
key.as_bytes(),
&header.filename_nonce,
&binding.aad(v2::header::AadPurpose::Filename),
)?;
let filename = parse_string_from_bytes(&filename_bytes)?;
let (content, _) = v2::crypt::decrypt_bytes(
encrypted_file.ciphertext(),
key.as_bytes(),
&header.content_nonce,
&binding.aad(v2::header::AadPurpose::Content),
)?;
Ok((filename, content, algorithm))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kdf::MAX_KDF_MEMORY_KIB;
use shadow_crypt_core::profile::SecurityProfile;
#[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 = v1::header_ops::serialize(&header);
bytes.extend_from_slice(b"ciphertext");
let password = SecureString::new("pw".to_string());
assert!(decrypt_v1(&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 = v2::header_ops::serialize(&header);
bytes.extend_from_slice(b"ciphertext");
let password = SecureString::new("pw".to_string());
assert!(decrypt_v2(&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, _) =
v2::key_ops::derive_key(password.as_str().as_bytes(), &salt, &kdf_params).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 = v2::header_ops::serialize(&swapped_header);
bytes.extend_from_slice(&filename_ct);
assert!(decrypt_v2(&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 content_nonce = [2u8; 24];
let filename_nonce = [3u8; 24];
let (key, _) =
v2::key_ops::derive_key(password.as_str().as_bytes(), &salt, &kdf_params).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 header = v2::header::FileHeader::new(
salt,
kdf_params,
content_nonce,
filename_nonce,
filename_ct,
)
.unwrap();
let mut bytes = v2::header_ops::serialize(&header);
bytes.extend_from_slice(&content_ct);
let (filename, content, _) = decrypt_v2(&bytes, &password).unwrap();
assert_eq!(filename.as_str(), "name.txt");
assert_eq!(content.as_slice(), b"content");
}
}