From 5d1e0ca908119a204fecb633b1cd233dbf21072b Mon Sep 17 00:00:00 2001 From: jujuforce Date: Wed, 16 Jul 2025 15:49:11 +0000 Subject: [PATCH 01/11] feat: exfat content extraction (#1) Co-authored-by: jujuforce Co-authored-by: beerpsi Reviewed-on: https://gitea.tendokyu.moe/beerpsi/fsdecrypt/pulls/1 Co-authored-by: jujuforce Co-committed-by: jujuforce --- Cargo.lock | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 63 ++++++++++++-- 3 files changed, 297 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4824116..6942941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aes" @@ -19,6 +19,21 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "bytemuck", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -28,6 +43,26 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cbc" version = "0.1.2" @@ -43,6 +78,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_num" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb5a345c406bf67f62bd14f284c9cb041ce5107725c22e665a163c871a4fb7b" +dependencies = [ + "num-traits", +] + [[package]] name = "cipher" version = "0.4.4" @@ -94,12 +138,130 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "endify" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "412c0c5048548a61a2ed94233a7305d2dfa4d1d3acc69039da77efa84e51df21" +dependencies = [ + "endify-derive", +] + +[[package]] +name = "endify-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a204d4aa704d0a74ee3bebccc65f24bca0cdef2588f08eaafda467cd4d3c8a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumeric" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2f9bd649bd8c884aa518a798861fd111699dfc7d7dcc193bc2b658c8b9188d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "exfat-fs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbdfa8ecf36ab2341032974d3644a56f1e40c9da3b643118492cf97f357f3c5" +dependencies = [ + "bitflags", + "bytemuck", + "checked_num", + "derive_builder", + "endify", + "enumeric", + "thiserror", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fsdecrypt" version = "0.1.1" @@ -108,6 +270,7 @@ dependencies = [ "anyhow", "cbc", "crc32fast", + "exfat-fs", "hex-literal", "indicatif", ] @@ -128,6 +291,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indicatif" version = "0.17.8" @@ -172,6 +341,15 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -184,12 +362,73 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + [[package]] name = "unicode-width" version = "0.1.13" diff --git a/Cargo.toml b/Cargo.toml index fd5fd16..f8966b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ cbc = "0.1.2" crc32fast = "1.4.2" hex-literal = "0.4.1" indicatif = "0.17.8" +exfat-fs = "0.1.3" diff --git a/src/main.rs b/src/main.rs index 09bf4ff..62381a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use std::{ - fs::File, + fs::{create_dir_all, File}, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, path::Path, }; +use exfat_fs::dir::{entry::fs::FsElement, Root}; + use aes::{ cipher::{block_padding::NoPadding, BlockDecryptMut, InnerIvInit, KeyInit, KeyIvInit}, Aes128Dec, @@ -21,6 +23,43 @@ mod crypto; const PAGE_SIZE: u64 = 4096; +fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { + println!("Extracting contents of {}", exfat_path.display()); + + let file = File::open(exfat_path)?; + let mut root = Root::open(file)?; + + // Create output directory with same name as exfat file (without extension) + let output_dir = exfat_path.with_extension(""); + + create_dir_all(&output_dir)?; + extract_fs_elements(root.items(), &output_dir)?; + + Ok(()) +} + +fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> Result<()> { + for element in elements { + match element { + FsElement::F(ref mut file) => { + let dest_path = output_dir.join(file.name()); + let mut dest = File::create(dest_path)?; + + std::io::copy(file, &mut dest)?; + } + FsElement::D(directory) => { + let dest_path = output_dir.join(directory.name()); + create_dir_all(&dest_path)?; + + let mut children = directory.open()?; + extract_fs_elements(&mut children, &dest_path)?; + } + } + } + + Ok(()) +} + fn main() -> Result<()> { let args = std::env::args().collect::>(); @@ -83,11 +122,7 @@ fn main() -> Result<()> { let data_offset = bootid.header_block_count * bootid.block_size; let key = keys.key; - let iv = if bootid.use_custom_iv { - None - } else { - keys.iv - }; + let iv = if bootid.use_custom_iv { None } else { keys.iv }; let iv = match iv { Some(iv) => iv, None => { @@ -160,7 +195,7 @@ fn main() -> Result<()> { .template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")? ); - pb.set_prefix(output_filename); + pb.set_prefix(output_filename.clone()); reader.seek(SeekFrom::Start(data_offset))?; for _ in 0..output_size / PAGE_SIZE { @@ -184,6 +219,20 @@ fn main() -> Result<()> { writer.flush()?; pb.finish(); + // Extract exfat contents if this is an exfat file + if bootid.container_type == ContainerType::OPTION + && output_path.extension().unwrap_or_default() == "exfat" + { + if let Err(e) = extract_exfat_contents(&output_path) { + println!("WARNING: Failed to extract exfat contents: {e:#?}"); + } else { + println!("Extracted exfat contents: {:?}", output_path); + println!("Deleting exfat file: {:?}", output_path); + + std::fs::remove_file(output_path)?; + } + } + page.clear(); page_iv.fill(0); bootid_bytes.fill(0); From 1d79e965098b914f83cefb02dc09ca66631b6cdd Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 16 Jul 2025 23:36:42 +0700 Subject: [PATCH 02/11] feat: set access/modified time from the exfat --- src/main.rs | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 62381a9..17f199a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use std::{ - fs::{create_dir_all, File}, + fs::{create_dir_all, File, FileTimes}, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, path::Path, + time::{Duration, SystemTime}, }; +use chrono::{FixedOffset, TimeZone}; use exfat_fs::dir::{entry::fs::FsElement, Root}; use aes::{ @@ -23,6 +25,29 @@ mod crypto; const PAGE_SIZE: u64 = 4096; +fn exfat_timestamp_to_system_time( + timestamp: &exfat_fs::timestamp::Timestamp, +) -> Result { + let exfat_date = timestamp.date(); + let exfat_time = timestamp.time(); + // exFAT UTC offset is in 15-minute intervals, so 1 = UTC+00:15, 2 = UTC+00:30, etc. + let exfat_utc_offset = timestamp.utc_offset() as i32 * 15 * 60; + let chrono_date_time = FixedOffset::east_opt(exfat_utc_offset) + .ok_or_else(|| anyhow!("invaid utc offset: {}", timestamp.utc_offset()))? + .with_ymd_and_hms( + exfat_date.year as i32, + exfat_date.month as u32, + exfat_date.day as u32, + exfat_time.hour as u32, + exfat_time.minute as u32, + exfat_time.second as u32, + ) + .unwrap(); + + return Ok(SystemTime::UNIX_EPOCH + + Duration::from_micros(chrono_date_time.timestamp_micros().try_into()?)); +} + fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { println!("Extracting contents of {}", exfat_path.display()); @@ -46,6 +71,16 @@ fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> R let mut dest = File::create(dest_path)?; std::io::copy(file, &mut dest)?; + + dest.set_times( + FileTimes::new() + .set_accessed(exfat_timestamp_to_system_time( + file.timestamps().accessed(), + )?) + .set_modified(exfat_timestamp_to_system_time( + file.timestamps().modified(), + )?), + )?; } FsElement::D(directory) => { let dest_path = output_dir.join(directory.name()); From 7bcd2efb0618539575028d8388e61ab4cdd68b2a Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 16 Jul 2025 23:45:13 +0700 Subject: [PATCH 03/11] Release 0.1.2 --- Cargo.lock | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- 2 files changed, 229 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6942941..a3e5c0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,21 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -43,6 +58,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytemuck" version = "1.23.1" @@ -72,6 +93,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "cc" +version = "1.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -87,6 +117,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -110,6 +154,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -264,11 +314,12 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.1" +version = "0.1.2" dependencies = [ "aes", "anyhow", "cbc", + "chrono", "crc32fast", "exfat-fs", "hex-literal", @@ -291,6 +342,30 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -329,6 +404,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -341,6 +426,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "num-traits" version = "0.2.19" @@ -356,6 +447,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "portable-atomic" version = "1.7.0" @@ -380,6 +477,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -441,6 +550,123 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index f8966b5..8701375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.1" +version = "0.1.2" edition = "2021" [dependencies] @@ -11,3 +11,4 @@ crc32fast = "1.4.2" hex-literal = "0.4.1" indicatif = "0.17.8" exfat-fs = "0.1.3" +chrono = "0.4.41" From 8b34b87732f74b50b442fd2e0ce79bd9e94ce9a0 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Thu, 17 Jul 2025 19:57:03 +0700 Subject: [PATCH 04/11] feat: NTFS support --- Cargo.lock | 407 ++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 +- src/main.rs | 96 ++++++++++--- 3 files changed, 470 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3e5c0a..5bd6d2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,18 +28,103 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "binrw" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab81d22cbd2d745852348b2138f3db2103afa8ce043117a374581926a523e267" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b019a3efebe7f453612083202887b6f1ace59e20d010672e336eea4ed5be97" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -81,9 +166,15 @@ checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cbc" version = "0.1.2" @@ -141,6 +232,52 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "console" version = "0.15.8" @@ -151,9 +288,15 @@ dependencies = [ "lazy_static", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -209,7 +352,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.104", ] [[package]] @@ -220,7 +363,16 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.104", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", ] [[package]] @@ -241,7 +393,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -251,9 +403,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn", + "syn 2.0.104", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.104", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -277,7 +459,7 @@ checksum = "e7a204d4aa704d0a74ee3bebccc65f24bca0cdef2588f08eaafda467cd4d3c8a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -288,7 +470,18 @@ checksum = "1f2f9bd649bd8c884aa518a798861fd111699dfc7d7dcc193bc2b658c8b9188d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] @@ -314,16 +507,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.2" +version = "0.1.3" dependencies = [ "aes", "anyhow", "cbc", "chrono", + "clap", "crc32fast", "exfat-fs", "hex-literal", "indicatif", + "ntfs", ] [[package]] @@ -336,6 +531,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex-literal" version = "0.4.1" @@ -404,6 +611,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "js-sys" version = "0.3.77" @@ -432,6 +645,50 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nt-string" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64f73b19d9405e886b53b9dee286e7fbb622a5276a7fd143c2d8e4dac3a0c6c" +dependencies = [ + "displaydoc", + "widestring", +] + +[[package]] +name = "ntfs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fe2140f3b6eef2648fa331ca7af7652da1bf3bb84f715878bf50f2f8f03c38" +dependencies = [ + "arrayvec", + "binrw", + "bitflags", + "byteorder", + "derive_more", + "displaydoc", + "enumn", + "memoffset", + "nt-string", + "strum_macros", + "time", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -453,12 +710,24 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "portable-atomic" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -477,12 +746,47 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "shlex" version = "1.3.0" @@ -495,6 +799,30 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.104" @@ -523,7 +851,37 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", ] [[package]] @@ -544,6 +902,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.4" @@ -572,7 +936,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -594,7 +958,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -608,6 +972,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "windows-core" version = "0.61.2" @@ -629,7 +999,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -640,7 +1010,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -676,6 +1046,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 8701375..fcd1e67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.2" +version = "0.1.3" edition = "2021" [dependencies] @@ -12,3 +12,5 @@ hex-literal = "0.4.1" indicatif = "0.17.8" exfat-fs = "0.1.3" chrono = "0.4.41" +clap = { version = "4.5.41", features = ["derive"] } +ntfs = "0.4.0" diff --git a/src/main.rs b/src/main.rs index 17f199a..d04a76c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,12 @@ use std::{ fs::{create_dir_all, File, FileTimes}, io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, - path::Path, + path::{Path, PathBuf}, time::{Duration, SystemTime}, }; use chrono::{FixedOffset, TimeZone}; +use clap::Parser; use exfat_fs::dir::{entry::fs::FsElement, Root}; use aes::{ @@ -19,6 +20,7 @@ use crypto::{ NTFS_HEADER, OPTION_IV, OPTION_KEY, }; use indicatif::{ProgressBar, ProgressStyle}; +use ntfs::{indexes::NtfsFileNameIndex, Ntfs}; mod bootid; mod crypto; @@ -58,12 +60,12 @@ fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { let output_dir = exfat_path.with_extension(""); create_dir_all(&output_dir)?; - extract_fs_elements(root.items(), &output_dir)?; + extract_exfat_elements(root.items(), &output_dir)?; Ok(()) } -fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> Result<()> { +fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) -> Result<()> { for element in elements { match element { FsElement::F(ref mut file) => { @@ -87,7 +89,7 @@ fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> R create_dir_all(&dest_path)?; let mut children = directory.open()?; - extract_fs_elements(&mut children, &dest_path)?; + extract_exfat_elements(&mut children, &dest_path)?; } } } @@ -95,13 +97,46 @@ fn extract_fs_elements(elements: &mut [FsElement], output_dir: &Path) -> R Ok(()) } -fn main() -> Result<()> { - let args = std::env::args().collect::>(); +fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result { + let vhd_filename = format!("internal_{sequence_number}.vhd"); + let output_path = image_path.with_extension("vhd"); - if args.len() < 2 { - println!("Usage: fsdecrypt [ ...]"); - return Ok(()); - } + let mut fs = File::open(image_path)?; + + let mut ntfs = Ntfs::new(&mut fs)?; + + ntfs.read_upcase_table(&mut fs)?; + + let root_directory = ntfs.root_directory(&mut fs)?; + let index = root_directory.directory_index(&mut fs)?; + let mut finder = index.finder(); + let entry = NtfsFileNameIndex::find(&mut finder, &ntfs, &mut fs, &vhd_filename) + .ok_or_else(|| anyhow!("could not find VHD {vhd_filename}"))??; + let file = entry.to_file(&ntfs, &mut fs)?; + let data_item = file + .data(&mut fs, "") + .ok_or_else(|| anyhow!("file data does not exist"))??; + let data_attribute = data_item.to_attribute()?; + let mut data_value = data_attribute.value(&mut fs)?.attach(&mut fs); + + let mut output_file = File::create(&output_path)?; + std::io::copy(&mut data_value, &mut output_file)?; + + Ok(output_path) +} + +#[derive(Parser)] +#[command(version, about = "decryptor for some SEGA containers", long_about = None)] +struct Cli { + #[arg(long, help = "do not extract contents of decrypted image")] + no_extract: bool, + + #[arg(required = true)] + files: Vec, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); let bootid_cipher = Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV).map_err(|e| anyhow!(e))?; @@ -109,8 +144,7 @@ fn main() -> Result<()> { let mut page: Vec = Vec::with_capacity(PAGE_SIZE as usize); let mut page_iv = [0u8; 16]; - for path in args.iter().skip(1) { - let path = Path::new(path); + for path in &cli.files { let file = File::open(path)?; let mut reader = BufReader::with_capacity(0x40000, file); @@ -254,17 +288,35 @@ fn main() -> Result<()> { writer.flush()?; pb.finish(); - // Extract exfat contents if this is an exfat file - if bootid.container_type == ContainerType::OPTION - && output_path.extension().unwrap_or_default() == "exfat" - { - if let Err(e) = extract_exfat_contents(&output_path) { - println!("WARNING: Failed to extract exfat contents: {e:#?}"); - } else { - println!("Extracted exfat contents: {:?}", output_path); - println!("Deleting exfat file: {:?}", output_path); + if !cli.no_extract { + match bootid.container_type { + ContainerType::OS | ContainerType::APP => { + match extract_internal_vhd(&output_path, bootid.sequence_number) { + Ok(extracted_path) => { + println!("Extracted internal VHD: {}", extracted_path.display()); + println!("Deleting original NTFS image: {}", output_path.display()); - std::fs::remove_file(output_path)?; + std::fs::remove_file(output_path)?; + } + Err(e) => { + println!("WARNING: Failed to extract internal VHD: {e:#?}"); + } + } + } + ContainerType::OPTION => match extract_exfat_contents(&output_path) { + Ok(_) => { + println!("Extracted exfat contents: {}", output_path.display()); + println!("Deleting exfat file: {}", output_path.display()); + + std::fs::remove_file(output_path)?; + } + Err(e) => { + println!("WARNING: Failed to extract exfat contents: {e:#?}"); + } + }, + _ => { + println!("WARNING: Unknown container type: {}", bootid.container_type); + } } } From ece90ebb5b2ebfab9514325140d16da79d858f14 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Thu, 17 Jul 2025 20:54:53 +0700 Subject: [PATCH 05/11] ntfs extraction + add spinner for extracting images --- src/main.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index d04a76c..44b8ea1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,10 @@ use crypto::{ NTFS_HEADER, OPTION_IV, OPTION_KEY, }; use indicatif::{ProgressBar, ProgressStyle}; -use ntfs::{indexes::NtfsFileNameIndex, Ntfs}; +use ntfs::{ + indexes::NtfsFileNameIndex, structured_values::NtfsStandardInformation, Ntfs, + NtfsAttributeType, NtfsTime, +}; mod bootid; mod crypto; @@ -51,17 +54,26 @@ fn exfat_timestamp_to_system_time( } fn extract_exfat_contents(exfat_path: &Path) -> Result<()> { - println!("Extracting contents of {}", exfat_path.display()); + // 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 mut root = Root::open(file)?; - // Create output directory with same name as exfat file (without extension) - let output_dir = exfat_path.with_extension(""); - create_dir_all(&output_dir)?; extract_exfat_elements(root.items(), &output_dir)?; + pb.finish(); + Ok(()) } @@ -97,6 +109,16 @@ fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) - Ok(()) } +fn ntfs_time_to_system_time(ntfs_time: NtfsTime) -> SystemTime { + // An NTFS "interval" is 100 nanoseconds. + // The Windows epoch is 1601-01-01, while the Unix epoch is 1970-01-01. + let intervals_since_windows_epoch = ntfs_time.nt_timestamp(); + let intervals_since_unix_epoch = intervals_since_windows_epoch - 116_444_736_000_000_000; + let nanos_since_unix_epoch = intervals_since_unix_epoch * 100; + + return SystemTime::UNIX_EPOCH + Duration::from_nanos(nanos_since_unix_epoch); +} + fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result { let vhd_filename = format!("internal_{sequence_number}.vhd"); let output_path = image_path.with_extension("vhd"); @@ -120,8 +142,41 @@ fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result { + let info = attribute.resident_structured_value::()?; + + output_file.set_times( + FileTimes::new() + .set_accessed(ntfs_time_to_system_time(info.access_time())) + .set_modified(ntfs_time_to_system_time(info.modification_time())), + )?; + + break; + } + _ => continue, + } + } + Ok(output_path) } @@ -292,10 +347,8 @@ fn main() -> Result<()> { match bootid.container_type { ContainerType::OS | ContainerType::APP => { match extract_internal_vhd(&output_path, bootid.sequence_number) { - Ok(extracted_path) => { - println!("Extracted internal VHD: {}", extracted_path.display()); - println!("Deleting original NTFS image: {}", output_path.display()); - + Ok(_) => { + println!("Deleting NTFS image: {}", output_path.display()); std::fs::remove_file(output_path)?; } Err(e) => { @@ -305,8 +358,7 @@ fn main() -> Result<()> { } ContainerType::OPTION => match extract_exfat_contents(&output_path) { Ok(_) => { - println!("Extracted exfat contents: {}", output_path.display()); - println!("Deleting exfat file: {}", output_path.display()); + println!("Deleting exFAT image: {}", output_path.display()); std::fs::remove_file(output_path)?; } From 8c5998a85c5f0bd422eeb7aa417558d69c85fa46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8E=AF=E5=87=9B?= Date: Tue, 4 Nov 2025 05:53:17 +0000 Subject: [PATCH 06/11] perf-buf-size (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Increased IO throughput when laying out VHD file `std::io::copy` will only setup an 8KiB stack buffer, which results sub optimal performance when passing `File` directly. Reviewed-on: https://gitea.tendokyu.moe/beerpsi/fsdecrypt/pulls/2 Co-authored-by: 莯凛 Co-committed-by: 莯凛 --- Cargo.lock | 362 ++++++++++++++++++++++++++-------------------------- Cargo.toml | 17 ++- src/main.rs | 5 +- 3 files changed, 199 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5bd6d2a..e90b5d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,12 +13,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -30,9 +24,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -60,29 +54,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "array-init" @@ -127,9 +121,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "bytemuck", ] @@ -151,22 +145,22 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -186,18 +180,19 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.29" +version = "1.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "checked_num" @@ -210,11 +205,10 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -234,9 +228,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -244,9 +238,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -256,14 +250,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -280,15 +274,15 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "console" -version = "0.15.8" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" dependencies = [ "encode_unicode", - "lazy_static", "libc", + "once_cell", "unicode-width", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -305,18 +299,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -352,7 +346,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -363,14 +357,14 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", ] @@ -393,7 +387,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -403,7 +397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -416,7 +410,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -427,7 +421,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -438,9 +432,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "endify" @@ -459,7 +453,7 @@ checksum = "e7a204d4aa704d0a74ee3bebccc65f24bca0cdef2588f08eaafda467cd4d3c8a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -470,7 +464,7 @@ checksum = "1f2f9bd649bd8c884aa518a798861fd111699dfc7d7dcc193bc2b658c8b9188d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -481,7 +475,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -499,6 +493,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + [[package]] name = "fnv" version = "1.0.7" @@ -545,15 +545,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex-literal" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -581,36 +581,27 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "indicatif" -version = "0.17.8" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", - "instant", - "number_prefix", "portable-atomic", "unicode-width", + "unit-prefix", + "web-time", ] [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -619,31 +610,25 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.155" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memoffset" @@ -698,12 +683,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.21.3" @@ -718,9 +697,9 @@ checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "powerfmt" @@ -730,9 +709,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -757,34 +736,43 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -825,9 +813,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -836,29 +824,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "num-conv", @@ -870,15 +858,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -886,21 +874,27 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" [[package]] name = "utf8parse" @@ -910,41 +904,42 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -952,26 +947,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "widestring" version = "1.2.0" @@ -980,9 +985,9 @@ checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" dependencies = [ "windows-implement", "windows-interface", @@ -993,74 +998,75 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -1073,48 +1079,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/Cargo.toml b/Cargo.toml index fcd1e67..1fd949c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,17 @@ edition = "2021" [dependencies] aes = "0.8.4" -anyhow = "1.0.86" +anyhow = "1.0.100" cbc = "0.1.2" -crc32fast = "1.4.2" -hex-literal = "0.4.1" -indicatif = "0.17.8" +crc32fast = "1.5.0" +hex-literal = "1.0.0" +indicatif = "0.18.0" exfat-fs = "0.1.3" -chrono = "0.4.41" -clap = { version = "4.5.41", features = ["derive"] } +chrono = "0.4.42" +clap = { version = "4.5.48", features = ["derive"] } ntfs = "0.4.0" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "none" diff --git a/src/main.rs b/src/main.rs index 44b8ea1..570528c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,6 +142,7 @@ fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result Result Date: Tue, 4 Nov 2025 13:05:10 +0700 Subject: [PATCH 07/11] port buffering to exfat extraction --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e90b5d6..80d7cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.3" +version = "0.1.4" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 1fd949c..3b231d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.3" +version = "0.1.4" edition = "2021" [dependencies] diff --git a/src/main.rs b/src/main.rs index 570528c..c48a12c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,8 +84,6 @@ fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) - let dest_path = output_dir.join(file.name()); let mut dest = File::create(dest_path)?; - std::io::copy(file, &mut dest)?; - dest.set_times( FileTimes::new() .set_accessed(exfat_timestamp_to_system_time( @@ -95,6 +93,10 @@ fn extract_exfat_elements(elements: &mut [FsElement], output_dir: &Path) - file.timestamps().modified(), )?), )?; + + let mut writer = BufWriter::with_capacity(256 * 1024, &mut dest); + + std::io::copy(file, &mut writer)?; } FsElement::D(directory) => { let dest_path = output_dir.join(directory.name()); From 0c4a521bc2a660565fb1654d6b2041b13108bd98 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Tue, 20 Jan 2026 18:00:19 +0700 Subject: [PATCH 08/11] feat: directly extract VHD for apps and packs --- Cargo.lock | 10 +- Cargo.toml | 2 + src/crypto.rs | 1 + src/main.rs | 227 ++++++++++----------------------------- src/stream.rs | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 355 insertions(+), 176 deletions(-) create mode 100644 src/stream.rs diff --git a/Cargo.lock b/Cargo.lock index 80d7cd5..3953943 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -513,12 +513,14 @@ dependencies = [ "anyhow", "cbc", "chrono", + "cipher", "clap", "crc32fast", "exfat-fs", "hex-literal", "indicatif", "ntfs", + "thiserror", ] [[package]] @@ -824,18 +826,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3b231d0..6da95c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ exfat-fs = "0.1.3" chrono = "0.4.42" clap = { version = "4.5.48", features = ["derive"] } ntfs = "0.4.0" +thiserror = "2.0.18" +cipher = { version = "0.4.4", features = ["alloc"] } [profile.release] lto = "thin" diff --git a/src/crypto.rs b/src/crypto.rs index 6fb09be..6e282d8 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -12,6 +12,7 @@ pub const OPTION_IV: [u8; 16] = hex!("c063bf6f562d084d7963c987f5281761"); pub type Aes128CbcDec = cbc::Decryptor; +#[derive(Debug, Clone, Copy)] pub struct GameKeys { pub key: [u8; 16], pub iv: Option<[u8; 16]>, diff --git a/src/main.rs b/src/main.rs index c48a12c..b45e171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::{ fs::{create_dir_all, File, FileTimes}, - io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}, + io::{BufRead, BufReader, BufWriter, Write}, path::{Path, PathBuf}, time::{Duration, SystemTime}, }; @@ -9,26 +9,19 @@ use chrono::{FixedOffset, TimeZone}; use clap::Parser; use exfat_fs::dir::{entry::fs::FsElement, Root}; -use aes::{ - cipher::{block_padding::NoPadding, BlockDecryptMut, InnerIvInit, KeyInit, KeyIvInit}, - Aes128Dec, -}; use anyhow::{anyhow, Result}; -use bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY}; -use crypto::{ - calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER, - NTFS_HEADER, OPTION_IV, OPTION_KEY, -}; +use bootid::ContainerType; use indicatif::{ProgressBar, ProgressStyle}; use ntfs::{ indexes::NtfsFileNameIndex, structured_values::NtfsStandardInformation, Ntfs, NtfsAttributeType, NtfsTime, }; +use crate::stream::FscryptDecryptor; + mod bootid; mod crypto; - -const PAGE_SIZE: u64 = 4096; +mod stream; fn exfat_timestamp_to_system_time( timestamp: &exfat_fs::timestamp::Timestamp, @@ -125,7 +118,7 @@ fn extract_internal_vhd(image_path: &Path, sequence_number: u8) -> Result Result8} [{elapsed}<{eta}, {bytes_per_sec}]")? + ); + pb.set_prefix(format!("{}", output_path.file_name().unwrap().display())); - std::io::copy(&mut data_value, &mut writer)?; - writer.flush()?; - drop(writer); + loop { + let buffer = data_value.fill_buf()?; + let length = buffer.len(); + + if length == 0 { + break; + } + + output_file.write_all(buffer)?; + data_value.consume(length); + pb.inc(length as u64); + } + output_file.flush()?; pb.finish(); @@ -198,164 +200,49 @@ struct Cli { fn main() -> Result<()> { let cli = Cli::parse(); - let bootid_cipher = - Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV).map_err(|e| anyhow!(e))?; - let mut bootid_bytes = [0u8; std::mem::size_of::()]; - let mut page: Vec = Vec::with_capacity(PAGE_SIZE as usize); - let mut page_iv = [0u8; 16]; - for path in &cli.files { - let file = File::open(path)?; - let mut reader = BufReader::with_capacity(0x40000, file); - - reader.read_exact(&mut bootid_bytes)?; - - if let Err(e) = bootid_cipher - .clone() - .decrypt_padded_mut::(&mut bootid_bytes) - { - println!("ERROR: Could not decrypt BootID: {e:#?}"); - continue; - } - - let bootid = unsafe { std::mem::transmute::<[u8; 96], BootId>(bootid_bytes) }; - - if bootid.container_type != ContainerType::OS - && bootid.container_type != ContainerType::APP - && bootid.container_type != ContainerType::OPTION - { - println!("ERROR: Unknown container type {}", bootid.container_type); - continue; - } - - let os_id = std::str::from_utf8(&bootid.os_id)?; - let game_id = std::str::from_utf8(&bootid.game_id)?; - let id = match bootid.container_type { - ContainerType::OS => os_id, - _ => game_id, - }; - - let keys = match bootid.container_type { - ContainerType::OS => get_game_keys(os_id), - ContainerType::APP => get_game_keys(game_id), - _ => Some(GameKeys { - key: OPTION_KEY, - iv: Some(OPTION_IV), - }), - }; - - let Some(keys) = keys else { - println!("ERROR: Key not found for {id}. If you're using a custom key file, ensure the key file is 16/32 bytes and named {id}.bin."); - continue; - }; - - let data_offset = bootid.header_block_count * bootid.block_size; - let key = keys.key; - let iv = if bootid.use_custom_iv { None } else { keys.iv }; - let iv = match iv { - Some(iv) => iv, - None => { - reader.seek(SeekFrom::Start(data_offset))?; - - let reference = Read::by_ref(&mut reader); - - reference.take(4096).read_to_end(&mut page)?; - - if bootid.container_type == ContainerType::OPTION { - calculate_file_iv(key, EXFAT_HEADER, &page)? - } else { - calculate_file_iv(key, NTFS_HEADER, &page)? - } - } - }; - - let output_filename = match bootid.container_type { - ContainerType::OS => format!( - "{os_id}_{:<04}.{:<02}.{:<02}_{}_{}.ntfs", - bootid.os_version.major, - bootid.os_version.minor, - bootid.os_version.release, - bootid.target_timestamp, - bootid.sequence_number - ), - ContainerType::APP => { - if bootid.sequence_number > 0 { - format!( - "{game_id}_{}.{:<02}.{:<02}_{}_{}_{}.{:<02}.{:<02}.ntfs", - unsafe { bootid.target_version.version.major }, - unsafe { bootid.target_version.version.minor }, - unsafe { bootid.target_version.version.release }, - bootid.target_timestamp, - bootid.sequence_number, - bootid.source_version.major, - bootid.source_version.minor, - bootid.source_version.release, - ) - } else { - format!( - "{game_id}_{}.{:<02}.{:<02}_{}_{}.ntfs", - unsafe { bootid.target_version.version.major }, - unsafe { bootid.target_version.version.minor }, - unsafe { bootid.target_version.version.release }, - bootid.target_timestamp, - bootid.sequence_number, - ) - } - } - _ => format!( - "{game_id}_{}_{}_{}.exfat", - unsafe { std::str::from_utf8(&bootid.target_version.option)? }, - bootid.target_timestamp, - bootid.sequence_number, - ), - }; + let file = FscryptDecryptor::new(File::open(path)?).map_err(|e| anyhow!(e))?; + let bootid = file.bootid.clone(); + let output_filename = file.filename()?; let output_path = path.with_file_name(&output_filename); - let output_file = File::create(&output_path)?; - let output_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size; - output_file.set_len(output_size)?; + // 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 { + let mut output_file = File::create(&output_path)?; - let mut writer = BufWriter::with_capacity(0x40000, output_file); - let cipher = Aes128Dec::new_from_slice(&key).map_err(|e| anyhow!(e))?; + output_file.set_len(file.len())?; - let pb = ProgressBar::new(output_size) - .with_style( - ProgressStyle::default_bar() - .template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")? - ); + let pb = ProgressBar::new(file.len()) + .with_style( + ProgressStyle::default_bar() + .template("{prefix} [{bar:20!.bright.yellow/dim.white}] {bytes:>8} [{elapsed}<{eta}, {bytes_per_sec}]")? + ); - pb.set_prefix(output_filename.clone()); - reader.seek(SeekFrom::Start(data_offset))?; + pb.set_prefix(output_filename.clone()); - for _ in 0..output_size / PAGE_SIZE { - let file_offset = reader.stream_position()? - data_offset; - let reference = Read::by_ref(&mut reader); + let mut reader = BufReader::with_capacity(0x40000, file); - calculate_page_iv(file_offset, &iv, &mut page_iv); - page.clear(); - reference.take(PAGE_SIZE).read_to_end(&mut page)?; + loop { + let buffer = reader.fill_buf()?; + let length = buffer.len(); - let page_cipher = Aes128CbcDec::inner_iv_slice_init(cipher.clone(), &page_iv) - .map_err(|e| anyhow!(e))?; - page_cipher - .decrypt_padded_mut::(&mut page) - .map_err(|e| anyhow!(e))?; + if length == 0 { + break; + } - writer.write_all(&page)?; - pb.inc(PAGE_SIZE); + output_file.write_all(&buffer)?; + reader.consume(length); + + pb.inc(length as u64); + } } - writer.flush()?; - pb.finish(); - if !cli.no_extract { match bootid.container_type { ContainerType::OS | ContainerType::APP => { - match extract_internal_vhd(&output_path, bootid.sequence_number) { - Ok(_) => { - println!("Deleting NTFS image: {}", output_path.display()); - std::fs::remove_file(output_path)?; - } + match extract_internal_vhd(&path, bootid.sequence_number) { + Ok(_) => {} Err(e) => { println!("WARNING: Failed to extract internal VHD: {e:#?}"); } @@ -376,10 +263,6 @@ fn main() -> Result<()> { } } } - - page.clear(); - page_iv.fill(0); - bootid_bytes.fill(0); } Ok(()) diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..d7f4a58 --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,291 @@ +use std::{ + io::{self, Read, Seek, SeekFrom}, + str::Utf8Error, +}; + +use aes::cipher::{ + block_padding::{NoPadding, UnpadError}, + BlockDecryptMut, InvalidLength, +}; +use cipher::KeyIvInit; + +use crate::{ + bootid::{BootId, ContainerType, BOOTID_IV, BOOTID_KEY}, + crypto::{ + calculate_file_iv, calculate_page_iv, get_game_keys, Aes128CbcDec, GameKeys, EXFAT_HEADER, + NTFS_HEADER, OPTION_IV, OPTION_KEY, + }, +}; + +const PAGE_SIZE: u64 = 4096; + +#[derive(Debug, thiserror::Error)] +pub enum DecryptError { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + + #[error(transparent)] + Unknown(#[from] anyhow::Error), + + #[error("Invalid IV length: {0:?}")] + AesInvalidLength(InvalidLength), + + #[error("Malformed padding: {0:?}")] + AesUnpadError(UnpadError), + + #[error("Unknown container type {0}")] + InvalidContainerType(u8), + + #[error("There were no decryption keys for this container.")] + NoMatchingKeys, +} + +/// Decryptor for an fscrypt container. +pub struct FscryptDecryptor { + /// The fscrypt file. + input: R, + + /// The fscrypt file's bootid. + pub bootid: BootId, + + key: [u8; 16], + iv: [u8; 16], + + encrypted_page: Vec, + plaintext_pos: u64, + page: Option>, +} + +impl FscryptDecryptor { + pub fn len(&self) -> u64 { + self.bootid.block_size * (self.bootid.block_count - self.bootid.header_block_count) + } + + pub fn filename(&self) -> Result { + let bootid = self.bootid; + let os_id = std::str::from_utf8(&bootid.os_id)?; + let game_id = std::str::from_utf8(&bootid.game_id)?; + + let filename = match bootid.container_type { + ContainerType::OS => format!( + "{os_id}_{:<04}.{:<02}.{:<02}_{}_{}.ntfs", + bootid.os_version.major, + bootid.os_version.minor, + bootid.os_version.release, + bootid.target_timestamp, + bootid.sequence_number + ), + ContainerType::APP => { + if bootid.sequence_number > 0 { + format!( + "{game_id}_{}.{:<02}.{:<02}_{}_{}_{}.{:<02}.{:<02}.ntfs", + unsafe { bootid.target_version.version.major }, + unsafe { bootid.target_version.version.minor }, + unsafe { bootid.target_version.version.release }, + bootid.target_timestamp, + bootid.sequence_number, + bootid.source_version.major, + bootid.source_version.minor, + bootid.source_version.release, + ) + } else { + format!( + "{game_id}_{}.{:<02}.{:<02}_{}_{}.ntfs", + unsafe { bootid.target_version.version.major }, + unsafe { bootid.target_version.version.minor }, + unsafe { bootid.target_version.version.release }, + bootid.target_timestamp, + bootid.sequence_number, + ) + } + } + _ => format!( + "{game_id}_{}_{}_{}.exfat", + unsafe { std::str::from_utf8(&bootid.target_version.option)? }, + bootid.target_timestamp, + bootid.sequence_number, + ), + }; + + Ok(filename) + } +} + +impl FscryptDecryptor { + pub fn new(mut input: R) -> Result { + let bootid_cipher = Aes128CbcDec::new_from_slices(&BOOTID_KEY, &BOOTID_IV) + .map_err(|e| DecryptError::AesInvalidLength(e))?; + let mut bootid_bytes = [0u8; std::mem::size_of::()]; + + input.read_exact(&mut bootid_bytes)?; + bootid_cipher + .decrypt_padded_mut::(&mut bootid_bytes) + .map_err(|e| DecryptError::AesUnpadError(e))?; + + let bootid: BootId = unsafe { std::mem::transmute(bootid_bytes) }; + + if bootid.container_type != ContainerType::OS + && bootid.container_type != ContainerType::APP + && bootid.container_type != ContainerType::OPTION + { + return Err(DecryptError::InvalidContainerType(bootid.container_type)); + } + + let data_offset = bootid.header_block_count * bootid.block_size; + let mut page = vec![0u8; PAGE_SIZE as usize]; + let keys = match bootid.container_type { + ContainerType::OS => get_game_keys(std::str::from_utf8(&bootid.os_id)?), + ContainerType::APP => get_game_keys(std::str::from_utf8(&bootid.game_id)?), + _ => Some(GameKeys { + key: OPTION_KEY, + iv: Some(OPTION_IV), + }), + }; + let Some(keys) = keys else { + return Err(DecryptError::NoMatchingKeys); + }; + let iv = if bootid.use_custom_iv { None } else { keys.iv }; + let iv = match iv { + Some(iv) => iv, + None => { + input.seek(SeekFrom::Start(data_offset))?; + + let reference = Read::by_ref(&mut input); + + reference.take(PAGE_SIZE).read_to_end(&mut page)?; + + if bootid.container_type == ContainerType::OPTION { + calculate_file_iv(keys.key, EXFAT_HEADER, &page)? + } else { + calculate_file_iv(keys.key, NTFS_HEADER, &page)? + } + } + }; + + input.seek(SeekFrom::Start(data_offset))?; + + Ok(Self { + input, + bootid, + key: keys.key, + iv, + encrypted_page: Vec::with_capacity(PAGE_SIZE as usize), + plaintext_pos: 0, + page: None, + }) + } + + fn decrypt_page(&mut self) -> io::Result<()> { + let page = &self.encrypted_page; + let file_offset = self.input.stream_position()? + - (self.bootid.header_block_count * self.bootid.block_size) + - PAGE_SIZE; + let mut page_iv = [0u8; 16]; + + calculate_page_iv(file_offset, &self.iv, &mut page_iv); + + let page_cipher = Aes128CbcDec::new_from_slices(&self.key, &page_iv).unwrap(); + + self.page = Some( + page_cipher + .decrypt_padded_vec_mut::(&page) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "failed to decrypt"))?, + ); + self.encrypted_page.clear(); + + Ok(()) + } + + fn read_from_page(&mut self, buf: &mut [u8]) -> usize { + let Some(ref page) = self.page else { + return 0; + }; + + let page_offset = (self.plaintext_pos % PAGE_SIZE) as usize; + let to_read = std::cmp::min(page.len() - page_offset, buf.len()); + + buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]); + + self.plaintext_pos += to_read as u64; + + if self.plaintext_pos % PAGE_SIZE == 0 { + self.page = None; + } + + to_read + } +} + +impl Read for FscryptDecryptor { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.page.is_none() { + if (self.encrypted_page.len() as u64) < PAGE_SIZE { + Read::by_ref(&mut self.input) + .take(PAGE_SIZE) + .read_to_end(&mut self.encrypted_page)?; + } + + self.decrypt_page()?; + } + + Ok(self.read_from_page(buf)) + } +} + +impl Seek for FscryptDecryptor { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + let start = self.bootid.header_block_count * self.bootid.block_size; + let target_pos = match pos { + SeekFrom::Start(offset) => offset, + SeekFrom::Current(offset) => { + let res = (self.plaintext_pos as i64) + offset; + + if res < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "cannot seek before the start", + )); + } + + res as u64 + } + SeekFrom::End(offset) => { + let length = (self.bootid.block_count - self.bootid.header_block_count) + * self.bootid.block_size; + let res = (length as i64) + offset; + + if res < 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "cannot seek before the start", + )); + } + + res as u64 + } + }; + let page_index = self.plaintext_pos / PAGE_SIZE; + let target_index = target_pos / PAGE_SIZE; + let target_offset = target_pos % PAGE_SIZE; + + if page_index == target_index { + self.plaintext_pos = target_pos; + return Ok(target_pos); + } + + self.page = None; + self.input + .seek(SeekFrom::Start(start + target_index * PAGE_SIZE))?; + self.plaintext_pos = target_index * PAGE_SIZE; + + if target_offset > 0 { + let mut to_drop = vec![0; target_offset as usize]; + self.read_exact(&mut to_drop)?; + } + + Ok(target_pos) + } +} From 023627426ece784b0bc6ab4acc60d4b54cc5ab6f Mon Sep 17 00:00:00 2001 From: beerpsi Date: Tue, 20 Jan 2026 18:02:42 +0700 Subject: [PATCH 09/11] Release 0.1.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3953943..42acc5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.4" +version = "0.1.5" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 6da95c9..182cc7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.4" +version = "0.1.5" edition = "2021" [dependencies] From 362fb91ff75f64c5732acb777a59d5fd87fef6a4 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 21 Jan 2026 09:05:10 +0700 Subject: [PATCH 10/11] feat: also extract exfat without saving intermediary exfat image --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/bootid.rs | 29 +++++++++++++++++++++++- src/main.rs | 61 ++++++++++++++++++++++++++++++++------------------- src/stream.rs | 37 +++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 25 deletions(-) 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) + } +} From 4c3ebd977151bc7c0a9b27e84b8b16c3fc25e4c6 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 21 Jan 2026 09:26:00 +0700 Subject: [PATCH 11/11] fix IV derivation --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/stream.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4430fe3..b3a06ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsdecrypt" -version = "0.1.6" +version = "0.1.7" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index f7f2168..afc83a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsdecrypt" -version = "0.1.6" +version = "0.1.7" edition = "2021" [dependencies] diff --git a/src/stream.rs b/src/stream.rs index 0a2661e..13e94c6 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -138,7 +138,7 @@ impl FscryptDecryptor { } let data_offset = bootid.header_block_count * bootid.block_size; - let mut page = vec![0u8; PAGE_SIZE as usize]; + let mut page: Vec = Vec::with_capacity(PAGE_SIZE as usize); let keys = match bootid.container_type { ContainerType::OS => get_game_keys(std::str::from_utf8(&bootid.os_id)?), ContainerType::APP => get_game_keys(std::str::from_utf8(&bootid.game_id)?),