diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index c94a8b00..1762a14c 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -66,12 +66,16 @@ + + + + diff --git a/ArcFormats/KiriKiri/ArcXP3.cs b/ArcFormats/KiriKiri/ArcXP3.cs index d706ed53..0783ceb2 100644 --- a/ArcFormats/KiriKiri/ArcXP3.cs +++ b/ArcFormats/KiriKiri/ArcXP3.cs @@ -259,7 +259,10 @@ NextEntry: header.BaseStream.Position = dir_offset; } } - return new ArcFile (file, this, dir); + var arc = new ArcFile (file, this, dir); + if (crypt_algorithm.IsValueCreated) + crypt_algorithm.Value.Init (arc); + return arc; } static readonly Regex ObfuscatedPathRe = new Regex (@"[^\\/]+[\\/]\.\.[\\/]"); diff --git a/ArcFormats/KiriKiri/ChainReactionCrypt.cs b/ArcFormats/KiriKiri/ChainReactionCrypt.cs new file mode 100644 index 00000000..531e3d29 --- /dev/null +++ b/ArcFormats/KiriKiri/ChainReactionCrypt.cs @@ -0,0 +1,206 @@ +//! \file ChainReactionCrypt.cs +//! \date Mon Mar 07 15:59:47 2016 +//! \brief KiriKiri XP3 ecryption filter used in some games. +// +// Copyright (C) 2016 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using GameRes.Compression; +using GameRes.Utility; + +namespace GameRes.Formats.KiriKiri +{ + // this encryption scheme encrypts first N bytes of file, where N varies depending on file's hash, and + // those variations are stored within "plugin/list.bin" file. by default N=512 (used when hash is not + // found within "list.bin"). + // + // this implementation looks for "list.bin" upon archive open, parses it and remembers encryption + // threshold values in a dictionary. + // + // such implementation has some flaws, for one, it would fail if "list.bin" is stored within archive other + // than one being opened. + + [Serializable] + public class ChainReactionCrypt : ICrypt + { + public override void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count) + { + uint limit = GetEncryptionLimit (entry); + uint key = entry.Hash; + for (int i = 0; i < count && offset < limit; ++i, ++offset) + { + values[pos+i] ^= (byte)(offset ^ (key >> (((int)offset & 3) << 3))); + } + } + + public override void Encrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count) + { + throw new NotImplementedException (Strings.arcStrings.MsgEncNotImplemented); + // despite the fact that algorithm is symmetric, creating an archive without updating "list.bin" + // wouldn't make much sense +// Decrypt (entry, offset, values, pos, count); + } + + uint GetEncryptionLimit (Xp3Entry entry) + { + uint limit; + if (EncryptionThresholdMap != null && EncryptionThresholdMap.TryGetValue (entry.Hash, out limit)) + return limit; + else + return 0x200; + } + + [NonSerialized] + Dictionary EncryptionThresholdMap; + + public override void Init (ArcFile arc) + { + var list_bin = arc.Dir.Where (e => e.Name == "plugin/list.bin").FirstOrDefault() as Xp3Entry; + if (null == list_bin || list_bin.UnpackedSize <= 0x30) + return; + var bin = new byte[list_bin.UnpackedSize]; + using (var input = arc.OpenEntry (list_bin)) + input.Read (bin, 0, bin.Length); + + for (int i = 0; i < 3; ++i) + { + bin = DecodeListBin (bin); + if (null == bin) + return; + } + if (null == EncryptionThresholdMap) + EncryptionThresholdMap = new Dictionary(); + else + EncryptionThresholdMap.Clear(); + + ParseListBin (bin); + } + + void ParseListBin (byte[] data) + { + using (var mem = new MemoryStream (data)) + using (var input = new StreamReader (mem)) + { + var converter = new UInt32Converter(); + string line; + while ((line = input.ReadLine()) != null) + { + if (0 == line.Length || '0' != line[0]) + continue; + var pair = line.Split (','); + if (pair.Length > 1) + { + uint hash = (uint)converter.ConvertFromString (pair[0]); + uint threshold = (uint)converter.ConvertFromString (pair[1]); + EncryptionThresholdMap[hash] = threshold; + } + } + } + } + + static byte[] DecodeListBin (byte[] data) + { + var header = new byte[0x30]; + DecodeDPD (data, 0, 0x30, header); + int packed_size = LittleEndian.ToInt32 (header, 0x0C); + int unpacked_size = LittleEndian.ToInt32 (header, 0x10); + if (packed_size <= 0 || packed_size > data.Length-0x30) + return null; + if (Binary.AsciiEqual (header, 0, "DPDC")) + { + var decrypted = new byte[packed_size]; + DecodeDPD (data, 0x30, packed_size, decrypted); + return decrypted; + } + if (Binary.AsciiEqual (header, 0, "SZLC")) // LZSS + { + using (var input = new MemoryStream (data, 0x30, packed_size)) + using (var lzss = new LzssReader (input, packed_size, unpacked_size)) + { + lzss.Unpack(); + return lzss.Data; + } + } + if (Binary.AsciiEqual (header, 0, "ELRC")) // RLE + { + var unpacked = new byte[unpacked_size]; + int min_repeat = LittleEndian.ToInt32 (header, 0x1C); + DecodeRLE (data, 0x30, packed_size, unpacked, min_repeat); + return unpacked; + } + return null; + } + + static void DecodeRLE (byte[] input, int offset, int length, byte[] output, int min_repeat) + { + int src = offset; + int src_end = offset+length; + int dst = 0; + while (src < src_end) + { + byte b = input[src++]; + int repeat = 1; + while (repeat < min_repeat && src < src_end && input[src] == b) + { + ++repeat; + ++src; + } + if (repeat == min_repeat) + { + byte ctl = input[src++]; + if (ctl > 0x7F) + repeat += input[src++] + ((ctl & 0x7F) << 8) + 0x80; + else + repeat += ctl; + } + for (int i = 0; i < repeat; ++i) + output[dst++] = b; + } + } + + unsafe static void DecodeDPD (byte[] src, int offset, int length, byte[] dst) + { + if (offset > src.Length || length > dst.Length || length > src.Length - offset) + throw new IndexOutOfRangeException(); + if (length < 8) + return; + int tail = length & 3; + if (tail != 0) + Buffer.BlockCopy (src, offset+length-tail, dst, length-tail, tail); + length /= 4; + fixed (byte* src8 = &src[offset], dst8 = dst) + { + uint* src32 = (uint*)src8; + uint* dst32 = (uint*)dst8; + for (int i = 0; i < length-1; ++i) + { + dst32[i] = src32[i] ^ src32[i+1]; + } + dst32[length-1] = dst32[0] ^ src32[length-1]; + } + } + } +} diff --git a/ArcFormats/KiriKiri/CryptAlgorithms.cs b/ArcFormats/KiriKiri/CryptAlgorithms.cs index bfe7bf20..e2037eea 100644 --- a/ArcFormats/KiriKiri/CryptAlgorithms.cs +++ b/ArcFormats/KiriKiri/CryptAlgorithms.cs @@ -56,6 +56,13 @@ namespace GameRes.Formats.KiriKiri { throw new NotImplementedException (Strings.arcStrings.MsgEncNotImplemented); } + + /// + /// Perform necessary initialization specific to an archive being opened. + /// + public virtual void Init (ArcFile arc) + { + } } [Serializable] @@ -112,6 +119,44 @@ namespace GameRes.Formats.KiriKiri } } + [Serializable] + public class MizukakeCrypt : ICrypt + { + public override bool HashAfterCrypt { get { return true; } } + + public override void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count) + { + if (offset <= 0x103 && offset + count > 0x103) + values[pos+0x103-offset]--; + for (int i = 0; i < count; ++i) + { + values[pos+i] ^= 0xB6; + } + if (offset > 0x3F82) + return; + if (offset + count > 0x3F82) + values[pos+0x3F82-offset] ^= 1; + if (offset > 0x83) + return; + if (offset + count > 0x83) + values[pos+0x83-offset] ^= 3; + } + + public override void Encrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count) + { + for (int i = 0; i < count; ++i) + { + values[pos+i] ^= 0xB6; + } + if (offset <= 0x3F82 && offset + count > 0x3F82) + values[pos+0x3F82-offset] ^= 1; + if (offset <= 0x83 && offset + count > 0x83) + values[pos+0x83-offset] ^= 3; + if (offset <= 0x103 && offset + count > 0x103) + values[pos+0x103-offset]++; + } + } + [Serializable] public class HashCrypt : ICrypt { @@ -504,7 +549,7 @@ namespace GameRes.Formats.KiriKiri } [Serializable] - public class IncubusCrypt : ICrypt + public class PoringSoftCrypt : ICrypt { public override byte Decrypt (Xp3Entry entry, long offset, byte value) { @@ -578,7 +623,7 @@ namespace GameRes.Formats.KiriKiri var ext_bin = new byte[16]; Encodings.cp932.GetBytes (ext, 0, Math.Min (4, ext.Length), ext_bin, 0); key = ~LittleEndian.ToUInt32 (ext_bin, 0); - if (".asd.ks.tjs".Contains (ext)) + if (".asd\0.ks\0.tjs\0".Contains (ext+'\0')) return entry.Size; } else diff --git a/GUI/Resources/Formats.dat b/GUI/Resources/Formats.dat index 39cd16b3..3c82abf9 100644 Binary files a/GUI/Resources/Formats.dat and b/GUI/Resources/Formats.dat differ diff --git a/supported.html b/supported.html index bb962cde..b075f61a 100644 --- a/supported.html +++ b/supported.html @@ -44,6 +44,7 @@ Salmon Pink
Shisho-san to Issho
Sensei 2
Shoujo Settai
+Suzuri-sensei to 26-ko no Ecchi na Oppai
Tsukushite Agechau series
*.ggd\xB9\xAA\xB3\xB3
\xAB\xAD\xAA\xBA
\xB7\xB6\xB8\xB7
\xCD\xCA\xC9\xB8Yes @@ -199,6 +200,7 @@ Imouto Style
Inaho no Mirai
Mayoeru Futari to Sekai no Subete
Mahoutsukai no Yoru
+Nakadashi Hara Maid series
Natsupochi
Nidaime wa ☆ Mahou Shoujo
Nuki Doki!