diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml index 582c172..eb13725 100644 --- a/.github/FUNDING.yaml +++ b/.github/FUNDING.yaml @@ -1,4 +1,5 @@ github: nicks96432 +ko_fi: nicks96432 patreon: nicks96432 custom: - "https://www.paypal.me/nicks96432" diff --git a/Cargo.lock b/Cargo.lock index 4d0af88..10dc922 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,14 +234,14 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -713,7 +713,9 @@ name = "mltd-asset-extract" version = "0.1.0" dependencies = [ "acb", + "aes", "byteorder", + "cbc", "clap", "ctor", "env_logger", @@ -1250,9 +1252,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -1432,15 +1434,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -1459,21 +1452,6 @@ dependencies = [ "windows-targets 0.52.0", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -1504,12 +1482,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1522,12 +1494,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1540,12 +1506,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1558,12 +1518,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1576,12 +1530,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1594,12 +1542,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1612,12 +1554,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1632,9 +1568,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.33" +version = "0.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" dependencies = [ "memchr", ] diff --git a/extract/Cargo.toml b/extract/Cargo.toml index bd113d2..e7f872f 100644 --- a/extract/Cargo.toml +++ b/extract/Cargo.toml @@ -11,6 +11,10 @@ version = "0.1.0" # For decoding ACB audio acb = { path = "../acb" } +# For text asset decryption +aes = "0.8.3" +cbc = "0.1.2" + # For reading numbers from binary data byteorder = "1.5.0" diff --git a/extract/src/class/asset_bundle.rs b/extract/src/class/asset_bundle.rs index df7bf15..95179b5 100644 --- a/extract/src/class/asset_bundle.rs +++ b/extract/src/class/asset_bundle.rs @@ -13,7 +13,7 @@ use rabex::read_ext::ReadUrexExt; use crate::utils::ReadAlignedExt; use crate::version::*; -pub fn _construct_p_ptr( +pub(super) fn _construct_p_ptr( reader: &mut R, serialized_file: &SerializedFile, ) -> Result> @@ -94,7 +94,7 @@ where Ok(asset_info) } -pub fn _construct_asset_bundle( +pub(super) fn _construct_asset_bundle( data: &[u8], serialized_file: &SerializedFile, ) -> Result> diff --git a/extract/src/class/sprite.rs b/extract/src/class/sprite.rs index 8893af4..b80c22a 100644 --- a/extract/src/class/sprite.rs +++ b/extract/src/class/sprite.rs @@ -18,7 +18,7 @@ use crate::version::*; use super::asset_bundle::_construct_p_ptr; use super::mesh::{construct_sub_mesh, construct_vertex_data}; -pub fn _construct_sprite( +pub(super) fn _construct_sprite( data: &[u8], serialized_file: &SerializedFile, ) -> Result> diff --git a/extract/src/class/text_asset.rs b/extract/src/class/text_asset.rs index 46eb154..a74c5c7 100644 --- a/extract/src/class/text_asset.rs +++ b/extract/src/class/text_asset.rs @@ -6,19 +6,21 @@ use std::path::Path; use std::slice::from_raw_parts; use std::str::FromStr; -use byteorder::BigEndian; -use byteorder::ByteOrder; -use byteorder::LittleEndian; +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::inout::InOutBufReserved; +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use aes::Aes192; +use byteorder::{BigEndian, ByteOrder, LittleEndian}; +use cbc::{Decryptor, Encryptor}; use rabex::files::SerializedFile; use rabex::objects::classes::TextAsset; use rabex::read_ext::ReadUrexExt; -use crate::utils::ffmpeg; -use crate::utils::ReadAlignedExt; +use crate::utils::{ffmpeg, ReadAlignedExt}; use crate::version::*; use crate::ExtractorArgs; -pub fn _construct_text_asset( +pub(super) fn _construct_text_asset( data: &[u8], serialized_file: &SerializedFile, ) -> Result> @@ -85,3 +87,56 @@ where output_path, ) } + +pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY: &[u8; 8] = b"Millicon"; +pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___"; +pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1000; + +#[rustfmt::skip] +/// The AES-192-CBC key used to decrypt the text asset. +/// +/// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and +/// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where +/// the first 24 bytes of the derived key are used as the actual key. +pub const MLTD_TEXT_DECRYPT_KEY: &[u8; 24] = &[ + 0xad, 0x3f, 0x0f, 0x89, 0xee, 0x51, 0xc5, 0x37, + 0x73, 0x1f, 0x17, 0x96, 0xf7, 0x5c, 0x71, 0x84, + 0x01, 0x61, 0x75, 0x6d, 0xa0, 0xd4, 0x86, 0xc9, +]; + +#[rustfmt::skip] +/// The AES-192-CBC initialization vector used to decrypt the text asset. +/// +/// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and +/// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where +/// the last 16 bytes of the derived key are used as the actual IV. +pub const MLTD_TEXT_DECRYPT_IV: &[u8; 16] = &[ + 0x4e, 0x40, 0xb3, 0x8a, 0xeb, 0xf1, 0xa8, 0x53, + 0x12, 0x2c, 0x5f, 0xad, 0xcc, 0xa3, 0x68, 0x5d, +]; + +pub type MltdTextEncryptor = Encryptor; +pub type MltdTextDecryptor = Decryptor; + +pub fn encrypt_text(text: &[u8]) -> Result, Box> { + let encryptor = + MltdTextEncryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); + let mut buf = text.to_owned(); + + let buf = InOutBufReserved::from_mut_slice(&mut buf, text.len()).map_err(|e| e.to_string())?; + let buf = encryptor.encrypt_padded_inout_mut::(buf).map_err(|e| e.to_string())?; + + Ok(buf.to_owned()) +} + +pub fn decrypt_text(cipher: &[u8]) -> Result, Box> { + let decryptor = + MltdTextDecryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); + let mut buf = cipher.to_owned(); + + let buf = decryptor + .decrypt_padded_inout_mut::(buf.as_mut_slice().into()) + .map_err(|e| e.to_string())?; + + Ok(buf.to_owned()) +} diff --git a/extract/src/class/texture_2d.rs b/extract/src/class/texture_2d.rs index 12bde38..4df2fd8 100644 --- a/extract/src/class/texture_2d.rs +++ b/extract/src/class/texture_2d.rs @@ -22,7 +22,7 @@ use crate::environment::Environment; use crate::utils::{ffmpeg, solve_puzzle, ReadAlignedExt}; use crate::{version::*, ExtractorArgs}; -fn _construct_texture_2d( +pub(super) fn _construct_texture_2d( data: &[u8], serialized_file: &SerializedFile, ) -> Result> diff --git a/extract/src/environment.rs b/extract/src/environment.rs index e4f51e6..d27ad6e 100644 --- a/extract/src/environment.rs +++ b/extract/src/environment.rs @@ -5,7 +5,7 @@ use std::io::{Read, Seek, SeekFrom}; use byteorder::{BigEndian, ReadBytesExt}; use rabex::read_ext::ReadUrexExt; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Environment { /// resource data that loaded from bundles resources: HashMap>, @@ -16,7 +16,7 @@ pub struct Environment { impl Environment { pub fn new() -> Self { - Self { resources: HashMap::new(), objects: HashMap::new() } + Self::default() } pub fn register_cab(&mut self, path: &str, buf: Vec) { diff --git a/extract/src/lib.rs b/extract/src/lib.rs index df96a40..427f53a 100644 --- a/extract/src/lib.rs +++ b/extract/src/lib.rs @@ -1,13 +1,12 @@ -mod class; -mod environment; -mod utils; -mod version; +pub mod class; +pub mod environment; +pub mod utils; +pub mod version; use std::error::Error; use std::fs::{read_dir, File}; use std::io::{Cursor, Seek, SeekFrom}; use std::path::{Path, PathBuf}; -use std::process::exit; use std::sync::atomic::{AtomicU64, Ordering}; #[cfg(not(feature = "debug"))] @@ -23,7 +22,7 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::ThreadPoolBuilder; use crate::class::asset_bundle::construct_asset_bundle; -use crate::class::text_asset::{construct_text_asset, extract_acb}; +use crate::class::text_asset::{construct_text_asset, decrypt_text, extract_acb}; use crate::class::texture_2d::extract_texture_2d; use crate::environment::{check_file_type, FileType}; @@ -31,8 +30,8 @@ use crate::environment::{check_file_type, FileType}; #[command(author, version, about, arg_required_else_help(true))] pub struct ExtractorArgs { /// The input directory or file - #[arg(value_name = "PATH")] - input: PathBuf, + #[arg(value_name = "PATH", num_args = 1..)] + input_paths: Vec, /// The output directory #[arg(short, long, value_name = "DIR", display_order = 1)] @@ -71,17 +70,36 @@ pub fn extract_media(args: &ExtractorArgs) -> Result<(), Box> { #[cfg(not(feature = "debug"))] create_dir_all(&args.output)?; - let input_realpath = args.input.canonicalize()?; + let mut entries = Vec::new(); + + for p in &args.input_paths { + let input_realpath = p.canonicalize()?; + + if input_realpath.is_file() { + entries.push(input_realpath); + } else if !input_realpath.is_dir() { + log::warn!("Input path is not a file or directory"); + } else { + let input_paths: Vec<_> = read_dir(input_realpath)?.collect(); + let mut input_paths = input_paths + .into_iter() + .filter(|r| { + if let Err(e) = r { + log::warn!("failed to read directory entry: {}", e); + } + r.is_ok() + }) + .map(|e| e.unwrap().path()) + .collect(); - if input_realpath.is_file() { - return extract_file(&input_realpath, args); + entries.append(&mut input_paths); + } } - if !input_realpath.is_dir() { - log::error!("Input path is not a file or directory"); - exit(1); - } + extract_files(&entries, args) +} +fn extract_files(input_paths: &[PathBuf], args: &ExtractorArgs) -> Result<(), Box> { log::debug!("setting progress bar"); let template = "{msg:60} {eta:4} [{wide_bar:.cyan/blue}] {percent:3}%"; @@ -95,9 +113,7 @@ pub fn extract_media(args: &ExtractorArgs) -> Result<(), Box> { } .progress_chars("##-"); - let entries: Vec<_> = read_dir(&args.input)?.collect(); - let progress_bar = ProgressBar::new(entries.len() as u64).with_style(progress_bar_style); - + let progress_bar = ProgressBar::new(input_paths.len() as u64).with_style(progress_bar_style); let finished_count = AtomicU64::new(0); log::debug!("setting thread pool"); @@ -105,22 +121,14 @@ pub fn extract_media(args: &ExtractorArgs) -> Result<(), Box> { let thread_pool_builder = ThreadPoolBuilder::new().num_threads(args.parallel as usize); thread_pool_builder.build_global()?; - entries.par_iter().for_each(|entry| { - let entry = match entry { - Ok(e) => e, - Err(e) => { - progress_bar.suspend(|| log::warn!("failed to read directory entry: {}", e)); - return; - } - }; - - if let Err(e) = extract_file(&entry.path(), args) { + input_paths.par_iter().for_each(|entry| { + if let Err(e) = extract_file(entry, args) { log::warn!("failed to extract file: {}", e); }; let cur_finished_count = finished_count.fetch_add(1, Ordering::AcqRel); progress_bar.inc(1); - progress_bar.set_message(format!("{}/{}", cur_finished_count, entries.len())); + progress_bar.set_message(format!("{}/{}", cur_finished_count, input_paths.len())); }); Ok(()) @@ -218,11 +226,11 @@ fn extract_object( match text_asset.m_Name.contains("acb") { true => extract_acb(data, &output_dir, args, serialized_file)?, false => { + let output_path = output_dir.join(text_asset.m_Name).with_extension("txt"); + log::info!("writing text to {}", output_path.display()); + #[cfg(not(feature = "debug"))] - write( - output_dir.join(text_asset.m_Name).with_extension(&args.audio_ext), - text_asset.m_Script.as_bytes(), - )?; + write(output_path, decrypt_text(text_asset.m_Script.as_bytes())?)?; } } } diff --git a/extract/tests/test_text.unity3d b/extract/tests/test_text.unity3d new file mode 100644 index 0000000..a98121d Binary files /dev/null and b/extract/tests/test_text.unity3d differ