diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index fca1beb0..e0091ed1 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -169,6 +169,7 @@
+
diff --git a/ArcFormats/FamilyAdvSystem/ArcCSAF.cs b/ArcFormats/FamilyAdvSystem/ArcCSAF.cs
new file mode 100644
index 00000000..3736b7f3
--- /dev/null
+++ b/ArcFormats/FamilyAdvSystem/ArcCSAF.cs
@@ -0,0 +1,351 @@
+//! \file ArcCSAF.cs
+//! \date 2019 Jan 01
+//! \brief Family Adv System resource archive.
+//
+// Copyright (C) 2019 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.Composition;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using GameRes.Utility;
+
+namespace GameRes.Formats.FamilyAdvSystem
+{
+
+ [Export(typeof(ArchiveFormat))]
+ public class CsafOpener : ArchiveFormat
+ {
+ public override string Tag { get { return "CSAF"; } }
+ public override string Description { get { return "Family Adv System resource archive"; } }
+ public override uint Signature { get { return 0x46415343; } } // 'CSAF'
+ public override bool IsHierarchic { get { return false; } }
+ public override bool CanWrite { get { return false; } }
+
+ public CsafOpener ()
+ {
+ Extensions = new string[] { "" };
+ }
+
+ static readonly string DefaultKey = "江ノ島の南";
+ static readonly byte[] DefaultIV = Encoding.ASCII.GetBytes ("FamilyAdvSystem ");
+
+ public override ArcFile TryOpen (ArcView file)
+ {
+ uint flags = file.View.ReadUInt32 (4);
+ if ((flags & 0x7FFFFFFF) != 0x10000)
+ return null;
+ int count = file.View.ReadInt32 (8);
+ if (!IsSaneCount (count))
+ return null;
+ bool is_encrypted = (flags >> 31) != 0;
+ uint index_size = (uint)((count * 24 + 31) & -4096) + 0xFE0u;
+ uint names_size = file.View.ReadUInt32 (12);
+ var arc_md5 = file.View.ReadBytes (0x10, 0x10);
+ var index = new byte[index_size + names_size];
+ CsafEncryption enc = null;
+ try
+ {
+ if (is_encrypted)
+ {
+ file.View.Read (0x20, index, 0, index_size);
+ enc = new CsafEncryption (DefaultKey, DefaultIV);
+ using (var decryptor = enc.CreateDecryptor (0))
+ using (var enc_names = file.CreateStream (0x20 + index_size, names_size))
+ using (var dec_names = new InputCryptoStream (enc_names, decryptor))
+ {
+ dec_names.Read (index, (int)index_size, (int)names_size);
+ }
+ }
+ else
+ {
+ file.View.Read (0x20, index, 0, index_size + names_size);
+ }
+ using (var md5 = MD5.Create())
+ {
+ var hash = md5.ComputeHash (index);
+ if (!hash.SequenceEqual (arc_md5))
+ return null;
+ int index_pos = 0x10;
+ int name_pos = (int)index_size;
+ var dir = new List (count);
+ for (int i = 0; i < count; ++i)
+ {
+ int j;
+ for (j = name_pos; j+1 < index.Length; j += 2)
+ {
+ if (index[j] == 0 && index[j+1] == 0)
+ break;
+ }
+ int name_length = j - name_pos;
+ var name = Encoding.Unicode.GetString (index, name_pos, name_length);
+// hash = md5.ComputeHash (index, name_pos, name_length); // == [index_pos-0x10]
+ name_pos += name_length + 10;
+
+ var entry = Create (name);
+ entry.Offset = (long)index.ToUInt32 (index_pos) << 12;
+ entry.Size = index.ToUInt32 (index_pos+4);
+ index_pos += 0x18;
+ if (!entry.CheckPlacement (file.MaxOffset))
+ return null;
+ dir.Add (entry);
+ }
+ if (!is_encrypted)
+ return new ArcFile (file, this, dir);
+ var arc = new CsafArchive (file, this, dir, enc);
+ enc = null;
+ return arc;
+ }
+ }
+ finally
+ {
+ if (enc != null)
+ enc.Dispose();
+ }
+ }
+
+ public override Stream OpenEntry (ArcFile arc, Entry entry)
+ {
+ if (0 == entry.Size)
+ return Stream.Null;
+ var carc = arc as CsafArchive;
+ if (null == carc)
+ return base.OpenEntry (arc, entry);
+ var input = new CsafStream (carc);
+ return new StreamRegion (input, entry.Offset, entry.Size);
+ }
+ }
+
+ internal class CsafEncryption : IDisposable
+ {
+ Aes m_aes;
+ MD5 m_md5;
+ byte[] m_key;
+ byte[] m_iv;
+
+ public CsafEncryption (string password, byte[] iv)
+ {
+ m_md5 = MD5.Create();
+ m_aes = Aes.Create();
+ m_aes.Mode = CipherMode.CBC;
+ m_aes.Padding = PaddingMode.None;
+ m_key = InitKey (password);
+ m_iv = iv;
+ }
+
+ public ICryptoTransform CreateDecryptor (int block_num)
+ {
+ var block_key = GetBlockKey (block_num);
+ return m_aes.CreateDecryptor (block_key, m_iv);
+ }
+
+ byte[] InitKey (string pass_phrase)
+ {
+ var key = new byte[32];
+ if (!string.IsNullOrEmpty (pass_phrase))
+ {
+ var bytes = Encoding.Unicode.GetBytes (pass_phrase);
+ var hash = m_md5.ComputeHash (bytes);
+ Buffer.BlockCopy (hash, 0, key, 0, 16);
+ hash = m_md5.ComputeHash (bytes, 1, bytes.Length - 2);
+ Buffer.BlockCopy (hash, 0, key, 16, 16);
+ }
+ return key;
+ }
+
+ byte[] GetBlockKey (int block_num)
+ {
+ int offset = block_num / 8;
+ int shift = block_num & 7;
+ var key = new byte[32];
+ var buf = new byte[16];
+ for (int i = 0; i < 16; ++i)
+ {
+ buf[i] = Binary.RotByteL (m_key[(offset + i) & 0xF], shift);
+ }
+ var hash = m_md5.ComputeHash (buf, 0, 16);
+ Buffer.BlockCopy (hash, 0, key, 0, 16);
+ for (int i = 0; i < 16; ++i)
+ {
+ buf[i] = Binary.RotByteL (m_key[16 + ((offset + i) & 0xF)], shift);
+ }
+ hash = m_md5.ComputeHash (buf, 0, 16);
+ Buffer.BlockCopy (hash, 0, key, 16, 16);
+ return key;
+ }
+
+ #region IDisposable Members
+ bool m_disposed = false;
+ public void Dispose ()
+ {
+ if (!m_disposed)
+ {
+ m_aes.Dispose();
+ m_md5.Dispose();
+ m_disposed = true;
+ }
+ }
+ #endregion
+ }
+
+ internal class CsafArchive : ArcFile
+ {
+ public readonly CsafEncryption Encryption;
+
+ public CsafArchive (ArcView arc, ArchiveFormat impl, ICollection dir, CsafEncryption enc)
+ : base (arc, impl, dir)
+ {
+ Encryption = enc;
+ }
+
+ #region IDisposable Members
+ bool _csaf_disposed = false;
+ protected override void Dispose (bool disposing)
+ {
+ if (_csaf_disposed)
+ return;
+
+ if (disposing)
+ Encryption.Dispose();
+ _csaf_disposed = true;
+ base.Dispose (disposing);
+ }
+ #endregion
+ }
+
+ internal class CsafStream : Stream
+ {
+ readonly long m_length;
+ ArcView.Frame m_view;
+ CsafEncryption m_encryption;
+ long m_position = 0;
+ byte[] m_block = new byte[0x1000];
+ long m_block_start = 0;
+ int m_block_length = 0;
+
+ public override bool CanRead { get { return true; } }
+ public override bool CanSeek { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+
+ public CsafStream (CsafArchive arc)
+ {
+ m_length = arc.File.MaxOffset;
+ m_view = arc.File.CreateFrame();
+ m_encryption = arc.Encryption;
+ }
+
+ public override int Read (byte[] buffer, int offset, int count)
+ {
+ int read = 0;
+ while (count > 0)
+ {
+ if (!(m_position >= m_block_start && m_position < m_block_start + m_block_length))
+ {
+ if (!ReadBlock())
+ break;
+ }
+ int block_pos = (int)m_position & 0xFFF;
+ int avail = Math.Min (count, m_block_length - block_pos);
+ Buffer.BlockCopy (m_block, block_pos, buffer, offset, avail);
+ m_position += avail;
+ offset += avail;
+ read += avail;
+ count -= avail;
+ }
+ return read;
+ }
+
+ bool ReadBlock ()
+ {
+ if (m_position >= m_length)
+ return false;
+ m_block_start = m_position & ~0xFFFL;
+ m_block_length = m_view.Read (m_block_start, m_block, 0, 0x1000);
+ if (m_block_length != 0x1000)
+ return false;
+ using (var decryptor = m_encryption.CreateDecryptor ((int)(m_block_start >> 12)))
+ using (var enc = new BinMemoryStream (m_block))
+ using (var dec = new InputCryptoStream (enc, decryptor))
+ dec.Read (m_block, 0, m_block_length);
+ return true;
+ }
+
+ #region IO.Stream members
+ public override long Length { get { return m_length; } }
+ public override long Position
+ {
+ get { return m_position; }
+ set { m_position = value; }
+ }
+
+ public override void Flush ()
+ {
+ }
+
+ public override long Seek (long offset, SeekOrigin origin)
+ {
+ if (SeekOrigin.Begin == origin)
+ Position = offset;
+ else if (SeekOrigin.Current == origin)
+ Position = m_position + offset;
+ else
+ Position = m_length + offset;
+
+ return m_position;
+ }
+
+ public override void SetLength (long length)
+ {
+ throw new NotSupportedException ("CsafStream.SetLength method is not supported");
+ }
+
+ public override void Write (byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException ("CsafStream.Write method is not supported");
+ }
+
+ public override void WriteByte (byte value)
+ {
+ throw new NotSupportedException("CsafStream.WriteByte method is not supported");
+ }
+ #endregion
+
+ #region IDisposable Members
+ bool _disposed = false;
+ protected override void Dispose (bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ m_view.Dispose();
+ }
+ _disposed = true;
+ base.Dispose (disposing);
+ }
+ }
+ #endregion
+ }
+}