diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 1935346e..bfce508c 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -104,6 +104,7 @@ + diff --git a/ArcFormats/Qlie/ArcQLIE.cs b/ArcFormats/Qlie/ArcQLIE.cs index d6e9c028..acb95fd9 100644 --- a/ArcFormats/Qlie/ArcQLIE.cs +++ b/ArcFormats/Qlie/ArcQLIE.cs @@ -51,23 +51,12 @@ namespace GameRes.Formats.Qlie internal class QlieArchive : ArcFile { - /// - /// Hash generated from the key data contained within archive index. - /// - public uint Hash; + public readonly IEncryption Encryption; - /// - /// Internal game data used to decrypt encrypted entries. - /// null if not used. - /// - public byte[] GameKeyData; - - public QlieArchive (ArcView arc, ArchiveFormat impl, ICollection dir, - uint hash, byte[] key_data) + public QlieArchive (ArcView arc, ArchiveFormat impl, ICollection dir, IEncryption enc) : base (arc, impl, dir) { - Hash = hash; - GameKeyData = key_data; + Encryption = enc; } } @@ -115,11 +104,10 @@ namespace GameRes.Formats.Qlie return null; long index_offset = file.MaxOffset - 0x1c; if (!file.View.AsciiEqual (index_offset, "FilePackVer") - || !file.View.AsciiEqual (index_offset+0xC, ".0")) + || '.' != file.View.ReadByte (index_offset+0xC)) return null; - int pack_version = file.View.ReadByte (index_offset+0xB) - '0'; - if (pack_version < 1 || pack_version > 3) - throw new NotSupportedException ("Not supported QLIE archive version"); + var pack_version = new Version (file.View.ReadByte (index_offset+0xB) - '0', + file.View.ReadByte (index_offset+0xD) - '0'); int count = file.View.ReadInt32 (index_offset+0x10); if (!IsSaneCount (count)) return null; @@ -129,9 +117,8 @@ namespace GameRes.Formats.Qlie byte[] arc_key = null; byte[] key_file = null; - uint name_key = 0xC4; // default name encryption key for versions 1 and 2 bool use_pack_keyfile = false; - if (3 == pack_version) + if (pack_version.Major >= 3) { key_file = FindKeyFile (file); use_pack_keyfile = key_file != null; @@ -139,51 +126,45 @@ namespace GameRes.Formats.Qlie if (use_pack_keyfile) arc_key = QueryEncryption (file); // use_pack_keyfile = null != arc_key; - - var key_data = file.View.ReadBytes (file.MaxOffset-0x41C, 0x100); - name_key = GenerateKey (key_data) & 0x0FFFFFFFu; } + var enc = QlieEncryption.Create (file, pack_version, arc_key); var name_buffer = new byte[0x100]; var dir = new List (count); - for (int i = 0; i < count; ++i) + using (var index = file.CreateStream (index_offset)) { - int name_length = file.View.ReadUInt16 (index_offset); - if (name_length > name_buffer.Length) - name_buffer = new byte[name_length]; - if (name_length != file.View.Read (index_offset+2, name_buffer, 0, (uint)name_length)) - return null; - - int key = name_length + ((int)name_key ^ 0x3e); - for (int k = 0; k < name_length; ++k) - name_buffer[k] ^= (byte)(((k + 1) ^ key) + k + 1); - - string name = Encodings.cp932.GetString (name_buffer, 0, name_length); - var entry = FormatCatalog.Instance.Create (name); - if (use_pack_keyfile) - entry.RawName = name_buffer.Take (name_length).ToArray(); - - index_offset += 2 + name_length; - entry.Offset = file.View.ReadInt64 (index_offset); - entry.Size = file.View.ReadUInt32 (index_offset+8); - if (!entry.CheckPlacement (file.MaxOffset)) - return null; - entry.UnpackedSize = file.View.ReadUInt32 (index_offset+12); - entry.IsPacked = 0 != file.View.ReadInt32 (index_offset+0x10); - entry.IsEncrypted = 0 != file.View.ReadInt32 (index_offset+0x14); - entry.Hash = file.View.ReadUInt32 (index_offset+0x18); - entry.KeyFile = key_file; - if (3 == pack_version && use_pack_keyfile && entry.Name.Contains ("pack_keyfile")) + for (int i = 0; i < count; ++i) { - // note that 'pack_keyfile' itself is encrypted using 'key.fkey' file contents. - key_file = ReadEntryBytes (file, entry, name_key, arc_key); + int name_length = index.ReadUInt16(); + if (enc.IsUnicode) + name_length *= 2; + if (name_length > name_buffer.Length) + name_buffer = new byte[name_length]; + if (name_length != index.Read (name_buffer, 0, name_length)) + return null; + var name = enc.DecryptName (name_buffer, name_length); + var entry = FormatCatalog.Instance.Create (name); + if (use_pack_keyfile) + entry.RawName = name_buffer.Take (name_length).ToArray(); + + entry.Offset = index.ReadInt64(); // [+00] + entry.Size = index.ReadUInt32(); // [+08] + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + entry.UnpackedSize = index.ReadUInt32(); // [+0C] + entry.IsPacked = 0 != index.ReadInt32(); // [+10] + entry.IsEncrypted = 0 != index.ReadInt32(); // [+14] + entry.Hash = index.ReadUInt32(); // [+18] + entry.KeyFile = key_file; + if (3 == pack_version.Major && use_pack_keyfile && entry.Name.Contains ("pack_keyfile")) + { + // note that 'pack_keyfile' itself is encrypted using 'key.fkey' file contents. + key_file = ReadEntryBytes (file, entry, enc); + } + dir.Add (entry); } - dir.Add (entry); - index_offset += 0x1c; } - if (pack_version < 3) - name_key = 0; - return new QlieArchive (file, this, dir, name_key, arc_key); + return new QlieArchive (file, this, dir, enc); } public override Stream OpenEntry (ArcFile arc, Entry entry) @@ -192,125 +173,27 @@ namespace GameRes.Formats.Qlie var qarc = arc as QlieArchive; if (null == qent || null == qarc || (!qent.IsEncrypted && !qent.IsPacked)) return arc.File.CreateStream (entry.Offset, entry.Size); - var data = ReadEntryBytes (arc.File, qent, qarc.Hash, qarc.GameKeyData); + var data = ReadEntryBytes (arc.File, qent, qarc.Encryption); return new BinMemoryStream (data, entry.Name); } - private byte[] ReadEntryBytes (ArcView file, QlieEntry entry, uint hash, byte[] game_key) + private byte[] ReadEntryBytes (ArcView file, QlieEntry entry, IEncryption enc) { var data = file.View.ReadBytes (entry.Offset, entry.Size); if (entry.IsEncrypted) { - if (entry.KeyFile != null) - DecryptV3 (data, 0, data.Length, entry.RawName, hash, entry.KeyFile, game_key); - else - Decrypt (data, 0, data.Length, hash); + enc.DecryptEntry (data, 0, data.Length, entry); } if (entry.IsPacked) { - var unpacked = Decompress (data); - if (null != unpacked) - data = unpacked; + data = Decompress (data) ?? data; } return data; } - private void Decrypt (byte[] buffer, int offset, int length, uint key) - { - if (offset < 0) - throw new ArgumentOutOfRangeException ("offset"); - if (length > buffer.Length || offset > buffer.Length - length) - throw new ArgumentOutOfRangeException ("length"); - - ulong hash = 0xA73C5F9DA73C5F9Dul; - ulong xor = ((uint)length + key) ^ 0xFEC9753Eu; - xor |= xor << 32; - unsafe - { - fixed (byte* raw = buffer) - { - ulong* encoded = (ulong*)(raw + offset); - for (int i = 0; i < length / 8; ++i) - { - hash = MMX.PAddD (hash, 0xCE24F523CE24F523ul) ^ xor; - xor = *encoded ^ hash; - *encoded++ = xor; - } - } - } - } - - private void DecryptV3 (byte[] data, int offset, int length, byte[] file_name, - uint arc_hash, byte[] key_file, byte[] game_key) - { - // play it safe with 'unsafe' sections - if (offset < 0) - throw new ArgumentOutOfRangeException ("offset"); - if (length > data.Length || offset > data.Length - length) - throw new ArgumentOutOfRangeException ("length"); - - if (length < 8) - return; - - uint hash = 0x85F532; - uint seed = 0x33F641; - - for (uint i = 0; i < file_name.Length; i++) - { - hash += (i & 0xFF) * file_name[i]; - seed ^= hash; - } - - seed += arc_hash ^ (7 * ((uint)data.Length & 0xFFFFFF) + (uint)data.Length - + hash + (hash ^ (uint)data.Length ^ 0x8F32DCu)); - seed = 9 * (seed & 0xFFFFFF); - - if (game_key != null) - seed ^= 0x453A; - - var mt = new QlieMersenneTwister (seed); - if (key_file != null) - mt.XorState (key_file); - if (game_key != null) - mt.XorState (game_key); - - // game code fills dword[41] table, but only the first 16 qwords are used - ulong[] table = new ulong[16]; - for (int i = 0; i < table.Length; ++i) - table[i] = mt.Rand64(); - - // compensate for 9 discarded dwords - for (int i = 0; i < 9; ++i) - mt.Rand(); - - ulong hash64 = mt.Rand64(); - uint t = mt.Rand() & 0xF; - unsafe - { - fixed (byte* raw_data = &data[offset]) - { - ulong* data64 = (ulong*)raw_data; - int qword_length = length / 8; - for (int i = 0; i < qword_length; ++i) - { - hash64 = MMX.PAddD (hash64 ^ table[t], table[t]); - - ulong d = data64[i] ^ hash64; - data64[i] = d; - - hash64 = MMX.PAddB (hash64, d) ^ d; - hash64 = MMX.PAddW (MMX.PSllD (hash64, 1), d); - - t++; - t &= 0xF; - } - } - } - } - internal static byte[] Decompress (byte[] input) { - if (LittleEndian.ToUInt32 (input, 0) != 0xFF435031) + if (LittleEndian.ToUInt32 (input, 0) != 0xFF435031) // '1PC\xFF' return null; bool is_16bit = 0 != (input[4] & 1); @@ -393,25 +276,6 @@ namespace GameRes.Formats.Qlie return output; } - uint GenerateKey (byte[] key_data) - { - ulong hash = 0; - ulong key = 0; - unsafe - { - fixed (byte* data = key_data) - { - ulong* data64 = (ulong*)data; - for (int i = key_data.Length / 8; i > 0; --i) - { - hash = MMX.PAddW (hash, 0x0307030703070307); - key = MMX.PAddW (key, *data64++ ^ hash); - } - } - } - return (uint)(key ^ (key >> 32)); - } - public override ResourceOptions GetDefaultOptions () { return new QlieOptions { diff --git a/ArcFormats/Qlie/Encryption.cs b/ArcFormats/Qlie/Encryption.cs new file mode 100644 index 00000000..45e47c8f --- /dev/null +++ b/ArcFormats/Qlie/Encryption.cs @@ -0,0 +1,339 @@ +//! \file Encryption.cs +//! \date Sun Mar 12 04:51:11 2017 +//! \brief QLIE encryption-related classes. +// +// Copyright (C) 2017 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.Text; + +namespace GameRes.Formats.Qlie +{ + internal interface IEncryption + { + bool IsUnicode { get; } + + uint CalculateHash (byte[] data, int length); + + string DecryptName (byte[] name, int name_length); + + void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry); + } + + internal abstract class QlieEncryption : IEncryption + { + public virtual bool IsUnicode { get { return false; } } + + /// + /// Hash generated from the key data contained within archive index. + /// + public uint ArcKey { get; protected set; } + + public static IEncryption Create (ArcView file, Version version, byte[] arc_key) + { + if (2 == version.Major || 1 == version.Major) + return new EncryptionV2(); + else if (3 == version.Major && 1 == version.Minor) + return new EncryptionV3_1 (file); + else if (3 == version.Major && 0 == version.Minor) + return new EncryptionV3 (file, arc_key); + else + throw new NotSupportedException ("Not supported QLIE archive version"); + } + + public abstract uint CalculateHash (byte[] data, int length); + + public abstract string DecryptName (byte[] name, int name_length); + + public abstract void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry); + } + + internal class EncryptionV2 : QlieEncryption + { + public EncryptionV2 () + { + ArcKey = 0xC4; + } + + public override uint CalculateHash (byte[] data, int length) + { + return 0; // not implemented + } + + public override string DecryptName (byte[] name, int name_length) + { + int key = name_length + ((int)ArcKey ^ 0x3E); + for (int k = 0; k < name_length; ++k) + name[k] ^= (byte)(((k + 1) ^ key) + k + 1); + + return Encodings.cp932.GetString (name, 0, name_length); + } + + public override void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry) + { + if (offset < 0) + throw new ArgumentOutOfRangeException ("offset"); + if (length > data.Length || offset > data.Length - length) + throw new ArgumentOutOfRangeException ("length"); + uint arc_key = 0; // ArcKey? + + ulong hash = 0xA73C5F9DA73C5F9Dul; + ulong xor = ((uint)length + arc_key) ^ 0xFEC9753Eu; + xor |= xor << 32; + unsafe + { + fixed (byte* raw = data) + { + ulong* encoded = (ulong*)(raw + offset); + for (int i = 0; i < length / 8; ++i) + { + hash = MMX.PAddD (hash, 0xCE24F523CE24F523ul) ^ xor; + xor = *encoded ^ hash; + *encoded++ = xor; + } + } + } + } + } + + internal class EncryptionV3 : EncryptionV2 + { + /// + /// Internal game data used to decrypt encrypted entries. + /// null if not used. + /// + public byte[] GameKeyData; + + public EncryptionV3 (ArcView file, byte[] game_key) + { + GameKeyData = game_key; + var key_data = file.View.ReadBytes (file.MaxOffset-0x41C, 0x100); + ArcKey = CalculateHash (key_data, key_data.Length) & 0x0FFFFFFFu; + } + + public override uint CalculateHash (byte[] data, int length) + { + if (length > data.Length) + throw new ArgumentOutOfRangeException ("length"); + unsafe + { + fixed (byte* data8 = data) + { + ulong hash = 0; + ulong key = 0; + ulong* data64 = (ulong*)data8; + for (int i = length / 8; i > 0; --i) + { + hash = MMX.PAddW (hash, 0x0307030703070307); + key = MMX.PAddW (key, *data64++ ^ hash); + } + return (uint)(key ^ (key >> 32)); + } + } + } + + public override void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry) + { + var key_file = entry.KeyFile; + if (null == key_file) + { + base.DecryptEntry (data, offset, length, entry); + return; + } + // play it safe with 'unsafe' sections + if (offset < 0) + throw new ArgumentOutOfRangeException ("offset"); + if (length > data.Length || offset > data.Length - length) + throw new ArgumentOutOfRangeException ("length"); + + if (length < 8) + return; + + var file_name = entry.RawName; + uint hash = 0x85F532; + uint seed = 0x33F641; + + for (uint i = 0; i < file_name.Length; i++) + { + hash += (i & 0xFF) * file_name[i]; + seed ^= hash; + } + + seed += ArcKey ^ (7 * ((uint)length & 0xFFFFFF) + (uint)length + + hash + (hash ^ (uint)length ^ 0x8F32DCu)); + seed = 9 * (seed & 0xFFFFFF); + + if (GameKeyData != null) + seed ^= 0x453A; + + var mt = new QlieMersenneTwister (seed); + if (key_file != null) + mt.XorState (key_file); + if (GameKeyData != null) + mt.XorState (GameKeyData); + + // game code fills dword[41] table, but only the first 16 qwords are used + ulong[] table = new ulong[16]; + for (int i = 0; i < table.Length; ++i) + table[i] = mt.Rand64(); + + // compensate for 9 discarded dwords + for (int i = 0; i < 9; ++i) + mt.Rand(); + + ulong hash64 = mt.Rand64(); + uint t = mt.Rand() & 0xF; + unsafe + { + fixed (byte* raw_data = &data[offset]) + { + ulong* data64 = (ulong*)raw_data; + int qword_length = length / 8; + for (int i = 0; i < qword_length; ++i) + { + hash64 = MMX.PAddD (hash64 ^ table[t], table[t]); + + ulong d = data64[i] ^ hash64; + data64[i] = d; + + hash64 = MMX.PAddB (hash64, d) ^ d; + hash64 = MMX.PAddW (MMX.PSllD (hash64, 1), d); + + t++; + t &= 0xF; + } + } + } + } + } + + internal class EncryptionV3_1 : QlieEncryption + { + public override bool IsUnicode { get { return true; } } + + public EncryptionV3_1 (ArcView file) + { + var key_data = file.View.ReadBytes (file.MaxOffset-0x41C, 0x100); + ArcKey = CalculateHash (key_data, key_data.Length) & 0x0FFFFFFFu; + } + + public override uint CalculateHash (byte[] data, int length) + { + if (length > data.Length) + throw new ArgumentOutOfRangeException ("length"); + unsafe + { + fixed (byte* data8 = data) + { + ulong hash = 0; + ulong key = 0; + ulong* data64 = (ulong*)data8; + for (int i = length / 8; i > 0; --i) + { + hash = MMX.PAddW (hash, 0xA35793A7A35793A7ul); + key = MMX.PAddW (key, *data64++ ^ hash); + key = MMX.PSllD (key, 3) | MMX.PSrlD (key, 29); + } + // MMX.PMAddWD (key, key >> 32); + return (uint)((short)key * (short)(key >> 32) + (short)(key >> 16) * (short)(key >> 48)); + } + } + } + + public override string DecryptName (byte[] name, int name_length) + { + int char_count = name_length / 2; + int hash = (char_count * char_count) ^ char_count; + hash ^= (int)(0x3E13 ^ (ArcKey >> 16) ^ ArcKey); + hash &= 0xFFFF; + int key = hash; + for (int i = 0; i < char_count; ++i) + { + key = hash + i + 8 * key; + name[i*2 ] ^= (byte)key; + name[i*2+1] ^= (byte)(key >> 8); + } + return Encoding.Unicode.GetString (name, 0, name_length); + } + + public override void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry) + { + if (offset < 0) + throw new ArgumentOutOfRangeException ("offset"); + if (length > data.Length || offset > data.Length - length) + throw new ArgumentOutOfRangeException ("length"); + if (length < 8) + return; + + var file_name = entry.Name; + uint hash = 0x85F532; + uint seed = 0x33F641; + + for (int i = 0; i < file_name.Length; i++) + { + hash += (uint)(file_name[i] << (i & 7)); + seed ^= hash; + } + + seed += ArcKey ^ (7 * ((uint)length & 0xFFFFFF) + (uint)length + + hash + (hash ^ (uint)length ^ 0x8F32DCu)); + seed = 9 * (seed & 0xFFFFFF); + var table = GenerateTable (0x20, seed); // originally 0x2C, 12 dwords not used + unsafe + { + fixed (byte* raw_data = &data[offset]) + { + ulong* data64 = (ulong*)raw_data; + int qword_length = length / 8; + uint k = 2 * (table[0xD] & 0xF); + ulong hash64 = table[6] | (ulong)table[7] << 32; + for (int i = 0; i < qword_length; ++i) + { + ulong t = table[k] | (ulong)table[k+1] << 32; + hash64 = MMX.PAddD (hash64 ^ t, t); + + ulong d = data64[i] ^ hash64; + data64[i] = d; + + hash64 = MMX.PAddB (hash64, d) ^ d; + hash64 = MMX.PAddW (MMX.PSllD (hash64, 1), d); + + k = (k + 2) & 0x1F; + } + } + } + } + + static uint[] GenerateTable (int length, uint seed) + { + const uint key = 0x8DF21431u; + var table = new uint[length]; + for (int i = 0; i < length; ++i) + { + ulong x = key * (ulong)(seed ^ key); + seed = (uint)((x >> 32) + x); + table[i] = seed; + } + return table; + } + } +}