diff --git a/Cargo.toml b/Cargo.toml index bbd8632..a418a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,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", "fastcdc", "flate2", "include-flate", "parse-size", "sha2", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "include-flate", "int-enum", "parse-size", "sha2", "utils-serde-base64bytes", "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/src/scripts/kirikiri/archive/xp3/crypt.json b/src/scripts/kirikiri/archive/xp3/crypt.json index 6433bf2..07a69ae 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt.json +++ b/src/scripts/kirikiri/archive/xp3/crypt.json @@ -23,6 +23,16 @@ "$type": "HashCrypt", "Title": "姉撮 ~堕ちていくお姉ちゃんを助けられない僕~" }, + "Aqua": { + "$type": "CxEncryption", + "Mask": 424, + "Offset": 1910, + "PrologOrder": "AAIB", + "OddBranchOrder": "AgAFAwEE", + "EvenBranchOrder": "AAEDBwQFBgI=", + "TpmFileName": "plugin/AQUA.tpm", + "Title": "AQUA" + }, "Baaba to Mama to no Choujuku Oyakodon": { "$type": "HashCrypt", "Title": "ばぁばとママとの超熟母娘丼 ~3世代での家庭内エッチ~ | 爱你深至夏海蔚蓝" diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs new file mode 100644 index 0000000..bb586a7 --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -0,0 +1,591 @@ +use super::*; +use anyhow::Result; +use std::path::PathBuf; +use std::sync::Weak; + +const S_CTL_BLOCK_SIGNATURE: &[u8] = b" Encryption control block"; + +#[derive(Debug)] +pub struct CxEncryption { + mask: u32, + offset: u32, + prolog_order: Vec, + odd_branch_order: Vec, + even_branch_order: Vec, + control_block: Arc>, + programs: Vec, +} + +impl CxEncryption { + pub fn new(schema: &CxSchema, filename: &str) -> Result> { + if schema.prolog_order.len() != 3 { + return Err(anyhow::anyhow!("Prolog order must have 3 elements")); + } + if schema.odd_branch_order.len() != 6 { + return Err(anyhow::anyhow!("Odd branch order must have 6 elements")); + } + if schema.even_branch_order.len() != 8 { + return Err(anyhow::anyhow!("Even branch order must have 8 elements")); + } + let control_block = if let Some(tpm_path) = &schema.tpm_file_name { + Self::read_tpm(tpm_path, filename)? + } else { + return Err(anyhow::anyhow!("TPM file name is required in schema")); + }; + let control_block = Arc::new(control_block); + let programs = Vec::with_capacity(0x80); + let mut obj = Self { + mask: schema.mask, + offset: schema.offset, + prolog_order: schema.prolog_order.bytes.clone(), + odd_branch_order: schema.odd_branch_order.bytes.clone(), + even_branch_order: schema.even_branch_order.bytes.clone(), + control_block: control_block, + programs, + }; + for seed in 0..0x80 { + obj.programs.push(obj.generate_program(seed)?); + } + Ok(Arc::new(obj)) + } + + fn new_program(&self, seed: u32) -> CxProgram { + CxProgram { + code: Vec::with_capacity(CX_PROGRAM_SIZE), + control_block: Arc::downgrade(&self.control_block), + length: 0, + seed, + } + } + + fn generate_program(&self, seed: u32) -> Result { + let mut program = self.new_program(seed); + for stage in (1..=5).rev() { + if self.emit_code(&mut program, stage) { + return Ok(program); + } + program.clear(); + } + Err(anyhow::anyhow!("Overly large CxEncryption bytecode")) + } + + fn read_tpm(tpm_path: &str, filename: &str) -> Result> { + let pfile = Self::get_tpm_path(tpm_path, filename)?; + let tpm = std::fs::read(&pfile)?; + let mut begin = 0; + let end = (tpm.len() - 0x1000) & !0x3; + while begin < end { + if &tpm[begin..begin + S_CTL_BLOCK_SIGNATURE.len()] == S_CTL_BLOCK_SIGNATURE { + let mut control_block = Vec::with_capacity(0x400); + let mut reader = MemReaderRef::new(&tpm[begin..]); + for _ in 0..0x400 { + control_block.push(!reader.read_u32()?); + } + return Ok(control_block); + } + begin += 4; + } + Err(anyhow::anyhow!( + "Control block signature not found in TPM file: {}", + pfile.display() + )) + } + + fn get_tpm_path(tpm_path: &str, filename: &str) -> Result { + let pb = PathBuf::from(filename); + let pdir = pb + .parent() + .ok_or_else(|| anyhow::anyhow!("Invalid TPM path"))?; + let pfile = pdir.join(tpm_path); + if pfile.is_file() { + return Ok(pfile); + } + let pfile = pdir.join("..").join(tpm_path); + if pfile.is_file() { + return Ok(pfile); + } + Err(anyhow::anyhow!("TPM file not found: {}", tpm_path)) + } + + fn emit_code(&self, program: &mut CxProgram, stage: i32) -> bool { + program.emit_nop(5) + && program.emit(MovEdiArg, 4) + && self.emit_body(program, stage) + && program.emit_nop(5) + && program.emit(Retn, 1) + } + + fn emit_body(&self, program: &mut CxProgram, stage: i32) -> bool { + if stage == 1 { + return self.emit_prolog(program); + } + if !program.emit(PushEbx, 1) { + return false; + } + if (program.get_random() & 1) != 0 { + if !self.emit_body(program, stage - 1) { + return false; + } + } else { + if !self.emit_body2(program, stage - 1) { + return false; + } + } + if !program.emit(MovEbxEax, 2) { + return false; + } + if (program.get_random() & 1) != 0 { + if !self.emit_body(program, stage - 1) { + return false; + } + } else { + if !self.emit_body2(program, stage - 1) { + return false; + } + } + self.emit_odd_branch(program) && program.emit(PopEbx, 1) + } + + fn emit_body2(&self, program: &mut CxProgram, stage: i32) -> bool { + if stage == 1 { + return self.emit_prolog(program); + } + let r = if (program.get_random() & 1) != 0 { + self.emit_body(program, stage - 1) + } else { + self.emit_body2(program, stage - 1) + }; + r && self.emit_even_branch(program) + } + fn emit_prolog(&self, program: &mut CxProgram) -> bool { + match self.prolog_order[(program.get_random() % 3) as usize] { + 2 => { + program.emit_nop(5) + && program.emit(MovEaxImmed, 2) + && { + let random = program.get_random() & 0x3ff; + program.emit_u32(random) + } + && program.emit(MovEaxIndirect, 0) + } + 1 => program.emit(MovEaxEdi, 2), + 0 => program.emit(MovEaxImmed, 1) && program.emit_random(), + _ => true, + } + } + + fn emit_even_branch(&self, program: &mut CxProgram) -> bool { + match self.even_branch_order[(program.get_random() & 7) as usize] { + 0 => program.emit(NotEax, 2), + 1 => program.emit(DecEax, 1), + 2 => program.emit(NegEax, 2), + 3 => program.emit(IncEax, 1), + 4 => { + program.emit_nop(5) + && program.emit(AndEaxImmed, 1) + && program.emit_u32(0x3ff) + && program.emit(MovEaxIndirect, 3) + } + 5 => { + program.emit(PushEbx, 1) + && program.emit(MovEbxEax, 2) + && program.emit(AndEbxImmed, 2) + && program.emit_u32(0xaaaaaaaa) + && program.emit(AndEaxImmed, 1) + && program.emit_u32(0x55555555) + && program.emit(ShrEbx1, 2) + && program.emit(ShlEax1, 2) + && program.emit(OrEaxEbx, 2) + && program.emit(PopEbx, 1) + } + 6 => program.emit(XorEaxImmed, 1) && program.emit_random(), + 7 => { + let mut r = if (program.get_random() & 1) != 0 { + program.emit(AddEaxImmed, 1) + } else { + program.emit(SubEaxImmed, 1) + }; + r = r && program.emit_random(); + r + } + _ => true, + } + } + + fn emit_odd_branch(&self, program: &mut CxProgram) -> bool { + match self.odd_branch_order[(program.get_random() % 6) as usize] { + 0 => { + program.emit(PushEcx, 1) + && program.emit(MovEcxEbx, 2) + && program.emit(AndEcx0F, 3) + && program.emit(ShrEaxCl, 2) + && program.emit(PopEcx, 1) + } + 1 => { + program.emit(PushEcx, 1) + && program.emit(MovEcxEbx, 2) + && program.emit(AndEcx0F, 3) + && program.emit(ShlEaxCl, 2) + && program.emit(PopEcx, 1) + } + 2 => program.emit(AddEaxEbx, 2), + 3 => program.emit(NegEax, 2) && program.emit(AddEaxEbx, 2), + 4 => program.emit(ImulEaxEbx, 3), + 5 => program.emit(SubEaxEbx, 2), + _ => true, + } + } + + fn get_base_offset(&self, hash: u32) -> u32 { + (hash & self.mask).wrapping_add(self.offset) + } + + fn execute_xcode(&self, mut hash: u32) -> Result<(u32, u32)> { + let seed = hash & 0x7f; + hash >>= 7; + let program = &self.programs[seed as usize]; + let ret1 = program.execute(hash)?; + let ret2 = program.execute(!hash)?; + Ok((ret1, ret2)) + } + + fn inner_decrypt( + &self, + mut key: u32, + mut offset: u64, + buffer: &mut [u8], + mut pos: usize, + mut count: usize, + ) -> Result<()> { + let base_offset = self.get_base_offset(key); + if offset < base_offset as u64 { + let base_length = ((base_offset as u64 - offset) as usize).min(count); + self.decode(key, offset, buffer, pos, base_length)?; + offset += base_length as u64; + pos += base_length; + count -= base_length; + } + if count > 0 { + key = (key >> 16) ^ key; + self.decode(key, offset, buffer, pos, count)?; + } + Ok(()) + } + + fn decode( + &self, + key: u32, + offset: u64, + buffer: &mut [u8], + pos: usize, + count: usize, + ) -> Result<()> { + let ret = self.execute_xcode(key)?; + let key1 = ret.1 >> 16; + let mut key2 = ret.1 & 0xffff; + let mut key3 = (ret.0 & 0xFF) as u8; + if key1 == key2 { + key2 = key2.wrapping_add(1); + } + if key3 == 0 { + key3 = 1; + } + if (key2 as u64) >= offset && (key2 as u64) < offset + (count as u64) { + buffer[pos + key2 as usize - offset as usize] ^= ((ret.0 >> 16) & 0xFF) as u8; + } + if (key1 as u64) >= offset && (key1 as u64) < offset + (count as u64) { + buffer[pos + key1 as usize - offset as usize] ^= ((ret.0 >> 8) & 0xFF) as u8; + } + for i in 0..count { + buffer[pos + i] ^= key3; + } + Ok(()) + } +} + +impl Crypt for Arc { + 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, self.clone()); + 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, self.clone()); + Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key))) + } +} + +const CX_PROGRAM_SIZE: usize = 0x80; + +#[derive(Debug)] +struct CxProgram { + code: Vec, + control_block: Weak>, + length: usize, + seed: u32, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, int_enum::IntEnum)] +enum CxByteCode { + Nop, + Retn, + MovEdiArg, + PushEbx, + PopEbx, + PushEcx, + PopEcx, + MovEaxEbx, + MovEbxEax, + MovEcxEbx, + MovEaxControlBlock, + MovEaxEdi, + MovEaxIndirect, + AddEaxEbx, + SubEaxEbx, + ImulEaxEbx, + AndEcx0F, + ShrEbx1, + ShlEax1, + ShrEaxCl, + ShlEaxCl, + OrEaxEbx, + NotEax, + NegEax, + DecEax, + IncEax, + Immed = 0x100, + MovEaxImmed, + AndEbxImmed, + AndEaxImmed, + XorEaxImmed, + AddEaxImmed, + SubEaxImmed, +} + +use CxByteCode::*; + +#[derive(Debug, Default)] +struct Context { + eax: u32, + ebx: u32, + ecx: u32, + edi: u32, + stack: Vec, +} + +impl CxProgram { + fn execute(&self, hash: u32) -> Result { + let mut context = Context::default(); + let mut iterator = self.code.iter(); + let mut immed = 0u32; + while let Some(code) = iterator.next() { + let code = *code; + const IMMED: u32 = Immed as u32; + if IMMED == (code & IMMED) { + immed = *iterator.next().ok_or_else(|| { + anyhow::anyhow!("Incomplete IMMED bytecode in CxEncryption program") + })?; + } + let bytecode = CxByteCode::try_from(code).map_err(|_| { + anyhow::anyhow!("Invalid bytecode in CxEncryption program: {:#X}", code) + })?; + match bytecode { + Nop => {} + Immed => {} + MovEdiArg => { + context.edi = hash; + } + PushEbx => { + context.stack.push(context.ebx); + } + PopEbx => { + context.ebx = context.stack.pop().ok_or_else(|| { + anyhow::anyhow!("Stack underflow in CxEncryption program") + })?; + } + PushEcx => { + context.stack.push(context.ecx); + } + PopEcx => { + context.ecx = context.stack.pop().ok_or_else(|| { + anyhow::anyhow!("Stack underflow in CxEncryption program") + })?; + } + MovEbxEax => { + context.ebx = context.eax; + } + MovEaxEdi => { + context.eax = context.edi; + } + MovEcxEbx => { + context.ecx = context.ebx; + } + MovEaxEbx => { + context.eax = context.ebx; + } + AndEcx0F => { + context.ecx &= 0x0F; + } + ShrEbx1 => { + context.ebx >>= 1; + } + ShlEax1 => { + context.eax <<= 1; + } + ShrEaxCl => { + context.eax >>= context.ecx; + } + ShlEaxCl => { + context.eax <<= context.ecx; + } + OrEaxEbx => { + context.eax |= context.ebx; + } + NotEax => { + context.eax = !context.eax; + } + NegEax => { + context.eax = context.eax.wrapping_neg(); + } + DecEax => { + context.eax = context.eax.wrapping_sub(1); + } + IncEax => { + context.eax = context.eax.wrapping_add(1); + } + AddEaxEbx => { + context.eax = context.eax.wrapping_add(context.ebx); + } + SubEaxEbx => { + context.eax = context.eax.wrapping_sub(context.ebx); + } + ImulEaxEbx => { + context.eax = context.eax.wrapping_mul(context.ebx); + } + AddEaxImmed => { + context.eax = context.eax.wrapping_add(immed); + } + SubEaxImmed => { + context.eax = context.eax.wrapping_sub(immed); + } + AndEbxImmed => { + context.ebx &= immed; + } + AndEaxImmed => { + context.eax &= immed; + } + XorEaxImmed => { + context.eax ^= immed; + } + MovEaxImmed => { + context.eax = immed; + } + MovEaxIndirect => { + let control_block = self + .control_block + .upgrade() + .ok_or_else(|| anyhow::anyhow!("Control block has been dropped"))?; + if context.eax as usize >= control_block.len() { + return Err(anyhow::anyhow!( + "Control block index out of bounds in CxEncryption program: {}", + context.eax + )); + } + context.eax = !control_block[context.eax as usize]; + } + Retn => { + if context.stack.len() != 0 { + return Err(anyhow::anyhow!( + "Stack not empty at RETN in CxEncryption program" + )); + } + return Ok(context.eax); + } + _ => { + return Err(anyhow::anyhow!( + "Unsupported bytecode in CxEncryption program: {:?}", + bytecode + )); + } + } + } + Err(anyhow::anyhow!( + "CxEncryption program without RETN bytecode" + )) + } + + fn clear(&mut self) { + self.length = 0; + self.code.clear(); + } + + fn emit_nop(&mut self, count: usize) -> bool { + if self.length + count > CX_PROGRAM_SIZE { + return false; + } + self.length += count; + return true; + } + + fn emit(&mut self, bytecode: CxByteCode, length: usize) -> bool { + if self.length + length > CX_PROGRAM_SIZE { + return false; + } + self.code.push(bytecode as u32); + self.length += length; + return true; + } + + fn emit_u32(&mut self, x: u32) -> bool { + if self.length + 4 > CX_PROGRAM_SIZE { + return false; + } + self.code.push(x); + self.length += 4; + 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); + self.seed ^ (seed << 16) ^ (seed >> 16) + } +} + +impl Read for CxEncryptionReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let offset = self.seg_start + self.pos; + let count = self.inner.read(buf)?; + if count == 0 { + return Ok(0); + } + let key = self.key.0; + let cx = &self.key.1; + if let Err(e) = cx.inner_decrypt(key, offset, buf, 0, count) { + return Err(std::io::Error::new(std::io::ErrorKind::Other, e)); + } + self.pos += count as u64; + Ok(count) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs similarity index 94% rename from src/scripts/kirikiri/archive/xp3/crypt.rs rename to src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 15c1687..ea050dd 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -1,12 +1,16 @@ +mod cx; + use super::archive::*; use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; +use crate::utils::serde_base64bytes::*; use anyhow::Result; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::io::{Read, Seek, SeekFrom}; +use std::sync::Arc; pub trait Crypt: std::fmt::Debug { /// Initializes the cryptographic context for the archive. @@ -57,6 +61,18 @@ pub trait Crypt: std::fmt::Debug { } } +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct CxSchema { + mask: u32, + offset: u32, + prolog_order: Base64Bytes, + odd_branch_order: Base64Bytes, + even_branch_order: Base64Bytes, + control_block_name: Option, + tpm_file_name: Option, +} + #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "PascalCase", tag = "$type")] enum CryptType { @@ -69,6 +85,7 @@ enum CryptType { key: u8, }, FlyingShineCrypt, + CxEncryption(CxSchema), } #[derive(Clone, Debug, Deserialize)] @@ -80,15 +97,16 @@ pub struct Schema { } impl Schema { - pub fn create_crypt(&self) -> Box { - match self.crypt { + pub fn create_crypt(&self, filename: &str) -> Result> { + Ok(match &self.crypt { CryptType::NoCrypt => Box::new(NoCrypt::new()), CryptType::FateCrypt => Box::new(FateCrypt::new()), CryptType::MizukakeCrypt => Box::new(MizukakeCrypt::new()), CryptType::HashCrypt => Box::new(HashCrypt::new()), - CryptType::XorCrypt { key } => Box::new(XorCrypt::new(key)), + CryptType::XorCrypt { key } => Box::new(XorCrypt::new(*key)), CryptType::FlyingShineCrypt => Box::new(FlyingShineCrypt::new()), - } + CryptType::CxEncryption(schema) => Box::new(cx::CxEncryption::new(&schema, filename)?), + }) } } @@ -483,6 +501,9 @@ impl Read for FlyingShineCryptReader { } } +// extended in cx.rs +seek_reader_key_impl!(CxEncryptionReader, (u32, Arc)); + #[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 5b70f87..6051be2 100644 --- a/src/scripts/kirikiri/archive/xp3/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -96,13 +96,17 @@ impl ScriptBuilder for Xp3ArchiveBuilder { fn build_script( &self, buf: Vec, - _filename: &str, + filename: &str, _encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { - Ok(Box::new(Xp3Archive::new(MemReader::new(buf), config)?)) + Ok(Box::new(Xp3Archive::new( + MemReader::new(buf), + config, + filename, + )?)) } fn build_script_from_file( @@ -114,19 +118,19 @@ impl ScriptBuilder for Xp3ArchiveBuilder { _archive: Option<&Box>, ) -> Result> { let file = std::fs::File::open(filename)?; - Ok(Box::new(Xp3Archive::new(file, config)?)) + Ok(Box::new(Xp3Archive::new(file, config, filename)?)) } fn build_script_from_reader( &self, reader: Box, - _filename: &str, + filename: &str, _encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { - Ok(Box::new(Xp3Archive::new(reader, config)?)) + Ok(Box::new(Xp3Archive::new(reader, config, filename)?)) } fn extensions(&self) -> &'static [&'static str] { @@ -164,8 +168,9 @@ impl Xp3Archive { pub fn new( stream: T, config: &ExtraConfig, + filename: &str, ) -> Result { - let mut archive = archive::Xp3Archive::new(stream, config)?; + let mut archive = archive::Xp3Archive::new(stream, config, filename)?; archive.entries.retain(|entry| { let i = &entry.name; !(i.find("$$$ This is a protected archive. $$$").is_some() diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index 2d439bc..963acb0 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -11,13 +11,14 @@ impl Xp3Archive { pub fn new( stream: T, _config: &ExtraConfig, + filename: &str, ) -> Result { let crypt: Box = if let Some(game_title) = &_config.xp3_game_title { query_crypt_schema(game_title) .ok_or_else(|| { anyhow::anyhow!("Unsupported game title for XP3 archive: {}", game_title) })? - .create_crypt() + .create_crypt(filename)? } else { Box::new(NoCrypt::new()) };