diff --git a/Cargo.lock b/Cargo.lock index 42acc5a..4430fe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.5" +version = "0.1.6" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 182cc7e..f7f2168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.5" +version = "0.1.6" edition = "2021" [dependencies] diff --git a/src/bootid.rs b/src/bootid.rs index 24e7733..132eea0 100644 --- a/src/bootid.rs +++ b/src/bootid.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::fmt::{Debug, Display}; use hex_literal::hex; @@ -75,3 +75,30 @@ pub struct BootId { // We don't need the entire bootID, so keep size down by not including the string table. // pub strings: [u8; 10156], } + +impl Debug for BootId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BootId") + .field("crc32", &self.crc32) + .field("length", &self.length) + .field("signature", &self.signature) + .field("unk1", &self.unk1) + .field("container_type", &self.container_type) + .field("sequence_number", &self.sequence_number) + .field("use_custom_iv", &self.use_custom_iv) + .field("game_id", &self.game_id) + .field("target_timestamp", &self.target_timestamp) + //.field("target_version", &self.target_version) + .field("block_count", &self.block_count) + .field("block_size", &self.block_size) + .field("header_block_count", &self.header_block_count) + .field("unk2", &self.unk2) + .field("os_id", &self.os_id) + .field("os_generation", &self.os_generation) + .field("source_timestamp", &self.source_timestamp) + .field("source_version", &self.source_version) + .field("os_version", &self.os_version) + .field("padding", &self.padding) + .finish() + } +} diff --git a/src/main.rs b/src/main.rs index b45e171..fded235 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,27 +50,46 @@ fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { // Create output directory with same name as exfat file (without extension) let output_dir = exfat_path.with_extension(""); - let pb = ProgressBar::new_spinner() - .with_style(ProgressStyle::default_bar().template("{prefix} {spinner}")?); - pb.set_prefix(format!( - "Extracting contents of {} to {}", - exfat_path.file_name().unwrap().display(), - output_dir.display(), - )); - pb.enable_steady_tick(Duration::from_millis(100)); - - let file = File::open(exfat_path)?; + let file = FscryptDecryptor::new(File::open(exfat_path)?).map_err(|e| anyhow!(e))?; let mut root = Root::open(file)?; + let pb = ProgressBar::new(calculate_exfat_size(root.items())?) + .with_style( + ProgressStyle::default_bar().template( + "{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]", + )? + ); + pb.set_prefix(format!( + "Extracting {}", + exfat_path.file_name().unwrap().display(), + )); + create_dir_all(&output_dir)?; - extract_exfat_elements(root.items(), &output_dir)?; + extract_exfat_elements(root.items(), &output_dir, &pb)?; pb.finish(); Ok(()) } -fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) -> Result<()> { +fn calculate_exfat_size(elements: &[FsElement>]) -> Result { + let mut total = 0; + + for element in elements { + match element { + FsElement::F(ref file) => total += file.len(), + FsElement::D(directory) => total += calculate_exfat_size(&directory.open()?)?, + } + } + + Ok(total) +} + +fn extract_exfat_elements( + elements: &mut [FsElement>], + output_dir: &Path, + pb: &ProgressBar, +) -> Result<()> { for element in elements { match element { FsElement::F(ref mut file) => { @@ -90,13 +109,15 @@ fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) - let mut writer = BufWriter::with_capacity(256 * 1024, &mut dest); std::io::copy(file, &mut writer)?; + writer.flush()?; + pb.inc(file.len()); } FsElement::D(directory) => { let dest_path = output_dir.join(directory.name()); create_dir_all(&dest_path)?; let mut children = directory.open()?; - extract_exfat_elements(&mut children, &dest_path)?; + extract_exfat_elements(&mut children, &dest_path, &pb)?; } } } @@ -208,7 +229,7 @@ fn main() -> Result<()> { // Can't directly extract options since we can't impl exfat_fs::dir::ReadOffset // for our wrapper decryptor - if cli.no_extract || file.bootid.container_type == ContainerType::OPTION { + if cli.no_extract { let mut output_file = File::create(&output_path)?; output_file.set_len(file.len())?; @@ -236,9 +257,9 @@ fn main() -> Result<()> { pb.inc(length as u64); } - } - if !cli.no_extract { + output_file.flush()?; + } else { match bootid.container_type { ContainerType::OS | ContainerType::APP => { match extract_internal_vhd(&path, bootid.sequence_number) { @@ -248,12 +269,8 @@ fn main() -> Result<()> { } } } - ContainerType::OPTION => match extract_exfat_contents(&output_path) { - Ok(_) => { - println!("Deleting exFAT image: {}", output_path.display()); - - std::fs::remove_file(output_path)?; - } + ContainerType::OPTION => match extract_exfat_contents(&path) { + Ok(_) => {} Err(e) => { println!("WARNING: Failed to extract exfat contents: {e:#?}"); } diff --git a/src/stream.rs b/src/stream.rs index d7f4a58..0a2661e 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,4 +1,5 @@ use std::{ + fs::File, io::{self, Read, Seek, SeekFrom}, str::Utf8Error, }; @@ -8,6 +9,7 @@ use aes::cipher::{ BlockDecryptMut, InvalidLength, }; use cipher::KeyIvInit; +use exfat_fs::disk::ReadOffset; use crate::{ bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY}, @@ -44,6 +46,7 @@ pub enum DecryptError { } /// Decryptor for an fscrypt container. +#[derive(Debug)] pub struct FscryptDecryptor { /// The fscrypt file. input: R, @@ -289,3 +292,37 @@ impl Seek for FscryptDecryptor { Ok(target_pos) } } + +impl ReadOffset for FscryptDecryptor { + type Err = std::io::Error; + + fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result { + let start = self.bootid.header_block_count * self.bootid.block_size; + let target_index = offset / PAGE_SIZE; + let target_offset = offset % PAGE_SIZE; + let mut page = [0u8; 4096]; + let mut page_iv = [0u8; 16]; + let page_offset = target_index * PAGE_SIZE; + + #[cfg(unix)] + std::os::unix::fs::FileExt::read_at(&self.input, &mut page, start + page_offset)?; + + #[cfg(windows)] + std::os::windows::fs::FileExt::seek_read(&self.input, &mut page, start + page_offset)?; + + calculate_page_iv(page_offset, &self.iv, &mut page_iv); + + let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap(); + + page_cipher + .decrypt_padded_mut::(&mut page) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?; + + let to_read = std::cmp::min(buffer.len(), (PAGE_SIZE - target_offset) as usize); + + buffer[..to_read] + .copy_from_slice(&page[target_offset as usize..target_offset as usize + to_read]); + + Ok(to_read) + } +}