|
using System; |
|
using System.IO; |
|
using System.Runtime.InteropServices; |
|
using System.Security.Cryptography; |
|
|
|
namespace ConsoleAppFramework |
|
{ |
|
public unsafe class CookieRunDJBF |
|
{ |
|
private static readonly byte[] Key = new byte[] |
|
{ |
|
0xC0, 0x01, 0xC1, 0xE1, 0x26, 0x11, 0x10, 0xDA, |
|
0x90, 0x90, 0x35, 0x81, 0xFE, 0xBA, 0xA9, 0x7F, |
|
0xA1, 0x45, 0x1C, 0x4F, 0x97, 0x88, 0x71, 0xFA, |
|
0xC3, 0xF1, 0xF8, 0x29, 0x3D, 0xDE, 0xE2, 0xB3 |
|
}; |
|
|
|
private static readonly byte[] IV = new byte[] |
|
{ |
|
0x58, 0xA8, 0xB9, 0xDD, 0x13, 0x61, 0x62, 0xAA, |
|
0x99, 0x88, 0x7A, 0x1F, 0xF2, 0x3F, 0x7C, 0x91 |
|
}; |
|
|
|
public static byte[] Decrypt(string path) |
|
{ |
|
using (var fs = File.OpenRead(path)) |
|
{ |
|
var header = fs.Read<Header>(); |
|
|
|
// swap endian |
|
header.Version = (ushort)((header.Version >> 8) | ((header.Version & 0xFF) << 8)); |
|
|
|
// flag fixes for various versions |
|
if (header.Version < 0x0101) |
|
header.Flags &= ~Flags.FastLZ; |
|
if (header.Version < 0x0102) |
|
header.Flags &= ~Flags.AES_CBC; |
|
|
|
// ver 0x0100 files have bad data if 128 bit aligned |
|
if (header.DataSuffixSize > 0xF) |
|
header.DataSuffixSize = 0; |
|
|
|
// copy out the remaining data and append the suffix bytes |
|
var dataSize = (int)(fs.Length - fs.Position); |
|
var buffer = new byte[dataSize + header.DataSuffixSize]; |
|
|
|
fs.Read(buffer, 0, dataSize); |
|
Array.Copy(header.DataSuffix, 0, buffer, dataSize, header.DataSuffixSize); |
|
|
|
// AES decrypt |
|
if ((header.Flags & (Flags.AES_CBC | Flags.AES_ECB)) != 0) |
|
buffer = AESDecrypt(header, buffer); |
|
|
|
// FastLZ decompress |
|
if (header.Flags.HasFlag(Flags.FastLZ)) |
|
buffer = FastLZDecompress(header, buffer); |
|
|
|
var checksum = CRCUnsafe.Crc32(buffer); |
|
if (checksum != header.Checksum) |
|
throw new Exception($"Checksum :: {checksum:X8} != {header.Checksum:X8}"); |
|
|
|
return buffer; |
|
} |
|
} |
|
|
|
private static byte[] AESDecrypt(Header header, byte[] data) |
|
{ |
|
// create AES instance with salted IV |
|
var aes = new RijndaelManaged() |
|
{ |
|
Key = Key, |
|
IV = CreateIV(header.Checksum), |
|
Padding = PaddingMode.None |
|
}; |
|
|
|
// set mode and created decryptor |
|
ICryptoTransform decryptor; |
|
if (header.Flags.HasFlag(Flags.AES_ECB)) |
|
{ |
|
aes.Mode = CipherMode.ECB; |
|
decryptor = aes.CreateDecryptor(aes.Key, null); |
|
} |
|
else |
|
{ |
|
aes.Mode = CipherMode.CBC; |
|
decryptor = aes.CreateDecryptor(aes.Key, aes.IV); |
|
} |
|
|
|
// decrypt |
|
using (var msIn = new MemoryStream(data)) |
|
using (var msOut = new MemoryStream()) |
|
using (var cs = new CryptoStream(msIn, decryptor, CryptoStreamMode.Read)) |
|
{ |
|
cs.CopyTo(msOut); |
|
|
|
// truncate padding |
|
if (msOut.Length > header.DataSizeLo) |
|
msOut.SetLength(header.DataSizeLo); |
|
|
|
return msOut.ToArray(); |
|
} |
|
} |
|
|
|
private static byte[] FastLZDecompress(Header header, byte[] data) |
|
{ |
|
var buffer = new byte[header.DataSizeLo]; |
|
|
|
fixed (byte* input = &data[0]) |
|
fixed (byte* output = &buffer[0]) |
|
{ |
|
var read = FastLZ_Decompress(input, data.Length, output, buffer.Length); |
|
|
|
if (read != header.DataSizeLo) |
|
throw new Exception($"Fast_LZ : {read} != {header.DataSizeLo}"); |
|
|
|
return buffer; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// <see cref="IV"/> is salted by adding the first byte |
|
/// of the checksum to all values |
|
/// </summary> |
|
private static byte[] CreateIV(uint checksum) |
|
{ |
|
var result = new byte[16]; |
|
var salt = checksum & 0xFF; |
|
|
|
for (var i = 0; i < IV.Length; i++) |
|
result[i] = (byte)((IV[i] + salt) & 0xFF); |
|
|
|
return result; |
|
} |
|
|
|
/// <summary> |
|
/// Custom implementation that supports both levels |
|
/// <para>https://github.com/ariya/FastLZ</para> |
|
/// </summary> |
|
private static int FastLZ_Decompress(byte* input, int length, byte* output, int maxout) |
|
{ |
|
const uint MAX_L2_DISTANCE = 8191; |
|
|
|
// magic identifier for compression level |
|
int level = ((*input) >> 5) + 1; |
|
|
|
byte* ip = input; |
|
byte* ip_limit = ip + length; |
|
byte* ip_bound = ip_limit - 2; |
|
byte* op = output; |
|
byte* op_limit = op + maxout; |
|
uint ctrl = (uint)*ip++ & 31; |
|
|
|
while (true) |
|
{ |
|
if (ctrl >= 32) |
|
{ |
|
uint len = (ctrl >> 5) - 1; |
|
uint ofs = (ctrl & 31) << 8; |
|
byte* ref_ = op - ofs - 1; |
|
|
|
byte code; |
|
if (len == 7 - 1) |
|
{ |
|
do |
|
{ |
|
if (ip > ip_bound) |
|
break; |
|
|
|
code = *ip++; |
|
len += code; |
|
} |
|
while (level == 2 && code == 255); |
|
} |
|
|
|
code = *ip++; |
|
ref_ -= code; |
|
len += 3; |
|
|
|
// match from 16-bit distance |
|
if (level == 2 && code == 255) |
|
{ |
|
if (ofs == (31 << 8)) |
|
{ |
|
if (ip > ip_bound) |
|
break; |
|
|
|
ofs = (uint)*ip++ << 8; |
|
ofs += *ip++; |
|
ref_ = op - ofs - MAX_L2_DISTANCE - 1; |
|
} |
|
} |
|
|
|
if (op + len > op_limit) |
|
break; |
|
if (ref_ < output) |
|
break; |
|
|
|
FastLZ_MemMove(op, ref_, len); |
|
op += len; |
|
} |
|
else |
|
{ |
|
ctrl++; |
|
|
|
if (op + ctrl > op_limit) |
|
break; |
|
if (ip + ctrl > ip_limit) |
|
break; |
|
|
|
FastLZ_MemMove(op, ip, ctrl); |
|
ip += ctrl; |
|
op += ctrl; |
|
} |
|
|
|
if (level == 2 && ip >= ip_limit) |
|
break; |
|
if (level == 1 && ip > ip_bound) |
|
break; |
|
|
|
ctrl = *ip++; |
|
} |
|
|
|
return (int)(op - output); |
|
} |
|
|
|
private static void FastLZ_MemMove(byte* dest, byte* src, uint count) |
|
{ |
|
do *dest++ = *src++; |
|
while (--count != 0); |
|
} |
|
|
|
[StructLayout(LayoutKind.Sequential, Pack = 1)] |
|
public struct Header |
|
{ |
|
public uint Magic; |
|
public ushort Version; // BE |
|
public ushort Reserved; |
|
public uint Checksum; |
|
public int DataSizeLo; |
|
public int DataSizeHi; |
|
public Flags Flags; |
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 15)] |
|
public byte[] DataSuffix; |
|
public byte DataSuffixSize; |
|
} |
|
|
|
[Flags] |
|
public enum Flags : byte |
|
{ |
|
AES_ECB = 0x1, |
|
AES_CBC = 0x2, // ≥ 0x0102 |
|
FastLZ = 0x80, // ≥ 0x0101 |
|
} |
|
} |
|
} |
Put CookieRunDJBFDecryptor.exe in the game's directory and run. The tool will create a folder called
Dump
containing the decrypted contents of each of the DBJ files.Just to note, decrypted files will have the extension of
.bin
.