diff --git a/Cargo.lock b/Cargo.lock index f8b2b1e..5dc2ecd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -156,6 +167,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -238,6 +258,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.59" @@ -278,10 +307,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "clap" version = "3.2.25" @@ -424,6 +463,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -482,6 +530,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" @@ -573,7 +631,7 @@ checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer", "const-oid", - "crypto-common", + "crypto-common 0.2.1", ] [[package]] @@ -824,6 +882,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" @@ -1138,6 +1206,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "int-enum" version = "1.2.0" @@ -1416,11 +1494,13 @@ name = "msg_tool" version = "0.3.1" dependencies = [ "adler", + "aes", "anyhow", "base64", "block_compression", "byteorder", "bytes", + "cbc", "clap 4.6.0", "crc32fast", "csv", @@ -2008,7 +2088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "digest", ] @@ -2019,7 +2099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "digest", ] diff --git a/Cargo.toml b/Cargo.toml index 4e8597e..8be3b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,13 @@ exclude = [".github", "*.py", "AGENTS.md"] [dependencies] adler = { version = "1", optional = true } +aes = { version = "0.8", optional = true } anyhow = "1" base64 = { version = "0.22", 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.1", optional = true } clap = { version = "4.5", features = ["derive"] } crc32fast = { version = "1.5", optional = true } csv = "1.3" @@ -95,7 +97,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", "bytes", "fastcdc", "flate2", "int-enum", "md5", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "aes", "bytes", "cbc", "fastcdc", "flate2", "int-enum", "md5", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "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 6c18e26..aeb8194 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -1256,6 +1256,10 @@ "ControlBlockName": "maitetsu_last_run.bin", "Title": "まいてつ Last Run!! | 爱上火车-Last Run!!- | 愛上火車-Last Run!!-" }, + "Make Me Lover": { + "$type": "KissCrypt", + "Title": "メイクMeラバー" + }, "Mama no Okugai Shuuchi Lesson": { "$type": "HashCrypt", "Title": "ママの屋外羞恥レッスン ~視線が快楽に変る時…~" diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cz.rs b/src/scripts/kirikiri/archive/xp3/crypt/cz.rs new file mode 100644 index 0000000..d28ec9d --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/crypt/cz.rs @@ -0,0 +1,174 @@ +use super::*; +use aes::Aes128Dec; +use aes::cipher::{BlockDecryptMut, KeyIvInit}; +use cbc::Decryptor; + +type Aes128CbcDec = Decryptor; + +const CZ_MAGIC: &[u8; 4] = b"\xFD\xD7\x90\xA5"; +const CZ_IV_SEED: u32 = 0xBFBFBFBF; +const CZ_HEADER_KEY: &[u8; 4] = b"\x9D\x1D\x9A\xF2"; +const CZ_DEFAULT_KEY: &[u8] = b"\x91\x10\xfcuE\x8f\xb5\xe6\xfe\xac\xbaDvX\xc2\x1a"; + +fn cz_decrypt_int(data: &[u8], offset: usize, key: u8) -> u32 { + let mut v: u32 = (data[offset] ^ key ^ CZ_HEADER_KEY[0]) as u32; + v |= ((data[offset + 1] ^ key ^ CZ_HEADER_KEY[1]) as u32) << 8; + v |= ((data[offset + 2] ^ key ^ CZ_HEADER_KEY[2]) as u32) << 16; + v |= ((data[offset + 3] ^ key ^ CZ_HEADER_KEY[3]) as u32) << 24; + v +} + +fn cz_create_iv(seed: u32) -> [u8; 16] { + let mut state = [0u32; 4]; + state[0] = 123456789; + state[1] = 972436830; + state[2] = 524018621; + state[3] = seed; + let mut iv = [0u8; 16]; + for i in 0..16 { + let a = state[3]; + let b = state[0] ^ (state[0] << 11); + state[0] = state[1]; + state[1] = state[2]; + state[2] = a; + state[3] = b ^ a ^ ((b ^ (a >> 11)) >> 8); + iv[i] = state[3] as u8; + } + iv +} + +#[derive(Debug)] +struct AesDecryptor { + aes: Aes128CbcDec, + entry: StreamRegion, + pos: u64, + original_size: u64, +} + +impl AesDecryptor { + fn new( + aes: Aes128CbcDec, + entry: StreamRegion, + original_size: u64, + ) -> AlignedReader<16, Self> { + AlignedReader::new(Self { + aes, + entry, + pos: 0, + original_size, + }) + } +} + +impl Read for AesDecryptor { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.entry.read_most(buf)?; + if readed % 16 != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Not enough data to decrypt", + )); + } + // NoPadding + for i in (0..readed).step_by(16) { + let block = &mut buf[i..i + 16]; + self.aes.decrypt_block_mut(block.into()); + } + let remaining = self.original_size - self.pos; + let readed = readed.min(remaining as usize); + self.pos += readed as u64; + Ok(readed) + } +} + +#[derive(Debug)] +pub struct KissCrypt { + base: BaseSchema, +} + +impl KissCrypt { + pub fn new(base: BaseSchema) -> Self { + Self { base } + } +} + +impl Crypt for KissCrypt { + fn hash_after_crypt(&self) -> bool { + self.base.hash_after_crypt + } + fn startup_tjs_not_encrypted(&self) -> bool { + self.base.startup_tjs_not_encrypted + } + fn obfuscated_index(&self) -> bool { + self.base.obfuscated_index + } + fn need_filter(&self, _filename: &str, buf: &[u8], buf_len: usize) -> bool { + buf_len >= 4 && buf.starts_with(CZ_MAGIC) + } + fn filter(&self, mut entry: Entry) -> Result> { + let mut header = [0u8; 15]; + entry.read_exact(&mut header)?; + let typ = [header[4] ^ 0x11, header[5] ^ 0x7F, header[6] ^ 0x9A]; + let key = typ[0]; + let _unpacked_size = cz_decrypt_int(&header, 7, key); + let packed_size = cz_decrypt_int(&header, 11, key); + if (packed_size as u64) < entry.index.original_size && (packed_size - 5) & 0xF == 0 { + let padded_size = packed_size - 5; + let original_size = padded_size + - (entry.peek_u8_at(15 + padded_size as u64 + 1)? + ^ entry.peek_u8_at(15 + padded_size as u64)?) as u32; + let iv_seed = entry.peek_u32_at(15 + padded_size as u64 + 1)? ^ CZ_IV_SEED; + let aes = Aes128CbcDec::new(CZ_DEFAULT_KEY.into(), &cz_create_iv(iv_seed).into()); + let entry = StreamRegion::with_size(entry, padded_size as u64)?; + let stream = AesDecryptor::new(aes, entry, original_size as u64); + if typ[0] == b'C' { + let stream = flate2::read::ZlibDecoder::new(stream); + return Ok(Box::new(stream)); + } + Ok(Box::new(stream)) + } else { + Ok(Box::new(entry)) + } + } + 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 ^ (entry.file_hash >> 19) ^ 0x4A9EEFF0; + Ok(Box::new(KissCryptReader::new(stream, cur_seg, key))) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + let key = entry.file_hash ^ (entry.file_hash >> 19) ^ 0x4A9EEFF0; + Ok(Box::new(KissCryptReader::new(stream, cur_seg, key))) + } +} + +impl Read for KissCryptReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.inner.read(buf)?; + let offset = self.seg_start + self.pos; + let mut i = 0usize; + while (i as u64 + offset) & 0xF != 0 { + i += 1; + } + while i < readed { + buf[i] ^= (self.key ^ (offset as u32 + i as u32)) as u8; + i += 0x10; + } + self.pos += readed as u64; + Ok(readed) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 37fdebf..76a6b62 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -1,5 +1,7 @@ mod cx; +mod cz; +use super::Entry; use super::archive::*; use crate::ext::io::*; use crate::scripts::base::*; @@ -99,6 +101,30 @@ pub trait Crypt: std::fmt::Debug { fn decrypt_seek_supported(&self) -> bool { false } + + /// Determine whether the file with the given name and content need to be extra processed after decryption. (e.g. extra decryption by file type) + fn need_filter(&self, _filename: &str, _buf: &[u8], _buf_len: usize) -> bool { + false + } + + /// Returns true if this crypt support seek when filtering + fn filter_seek_supported(&self) -> bool { + false + } + + /// Apply extra processing to the decrypted content of the file. + fn filter(&self, _entry: Entry) -> Result> { + Err(anyhow::anyhow!( + "This crypt does not support content filter after decrypt" + )) + } + + /// Apply extra processing to the decrypted content of the file, with seek support. + fn filter_with_seek(&self, _entry: Entry) -> Result> { + Err(anyhow::anyhow!( + "This crypt does not support content filter with seek after decrypt" + )) + } } #[derive(Clone, Debug, Deserialize)] @@ -189,6 +215,7 @@ enum CryptType { }, YuzuCrypt, HighRunningCrypt, + KissCrypt, } #[derive(Clone, Debug, Deserialize)] @@ -303,6 +330,7 @@ impl Schema { )), CryptType::YuzuCrypt => Box::new(YuzuCrypt::new(self.base.clone())), CryptType::HighRunningCrypt => Box::new(HighRunningCrypt::new(self.base.clone())), + CryptType::KissCrypt => Box::new(cz::KissCrypt::new(self.base.clone())), }) } } @@ -1345,6 +1373,8 @@ impl Read for HighRunningCryptReader { } } +seek_reader_key_impl!(KissCryptReader, u32); + #[test] fn test_deserialize_crypt() { for (key, schema) in CRYPT_SCHEMA.iter() { diff --git a/src/scripts/kirikiri/archive/xp3/mod.rs b/src/scripts/kirikiri/archive/xp3/mod.rs index db4c28b..cdc24cb 100644 --- a/src/scripts/kirikiri/archive/xp3/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -255,6 +255,35 @@ impl Script for Xp3Archive { let header_len = entry.read(&mut header)?; entry.rewind()?; entry.script_type = detect_script_type(&entry.index.name, &header, header_len); + if self + .archive + .crypt + .need_filter(&entry.index.name, &header, header_len) + { + if self.archive.crypt.filter_seek_supported() { + let index = entry.index.clone(); + let mut result = self.archive.crypt.filter_with_seek(entry)?; + let header_len = result.read(&mut header)?; + result.rewind()?; + let script_type = detect_script_type(&index.name, &header, header_len); + return Ok(Box::new(CustomFilterWithSeekEntry { + inner: result, + index, + script_type, + })); + } else { + let index = entry.index.clone(); + let mut result = self.archive.crypt.filter(entry)?; + let header_len = result.read(&mut header)?; + let script_type = detect_script_type(&index.name, &header, header_len); + let prefix = header[..header_len].to_vec(); + return Ok(Box::new(CustomFilterEntry { + inner: PrefixStream::new(prefix, result), + index, + script_type, + })); + } + } if self.decrypt_simple_crypt && header_len >= 5 && header[0] == 0xFE @@ -780,3 +809,67 @@ impl Read for MdfEntry { self.inner.read(buf) } } + +#[derive(Debug)] +struct CustomFilterEntry { + inner: PrefixStream>, + index: archive::Xp3Entry, + script_type: Option, +} + +impl Read for CustomFilterEntry { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl ArchiveContent for CustomFilterEntry { + fn name(&self) -> &str { + &self.index.name + } + + fn script_type(&self) -> Option<&ScriptType> { + self.script_type.as_ref() + } +} + +#[derive(Debug)] +struct CustomFilterWithSeekEntry { + inner: Box, + index: archive::Xp3Entry, + script_type: Option, +} + +impl Read for CustomFilterWithSeekEntry { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner.read(buf) + } +} + +impl Seek for CustomFilterWithSeekEntry { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.inner.seek(pos) + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.inner.rewind() + } + + fn stream_position(&mut self) -> std::io::Result { + self.inner.stream_position() + } +} + +impl ArchiveContent for CustomFilterWithSeekEntry { + fn name(&self) -> &str { + &self.index.name + } + + fn script_type(&self) -> Option<&ScriptType> { + self.script_type.as_ref() + } + + fn to_data<'a>(&'a mut self) -> Result> { + Ok(Box::new(self)) + } +}