diff --git a/ArcFormats/aNCHOR/ArcFPD.cs b/ArcFormats/aNCHOR/ArcFPD.cs index f9c1a3cc..2a4ad5c0 100644 --- a/ArcFormats/aNCHOR/ArcFPD.cs +++ b/ArcFormats/aNCHOR/ArcFPD.cs @@ -63,7 +63,7 @@ namespace GameRes.Formats.Anchor { return null; uint index_size = (uint)(data_start - offset); - var key = QueryKey(); + var key = DefaultScheme.ArchiveKey; var dir = new List(count); using (var input = file.CreateStream(offset, (uint)(data_start - offset))) @@ -96,6 +96,8 @@ namespace GameRes.Formats.Anchor { for (int i = 0; i < count; i++) { dir[i].Name = Binary.GetCString(names, (int)name_offsets[i], Encoding.UTF8); dir[i].Type = FormatCatalog.Instance.GetTypeFromName(dir[i].Name); + if (dir[i].Name.EndsWith(".epk")) + dir[i].Type = "script"; } } return new FpdArchive(file, this, dir, key); @@ -105,17 +107,25 @@ namespace GameRes.Formats.Anchor { public override Stream OpenEntry(ArcFile arc, Entry entry) { var farc = arc as FpdArchive; var pent = entry as PackedEntry; - var input = farc.File.CreateStream(entry.Offset, entry.Size); - var decrypted = new ByteStringEncryptedStream(input, farc.Key); + Stream input = farc.File.CreateStream(entry.Offset, entry.Size); + input = new ByteStringEncryptedStream(input, farc.Key); if (pent.IsPacked) - return new ZLibStream(decrypted, CompressionMode.Decompress); - else - return decrypted; - // TODO: epk decryption - } - - byte[] QueryKey() { - return DefaultScheme.ArchiveKey; + input = new ZLibStream(input, CompressionMode.Decompress); + if (pent.Name.EndsWith(".epk")) { + var mem = new MemoryStream(); + input.CopyTo(mem); + mem.Seek(-0x20, SeekOrigin.End); + var buf = new byte[4]; + mem.Read(buf, 0, 4); + uint last = Binary.BigEndian(BitConverter.ToUInt32(buf, 0)); + mem.Seek(0, SeekOrigin.Begin); + + var key = Encoding.UTF8.GetBytes(Path.GetFileNameWithoutExtension(pent.Name)); + var encryption = new Mk2Blowfish(key, DefaultScheme.EpkContext); + input = new InputCryptoStream(mem, encryption.CreateDecryptor()); + input = new LimitStream(input, last); + } + return input; } FpdScheme DefaultScheme = new FpdScheme(); @@ -129,6 +139,6 @@ namespace GameRes.Formats.Anchor { [Serializable] public class FpdScheme : ResourceScheme { public byte[] ArchiveKey; - public byte[] EpkKey; + public byte[] EpkContext; } } diff --git a/ArcFormats/aNCHOR/AudioFCD.cs b/ArcFormats/aNCHOR/AudioFCD.cs index 56f79fa1..e79c3ce1 100644 --- a/ArcFormats/aNCHOR/AudioFCD.cs +++ b/ArcFormats/aNCHOR/AudioFCD.cs @@ -27,6 +27,8 @@ using System; using System.ComponentModel.Composition; using System.IO; using System.Linq; +using GameRes.Cryptography; +using GameRes.Utility; namespace GameRes.Formats.Anchor { @@ -45,11 +47,57 @@ namespace GameRes.Formats.Anchor public override SoundInput TryOpen (IBinaryStream file) { file.Position = 4; - // guess: big endian, version=2, type=0 (ogg), offset=0xC - byte[] data = { 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x4F, 0x67, 0x67, 0x53 }; - if (!file.ReadBytes (0x0C).SequenceEqual (data)) - throw new NotSupportedException(); - return new OggInput (new StreamRegion (file.AsStream, 0x0C)); + + uint version = Binary.BigEndian (file.ReadUInt16()); + if (version != 2) + return null; + + uint count = Binary.BigEndian (file.ReadUInt16()); + uint offset = Binary.BigEndian (file.ReadUInt32()); + Stream region = new StreamRegion (file.AsStream, offset); + + if (count == 0 && offset == 0x0C) + return new OggInput (region); + + if (count == 1 && offset == 0x1C) + { + file.Position = 0x0C; + uint type = Binary.BigEndian (file.ReadUInt32()); + if (type != 2) + return null; + + var md5 = new MD5(); + md5.Initialize(); + md5.Update (file.ReadBytes (0xC), 0, 0xC); + md5.Update (DefaultScheme.Md5Addition, 0, 0x4000); + md5.Final(); + + var key = new byte[16]; + Buffer.BlockCopy (md5.State, 0, key, 0, 16); + var encryption = new Mk2Blowfish (key, DefaultScheme.Context); + var stream = new InputCryptoStream (region, encryption.CreateDecryptor()); + var mem = new MemoryStream(); + stream.CopyTo (mem); + mem.Position = 0; + return new OggInput (mem); + } + + return null; + } + + FcdScheme DefaultScheme = new FcdScheme(); + + public override ResourceScheme Scheme + { + get { return DefaultScheme; } + set { DefaultScheme = (FcdScheme)value; } } } + + [Serializable] + public class FcdScheme : ResourceScheme + { + public byte[] Context; + public byte[] Md5Addition; + } } diff --git a/ArcFormats/aNCHOR/Blowfish.cs b/ArcFormats/aNCHOR/Blowfish.cs new file mode 100644 index 00000000..1e5fd6ca --- /dev/null +++ b/ArcFormats/aNCHOR/Blowfish.cs @@ -0,0 +1,279 @@ +// vi: shiftwidth=8 noexpandtab +/**************************************************************************** + | + | Copyright (c) 2007 Novell, Inc. + | All Rights Reserved. + | + | This program is free software; you can redistribute it and/or + | modify it under the terms of version 2 of the GNU General Public License as + | published by the Free Software Foundation. + | + | This program is distributed in the hope that it will be useful, + | but WITHOUT ANY WARRANTY; without even the implied warranty of + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + | GNU General Public License for more details. + | + | You should have received a copy of the GNU General Public License + | along with this program; if not, contact Novell, Inc. + | + | To contact Novell about this file by physical or electronic mail, + | you may find current contact information at www.novell.com + | + | Author: Russ Young + | Thanks to: Bruce Schneier / Counterpane Labs + | for the Blowfish encryption algorithm and + | reference implementation. http://www.schneier.com/blowfish.html + |***************************************************************************/ + +//! \file Blowfish.cs +//! \date 2026-02-02 +//! \brief Modified Blowfish encryption algorithm implementation. +// + +using System; +using System.IO; +using System.Security.Cryptography; +using GameRes.Utility; + +namespace GameRes.Formats.Anchor +{ + /// + /// Class that provides blowfish encryption. + /// + public class Mk2Blowfish + { + const int N = 16; + + uint[] ctx; + + /// + /// Constructs and initializes a blowfish instance with the supplied key. + /// + /// The key to cipher with. + public Mk2Blowfish(byte[] key, byte[] _ctx) + { + short i; + short j; + short k; + uint data; + uint datal; + uint datar; + + ctx = new uint[N + 270]; + + for (i = 0; i < N + 2; ++i) + { + ctx[i] = BigEndian.ToUInt32 (_ctx, 4 * i); + } + + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 256; ++j) + { + ctx[N + 2 + i * 4 + j] = BigEndian.ToUInt32 (_ctx, 4 * (N + 2 + i * 256 + j)); + } + } + + j = 0; + for (i = 0; i < N + 2; ++i) + { + data = 0x00000000; + for (k = 0; k < 4; ++k) + { + data = (data << 8) | key[j]; + j++; + if (j >= key.Length) + { + j = 0; + } + } + ctx[i] = ctx[i] ^ data; + } + + datal = 0x00000000; + datar = 0x00000000; + + for (i = 0; i < N + 2; i += 2) + { + Encipher(ref datal, ref datar); + ctx[i] = datal; + ctx[i + 1] = datar; + } + + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 256; j += 2) + { + Encipher(ref datal, ref datar); + + ctx[N + 2 + i * 4 + j] = datal; + ctx[N + 3 + i * 4 + j] = datar; + } + } + } + + public ICryptoTransform CreateDecryptor () + { + return new Mk2BlowfishDecryptor (this); + } + + private uint F(uint x) + { + ushort a; + ushort b; + ushort c; + ushort d; + uint y; + + d = (ushort)(x & 0x00FF); + x >>= 8; + c = (ushort)(x & 0x00FF); + x >>= 8; + b = (ushort)(x & 0x00FF); + x >>= 8; + a = (ushort)(x & 0x00FF); + + y = ctx[18 + a] + ctx[22 + b]; + y = y ^ ctx[26 + c]; + y = y ^ ctx[30 + d]; + + return y; + } + + /// + /// Encrypts 8 bytes of data (1 block) + /// + /// The left part of the 8 bytes. + /// The right part of the 8 bytes. + private void Encipher(ref uint xl, ref uint xr) + { + uint Xl; + uint Xr; + uint temp; + short i; + + Xl = xl; + Xr = xr; + + for (i = 0; i < N; ++i) + { + Xl = Xl ^ ctx[i]; + Xr = F(Xl) ^ Xr; + + temp = Xl; + Xl = Xr; + Xr = temp; + } + + temp = Xl; + Xl = Xr; + Xr = temp; + + Xr = Xr ^ ctx[N]; + Xl = Xl ^ ctx[N + 1]; + + xl = Xl; + xr = Xr; + } + + /// + /// Decrypts 8 bytes of data (1 block) + /// + /// The left part of the 8 bytes. + /// The right part of the 8 bytes. + public void Decipher(ref uint xl, ref uint xr) + { + uint Xl; + uint Xr; + uint temp; + short i; + + Xl = xl; + Xr = xr; + + for (i = N + 1; i > 1; --i) + { + Xl = Xl ^ ctx[i]; + Xr = F(Xl) ^ Xr; + + /* Exchange Xl and Xr */ + temp = Xl; + Xl = Xr; + Xr = temp; + } + + /* Exchange Xl and Xr */ + temp = Xl; + Xl = Xr; + Xr = temp; + + Xr = Xr ^ ctx[1]; + Xl = Xl ^ ctx[0]; + + xl = Xl; + xr = Xr; + } + } + + /// + /// ICryptoTransform implementation for use with CryptoStream. + /// + public sealed class Mk2BlowfishDecryptor : ICryptoTransform + { + Mk2Blowfish m_bf; + + public const int BlockSize = 8; + + public bool CanTransformMultipleBlocks { get { return true; } } + public bool CanReuseTransform { get { return true; } } + public int InputBlockSize { get { return BlockSize; } } + public int OutputBlockSize { get { return BlockSize; } } + + public Mk2BlowfishDecryptor (Mk2Blowfish bf) + { + m_bf = bf; + } + + public int TransformBlock (byte[] inBuffer, int offset, int count, byte[] outBuffer, int outOffset) + { + for (int i = 0; i < count; i += BlockSize) + { + uint xl = BigEndian.ToUInt32 (inBuffer, offset+i); + uint xr = BigEndian.ToUInt32 (inBuffer, offset+i+4); + m_bf.Decipher (ref xl, ref xr); + BigEndian.Pack (xl, outBuffer, outOffset+i); + BigEndian.Pack (xr, outBuffer, outOffset+i+4); + } + return count; + } + + static readonly byte[] EmptyArray = new byte[0]; + + public byte[] TransformFinalBlock (byte[] inBuffer, int offset, int count) + { + if (0 == count) + return EmptyArray; + + var input = new byte[(count + BlockSize - 1) / BlockSize * BlockSize]; + Buffer.BlockCopy (inBuffer, 0, input, 0, inBuffer.Length); + + var output = new byte[input.Length]; + TransformBlock (input, offset, count, output, 0); + + Array.Resize (ref output, count); + return output; + } + + #region IDisposable implementation + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + _disposed = true; + } + GC.SuppressFinalize (this); + } + #endregion + } +}