From d3eef3548811699706b478bde92917bb98b5d9a0 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 7 May 2026 21:13:43 +0800 Subject: [PATCH] Allow multiple index keys (lllj PKG and DL have different index key) --- msg_tool_xp3data/crypt.json | 25 ++++++ msg_tool_xp3data/cx_cb/limelight.bin | Bin 0 -> 4096 bytes src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 80 ++++++++++++++++-- src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 2 +- 4 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 msg_tool_xp3data/cx_cb/limelight.bin diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index ae57b37..97e8d4e 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -1536,6 +1536,31 @@ "$type": "HashCrypt", "Title": "黒ギャル娘との淫欲相姦~翻弄される父親の性欲~" }, + "Limelight Lemonade Jam": { + "$type": "HxCrypt", + "Mask": 738, + "Offset": 643, + "PrologOrder": "AQAC", + "OddBranchOrder": "AQIEAwAF", + "EvenBranchOrder": "AgUABwYBAwQ=", + "IndexKey1": { + "Key": "fMktWafCUSPGVDvR/8LUx9f+yh3Y+PIq90XmzJ6xZhI=", + "Nonce": "aDnPYFowPzhVdOsfJweaYA==" + }, + "IndexKey2": [ + { + "Key": "/IZHSi5vZTrAxMC4OyQ/+yminq7Uz5yotLa47H1QGI4=", + "Nonce": "GiXPX9XR1GyQIMCSLOIy5w==" + }, + { + "Key": "kHMdDweFjeDFVTwQA10XQAPUAOfXzKp2ukygeLzGzHg=", + "Nonce": "CSBjzSXQNTioPhDp710WCQ==" + } + ], + "FilterKey": 8093658935271803197, + "ControlBlockName": "limelight.bin", + "Title": "ライムライト・レモネードジャム | 聚光灯下的青柠恋曲 | 橘光柠水随想曲" + }, "Little Hole": { "$type": "HashCrypt", "Title": "りとるほーる ~俺の娘がこんなに……なハズがない | Littlehole~我的女儿不可能这么……" diff --git a/msg_tool_xp3data/cx_cb/limelight.bin b/msg_tool_xp3data/cx_cb/limelight.bin new file mode 100644 index 0000000000000000000000000000000000000000..1f8edfc799cc3c79a9a3b32f104439d3c9ca0deb 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 7e1bcf1..bf2bcf2 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -6,7 +6,7 @@ use anyhow::Result; use chacha20::ChaCha20Legacy; use serde::{Deserializer, de}; use std::collections::HashSet; -use std::ops::Index; +use std::ops::{Deref, DerefMut, Index}; use std::path::PathBuf; use std::sync::{Mutex, Weak}; @@ -2031,7 +2031,7 @@ impl std::fmt::Debug for CxdecDb { pub struct HxCrypt { base: CxEncryption, key1: IndexKey, - key2: IndexKey, + key2: IndexKeys, filter_key: u64, file_mapping: HashMap, path_mapping: HashMap, @@ -2085,12 +2085,48 @@ impl<'de> Deserialize<'de> for IndexKey { } } +#[derive(Clone, Debug)] +pub struct IndexKeys(pub Vec); + +impl Deref for IndexKeys { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for IndexKeys { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum IndexKeysTmp { + List(Vec), + Single(IndexKey), +} + +impl<'de> Deserialize<'de> for IndexKeys { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let tmp = IndexKeysTmp::deserialize(deserializer)?; + Ok(match tmp { + IndexKeysTmp::List(list) => Self(list), + IndexKeysTmp::Single(one) => Self(vec![one]), + }) + } +} + impl HxCrypt { pub fn new( base: BaseSchema, cx: &CxSchema, index_key1: &IndexKey, - index_key2: &IndexKey, + index_key2: &IndexKeys, filter_key: u64, random_type: i32, file_list_name: Option<&str>, @@ -2216,10 +2252,13 @@ impl HxCrypt { Ok((file_map, path_map)) } - fn create_chacha20_crypt(&self, flags: u16) -> Result { + fn create_chacha20_crypt(&self, flags: u16, key_index: usize) -> Result { use chacha20::{KeyIvInit, cipher::StreamCipherSeek}; let key = match flags { - 0 => &self.key2, + 0 => self + .key2 + .get(key_index) + .ok_or_else(|| anyhow::anyhow!("Index out of bound"))?, 1 => &self.key1, _ => anyhow::bail!("Unknown hxv4 flags: {}", flags), }; @@ -2230,10 +2269,16 @@ impl HxCrypt { Ok(crypt) } - fn read_index(&self, mut stream: T, flags: u16) -> Result<()> { + fn read_index_stream_internel( + &self, + stream: &mut T, + flags: u16, + key_index: usize, + ) -> Result { use chacha20::cipher::StreamCipher; + stream.rewind()?; let len = stream.stream_length()?; - let mut crypt = self.create_chacha20_crypt(flags)?; + let mut crypt = self.create_chacha20_crypt(flags, key_index)?; let tlen = len as usize - 16; let mut buf = Vec::with_capacity(tlen); stream.seek(SeekFrom::Start(16))?; @@ -2242,7 +2287,26 @@ impl HxCrypt { let mut stream = flate2::read::ZlibDecoder::new(MemReaderRef::new(&buf[4..])); let mut buf = Vec::new(); stream.read_to_end(&mut buf)?; - let mut reader = MemReader::new(buf); + Ok(MemReader::new(buf)) + } + + fn read_index_stream(&self, mut stream: T, flags: u16) -> Result { + if flags != 0 { + self.read_index_stream_internel(&mut stream, flags, 0) + } else if self.key2.len() == 1 { + self.read_index_stream_internel(&mut stream, flags, 0) + } else { + for i in 0..self.key2.len() { + if let Ok(reader) = self.read_index_stream_internel(&mut stream, flags, i) { + return Ok(reader); + } + } + anyhow::bail!("All index key decrypt failed.") + } + } + + fn read_index(&self, stream: T, flags: u16) -> Result<()> { + let mut reader = self.read_index_stream(stream, flags)?; let root_obj = TjsValue::unpack(&mut reader, true, Encoding::Utf16LE, &None)?; if !root_obj.is_array() { anyhow::bail!("Index object is not an array."); diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 71bae32..0842197 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -289,7 +289,7 @@ enum CryptType { #[serde(flatten)] cx: CxSchema, index_key1: cx::IndexKey, - index_key2: cx::IndexKey, + index_key2: cx::IndexKeys, filter_key: u64, #[serde(default)] random_type: i32,