Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active September 30, 2022 13:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barncastle/3c96cec175bb65c2381a7887272614b9 to your computer and use it in GitHub Desktop.
Save barncastle/3c96cec175bb65c2381a7887272614b9 to your computer and use it in GitHub Desktop.
Code for decrypting Cookie Run Kakao DBJ files
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
}
}
}
@barncastle
Copy link
Author

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.

@barncastle
Copy link
Author

private static readonly byte[] Key = new byte[]
{
    0xC0, 0x29, 0xC1, 0xE1, 0x26, 0x88, 0x71, 0xFA,
    0xA1, 0x45, 0x1C, 0x4F, 0x97, 0xDE, 0xD2, 0xB3,
    0x90, 0x94, 0x35, 0x81, 0xFE, 0xBA, 0xA9, 0x7F,
    0xC3, 0xF1, 0xF8, 0x29, 0x3D, 0x11, 0x10, 0xFA,
};

private static readonly byte[] IV = new byte[]
{
    0x13, 0x61, 0x62, 0xAA, 0x38, 0xA8, 0xB9, 0xDD,
    0x99, 0x6F, 0xF2, 0x3F, 0x7C, 0x91, 0x88, 0x7A
};

AES Key and IV from QQ 2.0

@barncastle
Copy link
Author

barncastle commented Jul 14, 2022

In theory the game should support loading files without any encryption or compression. Using DJBF_Template.djb you would need to do the following:

  • Put the CRC32 of the data at position 8
  • Put the length of the data at position 12
  • Put the data at the end of the template (position 37)

EDIT: Whilst the game does load unencrypted file contents it then crashes when trying to parse them. However this doesn't seem to occur when the exact same contents is compressed and/or encrypted... I'm not sure what is going on here but the easiest solution is probably to write a tool that performs the encryption process too.

@probperiplum
Copy link

Would you be still interested in writing a program that does encode the text still with the DJBF header and all? I've tried looking for people who may know how to do it and they were either busy or simply not interested. Thanks.

@barncastle
Copy link
Author

I have another project that should handle this here. There's a download under the Releases section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment