From 31d0c5c9d0c00eb9b6343dd22c11f6999d8a5a29 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 8 Apr 2026 18:26:57 +0800 Subject: [PATCH] Add NanaCxCrypt --- msg_tool_xp3data/crypt.json | 84 ++++++ msg_tool_xp3data/cx_cb/grisaia_vol3.bin | Bin 0 -> 4096 bytes msg_tool_xp3data/cx_cb/grisaia_vol4.bin | Bin 0 -> 4096 bytes msg_tool_xp3data/cx_cb/haruoto.bin | Bin 0 -> 4096 bytes msg_tool_xp3data/cx_cb/momoiro.bin | Bin 0 -> 4096 bytes src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 274 ++++++++++++++---- src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 21 ++ 7 files changed, 329 insertions(+), 50 deletions(-) create mode 100644 msg_tool_xp3data/cx_cb/grisaia_vol3.bin create mode 100644 msg_tool_xp3data/cx_cb/grisaia_vol4.bin create mode 100644 msg_tool_xp3data/cx_cb/haruoto.bin create mode 100644 msg_tool_xp3data/cx_cb/momoiro.bin diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index f573fca..5ec667f 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -416,6 +416,48 @@ "ControlBlockName": "grisaia_vol2.bin", "Title": "グリザイア ファントムトリガー Vol. 2 | 灰色幻影扳机第2卷 | 灰色:幻影扳机 Vol. 2" }, + "Grisaia: Phantom Trigger Vol.3": { + "$type": "NanaCxCrypt", + "Mask": 501, + "Offset": 174, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "StartupTjsNotEncrypted": true, + "NamesSectionId": "dls:", + "RandomSeed": 2463534242, + "YuzKey": [ + 3868544114, + 706082328, + 1479507413, + 605919492, + 3150100769, + 4251163583 + ], + "ControlBlockName": "grisaia_vol3.bin", + "Title": "グリザイア ファントムトリガー Vol. 3 | 灰色幻影扳机第3卷 | 灰色:幻影扳机 Vol. 3" + }, + "Grisaia: Phantom Trigger Vol.4": { + "$type": "NanaCxCrypt", + "Mask": 501, + "Offset": 174, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "StartupTjsNotEncrypted": true, + "NamesSectionId": "dls:", + "RandomSeed": 2463534242, + "YuzKey": [ + 3868544114, + 706082328, + 1479507413, + 605919492, + 3150100769, + 4251163583 + ], + "ControlBlockName": "grisaia_vol4.bin", + "Title": "グリザイア ファントムトリガー Vol. 4 | 灰色幻影扳机第4卷 | 灰色:幻影扳机 Vol. 4" + }, "Grisaia: Phantom Trigger Vol.5": { "$type": "CxEncryption", "Mask": 389, @@ -476,6 +518,27 @@ "TpmFileName": "plugin/hatsujosw.tpm", "Title": "発情スイッチ ~美姉妹が催眠術に堕ちた先~" }, + "Haruoto Alice*Gram": { + "$type": "NanaCxCrypt", + "Mask": 572, + "Offset": 109, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "StartupTjsNotEncrypted": true, + "NamesSectionId": "dls:", + "RandomSeed": 2463534242, + "YuzKey": [ + 1406040976, + 3200797355, + 1926882100, + 1303657607, + 3795909009, + 676609281 + ], + "ControlBlockName": "haruoto.bin", + "Title": "春音アリス*グラム | 春音Alice*Gram" + }, "Haze Man -The Local Hero-": { "$type": "CxEncryption", "Mask": 419, @@ -1019,6 +1082,27 @@ "$type": "FlyingShineCrypt", "Title": "水恋~みずこい~" }, + "Momoiro Closet": { + "$type": "NanaCxCrypt", + "Mask": 408, + "Offset": 242, + "PrologOrder": "AAEC", + "OddBranchOrder": "AgUDBAEA", + "EvenBranchOrder": "AAIDAQUGBwQ=", + "StartupTjsNotEncrypted": true, + "NamesSectionId": "dls:", + "RandomSeed": 2463534242, + "YuzKey": [ + 1532499583, + 1492364298, + 3985974824, + 3368303250, + 1642426535, + 742252838 + ], + "ControlBlockName": "momoiro.bin", + "Title": "ももいろクローゼット" + }, "Momoiro Closet [English]": { "$type": "CxEncryption", "Mask": 389, diff --git a/msg_tool_xp3data/cx_cb/grisaia_vol3.bin b/msg_tool_xp3data/cx_cb/grisaia_vol3.bin new file mode 100644 index 0000000000000000000000000000000000000000..8788831eef34010575b833e537e1a526ac765b1e GIT binary patch literal 4096 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 literal 0 HcmV?d00001 diff --git a/msg_tool_xp3data/cx_cb/momoiro.bin b/msg_tool_xp3data/cx_cb/momoiro.bin new file mode 100644 index 0000000000000000000000000000000000000000..8047a750a6da6e9cbdafe553bcb0b650b25fac30 GIT binary patch literal 4096 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 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 0142552..7db855f 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -829,60 +829,67 @@ impl SenrenCxCrypt { names_section_id, }) } - fn read_yuzu_names(&self, archive: &mut Xp3Archive) -> Result<()> { - if let Some(section) = archive - .extras - .iter() - .find(|s| s.tag == self.names_section_id) + + fn read_yuzu_names( + reader: Box, + unpacked_size: u32, + ) -> Result<(HashMap, HashMap)> { + let mut decoded = MemWriter::with_capacity(unpacked_size as usize); { - let mut sreader = MemReaderRef::new(§ion.data); - let offset = sreader.read_u64()? + archive.base_offset; - let unpacked_size = sreader.read_u32()?; - let packed_size = sreader.read_u32()?; - let index_stream = - MutexWrapper::new(archive.inner.clone(), offset).take(packed_size as u64); - let mut decoded = MemWriter::from_vec(Vec::with_capacity(unpacked_size as usize)); - { - let mut decoder = flate2::read::ZlibDecoder::new(index_stream); - std::io::copy(&mut decoder, &mut decoded)?; - } - let decoded = decoded.into_inner(); - let mut reader = MemReader::new(decoded); - let mut hash_map = HashMap::new(); - let mut md5_map = HashMap::new(); - let mut dir_offset = 0u64; - while !reader.is_eof() { - let _entry_sign = reader.read_u32()?; - let mut entry_size = reader.read_u64()?; - dir_offset += 12 + entry_size; - let hash = reader.read_u32()?; - let name_len = reader.read_u16()?; - entry_size -= 6; - if (name_len as u64) * 2 <= entry_size { - let name = reader.read_exact_vec((name_len) as usize * 2)?; - let name = decode_to_string(Encoding::Utf16LE, &name, true)?; - if !hash_map.contains_key(&hash) { - hash_map.insert(hash, name.clone()); - } - let encoded = - encode_string(Encoding::Utf16LE, &name.to_ascii_lowercase(), true)?; - let md5 = format!("{:x}", md5::compute(encoded)); - md5_map.insert(md5, name); + let mut decoder = flate2::read::ZlibDecoder::new(reader); + std::io::copy(&mut decoder, &mut decoded)?; + } + let decoded = decoded.into_inner(); + let mut reader = MemReader::new(decoded); + let mut hash_map = HashMap::new(); + let mut md5_map = HashMap::new(); + let mut dir_offset = 0u64; + while !reader.is_eof() { + let _entry_sign = reader.read_u32()?; + let mut entry_size = reader.read_u64()?; + dir_offset += 12 + entry_size; + let hash = reader.read_u32()?; + let name_len = reader.read_u16()?; + entry_size -= 6; + if (name_len as u64) * 2 <= entry_size { + let name = reader.read_exact_vec((name_len) as usize * 2)?; + let name = decode_to_string(Encoding::Utf16LE, &name, true)?; + if !hash_map.contains_key(&hash) { + hash_map.insert(hash, name.clone()); } - reader.pos = dir_offset as usize; - md5_map.insert("$".into(), "startup.tjs".into()); + let encoded = encode_string(Encoding::Utf16LE, &name.to_ascii_lowercase(), true)?; + let md5 = format!("{:x}", md5::compute(encoded)); + md5_map.insert(md5, name); } - for entry in archive.entries.iter_mut() { - if let Some(name) = hash_map.get(&entry.file_hash) { - entry.name = name.clone(); - } else if let Some(name) = md5_map.get(&entry.name) { - entry.name = name.clone(); - } + reader.pos = dir_offset as usize; + md5_map.insert("$".into(), "startup.tjs".into()); + } + Ok((hash_map, md5_map)) + } +} + +fn read_yuzu_names(archive: &mut Xp3Archive, names_section_id: &str, convert: T) -> Result<()> +where + T: FnOnce(Box, u32) -> Result<(HashMap, HashMap)>, +{ + if let Some(section) = archive.extras.iter().find(|s| s.tag == names_section_id) { + let mut sreader = MemReaderRef::new(§ion.data); + let offset = sreader.read_u64()? + archive.base_offset; + let unpacked_size = sreader.read_u32()?; + let packed_size = sreader.read_u32()?; + let index_stream = + MutexWrapper::new(archive.inner.clone(), offset).take(packed_size as u64); + let (hash_map, md5_map) = convert(Box::new(index_stream), unpacked_size)?; + for entry in archive.entries.iter_mut() { + if let Some(name) = hash_map.get(&entry.file_hash) { + entry.name = name.clone(); + } else if let Some(name) = md5_map.get(&entry.name) { + entry.name = name.clone(); } } - archive.extras.retain(|s| s.tag != self.names_section_id); - Ok(()) } + archive.extras.retain(|s| s.tag != names_section_id); + Ok(()) } icx_enc_impl!(SenrenCxCrypt); @@ -892,7 +899,11 @@ impl Crypt for Arc { base_schema_impl!(); fn init(&self, archive: &mut Xp3Archive) -> Result<()> { default_init_crypt(archive)?; - self.read_yuzu_names(archive) + read_yuzu_names( + archive, + &self.names_section_id, + SenrenCxCrypt::read_yuzu_names, + ) } fn decrypt_supported(&self) -> bool { true @@ -1032,7 +1043,170 @@ impl Crypt for Arc { base_schema_impl!(); fn init(&self, archive: &mut Xp3Archive) -> Result<()> { default_init_crypt(archive)?; - self.base.read_yuzu_names(archive) + read_yuzu_names( + archive, + &self.base.names_section_id, + SenrenCxCrypt::read_yuzu_names, + ) + } + 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))) + } +} + +#[derive(Debug)] +struct NanaDecryptor { + state: [u32; 27], + seed: u64, +} + +impl NanaDecryptor { + fn new(key: &[u32], seed1: u32, seed2: u32) -> Self { + let mut state = [0u32; 27]; + let seed = (seed2 as u64) << 32 | (seed1 as u64); + let mut s = [0u32; 3]; + let mut k = key[0]; + s[0] = key[1]; + s[1] = key[2]; + s[2] = key[3]; + state[0] = k; + let mut dst = 1; + for i in 0..26usize { + let src = i % 3; + let m = s[src].rotate_right(8); + let n = (i as u32) ^ k.wrapping_add(m); + k = n ^ k.rotate_left(3); + state[dst] = k; + dst += 1; + s[src] = n; + } + Self { state, seed } + } + + fn decrypt(&self, data: &mut [u8]) { + let mut i = 0; + let mut offset = 0; + let mut length = data.len(); + while length > 0 { + offset += 1; + let mut key = self.transform_key(offset ^ self.seed); + let count = std::cmp::min(length, 8); + for _ in 0..count { + data[i] ^= (key & 0xFF) as u8; + key >>= 8; + i += 1; + } + length -= count; + } + } + + fn transform_key(&self, key: u64) -> u64 { + let mut lo = (key & 0xFFFFFFFF) as u32; + let mut hi = (key >> 32) as u32; + for i in 0..27 { + hi = hi.rotate_right(8); + hi = hi.wrapping_add(lo); + hi ^= self.state[i]; + lo = lo.rotate_left(3); + lo ^= hi; + } + (hi as u64) << 32 | (lo as u64) + } +} + +#[derive(Debug)] +pub struct NanaCxCrypt { + base: SenrenCxCrypt, + decryptor: NanaDecryptor, +} + +impl AsRef for NanaCxCrypt { + fn as_ref(&self) -> &BaseSchema { + self.base.as_ref() + } +} + +impl NanaCxCrypt { + pub fn new( + base: BaseSchema, + schema: &CxSchema, + filename: &str, + names_section_id: String, + random_seed: u32, + yuz_key: &[u32], + ) -> Result> { + if yuz_key.len() != 6 { + return Err(anyhow::anyhow!( + "Invalid Yuzu keys for NanaCxCrypt: expected 6, got {}", + yuz_key.len() + )); + } + let cx = SenrenCxCrypt::new_inner( + base, + schema, + filename, + Box::new(CxProgramNanaBuilder::new(random_seed)), + names_section_id, + )?; + let decryptor = NanaDecryptor::new(yuz_key, yuz_key[4], yuz_key[5]); + Ok(Arc::new(Self { + base: cx, + decryptor, + })) + } + + 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) + } +} + +icx_enc_impl!(NanaCxCrypt); +icx_enc_arc_impl!(NanaCxCrypt); + +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 diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index b1886b2..5aa8bf9 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -137,6 +137,14 @@ enum CryptType { names_section_id: String, random_seed: u32, }, + #[serde(rename_all = "PascalCase")] + NanaCxCrypt { + #[serde(flatten)] + cx: CxSchema, + names_section_id: String, + random_seed: u32, + yuz_key: Vec, + }, } #[derive(Clone, Debug, Deserialize)] @@ -192,6 +200,19 @@ impl Schema { names_section_id.clone(), *random_seed, )?), + CryptType::NanaCxCrypt { + cx, + names_section_id, + random_seed, + yuz_key, + } => Box::new(cx::NanaCxCrypt::new( + self.base.clone(), + cx, + filename, + names_section_id.clone(), + *random_seed, + &yuz_key, + )?), }) } }