AES Encryption format

On Windows and Linux platforms, the encryption method is AES. This applies to the profile and to all files ending in ".bin".

The files are encrypted using AES in CBC mode with the 192-bit key 0D0607070C01080506090904060D030F03060E010E02070B

It is important to note that the AES algorithm requires data in 16-byte blocks. Since the game does not store the actual length in the file, you must instead look for 0xFD in the last block to indicate the end-of-file. When encrypting, if you wish to follow how the game encrypts, you should pad with up to 4 0xFD bytes, and the rest with 0x00 bytes.

PHP code

If you have the mcrypt extension for PHP installed, the code is trivial:

$key = "\x0D\x06\x07\x07\x0C\x01\x08\x05\x06\x09\x09\x04\x06\x0D\x03\x0F\x03\x06\x0E\x01\x0E\x02\x07\x0B";
 
$encrypted = file_get_contents("pers2.dat");
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encrypted, MCRYPT_MODE_CBC, str_repeat("\x00", 16));
echo $decrypted;

Python code

Likewise, Python with the Python Cryptography Toolkit is equally simple:

def decode_pc(data):
    if len(data)%16 !=0: return ""
    # AES encryption key used (192 bits)
    key = "\x0D\x06\x07\x07\x0C\x01\x08\x05\x06\x09\x09\x04\x06\x0D\x03\x0F\x03\x06\x0E\x01\x0E\x02\x07\x0B"
    return AES.new(key, AES.MODE_CBC).decrypt(data)

Complete Python encryption/decryption routines in Python are on the 2D Boy forum.

Java code

GooTool's Java class for decryption/encryption follows. Note that Java ships by default to all users with a restricted-export encryption strength. Although the limit is easily lifted with a new file in the user's lib directory, this is not a very user-friendly requirement, so GooTool instead uses the BouncyCastle lightweight API.

package com.goofans.gootool.io;
 
import com.goofans.gootool.util.Utilities;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
 
import java.io.File;
import java.io.IOException;
 
/**
 * Encrypt/decrypt .bin files in AES format (Windows/Linux).
 *
 * @author David Croft
 * @version $Revision: 186$
 */
public class AESBinFormat
{
  private static final byte[] KEY = {0x0D, 0x06, 0x07, 0x07, 0x0C, 0x01, 0x08, 0x05,
          0x06, 0x09, 0x09, 0x04, 0x06, 0x0D, 0x03, 0x0F,
          0x03, 0x06, 0x0E, 0x01, 0x0E, 0x02, 0x07, 0x0B};
 
  private static final byte EOF_MARKER = (byte) 0xFD;
 
  private AESBinFormat()
  {
  }
 
  public static byte[] decodeFile(File file) throws IOException
  {
    byte[] inputBytes = Utilities.readFile(file);
    return decode(inputBytes);
  }
 
  // Java Crypto API - can't use because user will have to install 192-bit policy file.
//    SecretKey key = new SecretKeySpec(KEY, "AES");
//    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");//CBC
//    cipher.init(Cipher.DECRYPT_MODE, key);
//    byte[] decrypted = cipher.doFinal(bytes);
 
  private static byte[] decode(byte[] inputBytes) throws IOException
  {
    BufferedBlockCipher cipher = getCipher(false);
 
    byte[] outputBytes = new byte[cipher.getOutputSize(inputBytes.length)];
 
    int outputLen = cipher.processBytes(inputBytes, 0, inputBytes.length, outputBytes, 0);
 
    try {
      outputLen += cipher.doFinal(outputBytes, outputLen);
    }
    catch (InvalidCipherTextException e) {
      throw new IOException("Can't decrypt file: " + e.getLocalizedMessage());
    }
 
    for (int i = outputLen - 16; i < outputLen; ++i) {
      byte b = outputBytes[i];
      if (b == EOF_MARKER) {
        outputLen = i;
        break;
      }
    }
 
    byte[] finalBytes = new byte[outputLen];
    System.arraycopy(outputBytes, 0, finalBytes, 0, outputLen);
    return finalBytes;
  }
 
  public static void encodeFile(File file, byte[] input) throws IOException
  {
    byte[] bytes = encode(input);
    Utilities.writeFile(file, bytes);
  }
 
  private static byte[] encode(byte[] inputBytes) throws IOException
  {
    /* If input was multiple of 16, NO padding. Example: res\levels\BulletinBoardSystem\BulletinBoardSystem.level.bin */
    /* Otherwise pad to next 16 byte boundary */
 
    int origSize = inputBytes.length;
    if (origSize % 16 != 0) {
      int padding = 16 - origSize % 16;
 
      int newSize = origSize + padding;
 
      byte[] newInputBytes = new byte[newSize];
      System.arraycopy(inputBytes, 0, newInputBytes, 0, origSize);
      inputBytes = newInputBytes;
 
      /* Write up to 4 0xFD bytes immediately after the original file. The remainder can stay as the 0x00 provided by Arrays.copyOf. */
      for (int i = origSize; i < origSize + 4 && i < newSize; ++i) {
        inputBytes[i] = EOF_MARKER;
      }
    }
 
    BufferedBlockCipher cipher = getCipher(true);
 
    byte[] outputBytes = new byte[cipher.getOutputSize(inputBytes.length)];
 
    int outputLen = cipher.processBytes(inputBytes, 0, inputBytes.length, outputBytes, 0);
 
    try {
      outputLen += cipher.doFinal(outputBytes, outputLen);
    }
    catch (InvalidCipherTextException e) {
      throw new IOException("Can't encrypt file: " + e.getLocalizedMessage());
    }
 
    byte[] finalBytes = new byte[outputLen];
    System.arraycopy(outputBytes, 0, finalBytes, 0, outputLen);
    return finalBytes;
  }
 
  private static BufferedBlockCipher getCipher(boolean forEncryption)
  {
    BlockCipher engine = new AESEngine();
    BufferedBlockCipher cipher = new BufferedBlockCipher(new CBCBlockCipher(engine));
 
    cipher.init(forEncryption, new KeyParameter(KEY));
    return cipher;
  }
}