From dfa9410b2c3429b249dd63b8fe20cd855708a108 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 2 May 2026 13:54:39 +0800 Subject: [PATCH] Add NekoWorksCrypt (tested gamehttps://vndb.org/v32811) --- msg_tool_xp3data/crypt.json | 45 ++++++++++ src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 83 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index 2d91fa1..5d8bf26 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -500,6 +500,21 @@ "$type": "HashCrypt", "Title": "不可視な愛情~透明なボクに感じるママ~" }, + "Futamata Ren'ai": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "フタマタ恋愛 | 恋爱成双 | 劈腿之恋" + }, + "Futamata Ren'ai - Rui & Miyako Mini After Story": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "フタマタ恋愛 瑠衣&宮子ミニアフターストーリー | 恋爱成双 瑠衣&宫子 mini小剧场" + }, + "Futamata Ren'ai - Yua & Kirame Mini After Story": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "フタマタ恋愛 結愛&煌ミニアフターストーリー | 恋爱成双 结爱&煌 mini小剧场" + }, "Gakuen Butou no Folklore": { "$type": "NephriteCrypt", "Title": "学園舞闘のフォークロア" @@ -1151,6 +1166,11 @@ "$type": "SourireCrypt", "Title": "恋する彼女の不器用な舞台 | 恋爱中的她的笨拙舞台" }, + "Koibana Ren'ai": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "Title": "コイバナ恋愛 | 八卦恋爱" + }, "Koikishi Purely ☆ Kiss": { "$type": "CxEncryption", "Mask": 561, @@ -1831,6 +1851,21 @@ "TpmFileName": "plugin/relations.tpm", "Title": "リレーションズ シスター×シスター | リレーションズ シスター・シスター" }, + "Ren'ai x Royale": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "恋愛×ロワイアル | 恋爱×决胜战 | 恋爱×争夺战" + }, + "Ren'ai x Royale - Nonoka & Renna & Yuna Mini After Story": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "恋愛×ロワイアル 乃々香&蓮菜&由奈 ミニアフターストーリー | 恋爱×决胜战 乃乃香&莲菜&由奈小故事" + }, + "Ren'ai, Karichaimashita": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "恋愛、借りちゃいました | 恋爱,我就借走了 | 想谈个恋爱吗?那就给钱吧 | 恋爱,我就借走咯" + }, "RGH ~Koi to Hero to Gakuen to~": { "$type": "NephriteCrypt", "Title": "RGH~恋とヒーローと学園と~" @@ -1998,6 +2033,11 @@ "$type": "HashCrypt", "Title": "清楚なあの娘は、隠れビッチ ~いつでもどこでも欲情SEX~" }, + "Sekai-Ichi Yaritai Jugyou ~Rin-sensei no Loli Bitch na Hanten Sekai~": { + "$type": "NekoWorksCrypt", + "Key": "l4m0/ejjYWYBNve+9k1+TgAAAAAAAAAAAQAAABcAAAA=", + "Title": "世界一ヤリたい授業 ~林先生のロリビッチな反転世界~" + }, "Sekisaba!": { "$type": "SmxCrypt", "Mask": 5, @@ -2213,6 +2253,11 @@ "TpmFileName": "plugin/sukitama.tpm", "Title": "好きで好きでたまらない" }, + "Suki to Suki to de Sankaku Ren'ai": { + "$type": "NekoWorksCrypt", + "Key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "Title": "スキとスキとでサンカク恋愛 | 青春×好奇相伴的三角恋爱" + }, "Sukui no Serenade": { "$type": "CxEncryption", "Mask": 401, diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 0656e5d..3e778a8 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -246,6 +246,10 @@ enum CryptType { FestivalCrypt, PinPointCrypt, HybridCrypt, + #[serde(rename_all = "PascalCase")] + NekoWorksCrypt { + key: Base64Bytes, + }, } #[derive(Clone, Debug, Deserialize)] @@ -385,6 +389,9 @@ impl Schema { CryptType::FestivalCrypt => Box::new(FestivalCrypt::new(self.base.clone())), CryptType::PinPointCrypt => Box::new(PinPointCrypt::new(self.base.clone())), CryptType::HybridCrypt => Box::new(HybridCrypt::new(self.base.clone())), + CryptType::NekoWorksCrypt { key } => { + Box::new(NekoWorksCrypt::new(self.base.clone(), key.bytes.clone())?) + } }) } } @@ -1924,6 +1931,82 @@ impl Read for HybridCryptReader { } } +#[derive(Debug)] +pub struct NekoWorksCrypt { + base: BaseSchema, + key: Vec, +} + +impl NekoWorksCrypt { + pub fn new(base: BaseSchema, key: Vec) -> Result { + if key.len() < 31 { + anyhow::bail!("NekoWorksCrypt: key is too small."); + } + Ok(Self { base, key }) + } + + fn init_key(&self, mut hash: u32) -> [u8; 31] { + hash &= 0x7FFFFFFF; + hash = hash << 31 | hash; + let mut key = [0; 31]; + key.copy_from_slice(&self.key[..31]); + for i in 0..31 { + key[i] ^= hash as u8; + hash = (hash & 0xFFFFFFFE) << 23 | hash >> 8; + } + key + } +} + +impl Crypt for NekoWorksCrypt { + base_schema_impl!(); + 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> { + Ok(Box::new(NekoWorksCryptReader::new( + stream, + cur_seg, + self.init_key(entry.file_hash), + ))) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + Ok(Box::new(NekoWorksCryptReader::new( + stream, + cur_seg, + self.init_key(entry.file_hash), + ))) + } +} + +seek_reader_key_impl!(NekoWorksCryptReader, [u8; 31]); + +impl Read for NekoWorksCryptReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.inner.read(buf)?; + let mut offset = ((self.seg_start + self.pos) % 31) as usize; + for t in (&mut buf[..readed]).iter_mut() { + *t ^= self.key[offset]; + offset = (offset + 1) % 31; + } + self.pos += readed as u64; + Ok(readed) + } +} + #[test] fn test_deserialize_crypt() { for (key, schema) in CRYPT_SCHEMA.iter() {