From cbcac9d166b5a45446e56ff6abdcc1b8f285e0a4 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 8 Apr 2026 22:19:36 +0800 Subject: [PATCH] Add RiddleCxCrypt --- msg_tool_xp3data/crypt.json | 42 +++ msg_tool_xp3data/cx_cb/cafe_stella.bin | Bin 0 -> 4096 bytes msg_tool_xp3data/cx_cb/riddle.bin | Bin 0 -> 4096 bytes src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 293 +++++++++++++++++- src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 29 ++ 5 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 msg_tool_xp3data/cx_cb/cafe_stella.bin create mode 100644 msg_tool_xp3data/cx_cb/riddle.bin diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index 5ec667f..eb1d0bb 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -197,6 +197,28 @@ "Key": 49, "Title": "僕の未来は、恋と課金と。~Charge To The Future~ [体験版] | 我的未来是恋爱与氪金 [试用版]" }, + "Café Stella to Shinigami no Chou": { + "$type": "RiddleCxCrypt", + "Mask": 622, + "Offset": 146, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "NamesSectionId": "yuz:", + "RandomSeed": 2463534242, + "YuzKey": [ + 2660206490, + 3437206417, + 2258490061, + 2497682059, + 666440252, + 2154974748 + ], + "Key1": 1180159099, + "Key2": 2773455888, + "ControlBlockName": "cafe_stella.bin", + "Title": "喫茶ステラと死神の蝶 | 星光咖啡馆与死神之蝶 | 星光咖啡館與死神之蝶" + }, "Chikan Yuugi": { "$type": "CxEncryption", "Mask": 531, @@ -1371,6 +1393,26 @@ "Title": "リア充催眠~リアルが充実する催眠生活はじめました。~", "ObfuscatedIndex": true }, + "Riddle Joker": { + "$type": "RiddleCxCrypt", + "Mask": 280, + "Offset": 271, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "StartupTjsNotEncrypted": true, + "NamesSectionId": "yuz:", + "RandomSeed": 2463534242, + "YuzKey": [ + 2660206490, + 3437206417, + 2258490061, + 2497682059, + 3184993560, + 3577860684 + ], + "ControlBlockName": "riddle.bin" + }, "Riddle Joker [Steam]": { "$type": "CxEncryption", "Mask": 350, diff --git a/msg_tool_xp3data/cx_cb/cafe_stella.bin b/msg_tool_xp3data/cx_cb/cafe_stella.bin new file mode 100644 index 0000000000000000000000000000000000000000..d85d563fbcb8b6341a2d23cba5ab5d2f3fa2c57e GIT binary patch literal 4096 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 bool; fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool; fn emit_u32(&mut self, x: u32) -> bool; - fn emit_random(&mut self) -> bool; + fn emit_random(&mut self) -> bool { + let random = self.get_random(); + self.emit_u32(random) + } fn get_random(&mut self) -> u32; } @@ -722,11 +726,6 @@ impl ICxProgram for CxProgram { return true; } - fn emit_random(&mut self) -> bool { - let random = self.get_random(); - self.emit_u32(random) - } - fn get_random(&mut self) -> u32 { let seed = self.seed; self.seed = seed.wrapping_mul(1103515245).wrapping_add(12345); @@ -973,10 +972,6 @@ impl ICxProgram for CxProgramNana { fn emit_u32(&mut self, x: u32) -> bool { self.base.emit_u32(x) } - fn emit_random(&mut self) -> bool { - let random = self.get_random(); - self.emit_u32(random) - } fn get_random(&mut self) -> u32 { let mut s = self.base.seed ^ (self.base.seed << 17); s ^= (s << 18) | (s >> 15); @@ -1239,3 +1234,281 @@ impl Crypt for Arc { Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) } } + +#[derive(Debug)] +struct YuzDecryptor { + state: [u8; 64], +} + +impl YuzDecryptor { + fn new(key1: &[u32], key2: &[u32], seed1: u32, seed2: u32) -> Self { + let mut state = [0u8; 64]; + for i in 0..4 { + state[i * 4..i * 4 + 4].copy_from_slice(&key2[i].to_le_bytes()); + } + for i in 0..8 { + state[i * 4 + 16..i * 4 + 20].copy_from_slice(&key1[i].to_le_bytes()); + } + let t: u32 = !0; + state[48..52].copy_from_slice(&t.to_le_bytes()); + state[52..56].copy_from_slice(&t.to_le_bytes()); + state[56..60].copy_from_slice(&(!seed1).to_le_bytes()); + state[60..64].copy_from_slice(&(!seed2).to_le_bytes()); + Self { state } + } + + fn decrypt(&self, data: &mut [u8]) { + let mut state1 = [0u8; 64]; + let mut state2 = [0u8; 64]; + let mut i = 0; + let mut offset: u64 = 0; + let mut length = data.len(); + while length > 0 { + state1.copy_from_slice(&self.state); + state1[48..56].copy_from_slice(&(!offset).to_le_bytes()); + offset += 1; + Self::transform_state(&state1, &mut state2, 8); + let count = length.min(0x40); + for j in 0..count { + data[i] ^= state2[j]; + i += 1; + } + length -= count; + } + } + + fn transform_state(state: &[u8], target: &mut [u8], length: usize) { + let mut tmp = [0u32; 16]; + for i in 0..16 { + tmp[i] = !u32::from_le_bytes([ + state[i * 4], + state[i * 4 + 1], + state[i * 4 + 2], + state[i * 4 + 3], + ]); + } + if length > 0 { + for _ in 0..((length - 1) >> 1) + 1 { + let mut t1 = w!(tmp[4] + tmp[0]); + let mut t2 = (t1 ^ tmp[12]).rotate_left(16); + let mut t3 = w!(t2 + tmp[8]); + let mut t4 = (tmp[4] ^ t3).rotate_left(12); + let mut t5 = w!(t4 + t1); + let mut t6 = (t5 ^ t2).rotate_left(8); + tmp[12] = t6; + w!(t6 += t3); + tmp[4] = (t4 ^ t6).rotate_left(7); + t4 = (w!(tmp[5] + tmp[1]) ^ tmp[13]).rotate_left(16); + t3 = (tmp[5] ^ w!(t4 + tmp[9])).rotate_left(12); + t2 = w!(t3 + tmp[5] + tmp[1]); + tmp[13] = (t2 ^ t4).rotate_left(8); + w!(tmp[9] += tmp[13] + t4); + tmp[5] = (t3 ^ tmp[9]).rotate_left(7); + t4 = (w!(tmp[6] + tmp[2]) ^ tmp[14]).rotate_left(16); + w!(tmp[10] += t4); + t1 = (tmp[6] ^ tmp[10]).rotate_left(12); + t3 = w!(t1 + tmp[6] + tmp[2]); + tmp[14] = (t3 ^ t4).rotate_left(8); + tmp[6] = (t1 ^ w!(tmp[14] + tmp[10])).rotate_left(7); + w!(tmp[10] += tmp[14]); + t4 = w!(tmp[7] + tmp[3]) ^ tmp[15]; + w!(tmp[3] += tmp[7]); + t4 = t4.rotate_left(16); + w!(tmp[11] += t4); + t1 = (tmp[7] ^ tmp[11]).rotate_left(12); + t4 ^= w!(t1 + tmp[3]); + w!(tmp[3] += t1); + t4 = t4.rotate_left(8); + w!(tmp[11] += t4); + t1 = (t1 ^ tmp[11]).rotate_left(7); + w!(t5 += tmp[5]); + w!(t2 += tmp[6]); + t4 = (t5 ^ t4).rotate_left(16); + w!(tmp[10] += t4); + tmp[5] = (tmp[5] ^ tmp[10]).rotate_left(12); + tmp[0] = w!(tmp[5] + t5); + t4 = (tmp[0] ^ t4).rotate_left(8); + tmp[15] = t4; + w!(tmp[10] += t4); + tmp[5] = (tmp[5] ^ tmp[10]).rotate_left(7); + tmp[12] = (tmp[12] ^ t2).rotate_left(16); + w!(tmp[11] += tmp[12]); + t4 = (tmp[11] ^ tmp[6]).rotate_left(12); + tmp[1] = w!(t4 + t2); + tmp[12] = (tmp[12] ^ tmp[1]).rotate_left(8); + w!(tmp[11] += tmp[12]); + tmp[6] = (t4 ^ tmp[11]).rotate_left(7); + w!(t3 += t1); + t4 = (tmp[13] ^ t3).rotate_left(16); + t2 = w!(t4 + t6); + t1 = (t2 ^ t1).rotate_left(12); + tmp[2] = w!(t1 + t3); + tmp[13] = (t4 ^ tmp[2]).rotate_left(8); + tmp[8] = w!(tmp[13] + t2); + tmp[7] = (tmp[8] ^ t1).rotate_left(7); + t6 = (tmp[14] ^ w!(tmp[4] + tmp[3])).rotate_left(16); + t1 = (tmp[4] ^ w!(t6 + tmp[9])).rotate_left(12); + w!(tmp[3] += t1 + tmp[4]); + t3 = (t6 ^ tmp[3]).rotate_left(8); + w!(tmp[9] += t3 + t6); + tmp[4] = (t1 ^ tmp[9]).rotate_left(7); + tmp[14] = t3; + } + } + let mut pos = 0; + for i in 0..16 { + let d = + !u32::from_le_bytes([state[pos], state[pos + 1], state[pos + 2], state[pos + 3]]); + let d = w!(tmp[i] + d); + target[pos..pos + 4].copy_from_slice(&d.to_le_bytes()); + pos += 4; + } + } +} + +#[derive(Debug)] +pub struct RiddleCxCrypt { + base: SenrenCxCrypt, + decryptor: YuzDecryptor, + key1: u32, + key2: u32, +} + +impl AsRef for RiddleCxCrypt { + fn as_ref(&self) -> &BaseSchema { + self.base.as_ref() + } +} + +impl RiddleCxCrypt { + pub fn new( + base: BaseSchema, + schema: &CxSchema, + filename: &str, + names_section_id: String, + random_seed: u32, + yuz_key: &[u32], + key1: u32, + key2: u32, + ) -> Result> { + if yuz_key.len() != 6 { + return Err(anyhow::anyhow!( + "Invalid Yuzu keys for RiddleCxCrypt: expected 6, got {}", + yuz_key.len() + )); + } + let cx = SenrenCxCrypt::new_inner( + base, + schema, + filename, + Box::new(CxProgramNanaBuilder::new(random_seed)), + names_section_id, + )?; + let control_block = cx.base.control_block.as_ref(); + let decryptor = YuzDecryptor::new(&control_block, yuz_key, yuz_key[4], yuz_key[5]); + Ok(Arc::new(Self { + base: cx, + decryptor, + key1, + key2, + })) + } + + fn get_key_from_hash(&self, key: u32) -> u64 { + let lo = key ^ self.key2; + let mut hi = (key << 13) ^ key; + hi ^= hi >> 17; + hi ^= (hi << 5) ^ self.key1; + ((hi as u64) << 32) | (lo as u64) + } + + fn read_yuzu_names( + &self, + mut reader: Box, + unpacked_size: u32, + ) -> Result<(HashMap, HashMap)> { + let mut prefix = Vec::with_capacity(0x100); + (&mut reader).take(0x100).read_to_end(&mut prefix)?; + self.decryptor.decrypt(&mut prefix); + let reader = Box::new(PrefixStream::new(prefix, reader)); + SenrenCxCrypt::read_yuzu_names(reader, unpacked_size) + } +} + +impl ICxEncryption for RiddleCxCrypt { + fn get_base_offset(&self, hash: u32) -> u32 { + self.base.get_base_offset(hash) + } + fn inner_decrypt( + &self, + key: u32, + offset: u64, + buffer: &mut [u8], + pos: usize, + count: usize, + ) -> Result<()> { + if offset < 8 && count > 0 { + let mut key = self.get_key_from_hash(key); + key >>= offset << 3; + let first_chunk = count.min(8 - offset as usize); + for i in 0..first_chunk { + buffer[pos + i] ^= (key & 0xFF) as u8; + key >>= 8; + } + } + self.base.inner_decrypt(key, offset, buffer, pos, count) + } + fn decode( + &self, + key: u32, + offset: u64, + buffer: &mut [u8], + pos: usize, + count: usize, + ) -> Result<()> { + self.base.decode(key, offset, buffer, pos, count) + } +} +icx_enc_arc_impl!(RiddleCxCrypt); + +impl Crypt for Arc { + base_schema_impl!(); + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + default_init_crypt(archive)?; + read_yuzu_names( + archive, + &self.base.names_section_id, + |reader, unpacked_size| self.read_yuzu_names(reader, unpacked_size), + ) + } + 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 5aa8bf9..6e268b3 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -145,6 +145,18 @@ enum CryptType { random_seed: u32, yuz_key: Vec, }, + #[serde(rename_all = "PascalCase")] + RiddleCxCrypt { + #[serde(flatten)] + cx: CxSchema, + names_section_id: String, + random_seed: u32, + yuz_key: Vec, + #[serde(default)] + key1: u32, + #[serde(default)] + key2: u32, + }, } #[derive(Clone, Debug, Deserialize)] @@ -213,6 +225,23 @@ impl Schema { *random_seed, &yuz_key, )?), + CryptType::RiddleCxCrypt { + cx, + names_section_id, + random_seed, + yuz_key, + key1, + key2, + } => Box::new(cx::RiddleCxCrypt::new( + self.base.clone(), + cx, + filename, + names_section_id.clone(), + *random_seed, + &yuz_key, + *key1, + *key2, + )?), }) } }