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 0000000..8788831 Binary files /dev/null and b/msg_tool_xp3data/cx_cb/grisaia_vol3.bin differ diff --git a/msg_tool_xp3data/cx_cb/grisaia_vol4.bin b/msg_tool_xp3data/cx_cb/grisaia_vol4.bin new file mode 100644 index 0000000..8788831 Binary files /dev/null and b/msg_tool_xp3data/cx_cb/grisaia_vol4.bin differ diff --git a/msg_tool_xp3data/cx_cb/haruoto.bin b/msg_tool_xp3data/cx_cb/haruoto.bin new file mode 100644 index 0000000..b2d6776 Binary files /dev/null and b/msg_tool_xp3data/cx_cb/haruoto.bin differ diff --git a/msg_tool_xp3data/cx_cb/momoiro.bin b/msg_tool_xp3data/cx_cb/momoiro.bin new file mode 100644 index 0000000..8047a75 Binary files /dev/null and b/msg_tool_xp3data/cx_cb/momoiro.bin differ 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, + )?), }) } }