From e1ead588ea4da8dbe477253fab58384db4594575 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 6 May 2026 00:00:54 +0800 Subject: [PATCH] Add HxCrypt --- Cargo.lock | 78 +- Cargo.toml | 5 +- msg_tool_xp3data/crypt.json | 12 + msg_tool_xp3data/cx_cb/dc5.bin | Bin 0 -> 4096 bytes src/scripts/base.rs | 6 +- src/scripts/circus/archive/dat.rs | 6 + src/scripts/hexen_haus/img/png.rs | 10 +- src/scripts/kirikiri/archive/xp3/archive.rs | 6 +- src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 1082 ++++++++++++++++- src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 32 + src/scripts/kirikiri/archive/xp3/read.rs | 1 + 11 files changed, 1223 insertions(+), 15 deletions(-) create mode 100644 msg_tool_xp3data/cx_cb/dc5.bin diff --git a/Cargo.lock b/Cargo.lock index 55dada2..6b804f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,24 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.0" @@ -307,6 +325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", + "cipher", "cpufeatures", "rand_core", ] @@ -317,7 +336,8 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" dependencies = [ - "crypto-common", + "block-buffer 0.12.0", + "crypto-common 0.2.1", "inout", ] @@ -540,6 +560,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.1" @@ -623,15 +653,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2ea6706d74fca54e15f1d40b5cf7fe7f764aaec61352a9fcec58fe27e042fc8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "digest" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ - "block-buffer", + "block-buffer 0.12.0", "const-oid", - "crypto-common", + "crypto-common 0.2.1", ] [[package]] @@ -640,7 +681,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2de63d600bc7fab91180bc17385f29b342468dc8ef2af09dceba450a293de3da" dependencies = [ - "digest", + "digest 0.11.2", ] [[package]] @@ -882,6 +923,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1003,6 +1054,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hybrid-array" version = "0.4.11" @@ -1471,10 +1528,12 @@ dependencies = [ "aes", "anyhow", "base64", + "blake2", "block_compression", "byteorder", "bytes", "cbc", + "chacha20", "clap 4.6.1", "crc32fast", "crossbeam", @@ -1486,6 +1545,7 @@ dependencies = [ "fancy-regex", "fastcdc", "flate2", + "hex", "include-flate", "int-enum", "jieba-rs", @@ -2075,7 +2135,7 @@ checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.2", ] [[package]] @@ -2086,7 +2146,7 @@ checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.11.2", ] [[package]] @@ -2202,6 +2262,12 @@ dependencies = [ "toml", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 466b7b0..bb46143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,12 @@ adler = { version = "1", optional = true } aes = { version = "0.9", optional = true } anyhow = "1" base64 = { version = "0.22", optional = true } +blake2 = { version = "0.10", optional = true } block_compression = { version = "0.9", optional = true, default-features = false, features = ["bc7"] } byteorder = { version = "1.5", default-features = false, optional = true} bytes = { version = "1.11", optional = true } cbc = { version = "0.2", optional = true } +chacha20 = { version = "0.10", optional = true } clap = { version = "4.5", features = ["derive"] } crc32fast = { version = "1.5", optional = true } crossbeam = { version = "0.8", optional = true } @@ -27,6 +29,7 @@ encoding = "0.2" fancy-regex = { version = "0.18", optional = true } fastcdc = { version = "4.0", optional = true } flate2 = { version = "1.1", optional = true } +hex = { version = "0.4", optional = true } include-flate = { version = "0.3", optional = true } int-enum = { version = "1.2", optional = true } jieba-rs = { version = "0.9", optional = true } @@ -98,7 +101,7 @@ hexen-haus = ["memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "adler", "aes", "bytes", "cbc", "fastcdc", "flate2", "int-enum", "md5", "msg_tool_macro/kirikiri-arc", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "aes", "base64", "blake2", "bytes", "cbc", "chacha20/legacy", "fastcdc", "flate2", "hex", "int-enum", "md5", "msg_tool_macro/kirikiri-arc", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-lzss", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index 47082da..c14a495 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -410,6 +410,18 @@ "Key": 118, "Title": "ダウニャーさんと飼い主くん" }, + "D.C.5 ~Da Capo 5~": { + "$type": "HxCrypt", + "Mask": 742, + "Offset": 183, + "PrologOrder": "AAIB", + "OddBranchOrder": "AAEDBAUC", + "EvenBranchOrder": "AAIEBwEDBQY=", + "Key": "4ozUw00nStHvknH8ECpqL1BVC9nOrpdryY4wHKtBTFE=", + "Nonce": "pjhv8KmiW/gnbdoy6c5J6Q==", + "FilterKey": 12271333071625965214, + "ControlBlockName": "dc5.bin" + }, "Deatte 5-fun wa Ore no Mono!": { "$type": "XorCrypt", "Key": 53, diff --git a/msg_tool_xp3data/cx_cb/dc5.bin b/msg_tool_xp3data/cx_cb/dc5.bin new file mode 100644 index 0000000000000000000000000000000000000000..3398c65201d008c2cc7113e1bacbf00237abb4dd GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 844eb4e..d2849ee 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -12,7 +12,9 @@ pub trait ReadSeek: Read + Seek + std::fmt::Debug {} pub trait WriteSeek: Write + Seek {} /// A trait for types that can be displayed in debug format and are also support downcasting. -pub trait AnyDebug: std::fmt::Debug + std::any::Any {} +pub trait AnyDebug: std::fmt::Debug + std::any::Any { + fn as_any(&self) -> &dyn std::any::Any; +} /// A trait for reading in a stream with debug format. pub trait ReadDebug: Read + std::fmt::Debug {} @@ -23,8 +25,6 @@ impl ReadDebug for T {} impl WriteSeek for T {} -impl AnyDebug for T {} - /// A trait for script builders. pub trait ScriptBuilder: std::fmt::Debug { /// Returns the default encoding for the script. diff --git a/src/scripts/circus/archive/dat.rs b/src/scripts/circus/archive/dat.rs index e3d3e31..41454a4 100644 --- a/src/scripts/circus/archive/dat.rs +++ b/src/scripts/circus/archive/dat.rs @@ -188,6 +188,12 @@ pub struct DatExtraInfo { pub name_len: usize, } +impl AnyDebug for DatExtraInfo { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + #[derive(Debug)] /// Circus DAT Archive pub struct DatArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> { diff --git a/src/scripts/hexen_haus/img/png.rs b/src/scripts/hexen_haus/img/png.rs index 81c88de..64f0f91 100644 --- a/src/scripts/hexen_haus/img/png.rs +++ b/src/scripts/hexen_haus/img/png.rs @@ -54,7 +54,7 @@ impl ScriptBuilder for PngImageBuilder { } } -#[derive(Debug)] +#[derive(Clone, Debug)] /// Extra information for PNG image pub struct ExtraInfo { /// x offset @@ -63,6 +63,12 @@ pub struct ExtraInfo { pub offset_y: u32, } +impl AnyDebug for ExtraInfo { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + #[derive(Debug)] pub struct PngImage { reader: MemReader, @@ -115,6 +121,6 @@ impl Script for PngImage { fn extra_info<'a>(&'a self) -> Option> { self.extra .as_ref() - .map(|e| Box::new(e) as Box) + .map(|e| Box::new(e.clone()) as Box) } } diff --git a/src/scripts/kirikiri/archive/xp3/archive.rs b/src/scripts/kirikiri/archive/xp3/archive.rs index 520ca82..1b2f00d 100644 --- a/src/scripts/kirikiri/archive/xp3/archive.rs +++ b/src/scripts/kirikiri/archive/xp3/archive.rs @@ -1,6 +1,6 @@ use super::consts::*; use super::crypt::Crypt; -use crate::scripts::base::ReadSeek; +use crate::scripts::base::{AnyDebug, ReadSeek}; use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex}; @@ -29,7 +29,8 @@ pub struct ArchiveItem { pub segments: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone)] +#[allow(unused)] pub struct Xp3Entry { pub name: String, pub flags: u32, @@ -39,6 +40,7 @@ pub struct Xp3Entry { pub timestamp: Option, pub segments: Vec, pub extras: Vec, + pub extra: Option>>, } impl Xp3Entry { diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index e2966c7..5e940c5 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -1,7 +1,14 @@ use super::*; +use crate::ext::mutex::MutexExt; +use crate::utils::files::*; +use crate::utils::struct_pack::*; use anyhow::Result; +use chacha20::ChaCha20Legacy; +use serde::{Deserializer, de}; +use std::collections::HashSet; +use std::ops::Index; use std::path::PathBuf; -use std::sync::Weak; +use std::sync::{Mutex, Weak}; const S_CTL_BLOCK_SIGNATURE: &[u8] = b" Encryption control block"; @@ -1885,3 +1892,1076 @@ impl Crypt for Arc { Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) } } + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct FileHash([u8; 32]); + +impl std::fmt::Debug for FileHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FileHash(")?; + write!(f, "{}", hex::encode(self.0))?; + write!(f, ")") + } +} + +impl std::fmt::Display for FileHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +impl<'a> TryFrom<&'a [u8]> for FileHash { + type Error = anyhow::Error; + fn try_from(value: &'a [u8]) -> Result { + Ok(Self(value.try_into()?)) + } +} + +impl<'a> TryFrom<&'a str> for FileHash { + type Error = anyhow::Error; + fn try_from(value: &'a str) -> Result { + Self::try_from(hex::decode(value)?.as_slice()) + } +} + +impl<'de> Deserialize<'de> for FileHash { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let bytes = hex::decode(&s).map_err(de::Error::custom)?; + let arr = bytes.try_into().map_err(|bytes: Vec| { + de::Error::custom(format!( + "FileHash length mismatch: expected 32 bytes, got {}", + bytes.len() + )) + })?; + Ok(FileHash(arr)) + } +} + +impl FileHash { + fn to_string(&self) -> String { + hex::encode(&self.0) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +struct PathHash(u64); + +impl std::fmt::Debug for PathHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PathHash({:#x})", self.0) + } +} + +impl std::fmt::Display for PathHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(&self.0.to_be_bytes())) + } +} + +impl<'de> Deserialize<'de> for PathHash { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let bytes = hex::decode(&s).map_err(de::Error::custom)?; + let arr: [u8; 8] = bytes.try_into().map_err(|bytes: Vec| { + de::Error::custom(format!( + "PathHash length mismatch: expected 8 bytes, got {}", + bytes.len() + )) + })?; + Ok(PathHash(u64::from_be_bytes(arr))) + } +} + +impl<'a> TryFrom<&'a [u8]> for PathHash { + type Error = anyhow::Error; + fn try_from(value: &'a [u8]) -> Result { + let arr: [u8; 8] = value.try_into()?; + Ok(PathHash(u64::from_be_bytes(arr))) + } +} + +impl<'a> TryFrom<&'a str> for PathHash { + type Error = anyhow::Error; + fn try_from(value: &'a str) -> Result { + Self::try_from(hex::decode(value)?.as_slice()) + } +} + +impl PathHash { + fn to_string(&self) -> String { + hex::encode(&self.0.to_be_bytes()) + } +} + +#[derive(Clone, Deserialize)] +struct KeyPackage { + description: String, + sku: String, +} + +#[derive(Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CxdecDb { + #[allow(unused)] + file_hash_salt: String, + /// xp3 filename -> path hash -> file hash -> file name + file_list: HashMap>>>, + #[serde(default)] + key_packages: Vec, + #[allow(unused)] + path_hash_salt: String, + path_mapping: HashMap>, + project_name: String, +} + +impl std::fmt::Debug for CxdecDb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CxdecDb").finish_non_exhaustive() + } +} + +#[derive(Debug)] +pub struct HxCrypt { + base: CxEncryption, + key: [u8; 32], + nonce: [u8; 16], + filter_key: u64, + file_mapping: HashMap, + path_mapping: HashMap, + info_map: Mutex>, +} + +#[derive(Clone)] +pub struct IndexKey { + key: [u8; 32], + nonce: [u8; 16], +} + +impl std::fmt::Debug for IndexKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IndexKey") + .field("key", &hex::encode(&self.key)) + .field("nonce", &hex::encode(&self.nonce)) + .finish() + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct IndexKeyTmp { + key: String, + nonce: String, +} + +impl<'de> Deserialize<'de> for IndexKey { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + use base64::{Engine, engine::general_purpose::STANDARD}; + let s = IndexKeyTmp::deserialize(deserializer)?; + let bytes = STANDARD.decode(&s.key).map_err(de::Error::custom)?; + let key: [u8; 32] = bytes.try_into().map_err(|bytes: Vec| { + de::Error::custom(format!( + "Index key length mismatch: expected 32 bytes, got {}", + bytes.len() + )) + })?; + let hbytes = STANDARD.decode(&s.nonce).map_err(de::Error::custom)?; + let nonce: [u8; 16] = hbytes.try_into().map_err(|bytes: Vec| { + de::Error::custom(format!( + "Index key nonce length mismatch: expected 16 bytes, got {}", + bytes.len() + )) + })?; + Ok(Self { key, nonce }) + } +} + +impl HxCrypt { + pub fn new( + base: BaseSchema, + cx: &CxSchema, + index_key: Option<&IndexKey>, + filter_key: u64, + random_type: i32, + file_list_name: Option<&str>, + file_list_path: Option<&str>, + index_key_dict: &HashMap, + filename: &str, + ) -> Result { + let mut index_key = if let Some(fkey) = index_key { + Some(fkey.clone()) + } else { + None + }; + 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(); + if let Some(ind) = index_key_dict.get(s) { + index_key = Some(ind.clone()) + } + let index_key = index_key.ok_or_else(|| anyhow::anyhow!("Can not find index key."))?; + let (file_map, path_map) = if let Some(path) = file_list_path { + let data = std::fs::read(path)?; + let data = decode_to_string(Encoding::Utf8, &data, true)?; + Self::read_names(&data, s)? + } else if let Some(name) = file_list_name { + let flist = query_filename_list(name)?; + Self::read_names(&flist, s)? + } else { + let pdir = p.parent().map(|s| s.to_owned()).unwrap_or_default(); + if let Some(k) = Self::try_default_name(&pdir.join("filelist.json"), s)? { + k + } else if let Some(k) = Self::try_default_name(&pdir.join("filelist.lst"), s)? { + k + } else { + (HashMap::new(), HashMap::new()) + } + }; + Ok(Self { + base: CxEncryption::new_inner( + base, + cx, + filename, + Box::new(HxProgramBuilder::new(random_type)), + )?, + key: index_key.key, + nonce: index_key.nonce, + filter_key, + file_mapping: file_map, + path_mapping: path_map, + info_map: Mutex::new(HashMap::new()), + }) + } + + fn try_default_name>( + s: &P, + b: &str, + ) -> Result, HashMap)>> { + let n = match get_ignorecase_path(s) { + Ok(s) => s, + Err(_) => return Ok(None), + }; + if !n.exists() { + return Ok(None); + } + let s = std::fs::read(&n)?; + let data = decode_to_string(Encoding::Utf8, &s, true)?; + let names = Self::read_names(&data, b)?; + eprintln!( + "Read {} file entries and {} directory entries from filelist {}.", + names.0.len(), + names.1.len(), + n.display() + ); + Ok(Some(names)) + } + + fn read_names( + s: &str, + basename: &str, + ) -> Result<(HashMap, HashMap)> { + if let Ok(s) = serde_json::from_str::(&s) { + let path_map: HashMap<_, _> = s + .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) = s.file_list.get(basename) { + 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() + }; + return Ok((file_map, path_map)); + } + let mut file_map = HashMap::new(); + let mut path_map = HashMap::new(); + for line in s.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let mut iter = line.splitn(2, ':'); + let key = match iter.next() { + Some(v) => v, + None => continue, + }; + let value = match iter.next() { + Some(v) => v, + None => continue, + }; + if key.len() == 16 { + let key = PathHash::try_from(key)?; + path_map.insert(key, value.to_string()); + } else if key.len() == 64 { + let key = FileHash::try_from(key)?; + file_map.insert(key, value.to_string()); + } + } + Ok((file_map, path_map)) + } + + fn create_chacha20_crypt(&self) -> Result { + use chacha20::{KeyIvInit, cipher::StreamCipherSeek}; + let mut nonce = [0; 8]; + nonce.copy_from_slice(&self.nonce[..8]); + let mut crypt = ChaCha20Legacy::new((&self.key).into(), (&nonce).into()); + crypt.try_seek(64)?; + Ok(crypt) + } + + fn read_index(&self, mut stream: T) -> Result<()> { + use chacha20::cipher::StreamCipher; + let len = stream.stream_length()?; + let mut crypt = self.create_chacha20_crypt()?; + let tlen = len as usize - 16; + let mut buf = Vec::with_capacity(tlen); + stream.seek(SeekFrom::Start(16))?; + stream.read_to_end(&mut buf)?; + crypt.try_apply_keystream(&mut buf)?; + let mut stream = flate2::read::ZlibDecoder::new(MemReaderRef::new(&buf[4..])); + let mut buf = Vec::new(); + stream.read_to_end(&mut buf)?; + let mut reader = MemReader::new(buf); + let root_obj = TjsValue::unpack(&mut reader, true, Encoding::Utf16LE, &None)?; + if !root_obj.is_array() { + anyhow::bail!("Index object is not an array."); + } + let mut info_map = self.info_map.lock_blocking(); + info_map.clear(); + let set = create_garbage_filename_set("xp3hnp"); + for i in (0..root_obj.len()).step_by(2) { + let path_hash = PathHash::try_from( + root_obj[i] + .as_bytes() + .ok_or_else(|| anyhow::anyhow!("path hash is not bytes."))?, + )?; + let dir_obj = &root_obj[i + 1]; + if !dir_obj.is_array() { + anyhow::bail!("dir object at index {} is not array.", i + 1); + } + let (path_name, path_is_hash) = if let Some(n) = self.path_mapping.get(&path_hash) { + (n.to_owned(), false) + } else { + (path_hash.to_string() + "/", true) + }; + for j in (0..dir_obj.len()).step_by(2) { + let entry_hash = FileHash::try_from( + dir_obj[j] + .as_bytes() + .ok_or_else(|| anyhow::anyhow!("entry hash is not bytes."))?, + )?; + let entry_obj = &dir_obj[j + 1]; + if !entry_obj.is_array() { + anyhow::bail!("Entry object at index {},{} is not array.", i + 1, j + 1); + } + if entry_obj.len() < 2 { + anyhow::bail!("Entry object at index {},{} is too small.", i + 1, j + 1); + } + let entry_id = entry_obj[0] + .as_u64() + .ok_or_else(|| anyhow::anyhow!("Entry id is not int."))?; + let entry_key = entry_obj[1] + .as_u64() + .ok_or_else(|| anyhow::anyhow!("Entry key is not int."))?; + let (name, name_is_hash) = if let Some(n) = self.file_mapping.get(&entry_hash) { + (n.to_owned(), false) + } else { + (entry_hash.to_string(), true) + }; + let uname = Self::get_unicode_name(entry_id as u32); + let entry = HxEntry { + path: path_name.clone(), + name, + id: entry_id, + key: entry_key, + name_is_hash, + path_is_hash, + is_garbage: set.contains(&entry_hash), + }; + info_map.insert(uname, entry); + } + } + Ok(()) + } + + fn get_unicode_name(mut hash: u32) -> String { + let mut buf = [0u16; 4]; + let mut i = 0; + loop { + buf[i] = ((hash & 0x3FFF) + 0x5000) as u16; + hash >>= 14; + i += 1; + if hash == 0 { + break; + } + } + let s = String::from_utf16_lossy(&buf[..i]); + s + } + + fn create_filter_key(&self, entry_key: u64, header_key_seed: u64) -> Result { + let key0 = entry_key as u32; + let key1 = (entry_key >> 32) as u32; + let k0 = self.base.execute_xcode(key0)?; + let file_key_0 = (k0.0 as u64) | ((k0.1 as u64) << 32); + let k1 = self.base.execute_xcode(key1)?; + let file_key_1 = (k1.0 as u64) | ((k1.1 as u64) << 32); + let split_position = + (self.base.offset as u64 + ((entry_key >> 16) & self.base.mask as u64)) & 0xffffffff; + let mut header_key = [0u8; 16]; + let k3 = self.base.execute_xcode(header_key_seed as u32)?; + let mut v5 = (k3.0 as u64) | ((k3.1 as u64) << 32); + v5 = !v5; + let mut writer = MemWriterRef::new(&mut header_key); + writer.write_u64_be(v5)?; + let k3 = self.base.execute_xcode(v5 as u32)?; + v5 = (k3.0 as u64) | ((k3.1 as u64) << 32); + v5 = !v5; + writer.write_u64_be(v5)?; + Ok(HxFilterKey { + key: [file_key_0, file_key_1], + header_key, + split_position, + has_header_key: true, + flag: false, + }) + } +} + +impl AsRef for HxCrypt { + fn as_ref(&self) -> &CxEncryption { + &self.base + } +} + +struct CopyStream<'a> { + inner: Box, +} + +impl<'a> CopyStream<'a> { + pub fn new(stream: Box) -> Self { + Self { inner: stream } + } +} + +impl<'a> std::fmt::Debug for CopyStream<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CopyStream").finish_non_exhaustive() + } +} + +impl<'a> Read for CopyStream<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl Crypt for HxCrypt { + base_schema_impl!(); + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + if let Some(hxv4) = archive.extras.iter().find(|x| x.tag == "Hxv4") { + let mut reader = MemReaderRef::new(&hxv4.data); + let offset = reader.read_u64()? + archive.base_offset; + let size = reader.read_u32()?; + let _flags = reader.read_u16()?; + let stream = StreamRegion::with_size( + MutexWrapper::new(archive.inner.clone(), offset), + size as u64, + )?; + self.read_index(stream)?; + let info_map = self.info_map.lock_blocking(); + for entry in archive.entries.iter_mut() { + if let Some(info) = info_map.get(&entry.name) { + if info.is_garbage { + continue; + } + entry.name = format!("{}{}", info.path, info.name); + let info = info.clone(); + entry.extra = Some(Arc::new(Box::new(info))) + } + } + archive.entries.retain(|x| { + x.extra.is_some() || !info_map.get(&x.name).is_some_and(|x| x.is_garbage) + }); + } + archive.extras.retain(|x| x.tag != "Hxv4"); + Ok(()) + } + 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> { + let info = match entry.extra.as_ref() { + Some(info) => info, + None => return Ok(Box::new(CopyStream::new(stream))), + }; + let info = info + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow::anyhow!("extra info is not hx entry."))?; + let mut entry_key = info.key; + if (info.id & 0x100000000) == 0 { + entry_key ^= self.filter_key; + } + let header_key = !entry_key; + let key = self.create_filter_key(entry_key, header_key)?; + let filter = HxFilter::new(key); + let key = ( + entry.file_hash, + Box::new(filter) as Box, + ); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + let info = match entry.extra.as_ref() { + Some(info) => info, + None => return Ok(stream), + }; + let info = info + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow::anyhow!("extra info is not hx entry."))?; + let mut entry_key = info.key; + if (info.id & 0x100000000) == 0 { + entry_key ^= self.filter_key; + } + let header_key = !entry_key; + let key = self.create_filter_key(entry_key, header_key)?; + let filter = HxFilter::new(key); + let key = ( + entry.file_hash, + Box::new(filter) as Box, + ); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } +} + +#[derive(Debug)] +enum TjsValue { + Void, + Str(String), + ByteArray(Vec), + Int(i64), + #[allow(unused)] + Double(f64), + Array(Vec), + Dict(HashMap), +} + +impl TjsValue { + fn is_array(&self) -> bool { + matches!(self, Self::Array(_)) + } + + fn len(&self) -> usize { + match self { + Self::Str(s) => s.len(), + Self::ByteArray(arr) => arr.len(), + Self::Array(arr) => arr.len(), + Self::Dict(dict) => dict.len(), + _ => 0, + } + } + + fn as_bytes(&self) -> Option<&[u8]> { + match self { + Self::ByteArray(arr) => Some(arr), + _ => None, + } + } + + fn as_u64(&self) -> Option { + match self { + Self::Int(i) => Some(*i as u64), + _ => None, + } + } +} + +const VOID: TjsValue = TjsValue::Void; + +impl Index for TjsValue { + type Output = TjsValue; + fn index(&self, index: usize) -> &Self::Output { + match self { + Self::Array(arr) => arr.get(index).unwrap_or(&VOID), + _ => &VOID, + } + } +} + +fn unpack_string(reader: &mut R, big: bool, encoding: Encoding) -> Result { + let len = u32::unpack(reader, big, encoding, &None)? as usize; + let tlen = if encoding.is_utf16le() { len * 2 } else { len }; + let mut buf = vec![0u8; tlen]; + reader.read_exact(&mut buf)?; + let s = decode_to_string(encoding, &buf, true)?; + Ok(s) +} + +impl StructUnpack for TjsValue { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let typ = u8::unpack(reader, big, encoding, info)?; + Ok(match typ { + 0 => Self::Void, + 2 => Self::Str(unpack_string(reader, big, encoding)?), + 3 => { + let len = u32::unpack(reader, big, encoding, info)?; + let data = reader.read_exact_vec(len as usize)?; + Self::ByteArray(data) + } + 4 => { + let num = i64::unpack(reader, big, encoding, info)?; + Self::Int(num) + } + 5 => { + let num = f64::unpack(reader, big, encoding, info)?; + Self::Double(num) + } + 0x81 => { + let len = u32::unpack(reader, big, encoding, info)?; + let mut arr = Vec::with_capacity(len as usize); + for _ in 0..len { + arr.push(Self::unpack(reader, big, encoding, info)?); + } + Self::Array(arr) + } + 0xC1 => { + let len = u32::unpack(reader, big, encoding, info)?; + let mut dict = HashMap::with_capacity(len as usize); + for _ in 0..len { + let name = unpack_string(reader, big, encoding)?; + let obj = Self::unpack(reader, big, encoding, info)?; + dict.insert(name, obj); + } + Self::Dict(dict) + } + _ => anyhow::bail!("Unknown type id: {typ:02x}."), + }) + } +} + +#[derive(Clone, Debug)] +struct HxEntry { + path: String, + name: String, + id: u64, + key: u64, + name_is_hash: bool, + path_is_hash: bool, + is_garbage: bool, +} + +impl AnyDebug for HxEntry { + fn as_any(&self) -> &dyn std::any::Any { + self + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct HxSplitMix64 { + state: u64, +} + +impl HxSplitMix64 { + pub fn new(seed: u64) -> Self { + Self { state: seed } + } +} + +trait IRng: std::fmt::Debug { + fn next(&mut self) -> u64; +} + +impl IRng for HxSplitMix64 { + fn next(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9E3779B97F4A7C15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB); + z ^ (z >> 31) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Xoroshiro128PlusPlus { + state: [u64; 2], +} + +impl Xoroshiro128PlusPlus { + pub fn new(state: [u64; 2]) -> Self { + assert!( + state[0] != 0 || state[1] != 0, + "Initial state cannot be all zeros." + ); + Self { state } + } +} + +impl IRng for Xoroshiro128PlusPlus { + fn next(&mut self) -> u64 { + let s0 = self.state[0]; + let mut s1 = self.state[1]; + let result = s0.wrapping_add(s1).rotate_left(17).wrapping_add(s0); + s1 ^= s0; + self.state[0] = s0.rotate_left(49) ^ s1 ^ (s1 << 21); + self.state[1] = s1.rotate_left(28); + result + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Xoroshiro128StarStar { + state: [u64; 2], +} + +impl Xoroshiro128StarStar { + pub fn new(state: [u64; 2]) -> Self { + assert!( + state[0] != 0 || state[1] != 0, + "Initial state cannot be all zeros." + ); + Self { state } + } +} + +impl IRng for Xoroshiro128StarStar { + fn next(&mut self) -> u64 { + let s0 = self.state[0]; + let mut s1 = self.state[1]; + let result = s0.wrapping_mul(5).rotate_left(7).wrapping_mul(9); + s1 ^= s0; + self.state[0] = s0.rotate_left(24) ^ s1 ^ (s1 << 16); + self.state[1] = s1.rotate_left(37); + result + } +} + +#[derive(Debug)] +struct HxProgram { + base: CxProgram, + rng: Box, +} + +impl HxProgram { + pub fn new(seed: u32, control_block: Weak>, random_method: i32) -> Self { + let initial_seed = (seed as u64) | (!(seed as u64) << 32); + let mut seeder = HxSplitMix64::new(initial_seed); + let seed1 = seeder.next(); + let seed2 = seeder.next(); + let xoroshiro_seed = [seed1, seed2]; + Self { + base: CxProgram { + code: Vec::new(), + control_block, + length: 0, + seed, + }, + rng: if random_method == 0 { + Box::new(Xoroshiro128PlusPlus::new(xoroshiro_seed)) + } else { + Box::new(Xoroshiro128StarStar::new(xoroshiro_seed)) + }, + } + } +} + +impl ICxProgram for HxProgram { + fn execute(&self, hash: u32) -> Result { + self.base.execute(hash) + } + fn clear(&mut self) { + self.base.clear(); + } + fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool { + self.base.emit(bytecode, length) + } + fn emit_nop(&mut self, count: usize) -> bool { + self.base.emit_nop(count) + } + fn emit_u32(&mut self, x: u32) -> bool { + self.base.emit_u32(x) + } + fn get_random(&mut self) -> u32 { + self.rng.next() as u32 + } +} + +#[derive(Debug)] +struct HxProgramBuilder { + random_method: i32, +} + +impl HxProgramBuilder { + pub fn new(random_method: i32) -> Self { + Self { random_method } + } +} + +impl ICxProgramBuilder for HxProgramBuilder { + fn build( + &self, + seed: u32, + control_blocks: Weak>, + ) -> Box { + Box::new(HxProgram::new(seed, control_blocks, self.random_method)) + } +} + +struct HxFilterKey { + key: [u64; 2], + header_key: [u8; 16], + split_position: u64, + has_header_key: bool, + flag: bool, +} + +#[derive(Clone)] +struct HxFilterSpanDecryptor { + first_decrypt_key: u32, + key1: u8, + key2: u8, + span_position: [u64; 2], +} + +impl std::fmt::Debug for HxFilterSpanDecryptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HxFilterSpanDecryptor") + .field( + "firstDecryptKey", + &format_args!("{:#x}", &self.first_decrypt_key), + ) + .field("key1", &format_args!("{:#x}", &self.key1)) + .field("key2", &format_args!("{:#x}", &self.key2)) + .field( + "spanPosition", + &format_args!("{:#x}, {:#x}", self.span_position[0], self.span_position[1]), + ) + .finish() + } +} + +impl HxFilterSpanDecryptor { + pub fn new(key: u64, flag: bool) -> Self { + let decrypt_key_bytes = ((key >> 8) & 0xFFFF) as u32; + let mut first_decrypt_key = (key & 0xFF) as u32; + let mut span_position = [(key >> 48) & 0xFFFF, (key >> 32) & 0xFFFF]; + if span_position[0] == span_position[1] { + span_position[1] = span_position[1].wrapping_add(1); + } + if !flag && first_decrypt_key == 0 { + first_decrypt_key = 0xA5; + } + first_decrypt_key = first_decrypt_key.wrapping_mul(0x01010101); + let (key1, key2) = if flag { + (0, 0) + } else { + ( + (decrypt_key_bytes & 0xFF) as u8, + ((decrypt_key_bytes >> 8) & 0xFF) as u8, + ) + }; + Self { + first_decrypt_key, + key1, + key2, + span_position, + } + } + + pub fn decrypt(&self, position: u64, data: &mut [u8]) { + if data.is_empty() { + return; + } + let key_bytes = self.first_decrypt_key.to_le_bytes(); + for (i, byte) in data.iter_mut().enumerate() { + let key_index = ((position as usize) + i) & 3; + *byte ^= key_bytes[key_index]; + } + let data_len = data.len() as u64; + if self.key1 != 0 { + let pos1 = self.span_position[0]; + if pos1 >= position && pos1 < position + data_len { + let index = (pos1 - position) as usize; + data[index] ^= self.key1; + } + } + if self.key2 != 0 { + let pos2 = self.span_position[1]; + if pos2 >= position && pos2 < position + data_len { + let index = (pos2 - position) as usize; + data[index] ^= self.key2; + } + } + } +} + +struct HxFilter { + span_decryptors: [HxFilterSpanDecryptor; 2], + split_position: u64, + header_key: [u8; 16], + has_header_key: bool, +} + +impl HxFilter { + pub fn new(key: HxFilterKey) -> HxFilter { + HxFilter { + span_decryptors: [ + HxFilterSpanDecryptor::new(key.key[0], key.flag), + HxFilterSpanDecryptor::new(key.key[1], key.flag), + ], + split_position: key.split_position, + header_key: key.header_key, + has_header_key: key.has_header_key, + } + } + + fn decrypt_header(&self, position: u64, buffer: &mut [u8]) { + let header_len = self.header_key.len() as u64; + let overlap_start = position; + let overlap_end = (position + buffer.len() as u64).min(header_len); + if overlap_start >= overlap_end { + return; + } + for i in overlap_start..overlap_end { + let buffer_index = (i - position) as usize; + let key_index = i as usize; + buffer[buffer_index] ^= self.header_key[key_index]; + } + } + + pub fn decrypt(&self, position: u64, buffer: &mut [u8]) { + if buffer.is_empty() { + return; + } + if self.has_header_key { + self.decrypt_header(position, buffer); + } + let buffer_len = buffer.len() as u64; + let buffer_end_pos = position + buffer_len; + if buffer_end_pos <= self.split_position { + self.span_decryptors[0].decrypt(position, buffer); + } else if position >= self.split_position { + self.span_decryptors[1].decrypt(position, buffer); + } else { + let split_index = (self.split_position - position) as usize; + let (part1, part2) = buffer.split_at_mut(split_index); + self.span_decryptors[0].decrypt(position, part1); + self.span_decryptors[1].decrypt(self.split_position, part2); + } + } +} + +impl std::fmt::Debug for HxFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HxFilter") + .field("spanDecryptors", &self.span_decryptors) + .field( + "splitPosition", + &format_args!("{:#x}", &self.split_position), + ) + .field("hasHeaderKey", &self.has_header_key) + .field("headerKey", &hex::encode(self.header_key)) + .finish() + } +} + +impl ICxEncryption for HxFilter { + fn get_base_offset(&self, _hash: u32) -> u32 { + 0 + } + fn decode( + &self, + _key: u32, + _offset: u64, + _buffer: &mut [u8], + _pos: usize, + _count: usize, + ) -> Result<()> { + Ok(()) + } + fn inner_decrypt( + &self, + _key: u32, + offset: u64, + buffer: &mut [u8], + pos: usize, + count: usize, + ) -> Result<()> { + self.decrypt(offset, &mut buffer[pos..pos + count]); + Ok(()) + } +} + +fn calculate_file_hash(pathname: &str, file_hash_salt: &str) -> FileHash { + use blake2::{Blake2s256, Digest}; + let mut hasher = Blake2s256::new(); + (pathname.to_lowercase() + file_hash_salt) + .encode_utf16() + .for_each(|b| { + hasher.update(&b.to_le_bytes()); + }); + let result = hasher.finalize(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&result); + FileHash(hash) +} + +fn create_garbage_filename_set(file_hash_salt: &str) -> HashSet { + let mut set = HashSet::new(); + set.insert(calculate_file_hash("$$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ This is a protected archive. $$$ 著作者はこのアーカイブが正規の利用方法以外の方法で展開されることを望んでいません。 $$$ Warning! Extracting this archive may infringe on author's rights. 警告 このアーカイブを展開することにより、あなたは著作者の権利を侵害するおそれがあります。.txt", file_hash_salt)); + set +} + +#[test] +fn test_filehash_deserialize() { + assert_eq!( + FileHash([ + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf + ]), + serde_json::from_str( + "\"000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0F\"" + ) + .unwrap() + ); +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index f642d62..dfc2712 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -284,6 +284,20 @@ enum CryptType { #[serde(default)] random_type: i32, }, + #[serde(rename_all = "PascalCase")] + HxCrypt { + #[serde(flatten)] + cx: CxSchema, + #[serde(default, flatten)] + index_key: Option, + filter_key: u64, + #[serde(default)] + random_type: i32, + #[serde(default)] + file_list_name: Option, + #[serde(default)] + index_key_dict: HashMap, + }, } #[derive(Clone, Debug, Deserialize)] @@ -474,6 +488,24 @@ impl Schema { *file_crypt_flag, *random_type, )?), + CryptType::HxCrypt { + cx, + index_key, + filter_key, + random_type, + file_list_name, + index_key_dict, + } => Box::new(cx::HxCrypt::new( + self.base.clone(), + cx, + index_key.as_ref(), + *filter_key, + *random_type, + file_list_name.as_ref().map(|s| s.as_str()), + config.xp3_file_list_path.as_ref().map(|s| s.as_str()), + index_key_dict, + filename, + )?), }) } } diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index 5a29921..d002dd0 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -149,6 +149,7 @@ impl<'a> Xp3Archive<'a> { timestamp, segments, extras: entry_extras, + extra: None, }; if entry.name == "startup.tjs" && entry.flags != 0