From a52953a7ddacd450c59c1577d584a2fa9cdbe98a Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 4 May 2026 19:11:17 +0800 Subject: [PATCH] Add HxCryptLite (tested game: https://vndb.org/v16483 ) --- Cargo.lock | 2 - msg_tool_build/src/kr_arc.rs | 20 +- msg_tool_xp3data/Cargo.toml | 2 +- msg_tool_xp3data/crypt.json | 20 + msg_tool_xp3data/cx_cb/alia_ch.bin | Bin 0 -> 12288 bytes msg_tool_xp3data/cx_cb/heapps.bin | Bin 0 -> 12288 bytes src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 354 ++++++++++++++++++ src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 31 ++ src/utils/simple_pack.rs | 10 + 9 files changed, 431 insertions(+), 8 deletions(-) create mode 100644 msg_tool_xp3data/cx_cb/alia_ch.bin create mode 100644 msg_tool_xp3data/cx_cb/heapps.bin diff --git a/Cargo.lock b/Cargo.lock index 118c0e4..55dada2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1528,8 +1528,6 @@ dependencies = [ [[package]] name = "msg_tool_build" version = "0.4.0-alpha.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03574cc2818bf434ec34795cf6f3d0749a1e6b8ac156b273cb273d75d9391cf2" dependencies = [ "json", "zstd", diff --git a/msg_tool_build/src/kr_arc.rs b/msg_tool_build/src/kr_arc.rs index e2d51a6..a8ca2b3 100644 --- a/msg_tool_build/src/kr_arc.rs +++ b/msg_tool_build/src/kr_arc.rs @@ -50,11 +50,21 @@ pub fn gen_cx_cb + ?Sized, D: AsRef + ?Sized>( } let file = std::fs::File::open(file_path)?; let file_size = file.metadata()?.len(); - if file_size != 4096 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("File size must be 4096 bytes: {}", name), - )); + let typ = obj["$type"].as_str().unwrap(); + if typ == "HxCryptLite" { + if file_size != 0x3000 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("File size must be 12288 bytes: {}", name), + )); + } + } else { + if file_size != 4096 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("File size must be 4096 bytes: {}", name), + )); + } } let file = std::io::BufReader::new(file); pack.add_file(name, file)?; diff --git a/msg_tool_xp3data/Cargo.toml b/msg_tool_xp3data/Cargo.toml index e605fc6..ede7a81 100644 --- a/msg_tool_xp3data/Cargo.toml +++ b/msg_tool_xp3data/Cargo.toml @@ -19,4 +19,4 @@ unstable = ["msg_tool_build/unstable"] all-features = true [build-dependencies] -msg_tool_build = { version = "0.4.0-alpha.1", features = ["kirikiri-arc"] } +msg_tool_build = { path = "../msg_tool_build", features = ["kirikiri-arc"] } diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index d1e5259..47082da 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -123,6 +123,16 @@ "ControlBlockName": "akuratsu.bin", "Title": "あくらつ~恥辱の百合姉妹~" }, + "ALIA's CARNIVAL Flowering Sky [Offical Chinese Ver]": { + "$type": "HxCryptLite", + "Mask": 430, + "Offset": 306, + "PrologOrder": "AgAB", + "OddBranchOrder": "AQAEAgMF", + "EvenBranchOrder": "BwUBBAADAgY=", + "ControlBlockName": "alia_ch.bin", + "Title": "爱丽娅的明日盛典!Flowering Sky" + }, "Altered Pink": { "$type": "AlteredPinkCrypt", "Title": "アルタードピンク~特務戦隊デュエルレンジャー~" @@ -1462,6 +1472,16 @@ "$type": "HashCrypt", "Title": "桃尻インストラクター寝取りレッスン ~競泳水着でえろざんまい~" }, + "Motto Haramase Honoo no Oppai Chou Ero Appli Gakuen [Offical Chinese-Eng Ver]": { + "$type": "HxCryptLite", + "Mask": 589, + "Offset": 332, + "PrologOrder": "AQIA", + "OddBranchOrder": "AQIEAAUD", + "EvenBranchOrder": "BgUCAQQHAAM=", + "ControlBlockName": "heapps.bin", + "Title": "吹弹!丰盈!波涛汹涌 超情色APP学院" + }, "Muchimuchi Dekapai Mara Gui Maou-sama": { "$type": "PoringSoftCrypt", "Title": "ムチムチデカパイマラ喰い魔王様とおんぼろ四畳半同棲生活 | 与爆乳魔王的同居生活" diff --git a/msg_tool_xp3data/cx_cb/alia_ch.bin b/msg_tool_xp3data/cx_cb/alia_ch.bin new file mode 100644 index 0000000000000000000000000000000000000000..51cbacaa15c30df7d97f2b86a0037258f3243553 GIT binary patch literal 12288 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 LP5>wH|6bsKdT!H> literal 0 HcmV?d00001 diff --git a/msg_tool_xp3data/cx_cb/heapps.bin b/msg_tool_xp3data/cx_cb/heapps.bin new file mode 100644 index 0000000000000000000000000000000000000000..fdbd809699ebb29c095b7c7a1c69420a3cb38882 GIT binary patch literal 12288 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 L0iwYFdx8G}+FQ=> literal 0 HcmV?d00001 diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index f62ef23..e2966c7 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -1531,3 +1531,357 @@ impl Crypt for Arc { Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) } } + +#[derive(Debug)] +pub struct HxCryptLite { + base: CxEncryption, + header_key: Option>, + header_split_position: u64, + file_crypt_flag: bool, +} + +impl HxCryptLite { + pub fn new( + base: BaseSchema, + schema: &CxSchema, + filename: &str, + header_key: Option>, + header_split_position: u64, + file_crypt_flag: bool, + random_type: i32, + ) -> Result> { + if let Some(key) = header_key.as_ref() { + if key.len() < 8 { + anyhow::bail!("header_key is too small."); + } + } + Ok(Arc::new(Self { + base: CxEncryption::new_inner( + base, + schema, + filename, + Box::new(HxProgramLiteBuilder::new(random_type)), + )?, + header_key, + header_split_position, + file_crypt_flag, + })) + } +} + +impl AsRef for HxCryptLite { + fn as_ref(&self) -> &BaseSchema { + self.base.as_ref() + } +} + +#[derive(Debug)] +struct HxProgramLite { + base: CxProgram, + random_type: i32, + random_block: [u32; 0x270], + block_position: usize, +} + +impl HxProgramLite { + pub fn new(seed: u32, control_block: Weak>, random_method: i32) -> Self { + let block_position = 0x270; + let mut block = [0; 0x270]; + block[0] = seed; + for i in 1..0x270 { + block[i] = + ((block[i - 1] ^ (block[i - 1] >> 0x1E)) * 0x6C078965).wrapping_add(i as u32); + } + Self { + base: CxProgram { + code: Vec::with_capacity(CX_PROGRAM_SIZE), + control_block, + length: 0, + seed, + }, + random_type: random_method, + random_block: block, + block_position, + } + } + + fn get_random_new(&mut self) -> u32 { + if self.block_position == 0x270 { + self.transform_block(); + } + let s0 = self.random_block[self.block_position]; + let s1 = (s0 >> 11) ^ s0; + let s2 = ((s1 & 0xFF3A58AD) << 7) ^ s1; + let s3 = ((s2 & 0xFFFFDF8C) << 15) ^ s2; + let s4 = (s3 >> 18) ^ s3; + self.block_position += 1; + s4 + } + + fn transform_block(&mut self) { + self.block_position = 0; + let block = &mut self.random_block; + // 0-0xE2 + for i in 0..0xE3 { + let s0 = if (block[i + 1] & 1) != 0 { + 0x9908B0DFu32 + } else { + 0 + }; + let s1 = (((block[i] ^ block[i + 1]) & 0x7FFFFFFE) ^ block[i]) >> 1; + let s2 = s0 ^ s1 ^ block[i + 0x18D]; + block[i] = s2; + } + // 0xE3-0x26E + for i in 0..0x18C { + let s0 = if (block[i + 1 + 0xE3] & 1) != 0 { + 0x9908B0DFu32 + } else { + 0 + }; + let s1 = + (((block[i + 0xE3] ^ block[i + 1 + 0xE3]) & 0x7FFFFFFE) ^ block[i + 0xE3]) >> 1; + let s2 = s0 ^ s1 ^ block[i]; + block[i + 0xE3] = s2; + } + // 0x26F + let s0 = if (block[0] & 1) != 0 { + 0x9908B0DFu32 + } else { + 0 + }; + let s1 = (((block[0x26F] ^ block[0]) & 0x7FFFFFFE) ^ block[0x26F]) >> 1; + let s2 = s0 ^ s1 ^ block[0x18C]; + block[0x26F] = s2; + } +} + +impl ICxProgram for HxProgramLite { + 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 { + if self.random_type == 0 { + self.base.get_random() + } else { + self.get_random_new() + } + } +} + +#[derive(Debug)] +struct HxProgramLiteBuilder { + random_method: i32, +} + +impl HxProgramLiteBuilder { + pub fn new(random_method: i32) -> Self { + Self { random_method } + } +} + +impl ICxProgramBuilder for HxProgramLiteBuilder { + fn build( + &self, + seed: u32, + control_blocks: Weak>, + ) -> Box { + Box::new(HxProgramLite::new(seed, control_blocks, self.random_method)) + } +} + +struct HxFileDecryptor { + split_pos1: u64, + split_pos2: u64, + key: u32, + key1: u8, + key2: u8, +} + +impl HxFileDecryptor { + fn new(key: u64, file_key_flag: bool) -> Self { + let key_ptr = key.to_le_bytes(); + let mut global_key = key_ptr[0] as u32; + let mut key1 = key_ptr[1]; + let mut key2 = key_ptr[2]; + let split_pos1 = u16::from_le_bytes([key_ptr[6], key_ptr[7]]) as u64; + let mut split_pos2 = u16::from_le_bytes([key_ptr[4], key_ptr[5]]) as u64; + if split_pos1 == split_pos2 { + split_pos2 += 1; + } + if global_key == 0 { + global_key = 1; + } + global_key = global_key.wrapping_mul(0x01010101); + if file_key_flag { + key1 = 0; + key2 = 0; + } + Self { + split_pos1, + split_pos2, + key: global_key, + key1, + key2, + } + } + + fn decrypt(&self, data: &mut [u8], offset: u64, pos: usize, count: usize) { + if count == 0 { + return; + } + let key = self.key.to_le_bytes(); + let mut key_pos = (offset & 3) as usize; + for i in 0..count { + data[pos + i] ^= key[key_pos]; + key_pos = (key_pos + 1) & 3; + } + let count = count as u64; + if self.split_pos1 >= offset && self.split_pos1 < offset + count { + data[(self.split_pos1 - offset) as usize + pos] ^= self.key1; + } + if self.split_pos2 >= offset && self.split_pos2 < offset + count { + data[(self.split_pos2 - offset) as usize + pos] ^= self.key2; + } + } +} + +struct HxHeaderDecryptor { + key: [u8; 8], + pos: u64, +} + +impl HxHeaderDecryptor { + fn new(hash: u32, key: &[u8], pos: u64) -> Self { + let key_ptr = [ + u32::from_le_bytes([key[0], key[1], key[2], key[3]]), + u32::from_le_bytes([key[4], key[5], key[6], key[7]]), + ]; + let s0 = hash ^ key_ptr[1]; + let s1 = hash ^ (hash << 13); + let s2 = s1 ^ (s1 >> 17); + let s3 = s2 ^ (s2 << 5) ^ key_ptr[0]; + let key = ((s3 as u64) << 32) | (s0 as u64); + Self { + key: key.to_le_bytes(), + pos, + } + } + + fn decrypt(&self, data: &mut [u8], offset: u64, pos: usize, count: usize) { + let mut start_pos = offset; + if start_pos <= self.pos { + start_pos = self.pos; + } + let mut end_pos = offset + count as u64; + if end_pos >= self.pos + 8 { + end_pos = self.pos + 8; + } + if start_pos >= end_pos { + return; + } + let dlen = end_pos - start_pos; + let key_start_index = start_pos - self.pos; + let data_start_index = start_pos - offset + pos as u64; + for i in 0..dlen { + data[(data_start_index + i) as usize] ^= self.key[(key_start_index + i) as usize]; + } + } +} + +impl ICxEncryption for HxCryptLite { + fn get_base_offset(&self, _hash: u32) -> u32 { + _hash + } + fn inner_decrypt( + &self, + hash: u32, + offset: u64, + buffer: &mut [u8], + pos: usize, + count: usize, + ) -> Result<()> { + if let Some(key) = self.header_key.as_ref() { + let dec = HxHeaderDecryptor::new(hash, &key, self.header_split_position); + dec.decrypt(buffer, offset, pos, count); + } + let ret1 = self.base.execute_xcode(hash)?; + let ret2 = self.base.execute_xcode(hash ^ (hash >> 16))?; + let key1 = ((ret1.1 as u64) << 32) | (ret1.0 as u64); + let key2 = ((ret2.1 as u64) << 32) | (ret2.0 as u64); + let split_pos = (self.base.offset + (hash & self.base.mask)) as u64; + let dec1 = HxFileDecryptor::new(key1, self.file_crypt_flag); + let dec2 = HxFileDecryptor::new(key2, self.file_crypt_flag); + if split_pos > offset { + if split_pos < offset + count as u64 { + let blen1 = split_pos - offset; + let blen2 = offset + count as u64 - split_pos; + dec1.decrypt(buffer, offset, pos, blen1 as usize); + dec2.decrypt(buffer, offset + blen1, pos + blen1 as usize, blen2 as usize); + } else { + dec1.decrypt(buffer, offset, pos, count); + } + } else { + dec2.decrypt(buffer, offset, pos, count); + } + Ok(()) + } + fn decode( + &self, + _key: u32, + _offset: u64, + _buffer: &mut [u8], + _pos: usize, + _count: usize, + ) -> Result<()> { + Ok(()) + } +} + +icx_enc_arc_impl!(HxCryptLite); + +impl Crypt for Arc { + base_schema_impl!(); + 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 key = ( + entry.file_hash, + Box::new(self.clone()) 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 key = ( + entry.file_hash, + Box::new(self.clone()) as Box, + ); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 9df6cd1..f642d62 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -271,6 +271,19 @@ enum CryptType { ChocolatCrypt, XanaduCrypt, SisMikoCrypt, + #[serde(rename_all = "PascalCase")] + HxCryptLite { + #[serde(flatten)] + cx: CxSchema, + #[serde(default)] + header_key: Option, + #[serde(default)] + header_split_position: u64, + #[serde(default)] + file_crypt_flag: bool, + #[serde(default)] + random_type: i32, + }, } #[derive(Clone, Debug, Deserialize)] @@ -446,6 +459,21 @@ impl Schema { CryptType::SisMikoCrypt => { Box::new(chain_reaction::SisMikoCrypt::new(self.base.clone())) } + CryptType::HxCryptLite { + cx, + header_key, + header_split_position, + file_crypt_flag, + random_type, + } => Box::new(cx::HxCryptLite::new( + self.base.clone(), + cx, + filename, + header_key.as_ref().map(|s| s.bytes.clone()), + *header_split_position, + *file_crypt_flag, + *random_type, + )?), }) } } @@ -482,6 +510,9 @@ lazy_static::lazy_static! { for _ in 0..0x400 { list.push(entry.read_u32().expect(&errmsg)); } + while !entry.is_eof() { + list.push(entry.read_u32().expect(&errmsg)); + } table.insert(entry.name.clone(), list); } table diff --git a/src/utils/simple_pack.rs b/src/utils/simple_pack.rs index a6ab2d4..26d0993 100644 --- a/src/utils/simple_pack.rs +++ b/src/utils/simple_pack.rs @@ -47,6 +47,16 @@ pub struct SimplePackEntry<'a, T: Read> { pub name: String, } +impl<'a, T: Read> SimplePackEntry<'a, T> { + pub fn total_size(&self) -> u64 { + self.total + } + + pub fn is_eof(&self) -> bool { + self.current >= self.total + } +} + impl<'a, T: Read> Drop for SimplePackEntry<'a, T> { fn drop(&mut self) { let to_skip = self.total - self.current;