feat: also extract exfat without saving intermediary exfat image

This commit is contained in:
beerpsi
2026-01-21 09:05:10 +07:00
parent 023627426e
commit 362fb91ff7
5 changed files with 106 additions and 25 deletions
Generated
+1 -1
View File
@@ -507,7 +507,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fsdecrypt"
version = "0.1.5"
version = "0.1.6"
dependencies = [
"aes",
"anyhow",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "fsdecrypt"
version = "0.1.5"
version = "0.1.6"
edition = "2021"
[dependencies]
+28 -1
View File
@@ -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()
}
}
+39 -22
View File
@@ -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<File>], output_dir: &Path) -> Result<()> {
fn calculate_exfat_size(elements: &[FsElement<FscryptDecryptor<File>>]) -> Result<u64> {
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<FscryptDecryptor<File>>],
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<File>], 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:#?}");
}
+37
View File
@@ -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<R> {
/// The fscrypt file.
input: R,
@@ -289,3 +292,37 @@ impl<R: Read + Seek> Seek for FscryptDecryptor<R> {
Ok(target_pos)
}
}
impl ReadOffset for FscryptDecryptor<File> {
type Err = std::io::Error;
fn read_at(&self, offset: u64, buffer: &mut [u8]) -> Result<usize, Self::Err> {
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::<NoPadding>(&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)
}
}