From d2eb1bbf8f18a1e2cba45d6c6ec907df277e7905 Mon Sep 17 00:00:00 2001 From: morkt Date: Wed, 27 May 2015 13:57:34 +0400 Subject: [PATCH] implemented Entis archives decryption. --- ArcFormats/ArcNOA.cs | 389 ++++++++++++++++++++- ArcFormats/Properties/Settings.Designer.cs | 24 ++ ArcFormats/Properties/Settings.settings | 6 + ArcFormats/Strings/arcStrings.Designer.cs | 11 +- ArcFormats/Strings/arcStrings.resx | 3 + ArcFormats/Strings/arcStrings.ru-RU.resx | 3 + ArcFormats/WidgetNOA.xaml | 10 + ArcFormats/WidgetNOA.xaml.cs | 18 + ArcFormats/app.config | 6 + 9 files changed, 459 insertions(+), 11 deletions(-) create mode 100644 ArcFormats/WidgetNOA.xaml create mode 100644 ArcFormats/WidgetNOA.xaml.cs diff --git a/ArcFormats/ArcNOA.cs b/ArcFormats/ArcNOA.cs index cc83ff03..56e1ddfd 100644 --- a/ArcFormats/ArcNOA.cs +++ b/ArcFormats/ArcNOA.cs @@ -1,4 +1,4 @@ -//! \file ArcNOA.cs +//! \file ArcNOA.cs //! \date Thu Apr 23 15:57:17 2015 //! \brief Entis GLS engine archives implementation. // @@ -26,11 +26,21 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; +using System.Diagnostics; using System.IO; +using System.Text; +using GameRes.Formats.Properties; +using GameRes.Formats.Strings; using GameRes.Utility; namespace GameRes.Formats.Entis { + internal class NoaOptions : ResourceOptions + { + public string Scheme { get; set; } + public string PassPhrase { get; set; } + } + internal class NoaEntry : Entry { public byte[] Extra; @@ -38,6 +48,17 @@ namespace GameRes.Formats.Entis public uint Attr; } + internal class NoaArchive : ArcFile + { + public string Password; + + public NoaArchive (ArcView arc, ArchiveFormat impl, ICollection dir, string password = null) + : base (arc, impl, dir) + { + Password = password; + } + } + [Export(typeof(ArchiveFormat))] public class NoaOpener : ArchiveFormat { @@ -47,6 +68,28 @@ namespace GameRes.Formats.Entis public override bool IsHierarchic { get { return false; } } public override bool CanCreate { get { return false; } } + public NoaOpener () + { + Extensions = new string[] { "noa", "dat" }; + } + + public static readonly Dictionary> KnownKeys = + new Dictionary> { + { arcStrings.NOAIgnoreEncryption, new Dictionary() }, + { "Yatohime Zankikou", new Dictionary { + { "data1.noa", "arcdatapass" }, + { "data6.noa", "cfe7231hf9qccda" }, + { "data7.noa", "ceiuvw86680efq0hHDUHF673j" } } }, + { "You! Apron Chakuyou", new Dictionary { + { "containerb.noa", "7DQ1Xm7ZahIv1ZwlFgyMTMryKC6OP9V6cAgL64WD5JLyvmeEyqTSA5rUbRigOtebnnK4MuOptwsbOf4K8UBDH4kpAUOQgB71Qr1qxtHGxQl8KZKj6WIYWpPh0G3JOJat" } } }, + { "Do S Ane to Boku no Hounyou Kankei", new Dictionary { + { "d02.dat", "vwerc7s65r21bnfu" }, + { "d03.dat", "ctfvgbhnj67y8u" } } }, + { "Innyuu Famiresu", new Dictionary { + { "d01.dat", "vdiu$43AfUCfh9aksf" }, + { "d03.dat", "gaivnwq7365e021gf" } } }, + }; + public override ArcFile TryOpen (ArcView file) { if (!file.View.AsciiEqual (0, "Entis\x1a")) @@ -57,7 +100,27 @@ namespace GameRes.Formats.Entis var reader = new IndexReader (file); if (!reader.ParseDirEntry (0x40, "")) return null; - return new ArcFile (file, this, reader.Dir); + if (!reader.HasEncrypted) + return new ArcFile (file, this, reader.Dir); + + var options = Query (arcStrings.ArcEncryptedNotice); + string password = null; + if (!string.IsNullOrEmpty (options.PassPhrase)) + { + password = options.PassPhrase; + } + else if (!string.IsNullOrEmpty (options.Scheme)) + { + Dictionary filemap; + if (KnownKeys.TryGetValue (options.Scheme, out filemap)) + { + var filename = Path.GetFileName (file.Name).ToLowerInvariant(); + filemap.TryGetValue (filename, out password); + } + } + if (string.IsNullOrEmpty (password)) + return new ArcFile (file, this, reader.Dir); + return new NoaArchive (file, this, reader.Dir, password); } public override Stream OpenEntry (ArcFile arc, Entry entry) @@ -66,28 +129,84 @@ namespace GameRes.Formats.Entis if (null == nent || !arc.File.View.AsciiEqual (entry.Offset, "filedata")) return arc.File.CreateStream (entry.Offset, entry.Size); ulong size = arc.File.View.ReadUInt64 (entry.Offset+8); - if (size > uint.MaxValue) + if (size > int.MaxValue) throw new FileSizeException(); - if (0 == nent.Encryption) - return arc.File.CreateStream (entry.Offset+0x10, (uint)size); - if (0x40000000 != nent.Encryption) + if (0 == size) + return Stream.Null; + + var input = arc.File.CreateStream (entry.Offset+0x10, (uint)size); + try { - System.Diagnostics.Trace.WriteLine (string.Format ("{0}: unknown encryption scheme 0x{1:x8}", - nent.Name, nent.Encryption)); - return arc.File.CreateStream (entry.Offset+0x10, (uint)size); + var narc = arc as NoaArchive; + if (0 == nent.Encryption || size < 4 || null == narc || null == narc.Password) + return input; + if (0x40000000 != nent.Encryption) + { + Trace.WriteLine (string.Format ("{0}: unknown encryption scheme 0x{1:x8}", + nent.Name, nent.Encryption)); + return input; + } + uint nTotalBytes = (uint)(size - 4); + var pBSHF = new BSHFDecodeContext (0x10000); + pBSHF.AttachInputFile (input); + pBSHF.PrepareToDecodeBSHFCode (narc.Password); + + byte[] buf = new byte[nTotalBytes]; + uint decoded = pBSHF.DecodeBSHFCodeBytes (buf, nTotalBytes); + if (decoded < nTotalBytes) + throw new EndOfStreamException ("Unexpected end of encrypted stream"); + /* + byte[] bufCRC = new byte[4]; + int iCRC = 0; + for (int i = 0; i < buf.Length; ++i) + { + bufCRC[iCRC] ^= buf[i]; + iCRC = (iCRC + 1) & 0x03; + } + uint orgCRC = arc.File.View.ReadUInt32 (entry.Offset+0x10+nTotalBytes); + uint crc = LittleEndian.ToUInt32 (bufCRC, 0); + if (orgCRC != crc) + { + Trace.WriteLine (string.Format ("{0}: CRC mismatch", nent.Name)); + input.Position = 0; + return input; + } + */ + input.Dispose(); + return new MemoryStream (buf); } - return arc.File.CreateStream (entry.Offset+0x10, (uint)size); + catch + { + input.Dispose(); + throw; + } + } + + public override ResourceOptions GetDefaultOptions () + { + return new NoaOptions { + Scheme = Settings.Default.NOAScheme, + PassPhrase = Settings.Default.NOAPassPhrase, + }; + } + + public override object GetAccessWidget () + { + return new GUI.WidgetNOA(); } internal class IndexReader { ArcView m_file; List m_dir = new List(); + bool m_found_encrypted = false; const char PathSeparatorChar = '/'; public List Dir { get { return m_dir; } } + public bool HasEncrypted { get { return m_found_encrypted; } } + public IndexReader (ArcView file) { m_file = file; @@ -113,14 +232,20 @@ namespace GameRes.Formats.Entis var entry = new NoaEntry(); entry.Size = m_file.View.ReadUInt32 (dir_offset); dir_offset += 8; + entry.Attr = m_file.View.ReadUInt32 (dir_offset); dir_offset += 4; + entry.Encryption = m_file.View.ReadUInt32 (dir_offset); + if (0 != entry.Encryption) + m_found_encrypted = true; dir_offset += 4; + entry.Offset = base_offset + m_file.View.ReadInt64 (dir_offset); if (!entry.CheckPlacement (m_file.MaxOffset)) return false; dir_offset += 0x10; + uint extra_length = m_file.View.ReadUInt32 (dir_offset); dir_offset += 4; if (extra_length > 0 && 0 == (entry.Attr & 0x70)) @@ -132,12 +257,15 @@ namespace GameRes.Formats.Entis dir_offset += extra_length; uint name_length = m_file.View.ReadUInt32 (dir_offset); dir_offset += 4; + string name = m_file.View.ReadString (dir_offset, name_length); dir_offset += name_length; + if (string.IsNullOrEmpty (cur_dir)) entry.Name = name; else entry.Name = cur_dir + PathSeparatorChar + name; + entry.Type = FormatCatalog.Instance.GetTypeFromName (name); if (0x10 == entry.Attr) { if (!ParseDirEntry (entry.Offset+0x10, entry.Name)) @@ -156,4 +284,245 @@ namespace GameRes.Formats.Entis } } } + + internal abstract class ERISADecodeContext + { + protected int m_nIntBufCount; + protected uint m_dwIntBuffer; + protected uint m_nBufferingSize; + protected uint m_nBufCount; + protected byte[] m_ptrBuffer; + protected int m_ptrNextBuf; + + protected Stream m_pFile; + protected bool m_nFlagEOF; + + public ERISADecodeContext (uint nBufferingSize) + { + m_nIntBufCount = 0; + m_nBufferingSize = (nBufferingSize + 0x03) & ~0x03u; + m_nBufCount = 0; + m_ptrBuffer = new byte[nBufferingSize]; + m_pFile = null; + m_nFlagEOF = false; + } + + public bool EofFlag { get { return m_nFlagEOF; } } // GetEOFFlag + + public void AttachInputFile (Stream file) + { + m_pFile = file; + } + + public uint ReadNextData (byte[] ptrBuffer, uint nBytes) + { + if (m_pFile != null) + { + uint read = (uint)m_pFile.Read (ptrBuffer, 0, (int)nBytes); + m_nFlagEOF = read != nBytes; + return read; + } + else + { + throw new ApplicationException ("Uninitialized ERISA encryption context"); + } + } + + public abstract uint DecodeBytes (Array ptrDst, uint nCount); + + protected bool PrefetchBuffer() + { + if (0 == m_nIntBufCount) + { + if (0 == m_nBufCount) + { + m_ptrNextBuf = 0; // m_ptrBuffer; + m_nBufCount = ReadNextData (m_ptrBuffer, m_nBufferingSize); + if (0 == m_nBufCount) + { + return false; + } + if (0 != (m_nBufCount & 0x03)) + { + uint i = m_nBufCount; + m_nBufCount += 4 - (m_nBufCount & 0x03); + while (i < m_nBufCount) + m_ptrBuffer[i ++] = 0; + } + } + m_nIntBufCount = 32; + m_dwIntBuffer = + ((uint)m_ptrBuffer[m_ptrNextBuf] << 24) | ((uint)m_ptrBuffer[m_ptrNextBuf+1] << 16) + | ((uint)m_ptrBuffer[m_ptrNextBuf+2] << 8) | (uint)m_ptrBuffer[m_ptrNextBuf+3]; + m_ptrNextBuf += 4; + m_nBufCount -= 4; + } + return true; + } + + public void FlushBuffer () + { + m_nIntBufCount = 0; + m_nBufCount = 0; + } + + public int GetABit () + { + if (!PrefetchBuffer()) + { + return 1; + } + int nValue = ((int)m_dwIntBuffer) >> 31; + --m_nIntBufCount; + m_dwIntBuffer <<= 1; + return nValue; + } + + public uint GetNBits (int n) + { + uint nCode = 0; + while (n != 0) + { + if (!PrefetchBuffer()) + break; + + int nCopyBits = Math.Min (n, m_nIntBufCount); + nCode = (nCode << nCopyBits) | (m_dwIntBuffer >> (32 - nCopyBits)); + n -= nCopyBits; + m_nIntBufCount -= nCopyBits; + m_dwIntBuffer <<= nCopyBits; + } + return nCode; + } + } + + internal class BSHFDecodeContext : ERISADecodeContext + { + ERIBshfBuffer m_pBshfBuf; + uint m_dwBufPos; + + public BSHFDecodeContext (uint nBufferingSize) : base (nBufferingSize) + { + m_pBshfBuf = null; + } + + public void PrepareToDecodeBSHFCode (string pszPassword) + { + if (null == m_pBshfBuf) + { + m_pBshfBuf = new ERIBshfBuffer(); + } + if (string.IsNullOrEmpty (pszPassword)) + { + pszPassword = " "; + } + int char_count = Encoding.ASCII.GetByteCount (pszPassword); + int length = Math.Max (char_count, 32); + var pass_bytes = new byte[length]; + char_count = Encoding.ASCII.GetBytes (pszPassword, 0, pszPassword.Length, pass_bytes, 0); + if (char_count < 32) + { + pass_bytes[char_count++] = 0x1b; + for (int i = char_count; i < 32; ++i) + { + pass_bytes[i] = (byte)(pass_bytes[i % char_count] + pass_bytes[i - 1]); + } + } + m_pBshfBuf.m_strPassword = pass_bytes; + m_pBshfBuf.m_dwPassOffset = 0; + m_dwBufPos = 32; + } + + public override uint DecodeBytes (Array ptrDst, uint nCount) + { + return DecodeBSHFCodeBytes (ptrDst as byte[], nCount); + } + + public uint DecodeBSHFCodeBytes (byte[] ptrDst, uint nCount) + { + uint nDecoded = 0; + while (nDecoded < nCount) + { + if (m_dwBufPos >= 32) + { + for (int i = 0; i < 32; ++i) + { + if (0 == m_nBufCount) + { + m_ptrNextBuf = 0; + m_nBufCount = ReadNextData (m_ptrBuffer, m_nBufferingSize); + if (0 == m_nBufCount) + { + return nDecoded; + } + } + m_pBshfBuf.m_srcBSHF[i] = m_ptrBuffer[m_ptrNextBuf++]; + m_nBufCount--; + } + m_pBshfBuf.DecodeBuffer(); + m_dwBufPos = 0; + } + ptrDst[nDecoded++] = m_pBshfBuf.m_bufBSHF[m_dwBufPos++]; + } + return nDecoded; + } + } + + internal class ERIBshfBuffer + { + public byte[] m_strPassword; + public uint m_dwPassOffset = 0; + public byte[] m_bufBSHF = new byte[32]; + public byte[] m_srcBSHF = new byte[32]; + public byte[] m_maskBSHF = new byte[32]; + + public void DecodeBuffer () + { + int nPassLen = m_strPassword.Length; + if ((int)m_dwPassOffset >= nPassLen) + { + m_dwPassOffset = 0; + } + for (int i = 0; i < 32; ++i) + { + m_bufBSHF[i] = 0; + m_maskBSHF[i] = 0; + } + int iPos = (int) m_dwPassOffset++; + int iBit = 0; + for (int i = 0; i < 256; ++i) + { + iBit = (iBit + m_strPassword[iPos++]) & 0xFF; + if (iPos >= nPassLen) + { + iPos = 0; + } + int iOffset = (iBit >> 3); + int iMask = (0x80 >> (iBit & 0x07)); + while (0xFF == m_maskBSHF[iOffset]) + { + iBit = (iBit + 8) & 0xFF; + iOffset = (iBit >> 3); + } + while (0 != (m_maskBSHF[iOffset] & iMask)) + { + iBit ++; + iMask >>= 1; + if (0 == iMask) + { + iBit = (iBit + 8) & 0xFF; + iOffset = (iBit >> 3); + iMask = 0x80; + } + } + Debug.Assert (iMask != 0); + m_maskBSHF[iOffset] |= (byte) iMask; + + if (0 != (m_srcBSHF[(i >> 3)] & (0x80 >> (i & 0x07)))) + { + m_bufBSHF[iOffset] |= (byte)iMask; + } + } + } + } } diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs index e60c0531..945f21f7 100644 --- a/ArcFormats/Properties/Settings.Designer.cs +++ b/ArcFormats/Properties/Settings.Designer.cs @@ -297,5 +297,29 @@ namespace GameRes.Formats.Properties { this["LPKScheme"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string NOAScheme { + get { + return ((string)(this["NOAScheme"])); + } + set { + this["NOAScheme"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string NOAPassPhrase { + get { + return ((string)(this["NOAPassPhrase"])); + } + set { + this["NOAPassPhrase"] = value; + } + } } } diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings index 944377f5..e07cca84 100644 --- a/ArcFormats/Properties/Settings.settings +++ b/ArcFormats/Properties/Settings.settings @@ -71,5 +71,11 @@ Default + + + + + + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs index 2c548ede..e2d2d37d 100644 --- a/ArcFormats/Strings/arcStrings.Designer.cs +++ b/ArcFormats/Strings/arcStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18444 +// Runtime Version:4.0.30319.34209 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -387,6 +387,15 @@ namespace GameRes.Formats.Strings { } } + /// + /// Looks up a localized string similar to Ignore encryption. + /// + public static string NOAIgnoreEncryption { + get { + return ResourceManager.GetString("NOAIgnoreEncryption", resourceCulture); + } + } + /// /// Looks up a localized string similar to Compress contents. /// diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx index e81b8166..baba98b5 100644 --- a/ArcFormats/Strings/arcStrings.resx +++ b/ArcFormats/Strings/arcStrings.resx @@ -315,4 +315,7 @@ predefined encryption scheme. Archive directory is encrypted. Enter archive encryption key. + + Ignore encryption + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.ru-RU.resx b/ArcFormats/Strings/arcStrings.ru-RU.resx index b666781f..66becc3c 100644 --- a/ArcFormats/Strings/arcStrings.ru-RU.resx +++ b/ArcFormats/Strings/arcStrings.ru-RU.resx @@ -210,6 +210,9 @@ Записывается оглавление... + + Игнорировать шифрование + Сжать содержимое diff --git a/ArcFormats/WidgetNOA.xaml b/ArcFormats/WidgetNOA.xaml new file mode 100644 index 00000000..f272ed3d --- /dev/null +++ b/ArcFormats/WidgetNOA.xaml @@ -0,0 +1,10 @@ + + + diff --git a/ArcFormats/WidgetNOA.xaml.cs b/ArcFormats/WidgetNOA.xaml.cs new file mode 100644 index 00000000..401c30b5 --- /dev/null +++ b/ArcFormats/WidgetNOA.xaml.cs @@ -0,0 +1,18 @@ +using System.Windows.Controls; + +namespace GameRes.Formats.GUI +{ + /// + /// Interaction logic for WidgetNOA.xaml + /// + public partial class WidgetNOA : Grid + { + public WidgetNOA () + { + InitializeComponent (); + // select first scheme as default + if (-1 == Scheme.SelectedIndex) + Scheme.SelectedIndex = 0; + } + } +} diff --git a/ArcFormats/app.config b/ArcFormats/app.config index 2fb41a85..457d4f15 100644 --- a/ArcFormats/app.config +++ b/ArcFormats/app.config @@ -73,6 +73,12 @@ Default + + + + + +