mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-21 03:14:41 +08:00
Add KissCrypt
This commit is contained in:
174
src/scripts/kirikiri/archive/xp3/crypt/cz.rs
Normal file
174
src/scripts/kirikiri/archive/xp3/crypt/cz.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use super::*;
|
||||
use aes::Aes128Dec;
|
||||
use aes::cipher::{BlockDecryptMut, KeyIvInit};
|
||||
use cbc::Decryptor;
|
||||
|
||||
type Aes128CbcDec = Decryptor<Aes128Dec>;
|
||||
|
||||
const CZ_MAGIC: &[u8; 4] = b"\xFD\xD7\x90\xA5";
|
||||
const CZ_IV_SEED: u32 = 0xBFBFBFBF;
|
||||
const CZ_HEADER_KEY: &[u8; 4] = b"\x9D\x1D\x9A\xF2";
|
||||
const CZ_DEFAULT_KEY: &[u8] = b"\x91\x10\xfcuE\x8f\xb5\xe6\xfe\xac\xbaDvX\xc2\x1a";
|
||||
|
||||
fn cz_decrypt_int(data: &[u8], offset: usize, key: u8) -> u32 {
|
||||
let mut v: u32 = (data[offset] ^ key ^ CZ_HEADER_KEY[0]) as u32;
|
||||
v |= ((data[offset + 1] ^ key ^ CZ_HEADER_KEY[1]) as u32) << 8;
|
||||
v |= ((data[offset + 2] ^ key ^ CZ_HEADER_KEY[2]) as u32) << 16;
|
||||
v |= ((data[offset + 3] ^ key ^ CZ_HEADER_KEY[3]) as u32) << 24;
|
||||
v
|
||||
}
|
||||
|
||||
fn cz_create_iv(seed: u32) -> [u8; 16] {
|
||||
let mut state = [0u32; 4];
|
||||
state[0] = 123456789;
|
||||
state[1] = 972436830;
|
||||
state[2] = 524018621;
|
||||
state[3] = seed;
|
||||
let mut iv = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
let a = state[3];
|
||||
let b = state[0] ^ (state[0] << 11);
|
||||
state[0] = state[1];
|
||||
state[1] = state[2];
|
||||
state[2] = a;
|
||||
state[3] = b ^ a ^ ((b ^ (a >> 11)) >> 8);
|
||||
iv[i] = state[3] as u8;
|
||||
}
|
||||
iv
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AesDecryptor {
|
||||
aes: Aes128CbcDec,
|
||||
entry: StreamRegion<Entry>,
|
||||
pos: u64,
|
||||
original_size: u64,
|
||||
}
|
||||
|
||||
impl AesDecryptor {
|
||||
fn new(
|
||||
aes: Aes128CbcDec,
|
||||
entry: StreamRegion<Entry>,
|
||||
original_size: u64,
|
||||
) -> AlignedReader<16, Self> {
|
||||
AlignedReader::new(Self {
|
||||
aes,
|
||||
entry,
|
||||
pos: 0,
|
||||
original_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for AesDecryptor {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let readed = self.entry.read_most(buf)?;
|
||||
if readed % 16 != 0 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"Not enough data to decrypt",
|
||||
));
|
||||
}
|
||||
// NoPadding
|
||||
for i in (0..readed).step_by(16) {
|
||||
let block = &mut buf[i..i + 16];
|
||||
self.aes.decrypt_block_mut(block.into());
|
||||
}
|
||||
let remaining = self.original_size - self.pos;
|
||||
let readed = readed.min(remaining as usize);
|
||||
self.pos += readed as u64;
|
||||
Ok(readed)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KissCrypt {
|
||||
base: BaseSchema,
|
||||
}
|
||||
|
||||
impl KissCrypt {
|
||||
pub fn new(base: BaseSchema) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypt for KissCrypt {
|
||||
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
|
||||
}
|
||||
fn need_filter(&self, _filename: &str, buf: &[u8], buf_len: usize) -> bool {
|
||||
buf_len >= 4 && buf.starts_with(CZ_MAGIC)
|
||||
}
|
||||
fn filter(&self, mut entry: Entry) -> Result<Box<dyn ReadDebug>> {
|
||||
let mut header = [0u8; 15];
|
||||
entry.read_exact(&mut header)?;
|
||||
let typ = [header[4] ^ 0x11, header[5] ^ 0x7F, header[6] ^ 0x9A];
|
||||
let key = typ[0];
|
||||
let _unpacked_size = cz_decrypt_int(&header, 7, key);
|
||||
let packed_size = cz_decrypt_int(&header, 11, key);
|
||||
if (packed_size as u64) < entry.index.original_size && (packed_size - 5) & 0xF == 0 {
|
||||
let padded_size = packed_size - 5;
|
||||
let original_size = padded_size
|
||||
- (entry.peek_u8_at(15 + padded_size as u64 + 1)?
|
||||
^ entry.peek_u8_at(15 + padded_size as u64)?) as u32;
|
||||
let iv_seed = entry.peek_u32_at(15 + padded_size as u64 + 1)? ^ CZ_IV_SEED;
|
||||
let aes = Aes128CbcDec::new(CZ_DEFAULT_KEY.into(), &cz_create_iv(iv_seed).into());
|
||||
let entry = StreamRegion::with_size(entry, padded_size as u64)?;
|
||||
let stream = AesDecryptor::new(aes, entry, original_size as u64);
|
||||
if typ[0] == b'C' {
|
||||
let stream = flate2::read::ZlibDecoder::new(stream);
|
||||
return Ok(Box::new(stream));
|
||||
}
|
||||
Ok(Box::new(stream))
|
||||
} else {
|
||||
Ok(Box::new(entry))
|
||||
}
|
||||
}
|
||||
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 ^ (entry.file_hash >> 19) ^ 0x4A9EEFF0;
|
||||
Ok(Box::new(KissCryptReader::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 ^ (entry.file_hash >> 19) ^ 0x4A9EEFF0;
|
||||
Ok(Box::new(KissCryptReader::new(stream, cur_seg, key)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for KissCryptReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let readed = self.inner.read(buf)?;
|
||||
let offset = self.seg_start + self.pos;
|
||||
let mut i = 0usize;
|
||||
while (i as u64 + offset) & 0xF != 0 {
|
||||
i += 1;
|
||||
}
|
||||
while i < readed {
|
||||
buf[i] ^= (self.key ^ (offset as u32 + i as u32)) as u8;
|
||||
i += 0x10;
|
||||
}
|
||||
self.pos += readed as u64;
|
||||
Ok(readed)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod cx;
|
||||
mod cz;
|
||||
|
||||
use super::Entry;
|
||||
use super::archive::*;
|
||||
use crate::ext::io::*;
|
||||
use crate::scripts::base::*;
|
||||
@@ -99,6 +101,30 @@ pub trait Crypt: std::fmt::Debug {
|
||||
fn decrypt_seek_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Determine whether the file with the given name and content need to be extra processed after decryption. (e.g. extra decryption by file type)
|
||||
fn need_filter(&self, _filename: &str, _buf: &[u8], _buf_len: usize) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if this crypt support seek when filtering
|
||||
fn filter_seek_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Apply extra processing to the decrypted content of the file.
|
||||
fn filter(&self, _entry: Entry) -> Result<Box<dyn ReadDebug>> {
|
||||
Err(anyhow::anyhow!(
|
||||
"This crypt does not support content filter after decrypt"
|
||||
))
|
||||
}
|
||||
|
||||
/// Apply extra processing to the decrypted content of the file, with seek support.
|
||||
fn filter_with_seek(&self, _entry: Entry) -> Result<Box<dyn ReadSeek>> {
|
||||
Err(anyhow::anyhow!(
|
||||
"This crypt does not support content filter with seek after decrypt"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -189,6 +215,7 @@ enum CryptType {
|
||||
},
|
||||
YuzuCrypt,
|
||||
HighRunningCrypt,
|
||||
KissCrypt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -303,6 +330,7 @@ impl Schema {
|
||||
)),
|
||||
CryptType::YuzuCrypt => Box::new(YuzuCrypt::new(self.base.clone())),
|
||||
CryptType::HighRunningCrypt => Box::new(HighRunningCrypt::new(self.base.clone())),
|
||||
CryptType::KissCrypt => Box::new(cz::KissCrypt::new(self.base.clone())),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1345,6 +1373,8 @@ impl<R: Read> Read for HighRunningCryptReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
seek_reader_key_impl!(KissCryptReader<T>, u32);
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_crypt() {
|
||||
for (key, schema) in CRYPT_SCHEMA.iter() {
|
||||
|
||||
@@ -255,6 +255,35 @@ impl Script for Xp3Archive {
|
||||
let header_len = entry.read(&mut header)?;
|
||||
entry.rewind()?;
|
||||
entry.script_type = detect_script_type(&entry.index.name, &header, header_len);
|
||||
if self
|
||||
.archive
|
||||
.crypt
|
||||
.need_filter(&entry.index.name, &header, header_len)
|
||||
{
|
||||
if self.archive.crypt.filter_seek_supported() {
|
||||
let index = entry.index.clone();
|
||||
let mut result = self.archive.crypt.filter_with_seek(entry)?;
|
||||
let header_len = result.read(&mut header)?;
|
||||
result.rewind()?;
|
||||
let script_type = detect_script_type(&index.name, &header, header_len);
|
||||
return Ok(Box::new(CustomFilterWithSeekEntry {
|
||||
inner: result,
|
||||
index,
|
||||
script_type,
|
||||
}));
|
||||
} else {
|
||||
let index = entry.index.clone();
|
||||
let mut result = self.archive.crypt.filter(entry)?;
|
||||
let header_len = result.read(&mut header)?;
|
||||
let script_type = detect_script_type(&index.name, &header, header_len);
|
||||
let prefix = header[..header_len].to_vec();
|
||||
return Ok(Box::new(CustomFilterEntry {
|
||||
inner: PrefixStream::new(prefix, result),
|
||||
index,
|
||||
script_type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if self.decrypt_simple_crypt
|
||||
&& header_len >= 5
|
||||
&& header[0] == 0xFE
|
||||
@@ -780,3 +809,67 @@ impl Read for MdfEntry {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CustomFilterEntry {
|
||||
inner: PrefixStream<Box<dyn ReadDebug>>,
|
||||
index: archive::Xp3Entry,
|
||||
script_type: Option<ScriptType>,
|
||||
}
|
||||
|
||||
impl Read for CustomFilterEntry {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchiveContent for CustomFilterEntry {
|
||||
fn name(&self) -> &str {
|
||||
&self.index.name
|
||||
}
|
||||
|
||||
fn script_type(&self) -> Option<&ScriptType> {
|
||||
self.script_type.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CustomFilterWithSeekEntry {
|
||||
inner: Box<dyn ReadSeek>,
|
||||
index: archive::Xp3Entry,
|
||||
script_type: Option<ScriptType>,
|
||||
}
|
||||
|
||||
impl Read for CustomFilterWithSeekEntry {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for CustomFilterWithSeekEntry {
|
||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
|
||||
fn rewind(&mut self) -> std::io::Result<()> {
|
||||
self.inner.rewind()
|
||||
}
|
||||
|
||||
fn stream_position(&mut self) -> std::io::Result<u64> {
|
||||
self.inner.stream_position()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArchiveContent for CustomFilterWithSeekEntry {
|
||||
fn name(&self) -> &str {
|
||||
&self.index.name
|
||||
}
|
||||
|
||||
fn script_type(&self) -> Option<&ScriptType> {
|
||||
self.script_type.as_ref()
|
||||
}
|
||||
|
||||
fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
|
||||
Ok(Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user