Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active November 19, 2023 04:11
Show Gist options
  • Save barncastle/a21b62df945445b38daf91ede021a3ec to your computer and use it in GitHub Desktop.
Save barncastle/a21b62df945445b38daf91ede021a3ec to your computer and use it in GitHub Desktop.
Code for decrypting Blue Mammoth Games' Brawlhalla's SWZ files
// WEll512 implementation - https://gist.github.com/barncastle/0fb2279bdc337d2a7d951e1bd2e3c0df
using Ionic.Zlib;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
static class BrawlhallaSWZ
{
public static string[] Decrypt(Stream input, uint globalKey)
{
var checksum = ReadUInt32BE(input);
var seed = ReadUInt32BE(input);
// initialise WELL512
var rand = new WELL512(seed ^ globalKey);
// compute and compare the header checksum
// this also mixes the WELL512 state so is required
var hash = 0x2DF4A1CDu;
var hash_rounds = globalKey % 0x1F + 5;
for (var i = 0; i < hash_rounds; i++)
hash ^= rand.NextUInt();
Debug.Assert(hash == checksum);
// decrypt each string object
var results = new List<string>();
while (input.Position != input.Length)
{
if (ReadStringEntry(input, rand, out var stringEntry))
results.Add(stringEntry);
}
return results.ToArray();
}
public static byte[] Encrypt(uint seed, uint globalKey, params string[] stringEntries)
{
// initialise WELL512
var rand = new WELL512(seed ^ globalKey);
// compute the header checksum
var hash = 0x2DF4A1CDu;
var hash_rounds = globalKey % 0x1F + 5;
for (var i = 0; i < hash_rounds; i++)
hash ^= rand.NextUInt();
using var ms = new MemoryStream(0x1000);
WriteUInt32BE(ms, hash);
WriteUInt32BE(ms, seed);
foreach (var entry in stringEntries)
{
var stringBytes = Encoding.UTF8.GetBytes(entry);
WriteStringEntry(stringBytes, rand, ms);
}
return ms.ToArray();
}
private static bool ReadStringEntry(Stream input, WELL512 rand, out string result)
{
// read the object header XOR'ing the size fields
var compressedSize = ReadUInt32BE(input) ^ rand.NextUInt();
var decompressedSize = ReadUInt32BE(input) ^ rand.NextUInt();
var checksum = ReadUInt32BE(input);
if (compressedSize + input.Position > input.Length)
{
result = null;
return false;
}
// read the compressed data
var buffer = new byte[compressedSize];
input.Read(buffer);
// again required even if not
// validating the checksum
var hash = rand.NextUInt();
for (var i = 0; i < compressedSize; i++)
{
// decode the byte
var shift = i & 0xF;
buffer[i] ^= (byte)(((0xFFu << shift) & rand.NextUInt()) >> shift);
// update the local checksum
hash = buffer[i] ^ RotateRight(hash, i % 7 + 1);
}
Debug.Assert(checksum == hash);
// zlib decompress
var decompressedData = ZlibStream.UncompressBuffer(buffer);
result = Encoding.UTF8.GetString(decompressedData);
return true;
}
private static void WriteStringEntry(byte[] input, WELL512 rand, Stream output)
{
// zlib compress
var compressedInput = ZlibStream.CompressBuffer(input);
// calculate the field values
var compressedSize = (uint)compressedInput.Length ^ rand.NextUInt();
var decompressedSize = (uint)input.Length ^ rand.NextUInt();
// create the checksum
var checksum = rand.NextUInt();
for (var i = 0; i < compressedInput.Length; i++)
{
// update the checksum
checksum = compressedInput[i] ^ RotateRight(checksum, i % 7 + 1);
// encode the byte
var shift = i & 0xF;
compressedInput[i] ^= (byte)(((0xFFu << shift) & rand.NextUInt()) >> shift);
}
// write the fields and data
WriteUInt32BE(output, compressedSize);
WriteUInt32BE(output, decompressedSize);
WriteUInt32BE(output, checksum);
output.Write(compressedInput);
}
private static uint RotateRight(uint v, int bits)
{
return (v >> bits) | (v << (32 - bits));
}
private static uint ReadUInt32BE(Stream stream)
{
var buffer = new byte[4];
stream.Read(buffer);
return (uint)(buffer[3] | (buffer[2] << 8) | (buffer[1] << 16) | (buffer[0] << 24));
}
private static void WriteUInt32BE(Stream stream, uint value)
{
var buffer = new byte[4]
{
(byte)((value >> 24) & 0xFF),
(byte)((value >> 16) & 0xFF),
(byte)((value >> 08) & 0xFF),
(byte)((value >> 00) & 0xFF)
};
stream.Write(buffer);
}
}
@KevitoLegal
Copy link

I already modified the files I wanted, how do I apply them to the game?

@Talafhah1
Copy link

Talafhah1 commented Oct 7, 2022

Encrypting and repackaging will require a separate tool and is not something I'd be willing to make however everything required for it is already documented above.

The class implementation has encryption methods that allow you to compile the files into a .swz, but you'll need to build that yourself.
Also be mindful when editing .csv files, as some of the linebreaks they use are LF characters instead of CRLF characters ($0A vs. $0D 0A), and some text editors will squash both of those into one or the other depending on you settings, so be sure to verify the file with a hex editor. This issue is most noticeable with the file headers, the very first line in any .csv file that has the file name.

@Talafhah1
Copy link

Also I noticed filenames and extensions aren't preserved when dumping.
The following code takes a file stored as string data and gives the correct string file_name.

string file_name;
if (data[0] == '<')
{
    if (data.Substring(0, 10) == "<LevelDesc") file_name = data.Split("LevelName=\"")[1].Split('"')[0] + ".xml";
    else file_name = data.Substring(1, data.IndexOf('>') - 1) + ".xml";
}
else file_name = data.Substring(0, data.IndexOf('\n')) + ".csv";

@barncastle
Copy link
Author

@KevitoLegal: As @Talafhah1 explained, this is not something supported nor something I'm interested in doing. To be completely honest, I have no interest in this game - I just like reverse engineering file formats, and part of my process is writing functional snippets of code to help me understand how the logic works. As a byproduct, these snippets (hopefully) document the format and provide other developers a basis to create proper tools from.

The attached app is something I knocked together in a few minutes just to provide something more user-friendly than just code. If @Talafhah1 or anyone else wants to build a proper tool, please feel to use as much or little of my code - consider it licensed as WTFPL :)

@anto123arci
Copy link

Also I noticed filenames and extensions aren't preserved when dumping. The following code takes a file stored as string data and gives the correct string file_name.

string file_name;
if (data[0] == '<')
{
    if (data.Substring(0, 10) == "<LevelDesc") file_name = data.Split("LevelName=\"")[1].Split('"')[0] + ".xml";
    else file_name = data.Substring(1, data.IndexOf('>') - 1) + ".xml";
}
else file_name = data.Substring(0, data.IndexOf('\n')) + ".csv";

Was trying to implement this into a little program but am not able to substitute the data string with anything, man :c

@Talafhah1
Copy link

The Decrypt static method is called 4 times, once for each .swz file stream; its output is a string[] that has all the files included in the .swz as a string.
So, simply use a foreach loop on the elements of the output of Decrypt and run the filename block, then use the file_name string in the file stream writer.

@yachecker
Copy link

i cannot find anything
image
here 0_O

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