diff --git a/Cargo.lock b/Cargo.lock index 4595fab..fcf832b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,6 +1576,7 @@ dependencies = [ "siphasher", "stylua", "tendril", + "toml 1.1.2+spec-1.1.0", "unicode-segmentation", "url", "utf16string", @@ -2119,6 +2120,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_yaml_ng" version = "0.10.0" @@ -2264,7 +2274,7 @@ dependencies = [ "similar", "thiserror", "threadpool", - "toml", + "toml 0.8.23", ] [[package]] @@ -2386,11 +2396,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -2400,6 +2425,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2408,10 +2442,19 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.14.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", ] [[package]] @@ -2420,6 +2463,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "typenum" version = "1.20.0" @@ -2718,6 +2767,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 0dbe9ce..2c9354f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,9 +143,12 @@ utils-serde-base64bytes = ["base64"] utils-simple-pack = ["zstd"] utils-str = [] utils-xored-stream = [] +private = ["serde", "toml", "chacha20?/xchacha"] [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_System_Diagnostics_Debug"] } [build-dependencies] parse-size = "1.1" +serde = { version = "1", features = ["derive"], optional = true } +toml = { version = "1.1", optional = true } diff --git a/build.rs b/build.rs index ff77807..aac9ebd 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,164 @@ +#[cfg(feature = "private")] +mod private { + use serde::Deserialize; + use std::collections::HashMap; + use std::path::{Path, PathBuf}; + use std::process::Command; + #[derive(Debug, Deserialize)] + #[serde(untagged)] + enum DependencyType { + Git { git: String, commit: String }, + } + fn default_features() -> bool { + true + } + #[derive(Debug, Deserialize)] + struct Dependency { + #[serde(flatten)] + dep: DependencyType, + #[serde(default)] + package: Option, + #[serde(default)] + features: Vec, + #[serde(rename = "default-features", default = "default_features")] + default_features: bool, + } + #[derive(Debug, Deserialize)] + struct Manifest { + dependencies: HashMap, + features: HashMap>, + } + fn is_feature_enabled(name: &str) -> bool { + let key = format!("CARGO_FEATURE_{}", name.to_uppercase().replace('-', "_")); + std::env::var(key).is_ok() + } + + fn clone_and_checkout(dep_dir: &Path, git: &str, commit: &str) { + if dep_dir.exists() { + if dep_dir.join(".git").exists() { + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(dep_dir) + .output() + .expect("failed to run git rev-parse"); + if output.status.success() { + let current = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if current == commit { + return; + } + } + } + std::fs::remove_dir_all(dep_dir).expect("failed to remove old dependency directory"); + } + + let parent = dep_dir.parent().unwrap(); + std::fs::create_dir_all(parent).expect("failed to create parent directory"); + + let status = Command::new("git") + .args(["clone", git]) + .arg(dep_dir.to_str().unwrap()) + .status() + .expect("failed to run git clone"); + assert!(status.success(), "git clone failed"); + + let status = Command::new("git") + .args(["checkout", commit]) + .current_dir(dep_dir) + .status() + .expect("failed to run git checkout"); + assert!(status.success(), "git checkout failed"); + } + + fn dep_git_url<'a>(dep: &'a Dependency) -> &'a str { + match &dep.dep { + DependencyType::Git { git, .. } => git, + } + } + + fn dep_commit<'a>(dep: &'a Dependency) -> &'a str { + match &dep.dep { + DependencyType::Git { commit, .. } => commit, + } + } + + pub fn compile_private() { + println!("cargo:rerun-if-changed=private.toml"); + + let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let manifest_path = manifest_dir.join("private.toml"); + let content = std::fs::read_to_string(&manifest_path).expect("failed to read private.toml"); + let manifest: Manifest = toml::from_str(&content).expect("failed to parse private.toml"); + + // Collect dependencies whose feature is enabled + let mut deps_to_build: Vec = Vec::new(); + for (feat, dep_names) in &manifest.features { + if is_feature_enabled(feat) { + for dep_name in dep_names { + if !deps_to_build.contains(dep_name) { + deps_to_build.push(dep_name.clone()); + } + } + } + } + + if deps_to_build.is_empty() { + return; + } + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let target = std::env::var("TARGET").unwrap(); + let is_debug = match std::env::var("PROFILE").as_deref() { + Ok("debug") => true, + Ok("release") => false, + _ => std::env::var("DEBUG").unwrap_or_default() == "true", + }; + + for dep_name in &deps_to_build { + let dep = manifest + .dependencies + .get(dep_name) + .unwrap_or_else(|| panic!("dependency '{}' not found in private.toml", dep_name)); + + let package_name = dep.package.as_ref().unwrap_or(dep_name); + let dep_dir = out_dir.join(package_name); + + clone_and_checkout(&dep_dir, dep_git_url(dep), dep_commit(dep)); + + let target_dir = dep_dir.join("target"); + let mut cmd = Command::new("cargo"); + cmd.args(["build", "-p", package_name]) + .arg("--target-dir") + .arg(&target_dir) + .arg("--target") + .arg(&target); + + if !dep.features.is_empty() { + cmd.arg("--features"); + cmd.arg(dep.features.join(",")); + } + if !dep.default_features { + cmd.arg("--no-default-features"); + } + if !is_debug { + cmd.arg("--release"); + } + + let status = cmd + .current_dir(&dep_dir) + .status() + .expect("failed to run cargo build"); + assert!(status.success(), "cargo build failed for '{}'", dep_name); + + let profile = if is_debug { "debug" } else { "release" }; + let build_dir = target_dir.join(&target).join(profile); + println!("cargo:rustc-link-search={}", build_dir.display()); + + let lib_name = package_name.replace('-', "_"); + println!("cargo:rustc-link-lib=static={}", lib_name); + } + } +} + fn main() { #[cfg(windows)] let default_stack_size = "4194304"; // 4 MiB @@ -10,4 +171,6 @@ fn main() { println!("cargo:rustc-link-arg=/STACK:{}", stack_size); #[cfg(target_env = "gnu")] println!("cargo:rustc-link-arg=-Wl,-z,stack-size={}", stack_size); + #[cfg(feature = "private")] + private::compile_private(); } diff --git a/check_features.py b/check_features.py index 98371d3..5bdd9ca 100644 --- a/check_features.py +++ b/check_features.py @@ -3,7 +3,7 @@ import subprocess import sys def filter_name(name): - if name == 'zig': + if name == 'zig' or name == 'private': return False if name.startswith("utils-"): return False diff --git a/private.toml b/private.toml new file mode 100644 index 0000000..1e1ce6a --- /dev/null +++ b/private.toml @@ -0,0 +1,6 @@ +# Define private source (now closed source) package +[dependencies] +cxdec3_params = { git = "https://github.com/nextgal/cxdec3_params", commit = "77221aafc7d26bca3449fa009a0d9710f2783b57" } + +[features] +kirikiri-arc = ["cxdec3_params"] diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index 019da68..b67d87e 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -91,15 +91,6 @@ impl CxEncryption { filename: &str, program_builder: Box, ) -> Result { - if schema.prolog_order.len() != 3 { - return Err(anyhow::anyhow!("Prolog order must have 3 elements")); - } - if schema.odd_branch_order.len() != 6 { - return Err(anyhow::anyhow!("Odd branch order must have 6 elements")); - } - if schema.even_branch_order.len() != 8 { - return Err(anyhow::anyhow!("Even branch order must have 8 elements")); - } let control_block = if let Some(tpm_path) = &schema.tpm_file_name { Self::read_tpm(tpm_path, filename)? } else if let Some(control_block_name) = &schema.control_block_name { @@ -117,6 +108,24 @@ impl CxEncryption { "TPM file name or control block is required in schema" )); }; + Self::new_inner2(base, schema, program_builder, control_block) + } + + fn new_inner2( + base: BaseSchema, + schema: &CxSchema, + program_builder: Box, + control_block: Vec, + ) -> Result { + if schema.prolog_order.len() != 3 { + return Err(anyhow::anyhow!("Prolog order must have 3 elements")); + } + if schema.odd_branch_order.len() != 6 { + return Err(anyhow::anyhow!("Odd branch order must have 6 elements")); + } + if schema.even_branch_order.len() != 8 { + return Err(anyhow::anyhow!("Even branch order must have 8 elements")); + } let control_block = Arc::new(control_block); let programs = Vec::with_capacity(0x80); let mut obj = Self { @@ -2003,10 +2012,53 @@ impl PathHash { } } -#[derive(Clone, Deserialize)] +#[derive(Clone, Debug, Deserialize)] +#[allow(unused)] +#[serde(rename_all = "camelCase")] +struct KeyData { + boot_strap: String, + warning: String, + #[serde(with = "hex_vec")] + params: Vec, + archive_unique_key: String, + #[serde(rename = "seed", with = "hex_vec_optional", default)] + upper_key: Option>, +} + +#[allow(unused)] +mod hex_vec { + use serde::{Deserialize, Deserializer}; + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + hex::decode(s).map_err(serde::de::Error::custom) + } +} + +#[allow(unused)] +mod hex_vec_optional { + use serde::{Deserialize, Deserializer}; + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + { + let s: Option = Option::deserialize(deserializer)?; + match s { + Some(hex_str) => hex::decode(hex_str) + .map(Some) + .map_err(serde::de::Error::custom), + None => Ok(None), + } + } +} + +#[derive(Clone, Debug, Deserialize)] #[allow(unused)] struct KeyPackage { description: String, + key: KeyData, sku: String, } @@ -2039,8 +2091,8 @@ pub struct HxCrypt { key1: IndexKey, key2: IndexKeys, filter_key: AtomicU64, - file_mapping: HashMap, - path_mapping: HashMap, + file_mapping: Arc>, + path_mapping: Arc>, info_map: Mutex>, file_hash: FileHashOption, path_hash: PathHashOption, @@ -2186,8 +2238,39 @@ impl HxCrypt { key1: index_key1.clone(), key2: index_key2.clone(), filter_key: AtomicU64::new(filter_key), - file_mapping: file_map, - path_mapping: path_map, + file_mapping: Arc::new(file_map), + path_mapping: Arc::new(path_map), + info_map: Mutex::new(HashMap::new()), + file_hash: config.xp3_cxdec_file_hash, + path_hash: config.xp3_cxdec_path_hash, + }) + } + + #[allow(unused)] + fn new_inner( + base: BaseSchema, + cx: &CxSchema, + index_key1: IndexKey, + index_key2: IndexKeys, + filter_key: u64, + random_type: i32, + config: &ExtraConfig, + file_mapping: Arc>, + path_mapping: Arc>, + control_block: Vec, + ) -> Result { + Ok(Self { + base: CxEncryption::new_inner2( + base, + cx, + Box::new(HxProgramBuilder::new(random_type)), + control_block, + )?, + key1: index_key1, + key2: index_key2, + filter_key: AtomicU64::new(filter_key), + file_mapping, + path_mapping, info_map: Mutex::new(HashMap::new()), file_hash: config.xp3_cxdec_file_hash, path_hash: config.xp3_cxdec_path_hash, @@ -3071,6 +3154,343 @@ fn calculate_path_hash(pathname: &str, path_hash_salt: &str) -> PathHash { PathHash(u64::from_be_bytes(data)) } +#[cfg(feature = "private")] +mod private { + use super::*; + use chacha20::cipher::array::Array; + use chacha20::hchacha; + use msg_tool_macro::MyDebug; + include!(concat!(env!("OUT_DIR"), "/cxdec3_params/capi.rs")); + + const RANDOM_TYPE_FLAG: u8 = 0x80; + + fn hxkeys_new( + bootstrap: &str, + warning: &str, + param: &[u8], + uniq: &str, + upper_key: Option<[u8; 8]>, + ) -> Result<(HxKeys, CxParams)> { + let bootstrap = encode_string(Encoding::Utf16LE, bootstrap, true)?; + let warning = encode_string(Encoding::Utf16LE, warning, true)?; + let uniq = encode_string(Encoding::Utf16LE, uniq, true)?; + let (upper_key, upper_key_len) = match upper_key.as_ref() { + Some(key) => (key.as_ptr(), key.len()), + None => (std::ptr::null(), 0), + }; + let key = unsafe { + cxdec3_hxkeys_new( + bootstrap.as_ptr(), + bootstrap.len(), + warning.as_ptr(), + warning.len(), + param.as_ptr(), + param.len(), + uniq.as_ptr(), + uniq.len(), + upper_key, + upper_key_len, + ) + }; + if key.is_null() { + anyhow::bail!("Failed to create hx keys."); + } + let nkey = unsafe { ((*key).keys.clone(), (*key).params.clone()) }; + unsafe { cxdec3_hxkeys_free(key) }; + Ok(nkey) + } + + fn map_key_to_garbro(cx: &mut CxParams) { + const S3: [u8; 3] = [0, 1, 2]; + const S6: [u8; 6] = [2, 5, 3, 4, 1, 0]; + const S8: [u8; 8] = [0, 2, 3, 1, 5, 6, 7, 4]; + let mut o3 = [0; 3]; + let mut o6 = [0; 6]; + let mut o8 = [0; 8]; + for i in 0..3 { + o3[cx.prologue_perm[i] as usize] = S3[i]; + } + for i in 0..6 { + o6[cx.odd_branch_perm[i] as usize] = S6[i]; + } + for i in 0..8 { + o8[cx.even_branch_perm[i] as usize] = S8[i]; + } + cx.prologue_perm.copy_from_slice(&o3); + cx.odd_branch_perm.copy_from_slice(&o6); + cx.even_branch_perm.copy_from_slice(&o8); + } + + fn gen_index_keys(key: &HxKeys) -> Result<(IndexKey, IndexKey)> { + let subkeya = + hchacha::(&key.key.into(), &Array::try_from(&key.nonce_a[0..16])?); + let subkeyb = + hchacha::(&key.key.into(), &Array::try_from(&key.nonce_b[0..16])?); + Ok(( + IndexKey { + key: subkeya.into(), + nonce: (&key.nonce_a[16..]).try_into()?, + filter_key: None, + }, + IndexKey { + key: subkeyb.into(), + nonce: (&key.nonce_b[16..]).try_into()?, + filter_key: None, + }, + )) + } + + #[derive(MyDebug)] + pub struct Hxv4Crypt { + base: Mutex>, + file_mapping: Arc>, + path_mapping: Arc>, + key_packages: Vec, + project: String, + #[skip_fmt] + config: ExtraConfig, + } + + impl Hxv4Crypt { + pub fn new(filename: &str, config: &ExtraConfig) -> Result { + let p = std::path::Path::new(filename); + let b = p + .file_name() + .ok_or_else(|| anyhow::anyhow!("Failed to get file name from path."))?; + let s: &str = &b.to_string_lossy(); + let pdir = p.parent().map(|s| s.to_owned()).unwrap_or_default(); + let filep = get_ignorecase_path(&pdir.join("filelist.json"))?; + let data = std::fs::read(&filep)?; + let data = decode_to_string(Encoding::Utf8, &data, true)?; + let manifest = serde_json::from_str::(&data)?; + let mut path_map: HashMap<_, _> = manifest + .path_mapping + .iter() + .filter_map(|(k, v)| match v { + Some(v) => Some((k.clone(), v.clone())), + None => None, + }) + .collect(); + let file_map: HashMap<_, _> = if let Some(s) = manifest.file_list.get(s) { + s.iter() + .map(|s| s.1) + .flatten() + .filter_map(|(k, v)| match v { + Some(v) => Some((k.clone(), v.clone())), + None => None, + }) + .collect() + } else { + HashMap::new() + }; + eprintln!( + "Read {} file entries, {} directory entries and {} key packages from filelist {}.", + file_map.len(), + path_map.len(), + manifest.key_packages.len(), + filep.display() + ); + let default_path_hash = calculate_path_hash("", "xp3hnp"); + if !path_map.contains_key(&default_path_hash) { + path_map.insert(default_path_hash, String::new()); + } + Ok(Self { + base: Mutex::new(None), + file_mapping: Arc::new(file_map), + path_mapping: Arc::new(path_map), + key_packages: manifest.key_packages, + project: manifest.project_name, + config: config.clone(), + }) + } + + fn load_package(&self, pack: &KeyPackage, archive: &mut Xp3Archive) -> Result { + eprintln!("try key {} for {}", pack.sku, self.project); + let upper_key = match &pack.key.upper_key { + Some(key) => Some(key.as_slice().try_into()?), + None => None, + }; + let (key, mut params) = hxkeys_new( + &pack.key.boot_strap, + &pack.key.warning, + &pack.key.params, + &pack.key.archive_unique_key, + upper_key, + )?; + map_key_to_garbro(&mut params); + let (key1, key2) = gen_index_keys(&key)?; + let base = BaseSchema { + hash_after_crypt: false, + startup_tjs_not_encrypted: false, + obfuscated_index: false, + }; + let cx = CxSchema { + mask: params.mask as u32, + offset: params.offset as u32, + prolog_order: Base64Bytes { + bytes: params.prologue_perm.to_vec(), + }, + odd_branch_order: Base64Bytes { + bytes: params.odd_branch_perm.to_vec(), + }, + even_branch_order: Base64Bytes { + bytes: params.even_branch_perm.to_vec(), + }, + control_block_name: None, + tpm_file_name: None, + }; + let key2 = IndexKeys(vec![key2]); + let filter_key = u64::from_le_bytes(key.filter); + let random_type = if params.flags & RANDOM_TYPE_FLAG != 0 { + 1 + } else { + 0 + }; + let mut control_block = Vec::with_capacity(0x400); + let mut reader = MemReaderRef::new(&key.ctrlblk); + for _ in 0..0x400 { + control_block.push(!reader.read_u32()?); + } + let crypt = HxCrypt::new_inner( + base, + &cx, + key1, + key2, + filter_key, + random_type, + &self.config, + self.file_mapping.clone(), + self.path_mapping.clone(), + control_block, + )?; + crypt.init(archive)?; + Ok(crypt) + } + } + + impl Crypt for Hxv4Crypt { + fn startup_tjs_not_encrypted(&self) -> bool { + false + } + fn obfuscated_index(&self) -> bool { + false + } + fn hash_after_crypt(&self) -> bool { + false + } + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + if self.key_packages.len() == 0 { + eprintln!("WARNING: No key package specifed. Decrypt not works."); + crate::COUNTER.inc_warning(); + return Ok(()); + } + for package in self.key_packages.iter() { + if let Ok(crypt) = self.load_package(package, archive) { + let mut c = self.base.lock_blocking(); + c.replace(crypt); + return Ok(()); + } + } + Err(anyhow::anyhow!("Failed to decrypt index.")) + } + fn decrypt_supported(&self) -> bool { + true + } + fn decrypt_seek_supported(&self) -> bool { + true + } + fn decrypt<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + if self.key_packages.len() == 0 { + return Ok(Box::new(CopyStream::new(stream))); + } + let c = self.base.lock_blocking(); + let crypt = c + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Archive not inited."))?; + crypt.decrypt(entry, cur_seg, stream) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + if self.key_packages.len() == 0 { + return Ok(stream); + } + let c = self.base.lock_blocking(); + let crypt = c + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Archive not inited."))?; + crypt.decrypt_with_seek(entry, cur_seg, stream) + } + } + + #[test] + fn test_gen_keys() { + let keys = hxkeys_new("BOOTSTRAPbootstrap0123456789", "WARNINGwarning0123456789", b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a", "ArchiveUniqueKey0123456789", Some(*b"\x00\x11\x22\x33\x44\x55\x66\x77")).unwrap(); + let expected_key: [u8; 32] = + hex::decode("4fb07f17eb1d7d0f14fba645e067d5d90a973494f4161da962ee49ccfc9ad237") + .unwrap() + .try_into() + .unwrap(); + let expected_nonce_a: [u8; 24] = + hex::decode("c84f47adef9093396d421105bd8893c925b3853aef22d346") + .unwrap() + .try_into() + .unwrap(); + let expected_nonce_b: [u8; 24] = + hex::decode("98d9fc0c47eb2684aad17ca33ee8cb1aed30812ee8990500") + .unwrap() + .try_into() + .unwrap(); + assert_eq!(keys.0.key, expected_key); + assert_eq!(&keys.0.nonce_a[..24], &expected_nonce_a); + assert_eq!(&keys.0.nonce_b[..24], &expected_nonce_b); + } + + #[test] + fn test_real_keys() { + let (key, mut params) = hxkeys_new("LimeLightRemonadeJam (C)YUZUSOFT/JUNOS INC. All Rights Reserved.", "Warning! Extracting this game data may infringe on author's rights.", b"\x02\x00\x06\x05\x01\x04\x03\x07\x01\x05\x03\x02\x00\x04\x01\x00\x02\x01\xe2\x02\x83\x02", "{EnaAnjTukRirMikNay}", Some(*b"\xbf\x22\x36\x8a\x48\x21\x02\x06")).unwrap(); + map_key_to_garbro(&mut params); + assert_eq!(params.mask, 738); + assert_eq!(params.offset, 643); + use base64::Engine; + let b64 = base64::engine::general_purpose::STANDARD; + assert_eq!(b64.encode(params.prologue_perm), "AQAC"); + assert_eq!(b64.encode(params.odd_branch_perm), "AQIEAwAF"); + assert_eq!(b64.encode(params.even_branch_perm), "AgUABwYBAwQ="); + assert_eq!(u64::from_le_bytes(key.filter), 13089994567570788352); + let (ind1, ind2) = gen_index_keys(&key).unwrap(); + assert_eq!( + b64.encode(&ind1.key), + "fMktWafCUSPGVDvR/8LUx9f+yh3Y+PIq90XmzJ6xZhI=" + ); + assert_eq!(b64.encode(&ind1.nonce), "aDnPYFowPzhVdOsfJweaYA=="); + assert_eq!( + b64.encode(&ind2.key), + "kHMdDweFjeDFVTwQA10XQAPUAOfXzKp2ukygeLzGzHg=" + ); + assert_eq!(b64.encode(&ind2.nonce), "CSBjzSXQNTioPhDp710WCQ=="); + assert!((params.flags & RANDOM_TYPE_FLAG) == 0); + let expected_cb = CX_CB_TABLE.get("limelight.bin").unwrap(); + let mut cb = Vec::with_capacity(0x400); + let mut reader = MemReaderRef::new(&key.ctrlblk); + for _ in 0..0x400 { + cb.push(!reader.read_u32().unwrap()); + } + assert_eq!(&cb, expected_cb); + } +} + +#[cfg(feature = "private")] +pub use private::Hxv4Crypt; + #[test] fn test_filehash_deserialize() { assert_eq!( diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index db40bdf..4c9723d 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -19,6 +19,9 @@ use std::collections::{BTreeMap, HashMap}; use std::io::{Read, Seek, SeekFrom}; use std::sync::Arc; +#[cfg(feature = "private")] +pub use cx::Hxv4Crypt; + type CIS = CaseInsensitiveStr; pub fn default_init_crypt(archive: &mut Xp3Archive) -> Result<()> { diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index d002dd0..3b3eda4 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -13,15 +13,17 @@ impl<'a> Xp3Archive<'a> { config: &ExtraConfig, filename: &str, ) -> Result { - let crypt: Box = if let Some(game_title) = &config.xp3_game_title { - query_crypt_schema(game_title) - .ok_or_else(|| { - anyhow::anyhow!("Unsupported game title for XP3 archive: {}", game_title) - })? - .create_crypt(filename, config)? - } else { - Box::new(NoCrypt::new()) - }; + #[allow(unused_mut)] + let mut crypt: Box = + if let Some(game_title) = &config.xp3_game_title { + query_crypt_schema(game_title) + .ok_or_else(|| { + anyhow::anyhow!("Unsupported game title for XP3 archive: {}", game_title) + })? + .create_crypt(filename, config)? + } else { + Box::new(NoCrypt::new()) + }; let mut stream = Box::new(stream); let base_offset = 0; if base_offset != 0 { @@ -160,10 +162,20 @@ impl<'a> Xp3Archive<'a> { entries.push(entry); } else { let data = index_stream.read_exact_vec(size as usize)?; - extras.push(ExtraProp { - tag: sig.into(), - data, - }); + let tag = sig.into(); + #[cfg(feature = "private")] + if config.xp3_game_title.is_none() && tag == "Hxv4" { + match Hxv4Crypt::new(filename, config) { + Ok(c) => { + crypt = Box::new(c); + } + Err(e) => { + eprintln!("WARNING: Failed to load filelist.json: {}", e); + crate::COUNTER.inc_warning(); + } + } + } + extras.push(ExtraProp { tag, data }); } } }