Add StartupTjsNotEncrypted support

This commit is contained in:
2026-04-07 22:58:02 +08:00
parent c595331dd0
commit 6645ab6138
4 changed files with 117 additions and 27 deletions

View File

@@ -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",

View File

@@ -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<u8>,
control_block: Arc<Vec<u32>>,
programs: Vec<CxProgram>,
base: BaseSchema,
}
impl CxEncryption {
pub fn new(schema: &CxSchema, filename: &str) -> Result<Arc<Self>> {
pub fn new(base: BaseSchema, schema: &CxSchema, filename: &str) -> Result<Arc<Self>> {
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<CxEncryption> {
base_schema_impl!();
fn decrypt_supported(&self) -> bool {
true
}

View File

@@ -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<String>,
#[serde(flatten)]
base: BaseSchema,
}
impl Schema {
pub fn create_crypt(&self, filename: &str) -> Result<Box<dyn Crypt>> {
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<R: Read> Read for MizukakeCryptReader<R> {
}
#[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<R: Read> Read for HashCryptReader<R> {
#[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<R: Read> Read for XorCryptReader<R> {
}
#[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
}

View File

@@ -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 {