diff --git a/src/scripts/kirikiri/archive/xp3/crypt.json b/src/scripts/kirikiri/archive/xp3/crypt.json index 93df38b..c667a3b 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt.json +++ b/src/scripts/kirikiri/archive/xp3/crypt.json @@ -310,7 +310,8 @@ "ControlBlockName": "fate_hollow.bin" }, "Fate/stay night": { - "$type": "FateCrypt" + "$type": "FateCrypt", + "HashAfterCrypt": true }, "Fukashi na Aijou": { "$type": "HashCrypt", @@ -738,7 +739,8 @@ "OddBranchOrder": "BAABBQID", "EvenBranchOrder": "BwUCAwYBBAA=", "TpmFileName": "plugin/lavender.tpm", - "Title": "光輪の町、ラベンダーの少女" + "Title": "光輪の町、ラベンダーの少女", + "StartupTjsNotEncrypted": true }, "Kurenai no Tsuki": { "$type": "CxEncryption", @@ -833,7 +835,8 @@ }, "Mizu no Kakera ~Once Summer of Islet~": { "$type": "MizukakeCrypt", - "Title": "みずのかけら -once summer of islet-" + "Title": "みずのかけら -once summer of islet-", + "HashAfterCrypt": true }, "Mizu no Miyako no Patisserie": { "$type": "CxEncryption", @@ -1035,7 +1038,8 @@ "OddBranchOrder": "AAIEAQUD", "EvenBranchOrder": "BQMGAgEHBAA=", "ControlBlockName": "riajuu.bin", - "Title": "リア充催眠~リアルが充実する催眠生活はじめました。~" + "Title": "リア充催眠~リアルが充実する催眠生活はじめました。~", + "ObfuscatedIndex": true }, "Rui wa Tomo o Yobu": { "$type": "CxEncryption", @@ -1195,7 +1199,8 @@ }, "Sorairo no Shizuku": { "$type": "MizukakeCrypt", - "Title": "そらいろの雫" + "Title": "そらいろの雫", + "HashAfterCrypt": true }, "Soukan Chiryou Byoutou": { "$type": "FlyingShineCrypt", @@ -1429,7 +1434,8 @@ "OddBranchOrder": "AAQDAQIF", "EvenBranchOrder": "AwQABQcCAQY=", "ControlBlockName": "zecchou.bin", - "Title": "ぜっちょースパイラル!! ~下宿人はわがままエッチな女の子ばかり~" + "Title": "ぜっちょースパイラル!! ~下宿人はわがままエッチな女の子ばかり~", + "ObfuscatedIndex": true }, "Zettai Karen! Ojou-sama": { "$type": "CxEncryption", diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index 359ca4e..96b29c3 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -5,6 +5,20 @@ use std::sync::Weak; const S_CTL_BLOCK_SIGNATURE: &[u8] = b" Encryption control block"; +macro_rules! base_schema_impl { + () => { + 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 + } + }; +} + #[derive(Debug)] pub struct CxEncryption { mask: u32, @@ -14,10 +28,11 @@ pub struct CxEncryption { even_branch_order: Vec, control_block: Arc>, programs: Vec, + base: BaseSchema, } impl CxEncryption { - pub fn new(schema: &CxSchema, filename: &str) -> Result> { + pub fn new(base: BaseSchema, schema: &CxSchema, filename: &str) -> Result> { if schema.prolog_order.len() != 3 { return Err(anyhow::anyhow!("Prolog order must have 3 elements")); } @@ -47,6 +62,7 @@ impl CxEncryption { let control_block = Arc::new(control_block); let programs = Vec::with_capacity(0x80); let mut obj = Self { + base, mask: schema.mask, offset: schema.offset, prolog_order: schema.prolog_order.bytes.clone(), @@ -316,6 +332,7 @@ impl CxEncryption { } impl Crypt for Arc { + base_schema_impl!(); 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 15f850b..dd46e43 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -37,6 +37,19 @@ pub fn default_init_crypt(archive: &mut Xp3Archive) -> Result<()> { } pub trait Crypt: std::fmt::Debug { + #[allow(dead_code)] + /// whether Adler32 checksum should be calculated after contents have been encrypted. + fn hash_after_crypt(&self) -> bool; + + /// whether the startup.tjs script is not encrypted even when the archive is encrypted. + fn startup_tjs_not_encrypted(&self) -> bool; + + /// whether XP3 index is obfuscated: + /// - duplicate entries + /// - entries have additional dummy segments + #[allow(dead_code)] + fn obfuscated_index(&self) -> bool; + /// Initializes the cryptographic context for the archive. fn init(&self, archive: &mut Xp3Archive) -> Result<()> { default_init_crypt(archive) @@ -112,24 +125,36 @@ enum CryptType { CxEncryption(CxSchema), } +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct BaseSchema { + hash_after_crypt: bool, + startup_tjs_not_encrypted: bool, + obfuscated_index: bool, +} + #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Schema { #[serde(flatten)] crypt: CryptType, title: Option, + #[serde(flatten)] + base: BaseSchema, } impl Schema { 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::FlyingShineCrypt => Box::new(FlyingShineCrypt::new()), - CryptType::CxEncryption(schema) => Box::new(cx::CxEncryption::new(&schema, filename)?), + CryptType::FateCrypt => Box::new(FateCrypt::new(self.base.clone())), + CryptType::MizukakeCrypt => Box::new(MizukakeCrypt::new(self.base.clone())), + CryptType::HashCrypt => Box::new(HashCrypt::new(self.base.clone())), + CryptType::XorCrypt { key } => Box::new(XorCrypt::new(self.base.clone(), *key)), + CryptType::FlyingShineCrypt => Box::new(FlyingShineCrypt::new(self.base.clone())), + CryptType::CxEncryption(schema) => { + Box::new(cx::CxEncryption::new(self.base.clone(), &schema, filename)?) + } }) } } @@ -205,7 +230,17 @@ impl NoCrypt { } } -impl Crypt for NoCrypt {} +impl Crypt for NoCrypt { + fn hash_after_crypt(&self) -> bool { + false + } + fn startup_tjs_not_encrypted(&self) -> bool { + false + } + fn obfuscated_index(&self) -> bool { + false + } +} macro_rules! seek_impl { ($reader:ident<$t:ident>) => { @@ -280,16 +315,33 @@ macro_rules! seek_reader_key_impl { }; } +macro_rules! base_schema_impl { + () => { + 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 + } + }; +} + macro_rules! seek_crypt_base_impl { ($crypt:ident, $reader:ident) => { #[derive(Debug)] - pub struct $crypt {} + pub struct $crypt { + base: BaseSchema, + } impl $crypt { - pub fn new() -> Self { - Self {} + pub fn new(base: BaseSchema) -> Self { + Self { base } } } impl Crypt for $crypt { + base_schema_impl!(); fn decrypt_supported(&self) -> bool { true } @@ -368,15 +420,18 @@ impl Read for MizukakeCryptReader { } #[derive(Debug)] -pub struct HashCrypt {} +pub struct HashCrypt { + base: BaseSchema, +} impl HashCrypt { - pub fn new() -> Self { - Self {} + pub fn new(base: BaseSchema) -> Self { + Self { base } } } impl Crypt for HashCrypt { + base_schema_impl!(); fn decrypt_supported(&self) -> bool { true } @@ -424,16 +479,18 @@ impl Read for HashCryptReader { #[derive(Debug)] pub struct XorCrypt { + base: BaseSchema, key: u8, } impl XorCrypt { - pub fn new(key: u8) -> Self { - Self { key } + pub fn new(base: BaseSchema, key: u8) -> Self { + Self { base, key } } } impl Crypt for XorCrypt { + base_schema_impl!(); fn decrypt_supported(&self) -> bool { true } @@ -472,11 +529,13 @@ impl Read for XorCryptReader { } #[derive(Debug)] -pub struct FlyingShineCrypt {} +pub struct FlyingShineCrypt { + base: BaseSchema, +} impl FlyingShineCrypt { - pub fn new() -> Self { - Self {} + pub fn new(base: BaseSchema) -> Self { + Self { base } } fn adjust(&self, hash: u32) -> (u8, u32) { @@ -493,6 +552,7 @@ impl FlyingShineCrypt { } impl Crypt for FlyingShineCrypt { + base_schema_impl!(); fn decrypt_supported(&self) -> bool { true } diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index 8345f14..94e7e05 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -134,7 +134,7 @@ impl Xp3Archive { index_stream.skip(chunk_size)?; } } - entries.push(Xp3Entry { + let mut entry = Xp3Entry { name: name .ok_or_else(|| anyhow::anyhow!("Missing name chunk in file entry"))?, flags: flags @@ -149,7 +149,14 @@ impl Xp3Archive { timestamp, segments, extras: entry_extras, - }); + }; + if entry.name == "startup.tjs" + && entry.flags != 0 + && crypt.startup_tjs_not_encrypted() + { + entry.flags = 0; + } + entries.push(entry); } else { let data = index_stream.read_exact_vec(size as usize)?; extras.push(ExtraProp {