mirror of
https://github.com/crskycode/GARbro.git
synced 2024-12-24 03:44:13 +08:00
348 lines
12 KiB
C#
348 lines
12 KiB
C#
//! \file ChainReactionCrypt.cs
|
|
//! \date Mon Mar 07 15:59:47 2016
|
|
//! \brief KiriKiri XP3 ecryption filter used in some games.
|
|
//
|
|
// Copyright (C) 2016-2018 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
|
|
{
|
|
readonly string m_list_bin;
|
|
|
|
public ChainReactionCrypt () : this ("plugin/list.bin")
|
|
{
|
|
}
|
|
|
|
public ChainReactionCrypt (string list_file)
|
|
{
|
|
m_list_bin = list_file;
|
|
}
|
|
|
|
public override void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count)
|
|
{
|
|
uint limit = GetEncryptionLimit (entry);
|
|
if (offset >= limit)
|
|
return;
|
|
count = Math.Min ((int)(limit - offset), count);
|
|
uint key = entry.Hash;
|
|
int ofs = (int)offset;
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
values[pos+i] ^= (byte)((ofs+i) ^ (byte)(key >> (((ofs+i) & 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);
|
|
}
|
|
|
|
protected virtual uint GetEncryptionLimit (Xp3Entry entry)
|
|
{
|
|
uint limit;
|
|
if (EncryptionThresholdMap != null && EncryptionThresholdMap.TryGetValue (entry.Hash, out limit))
|
|
return limit;
|
|
else
|
|
return 0x200;
|
|
}
|
|
|
|
[NonSerialized]
|
|
Dictionary<uint, uint> EncryptionThresholdMap;
|
|
|
|
public override void Init (ArcFile arc)
|
|
{
|
|
var bin = ReadListBin (arc, m_list_bin);
|
|
if (null == bin || bin.Length <= 0x30)
|
|
return;
|
|
|
|
Init (bin);
|
|
}
|
|
|
|
internal void Init (byte[] bin)
|
|
{
|
|
if (!Binary.AsciiEqual (bin, "\"\x0D\x0A"))
|
|
{
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
bin = DecodeListBin (bin);
|
|
if (null == bin)
|
|
return;
|
|
}
|
|
}
|
|
if (null == EncryptionThresholdMap)
|
|
EncryptionThresholdMap = new Dictionary<uint, uint>();
|
|
else
|
|
EncryptionThresholdMap.Clear();
|
|
|
|
ParseListBin (bin);
|
|
}
|
|
|
|
internal byte[] ReadListBin (ArcFile arc, string list_name)
|
|
{
|
|
var list_bin = arc.Dir.FirstOrDefault (e => e.Name == list_name) as Xp3Entry;
|
|
if (null == list_bin)
|
|
return null;
|
|
var bin = new byte[list_bin.UnpackedSize];
|
|
using (var input = arc.OpenEntry (list_bin))
|
|
input.Read (bin, 0, bin.Length);
|
|
return 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];
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class HachukanoCrypt : ChainReactionCrypt
|
|
{
|
|
public HachukanoCrypt () : base ("plugins/list.txt")
|
|
{
|
|
StartupTjsNotEncrypted = true;
|
|
}
|
|
|
|
protected override uint GetEncryptionLimit (Xp3Entry entry)
|
|
{
|
|
uint limit = base.GetEncryptionLimit (entry);
|
|
switch (limit)
|
|
{
|
|
case 0: return 0;
|
|
case 1: return 0x100;
|
|
case 2: return 0x200;
|
|
case 3: return entry.UnpackedSize;
|
|
default: return limit;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class ChocolatCrypt : ChainReactionCrypt
|
|
{
|
|
public ChocolatCrypt () : base ("plugins/list.txt")
|
|
{
|
|
StartupTjsNotEncrypted = true;
|
|
}
|
|
|
|
protected override uint GetEncryptionLimit (Xp3Entry entry)
|
|
{
|
|
uint limit = base.GetEncryptionLimit (entry);
|
|
switch (limit)
|
|
{
|
|
case 0: return 0;
|
|
case 2: return entry.UnpackedSize;
|
|
default: return 0x100;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class XanaduCrypt : ChainReactionCrypt
|
|
{
|
|
public XanaduCrypt () : base ("plugins/list.txt")
|
|
{
|
|
StartupTjsNotEncrypted = true;
|
|
}
|
|
|
|
public override void Init (ArcFile arc)
|
|
{
|
|
var bin = ReadListBin (arc, "list2.txt");
|
|
if (null == bin)
|
|
bin = ReadListBin (arc, "plugins/list.txt");
|
|
if (null == bin)
|
|
return;
|
|
|
|
Init (bin);
|
|
}
|
|
|
|
protected override uint GetEncryptionLimit (Xp3Entry entry)
|
|
{
|
|
uint limit = base.GetEncryptionLimit (entry);
|
|
switch (limit)
|
|
{
|
|
case 0: return 0;
|
|
case 2: return entry.UnpackedSize;
|
|
default: return 0x100;
|
|
}
|
|
}
|
|
|
|
public override void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count)
|
|
{
|
|
uint limit = GetEncryptionLimit (entry);
|
|
if (offset >= limit)
|
|
return;
|
|
count = Math.Min ((int)(limit - offset), count);
|
|
uint key = entry.Hash ^ ~0x03020100u;
|
|
int ofs = (int)offset;
|
|
byte extra = (byte)(((ofs & 0xFF) >> 2) << 2);
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
if (((ofs + i) & 0xFF) == 0)
|
|
extra = 0;
|
|
else if (((ofs + i) & 3) == 0)
|
|
extra += 4;
|
|
values[pos+i] ^= (byte)((key >> (((ofs+i) & 3) << 3)) ^ extra);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class SisMikoCrypt : XanaduCrypt
|
|
{
|
|
public override void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count)
|
|
{
|
|
uint limit = GetEncryptionLimit (entry);
|
|
if (offset >= limit)
|
|
return;
|
|
count = Math.Min ((int)(limit - offset), count);
|
|
uint key = ~Binary.RotR (entry.Hash, 16);
|
|
int ofs = (int)offset;
|
|
for (int i = 0; i < count; ++i)
|
|
{
|
|
values[pos+i] ^= (byte)(key >> (((ofs+i) & 3) << 3));
|
|
}
|
|
}
|
|
}
|
|
}
|