diff --git a/Cargo.lock b/Cargo.lock index 5ad4c44..691381b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,7 +28,7 @@ checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ "cipher", "cpubits", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -111,6 +111,18 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -140,6 +152,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -335,8 +353,8 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cipher", - "cpufeatures", - "rand_core", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -509,6 +527,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -1018,7 +1045,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -1420,6 +1447,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1616,6 +1653,7 @@ dependencies = [ "adler", "aes", "anyhow", + "argon2", "base64", "blake2", "block_compression", @@ -1664,6 +1702,7 @@ dependencies = [ "serde_yaml_ng", "sha1", "sha2", + "sha3", "siphasher", "stylua", "tendril", @@ -1853,6 +1892,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -2078,9 +2128,15 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core", + "rand_core 0.10.1", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rand_core" version = "0.10.1" @@ -2270,7 +2326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "digest 0.11.3", ] @@ -2281,10 +2337,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "digest 0.11.3", ] +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 987962d..8697966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ exclude = [".github", "*.py", "AGENTS.md"] adler = { version = "1", optional = true } aes = { version = "0.9", optional = true } anyhow = "1" +argon2 = { version = "0.5", optional = true } base64 = { version = "0.22", optional = true } blake2 = { version = "0.10", optional = true } block_compression = { version = "0.9", optional = true, default-features = false, features = ["bc7"] } @@ -59,6 +60,7 @@ serde_json = "1" serde_yaml_ng = "0.10" sha1 = { version = "0.11", optional = true } sha2 = { version = "0.11", optional = true } +sha3 = { version = "0.11", optional = true } siphasher = { version = "1.0", optional = true } stylua = { version = "2.1", optional = true, default-features = false} tendril = { version = "0.5", optional = true } @@ -104,7 +106,7 @@ hexen-haus = ["dep:memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "dep:lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "dep:adler", "dep:aes", "dep:base64", "dep:blake2", "dep:bytes", "dep:cbc", "dep:chacha20", "chacha20/legacy", "dep:fastcdc", "flate2", "dep:hex", "dep:int-enum", "dep:md5", "dep:memchr", "msg_tool_macro/kirikiri-arc", "dep:msg_tool_xp3data", "dep:parse-size", "dep:pelite", "serde/rc", "dep:sha2", "dep:siphasher", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "dep:adler", "dep:aes", "dep:argon2", "dep:base64", "dep:blake2", "dep:bytes", "dep:cbc", "dep:chacha20", "chacha20/legacy", "chacha20/xchacha", "dep:fastcdc", "flate2", "dep:hex", "dep:int-enum", "dep:md5", "dep:memchr", "msg_tool_macro/kirikiri-arc", "dep:msg_tool_xp3data", "dep:parse-size", "dep:pelite", "serde/rc", "dep:sha2", "dep:sha3", "dep:siphasher", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "dep:libtlg-rs"] musica = [] musica-arc = ["musica", "dep:crc32fast", "flate2", "dep:include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] @@ -147,7 +149,7 @@ utils-serde-base64bytes = ["dep:base64"] utils-simple-pack = ["zstd"] utils-str = [] utils-xored-stream = [] -private = ["dep:serde", "dep:toml", "chacha20?/xchacha"] +private = ["dep:serde", "dep:toml"] [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_System_Diagnostics_Debug"] } diff --git a/private.toml b/private.toml index 1e1ce6a..83b201f 100644 --- a/private.toml +++ b/private.toml @@ -1,6 +1,4 @@ # 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 b67d87e..fd6ce5a 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -6,7 +6,12 @@ use crate::utils::files::*; use crate::utils::struct_pack::*; use anyhow::Result; use chacha20::ChaCha20Legacy; +use chacha20::cipher::array::Array; +use chacha20::hchacha; +use msg_tool_macro::{MyDebug, StructUnpack}; use serde::{Deserializer, de}; +use sha3::digest::XofReader; +use sha3::{Sha3_224, Shake256}; use std::collections::HashSet; use std::ops::{Deref, DerefMut, Index}; use std::path::PathBuf; @@ -3154,342 +3159,510 @@ 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")); +fn triple32(mut v: u32) -> u32 { + v ^= v >> 17; + v = v.wrapping_mul(0xED5AD4BB); + v ^= v >> 11; + v = v.wrapping_mul(0xAC4C1B51); + v ^= v >> 15; + v = v.wrapping_mul(0x31848BAB); + v ^= v >> 14; - const RANDOM_TYPE_FLAG: u8 = 0x80; + v +} - fn hxkeys_new( - bootstrap: &str, - warning: &str, - param: &[u8], - uniq: &str, - upper_key: Option<[u8; 8]>, +fn fnv_blake(data: &[u8], fnvbase: u32) -> Vec { + use blake2::{Blake2s256, Digest}; + let fnv_value = (0x811C9DC5u32 ^ fnvbase).wrapping_mul(0x01000193); + let mut hash_value = fnv_value; + let mut out = [0u8; 32]; + for (i, &byte) in data.iter().enumerate() { + hash_value = triple32(hash_value ^ (byte as u32)); + let offset = (i * 4) % 32; + let hash_bytes = hash_value.to_le_bytes(); + for j in 0..4 { + out[offset + j] ^= hash_bytes[j]; + } + } + let mut hasher = Blake2s256::new(); + hasher.update(data); + hasher.update(out); + + hasher.finalize().to_vec() +} + +/// Hx table keys +#[derive(Clone, Copy, msg_tool_macro::Default)] +pub struct HxKeys { + pub key: [u8; 32], + pub nonce_a: [u8; 32], + pub nonce_b: [u8; 32], + /// FilterKey + pub filter: [u8; 8], + /// ControlBlock + #[default([0; 0x1000])] + pub ctrlblk: [u8; 0x1000], +} + +/// Represents the configuration parameters for the WAMSOFT Cx encryption scheme. +/// +/// This structure corresponds to the 0x20-byte memory block found at `ecx + 0x3020` +/// (or passed to `CxLoadParams`). It contains the permutation tables for VM instruction +/// generation, algorithm selection flags, and encryption constants. +#[repr(C)] +#[derive(Clone, Copy, Default, StructUnpack)] +pub struct CxParams { + /// The permutation table for the "Even" branch of the VM code generation. + /// Maps to `EvenBranchOrder` on GarBRO (Case 0-7). + pub even_branch_perm: [u8; 8], + /// The permutation table for the "Odd" branch of the VM code generation. + /// Maps to `OddBranchOrder` on GarBRO (Case 0-5). + pub odd_branch_perm: [u8; 6], + /// The permutation table for the Prologue of the VM code generation. + /// Maps to `PrologOrder` on GarBRO (Case 0-2). + pub prologue_perm: [u8; 3], + /// Configuration flags. + /// + /// - Bit 7 (0x80): Random Algorithm Type (0 = Xoroshiro128PlusPlus, 1 = Xoroshiro128StarStar). + /// - Bit 0 (0x01): ControlBlock generate mode (0 => NOP, 1 => XOR) + pub flags: u8, + /// The encryption mask used in the key calculation. + pub mask: u16, + /// The encryption offset used in the key calculation. + pub offset: u16, +} + +impl CxParams { + pub fn new(src: &[u8]) -> Result { + let mut reader = MemReaderRef::new(src); + Self::unpack(&mut reader, false, Encoding::Utf8, &None) + } +} + +impl HxKeys { + pub fn new( + bootstrap: &[u8], // get from bootstrap.tjc + warning: &[u8], // get from cxdec.tpm + param: &[u8], // get from cxdec.tpm + uniq: &[u8], // get from cxdec.tpm + upper_key: Option<[u8; 8]>, // get from cxdec.tpm ) -> 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), + use anyhow::Error; + use argon2::{self, Algorithm, Argon2, Version}; + use sha3::Digest; + // workaround for seed = defaults (if cxdec:word_10081744 == 'f') + // consider make .keyPackages[].key.seed nullable + // + const DEFAULT_SEED: [u8; 8] = [0xCE, 0xEA, 0xAF, 0x2C, 0xEF, 0xBE, 0xAD, 0xDE]; // 0xDEADBEEF2CAFEACEuLL + let upper_key = if let Some(upper_key) = upper_key { + match u64::from_le_bytes(upper_key) == 0 { + true => DEFAULT_SEED, + false => upper_key, + } + } else { + DEFAULT_SEED }; - 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) - } + let params = CxParams::new(param)?; // will returned later for convenience + let bootstrap_and_warning: Vec = [bootstrap, warning].concat(); + let mut hasher = Sha3_224::new(); + hasher.update(param); + let params_hash = &hasher.finalize()[..16]; + let mut lower_key_full = vec![0u8; 64]; - 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); - } + // m=8KB, t=3, p=1, len=64 + let argon2 = Argon2::new( + Algorithm::Argon2i, + Version::V0x13, + argon2::Params::new(8, 3, 1, Some(64)).map_err(Error::msg)?, + ); + argon2 + .hash_password_into(&bootstrap_and_warning, params_hash, &mut lower_key_full) + .map_err(Error::msg)?; + let lower_key = &lower_key_full[..32]; + let fnvbase_bytes: [u8; 4] = upper_key[0..4].try_into().map_err(Error::msg)?; + let fnvbase = u32::from_le_bytes(fnvbase_bytes); + let upper_key = fnv_blake(&upper_key, fnvbase); + let b0 = fnv_blake(&bootstrap_and_warning, 0); + let b1 = fnv_blake(param, 1); + let b2 = fnv_blake(uniq, 2); + let mut key_buffer: Vec = [b0, b1, b2].concat(); + + if key_buffer.len() < 96 { + return Err(anyhow::anyhow!( + "Concatenated fnv_blake buffers are less than 96 bytes" + )); + } + + for i in 0..64 { + key_buffer[i] ^= lower_key[i % 32]; + } + for i in 0..32 { + key_buffer[64 + i] ^= upper_key[i]; + } + + // generate control block + let mut ctrlblk_pa = vec![0u8; 0x1000]; // cx->ControlBlock, partA + let mut ctrlblk_pb = vec![0u8; 0x1000]; // cx->ControlBlock, partB + let mut state = Shake256::default(); + sha3::digest::Update::update(&mut state, &lower_key_full); + let mut reader = sha3::digest::ExtendableOutput::finalize_xof(state); + reader.read(&mut ctrlblk_pa); + reader.read(&mut ctrlblk_pb); + if (params.flags & 1) == 1 { + ctrlblk_pa + .iter_mut() + .zip(ctrlblk_pb.iter()) + .for_each(|(dst, src)| { + *dst ^= *src; + }); + } - 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, + HxKeys { + key: key_buffer[0..32].try_into().map_err(Error::msg)?, + nonce_a: key_buffer[32..64].try_into().map_err(Error::msg)?, + nonce_b: key_buffer[64..96].try_into().map_err(Error::msg)?, + filter: key_buffer[64..72].try_into().map_err(Error::msg)?, + ctrlblk: ctrlblk_pa.try_into().unwrap(), }, + params, )) } +} - #[derive(MyDebug)] - pub struct Hxv4Crypt { - base: Mutex>, - file_mapping: Arc>, - path_mapping: Arc>, - key_packages: Vec, - project: String, - #[skip_fmt] - config: ExtraConfig, +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)?; + HxKeys::new(&bootstrap, &warning, param, &uniq, upper_key) +} + +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); +} - 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() +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(); - 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) + .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(), + }) } - 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(); + 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); - 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 (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 { - cb.push(!reader.read_u32().unwrap()); + control_block.push(!reader.read_u32()?); } - assert_eq!(&cb, expected_cb); + 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) } } -#[cfg(feature = "private")] -pub use private::Hxv4Crypt; +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_triple32() { + assert_eq!(triple32(0x281ff4b9), 0x3389ba89); + assert_eq!(triple32(0x4899abb), 0xca5cb43b); + assert_eq!(triple32(0x12fb3c7b), 0xe2855413); + assert_eq!(triple32(0x275bdef0), 0x8f95ac52); + assert_eq!(triple32(0x4e9302ee), 0x7b62fdd0); + assert_eq!(triple32(0x4df4823b), 0xe7483578); + assert_eq!(triple32(0x77b0cd89), 0x7d42d107); + assert_eq!(triple32(0x312bebee), 0xa038d73f); + assert_eq!(triple32(0x39203931), 0xf7b87c25); + assert_eq!(triple32(0x633b66f4), 0x98ff988); + assert_eq!(triple32(0x636d1fc1), 0x99897c6); + assert_eq!(triple32(0xb7a114f), 0xef0b8bd3); + assert_eq!(triple32(0x4c7d96c0), 0xc1ba0efe); + assert_eq!(triple32(0x7f26226e), 0x7449b080); + assert_eq!(triple32(0x1bd4bcc7), 0xea9264aa); + assert_eq!(triple32(0x13afe6fd), 0x66396c69); +} + +#[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); +} #[test] fn test_filehash_deserialize() { diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 4c9723d..af7ecb4 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -19,7 +19,6 @@ 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; diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index 2b1786a..3224283 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -163,7 +163,6 @@ impl<'a> Xp3Archive<'a> { } else { let data = index_stream.read_exact_vec(size as usize)?; let tag = sig.into(); - #[cfg(feature = "private")] if config.xp3_game_title.is_none() && tag == "Hxv4" { match Hxv4Crypt::new(filename, config) { Ok(c) => {