use crate::build_cli;
use crate::error::{DecryptionError, DecryptionResult};
use base64::{Engine as _, engine::general_purpose};
use scrypt::{Params, scrypt};
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::Path;
const RCLONE_MAGIC: &[u8] = b"RCLONE\x00\x00";
const SCRYPT_N: u32 = 16384;
const SCRYPT_R: u32 = 8;
const SCRYPT_P: u32 = 1;
const KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 24; const CHUNK_SIZE: usize = 65536;
fn increment_nonce(nonce: &mut [u8; 24]) {
for i in 0..24 {
if nonce[i] == 255 {
nonce[i] = 0;
} else {
nonce[i] += 1;
break;
}
}
}
fn increment_nonce_be(nonce: &mut [u8; 24]) {
for i in (0..24).rev() {
if nonce[i] == 255 {
nonce[i] = 0;
} else {
nonce[i] += 1;
break;
}
}
}
fn increment_nonce_64bit(nonce: &mut [u8; 24]) {
let mut counter = u64::from_le_bytes([nonce[16], nonce[17], nonce[18], nonce[19], nonce[20], nonce[21], nonce[22], nonce[23]]);
counter = counter.wrapping_add(1);
let counter_bytes = counter.to_le_bytes();
nonce[16..24].copy_from_slice(&counter_bytes);
}
fn is_likely_base64(s: &str) -> bool {
if s.is_empty() || s.len() > 100 {
return false;
}
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
}
pub struct RcloneDecryptor {
password: String,
salt: Vec<u8>,
}
impl RcloneDecryptor {
pub fn new(password: String, salt: String) -> DecryptionResult<Self> {
let salt_bytes = if salt.is_empty() {
Vec::new()
} else if salt.starts_with("0x") || salt.starts_with("0X") {
hex::decode(&salt[2..])?
} else if is_likely_base64(&salt) && salt.len() > 8 {
match general_purpose::STANDARD.decode(&salt) {
Ok(decoded) => decoded,
Err(_) => {
salt.as_bytes().to_vec()
}
}
} else {
salt.as_bytes().to_vec()
};
Ok(RcloneDecryptor { password, salt: salt_bytes })
}
pub fn get_salt_debug(&self) -> &[u8] {
&self.salt
}
pub fn decrypt_file<P: AsRef<Path>>(&self, input_path: P, output_path: P) -> DecryptionResult<()> {
let verbose = build_cli().get_matches().get_flag("verbose");
let mut input_file = BufReader::new(File::open(&input_path)?);
let mut output_file = BufWriter::new(File::create(&output_path)?);
let mut magic = [0u8; 8];
input_file.read_exact(&mut magic)?;
if verbose {
println!("Debug: Magic header: {:?}", magic);
println!("Debug: Expected: {:?}", RCLONE_MAGIC);
}
if magic != RCLONE_MAGIC {
return Err(DecryptionError::InvalidFormat);
}
let mut base_nonce = [0u8; NONCE_SIZE];
input_file.read_exact(&mut base_nonce)?;
if verbose {
println!("Debug: Full nonce (24 bytes): {:?}", base_nonce);
println!("Debug: Using salt: {} bytes", self.salt.len());
println!("Debug: Password length: {}", self.password.len());
};
let mut key = [0u8; KEY_SIZE];
let params = Params::new(
14, SCRYPT_R, SCRYPT_P, KEY_SIZE,
)
.map_err(|_| DecryptionError::InvalidPassword)?;
scrypt(self.password.as_bytes(), &self.salt, ¶ms, &mut key).map_err(|_| DecryptionError::InvalidPassword)?;
if verbose {
println!("Debug: Scrypt params - N: {}, r: {}, p: {}", SCRYPT_N, SCRYPT_R, SCRYPT_P);
println!("Debug: Key derived successfully ({} bytes)", key.len());
};
sodiumoxide::init().map_err(|_| DecryptionError::InvalidPassword)?;
let secretbox_key = sodiumoxide::crypto::secretbox::Key::from_slice(&key).ok_or(DecryptionError::InvalidPassword)?;
let mut remaining_data = Vec::new();
input_file.read_to_end(&mut remaining_data)?;
if verbose {
println!("Debug: Total encrypted data size: {} bytes", remaining_data.len());
};
let nonce_obj = sodiumoxide::crypto::secretbox::Nonce::from_slice(&base_nonce).ok_or(DecryptionError::InvalidFormat)?;
if let Ok(decrypted_data) = sodiumoxide::crypto::secretbox::open(&remaining_data, &nonce_obj, &secretbox_key) {
if verbose {
println!("Debug: Successfully decrypted as single block!");
println!("Debug: Decrypted data size: {} bytes", decrypted_data.len());
};
if !decrypted_data.is_empty() && verbose {
println!("Debug: First decrypted bytes: {:?}", &decrypted_data[..std::cmp::min(16, decrypted_data.len())]);
let chunk_str = String::from_utf8_lossy(&decrypted_data[..std::cmp::min(16, decrypted_data.len())]);
println!("Debug: First bytes as string: '{}'", chunk_str);
};
output_file.write_all(&decrypted_data)?;
output_file.flush()?;
println!("Successfully decrypted {} to {}", input_path.as_ref().display(), output_path.as_ref().display());
return Ok(());
}
if verbose {
println!("Debug: Single block failed, trying 64KB chunked decryption...");
};
let mut current_pos = 0;
let mut chunk_nonce = base_nonce;
let mut found_valid_chunk = false;
let mut total_decrypted_bytes = 0;
while current_pos < remaining_data.len() {
let remaining_bytes = remaining_data.len() - current_pos;
let chunk_size = if remaining_bytes > CHUNK_SIZE + 16 {
CHUNK_SIZE + 16 } else {
remaining_bytes };
if chunk_size < 16 {
if verbose {
println!("Debug: Remaining chunk too small ({} bytes), stopping", chunk_size);
};
break;
}
let chunk_data = &remaining_data[current_pos..current_pos + chunk_size];
let chunk_nonce_obj = sodiumoxide::crypto::secretbox::Nonce::from_slice(&chunk_nonce).ok_or(DecryptionError::InvalidFormat)?;
if verbose {
println!("Debug: Trying to decrypt chunk at pos {} with size {} bytes (remaining: {})", current_pos, chunk_size, remaining_bytes);
println!("Debug: Current nonce: {:?}", &chunk_nonce[..8]); };
if let Ok(decrypted_chunk) = sodiumoxide::crypto::secretbox::open(chunk_data, &chunk_nonce_obj, &secretbox_key) {
if verbose {
println!("Debug: ✅ Successfully decrypted chunk at pos {} -> {} decrypted bytes", current_pos, decrypted_chunk.len());
if current_pos == 0 {
println!("Debug: First chunk bytes: {:?}", &decrypted_chunk[..std::cmp::min(16, decrypted_chunk.len())]);
let chunk_str = String::from_utf8_lossy(&decrypted_chunk[..std::cmp::min(16, decrypted_chunk.len())]);
println!("Debug: First chunk as string: '{}'", chunk_str);
}
};
output_file.write_all(&decrypted_chunk)?;
current_pos += chunk_size;
total_decrypted_bytes += decrypted_chunk.len();
if verbose {
println!("Debug: Before increment: {:?}", &chunk_nonce[..8]);
};
increment_nonce(&mut chunk_nonce);
if verbose {
println!("Debug: After little-endian increment: {:?}", &chunk_nonce[..8]);
};
found_valid_chunk = true;
if verbose {
println!(
"Debug: Progress: {}/{} bytes processed, {} bytes decrypted so far",
current_pos,
remaining_data.len(),
total_decrypted_bytes
);
};
} else {
if verbose {
println!("Debug: ❌ Failed to decrypt chunk at pos {} with size {} bytes", current_pos, chunk_size);
};
if current_pos == CHUNK_SIZE + 16 && found_valid_chunk {
if verbose {
println!("Debug: Trying alternative nonce increment strategies for second chunk...");
};
let mut test_nonce = base_nonce;
increment_nonce_be(&mut test_nonce);
if verbose {
println!("Debug: Trying big-endian increment: {:?}", &test_nonce[..8]);
};
let test_nonce_obj = sodiumoxide::crypto::secretbox::Nonce::from_slice(&test_nonce).ok_or(DecryptionError::InvalidFormat)?;
if let Ok(decrypted_chunk) = sodiumoxide::crypto::secretbox::open(chunk_data, &test_nonce_obj, &secretbox_key) {
if verbose {
println!("Debug: ✅ SUCCESS with big-endian nonce increment!");
};
output_file.write_all(&decrypted_chunk)?;
current_pos += chunk_size;
total_decrypted_bytes += decrypted_chunk.len();
chunk_nonce = test_nonce;
increment_nonce_be(&mut chunk_nonce); continue;
}
test_nonce = base_nonce;
increment_nonce_64bit(&mut test_nonce);
if verbose {
println!("Debug: Trying 64-bit counter increment: {:?}", &test_nonce[..8]);
};
let test_nonce_obj = sodiumoxide::crypto::secretbox::Nonce::from_slice(&test_nonce).ok_or(DecryptionError::InvalidFormat)?;
if let Ok(decrypted_chunk) = sodiumoxide::crypto::secretbox::open(chunk_data, &test_nonce_obj, &secretbox_key) {
if verbose {
println!("Debug: ✅ SUCCESS with 64-bit counter nonce increment!");
};
output_file.write_all(&decrypted_chunk)?;
current_pos += chunk_size;
total_decrypted_bytes += decrypted_chunk.len();
chunk_nonce = test_nonce;
increment_nonce_64bit(&mut chunk_nonce); continue;
}
}
if found_valid_chunk {
if verbose {
println!("Debug: Already decrypted some chunks successfully, might have reached end");
};
break;
}
return Err(DecryptionError::InvalidPassword);
}
}
if !found_valid_chunk {
return Err(DecryptionError::InvalidPassword);
}
if verbose {
println!("Debug: Chunked decryption completed! Total decrypted: {} bytes", total_decrypted_bytes);
};
output_file.flush()?;
println!("Successfully decrypted {} to {} using chunked approach", input_path.as_ref().display(), output_path.as_ref().display());
Ok(())
}
}