Add NanaCxCrypt

This commit is contained in:
2026-04-08 18:26:57 +08:00
parent b5aa53690c
commit 31d0c5c9d0
7 changed files with 329 additions and 50 deletions

View File

@@ -416,6 +416,48 @@
"ControlBlockName": "grisaia_vol2.bin",
"Title": "グリザイア ファントムトリガー Vol. 2 | 灰色幻影扳机第2卷 | 灰色:幻影扳机 Vol. 2"
},
"Grisaia: Phantom Trigger Vol.3": {
"$type": "NanaCxCrypt",
"Mask": 501,
"Offset": 174,
"PrologOrder": "AAEC",
"OddBranchOrder": "AgUDBAEA",
"EvenBranchOrder": "AAIDAQUGBwQ=",
"StartupTjsNotEncrypted": true,
"NamesSectionId": "dls:",
"RandomSeed": 2463534242,
"YuzKey": [
3868544114,
706082328,
1479507413,
605919492,
3150100769,
4251163583
],
"ControlBlockName": "grisaia_vol3.bin",
"Title": "グリザイア ファントムトリガー Vol. 3 | 灰色幻影扳机第3卷 | 灰色:幻影扳机 Vol. 3"
},
"Grisaia: Phantom Trigger Vol.4": {
"$type": "NanaCxCrypt",
"Mask": 501,
"Offset": 174,
"PrologOrder": "AAEC",
"OddBranchOrder": "AgUDBAEA",
"EvenBranchOrder": "AAIDAQUGBwQ=",
"StartupTjsNotEncrypted": true,
"NamesSectionId": "dls:",
"RandomSeed": 2463534242,
"YuzKey": [
3868544114,
706082328,
1479507413,
605919492,
3150100769,
4251163583
],
"ControlBlockName": "grisaia_vol4.bin",
"Title": "グリザイア ファントムトリガー Vol. 4 | 灰色幻影扳机第4卷 | 灰色:幻影扳机 Vol. 4"
},
"Grisaia: Phantom Trigger Vol.5": {
"$type": "CxEncryption",
"Mask": 389,
@@ -476,6 +518,27 @@
"TpmFileName": "plugin/hatsujosw.tpm",
"Title": "発情スイッチ ~美姉妹が催眠術に堕ちた先~"
},
"Haruoto Alice*Gram": {
"$type": "NanaCxCrypt",
"Mask": 572,
"Offset": 109,
"PrologOrder": "AAEC",
"OddBranchOrder": "AgUDBAEA",
"EvenBranchOrder": "AAIDAQUGBwQ=",
"StartupTjsNotEncrypted": true,
"NamesSectionId": "dls:",
"RandomSeed": 2463534242,
"YuzKey": [
1406040976,
3200797355,
1926882100,
1303657607,
3795909009,
676609281
],
"ControlBlockName": "haruoto.bin",
"Title": "春音アリス*グラム | 春音Alice*Gram"
},
"Haze Man -The Local Hero-": {
"$type": "CxEncryption",
"Mask": 419,
@@ -1019,6 +1082,27 @@
"$type": "FlyingShineCrypt",
"Title": "水恋~みずこい~"
},
"Momoiro Closet": {
"$type": "NanaCxCrypt",
"Mask": 408,
"Offset": 242,
"PrologOrder": "AAEC",
"OddBranchOrder": "AgUDBAEA",
"EvenBranchOrder": "AAIDAQUGBwQ=",
"StartupTjsNotEncrypted": true,
"NamesSectionId": "dls:",
"RandomSeed": 2463534242,
"YuzKey": [
1532499583,
1492364298,
3985974824,
3368303250,
1642426535,
742252838
],
"ControlBlockName": "momoiro.bin",
"Title": "ももいろクローゼット"
},
"Momoiro Closet [English]": {
"$type": "CxEncryption",
"Mask": 389,

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -829,60 +829,67 @@ impl SenrenCxCrypt {
names_section_id,
})
}
fn read_yuzu_names(&self, archive: &mut Xp3Archive) -> Result<()> {
if let Some(section) = archive
.extras
.iter()
.find(|s| s.tag == self.names_section_id)
fn read_yuzu_names(
reader: Box<dyn ReadDebug>,
unpacked_size: u32,
) -> Result<(HashMap<u32, String>, HashMap<String, String>)> {
let mut decoded = MemWriter::with_capacity(unpacked_size as usize);
{
let mut sreader = MemReaderRef::new(&section.data);
let offset = sreader.read_u64()? + archive.base_offset;
let unpacked_size = sreader.read_u32()?;
let packed_size = sreader.read_u32()?;
let index_stream =
MutexWrapper::new(archive.inner.clone(), offset).take(packed_size as u64);
let mut decoded = MemWriter::from_vec(Vec::with_capacity(unpacked_size as usize));
{
let mut decoder = flate2::read::ZlibDecoder::new(index_stream);
std::io::copy(&mut decoder, &mut decoded)?;
}
let decoded = decoded.into_inner();
let mut reader = MemReader::new(decoded);
let mut hash_map = HashMap::new();
let mut md5_map = HashMap::new();
let mut dir_offset = 0u64;
while !reader.is_eof() {
let _entry_sign = reader.read_u32()?;
let mut entry_size = reader.read_u64()?;
dir_offset += 12 + entry_size;
let hash = reader.read_u32()?;
let name_len = reader.read_u16()?;
entry_size -= 6;
if (name_len as u64) * 2 <= entry_size {
let name = reader.read_exact_vec((name_len) as usize * 2)?;
let name = decode_to_string(Encoding::Utf16LE, &name, true)?;
if !hash_map.contains_key(&hash) {
hash_map.insert(hash, name.clone());
}
let encoded =
encode_string(Encoding::Utf16LE, &name.to_ascii_lowercase(), true)?;
let md5 = format!("{:x}", md5::compute(encoded));
md5_map.insert(md5, name);
let mut decoder = flate2::read::ZlibDecoder::new(reader);
std::io::copy(&mut decoder, &mut decoded)?;
}
let decoded = decoded.into_inner();
let mut reader = MemReader::new(decoded);
let mut hash_map = HashMap::new();
let mut md5_map = HashMap::new();
let mut dir_offset = 0u64;
while !reader.is_eof() {
let _entry_sign = reader.read_u32()?;
let mut entry_size = reader.read_u64()?;
dir_offset += 12 + entry_size;
let hash = reader.read_u32()?;
let name_len = reader.read_u16()?;
entry_size -= 6;
if (name_len as u64) * 2 <= entry_size {
let name = reader.read_exact_vec((name_len) as usize * 2)?;
let name = decode_to_string(Encoding::Utf16LE, &name, true)?;
if !hash_map.contains_key(&hash) {
hash_map.insert(hash, name.clone());
}
reader.pos = dir_offset as usize;
md5_map.insert("$".into(), "startup.tjs".into());
let encoded = encode_string(Encoding::Utf16LE, &name.to_ascii_lowercase(), true)?;
let md5 = format!("{:x}", md5::compute(encoded));
md5_map.insert(md5, name);
}
for entry in archive.entries.iter_mut() {
if let Some(name) = hash_map.get(&entry.file_hash) {
entry.name = name.clone();
} else if let Some(name) = md5_map.get(&entry.name) {
entry.name = name.clone();
}
reader.pos = dir_offset as usize;
md5_map.insert("$".into(), "startup.tjs".into());
}
Ok((hash_map, md5_map))
}
}
fn read_yuzu_names<T>(archive: &mut Xp3Archive, names_section_id: &str, convert: T) -> Result<()>
where
T: FnOnce(Box<dyn ReadDebug>, u32) -> Result<(HashMap<u32, String>, HashMap<String, String>)>,
{
if let Some(section) = archive.extras.iter().find(|s| s.tag == names_section_id) {
let mut sreader = MemReaderRef::new(&section.data);
let offset = sreader.read_u64()? + archive.base_offset;
let unpacked_size = sreader.read_u32()?;
let packed_size = sreader.read_u32()?;
let index_stream =
MutexWrapper::new(archive.inner.clone(), offset).take(packed_size as u64);
let (hash_map, md5_map) = convert(Box::new(index_stream), unpacked_size)?;
for entry in archive.entries.iter_mut() {
if let Some(name) = hash_map.get(&entry.file_hash) {
entry.name = name.clone();
} else if let Some(name) = md5_map.get(&entry.name) {
entry.name = name.clone();
}
}
archive.extras.retain(|s| s.tag != self.names_section_id);
Ok(())
}
archive.extras.retain(|s| s.tag != names_section_id);
Ok(())
}
icx_enc_impl!(SenrenCxCrypt);
@@ -892,7 +899,11 @@ impl Crypt for Arc<SenrenCxCrypt> {
base_schema_impl!();
fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
default_init_crypt(archive)?;
self.read_yuzu_names(archive)
read_yuzu_names(
archive,
&self.names_section_id,
SenrenCxCrypt::read_yuzu_names,
)
}
fn decrypt_supported(&self) -> bool {
true
@@ -1032,7 +1043,170 @@ impl Crypt for Arc<CabbageCxCrypt> {
base_schema_impl!();
fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
default_init_crypt(archive)?;
self.base.read_yuzu_names(archive)
read_yuzu_names(
archive,
&self.base.names_section_id,
SenrenCxCrypt::read_yuzu_names,
)
}
fn decrypt_supported(&self) -> bool {
true
}
fn decrypt_seek_supported(&self) -> bool {
true
}
fn decrypt<'a>(
&self,
entry: &Xp3Entry,
cur_seg: &Segment,
stream: Box<dyn Read + 'a>,
) -> Result<Box<dyn ReadDebug + 'a>> {
let key = (
entry.file_hash,
Box::new(self.clone()) as Box<dyn ICxEncryption + 'a>,
);
Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
}
fn decrypt_with_seek<'a>(
&self,
entry: &Xp3Entry,
cur_seg: &Segment,
stream: Box<dyn ReadSeek + 'a>,
) -> Result<Box<dyn ReadSeek + 'a>> {
let key = (
entry.file_hash,
Box::new(self.clone()) as Box<dyn ICxEncryption + 'a>,
);
Ok(Box::new(CxEncryptionReader::new(stream, cur_seg, key)))
}
}
#[derive(Debug)]
struct NanaDecryptor {
state: [u32; 27],
seed: u64,
}
impl NanaDecryptor {
fn new(key: &[u32], seed1: u32, seed2: u32) -> Self {
let mut state = [0u32; 27];
let seed = (seed2 as u64) << 32 | (seed1 as u64);
let mut s = [0u32; 3];
let mut k = key[0];
s[0] = key[1];
s[1] = key[2];
s[2] = key[3];
state[0] = k;
let mut dst = 1;
for i in 0..26usize {
let src = i % 3;
let m = s[src].rotate_right(8);
let n = (i as u32) ^ k.wrapping_add(m);
k = n ^ k.rotate_left(3);
state[dst] = k;
dst += 1;
s[src] = n;
}
Self { state, seed }
}
fn decrypt(&self, data: &mut [u8]) {
let mut i = 0;
let mut offset = 0;
let mut length = data.len();
while length > 0 {
offset += 1;
let mut key = self.transform_key(offset ^ self.seed);
let count = std::cmp::min(length, 8);
for _ in 0..count {
data[i] ^= (key & 0xFF) as u8;
key >>= 8;
i += 1;
}
length -= count;
}
}
fn transform_key(&self, key: u64) -> u64 {
let mut lo = (key & 0xFFFFFFFF) as u32;
let mut hi = (key >> 32) as u32;
for i in 0..27 {
hi = hi.rotate_right(8);
hi = hi.wrapping_add(lo);
hi ^= self.state[i];
lo = lo.rotate_left(3);
lo ^= hi;
}
(hi as u64) << 32 | (lo as u64)
}
}
#[derive(Debug)]
pub struct NanaCxCrypt {
base: SenrenCxCrypt,
decryptor: NanaDecryptor,
}
impl AsRef<BaseSchema> for NanaCxCrypt {
fn as_ref(&self) -> &BaseSchema {
self.base.as_ref()
}
}
impl NanaCxCrypt {
pub fn new(
base: BaseSchema,
schema: &CxSchema,
filename: &str,
names_section_id: String,
random_seed: u32,
yuz_key: &[u32],
) -> Result<Arc<Self>> {
if yuz_key.len() != 6 {
return Err(anyhow::anyhow!(
"Invalid Yuzu keys for NanaCxCrypt: expected 6, got {}",
yuz_key.len()
));
}
let cx = SenrenCxCrypt::new_inner(
base,
schema,
filename,
Box::new(CxProgramNanaBuilder::new(random_seed)),
names_section_id,
)?;
let decryptor = NanaDecryptor::new(yuz_key, yuz_key[4], yuz_key[5]);
Ok(Arc::new(Self {
base: cx,
decryptor,
}))
}
fn read_yuzu_names(
&self,
mut reader: Box<dyn ReadDebug>,
unpacked_size: u32,
) -> Result<(HashMap<u32, String>, HashMap<String, String>)> {
let mut prefix = Vec::with_capacity(0x100);
(&mut reader).take(0x100).read_to_end(&mut prefix)?;
self.decryptor.decrypt(&mut prefix);
let reader = Box::new(PrefixStream::new(prefix, reader));
SenrenCxCrypt::read_yuzu_names(reader, unpacked_size)
}
}
icx_enc_impl!(NanaCxCrypt);
icx_enc_arc_impl!(NanaCxCrypt);
impl Crypt for Arc<NanaCxCrypt> {
base_schema_impl!();
fn init(&self, archive: &mut Xp3Archive) -> Result<()> {
default_init_crypt(archive)?;
read_yuzu_names(
archive,
&self.base.names_section_id,
|reader, unpacked_size| self.read_yuzu_names(reader, unpacked_size),
)
}
fn decrypt_supported(&self) -> bool {
true

View File

@@ -137,6 +137,14 @@ enum CryptType {
names_section_id: String,
random_seed: u32,
},
#[serde(rename_all = "PascalCase")]
NanaCxCrypt {
#[serde(flatten)]
cx: CxSchema,
names_section_id: String,
random_seed: u32,
yuz_key: Vec<u32>,
},
}
#[derive(Clone, Debug, Deserialize)]
@@ -192,6 +200,19 @@ impl Schema {
names_section_id.clone(),
*random_seed,
)?),
CryptType::NanaCxCrypt {
cx,
names_section_id,
random_seed,
yuz_key,
} => Box::new(cx::NanaCxCrypt::new(
self.base.clone(),
cx,
filename,
names_section_id.clone(),
*random_seed,
&yuz_key,
)?),
})
}
}