Skip to content

Commit

Permalink
Add encryption operation & refine UX
Browse files Browse the repository at this point in the history
Signed-off-by: Bayu Satiyo <[email protected]>
  • Loading branch information
kiraio-moe committed Dec 25, 2023
1 parent 2659e48 commit 2853013
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 47 deletions.
50 changes: 50 additions & 0 deletions BadApple.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Reverse1999
{
internal class BadApple
{
const string LAST_OPEN_FILE = "last_open.txt";

internal static void SaveLastOpenedFile(string filePath)
{
try
{
// Write the last opened directory to a text file
using StreamWriter writer = new(LAST_OPEN_FILE);
writer.WriteLine($"last_opened={filePath}");
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}

internal static string GetLastOpenedFile()
{
string lastOpenedDirectory = string.Empty;

if (!File.Exists(LAST_OPEN_FILE))
return lastOpenedDirectory;

try
{
// Read the last opened directory from the text file
using StreamReader reader = new(LAST_OPEN_FILE);
string? line;
while ((line = reader.ReadLine()) != null)
{
if (line.StartsWith("last_opened="))
{
lastOpenedDirectory = line["last_opened=".Length..];
break;
}
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}

return lastOpenedDirectory;
}
}
}
217 changes: 170 additions & 47 deletions Timekeeper.cs
Original file line number Diff line number Diff line change
@@ -1,90 +1,213 @@
using System.Reflection;
using NativeFileDialogSharp;

namespace Reverse1999
{
public class Timekeeper
internal class Timekeeper
{
const byte DECRYPTION_KEY = 0x55;
const byte VERIFICATION_KEY = 0x6E;
const string DECRYPTED_BUNDLES_DIR = "bundles_decoded";
const string ENCRYPTED_BUNDLES_DIR = "bundles_encoded";
const string BUNDLES_DIR = "bundles";
const string VERSION = "1.0.0";
private static readonly byte[] UNITYFS_ID = { 0x55, 0x6E, 0x69, 0x74, 0x79, 0x46, 0x53 };
private static readonly byte[] UNITYFS_ENCRYPTED_ID =
{
0xDF,
0xE4,
0xE3,
0xFE,
0xF3,
0xCC,
0xD9
};

private enum OperationType
{
Decrypt,
Encrypt
}

class FileDecryptor
private class BundleDecryptor
{
public string DecryptedPath { get; }
private static byte[] XorDataChunk(byte[] chunk, byte key)
{
byte[] xoredChunk = new byte[chunk.Length];

for (int i = 0; i < chunk.Length; i++)
xoredChunk[i] = (byte)(chunk[i] ^ key);

return xoredChunk;
}

public FileDecryptor(string bundlesPath)
private static bool TestHeader(byte[] originalHeader, byte[] comparisonHeader)
{
DecryptedPath = Path.Combine(bundlesPath, DECRYPTED_BUNDLES_DIR);
for (int i = 0; i < comparisonHeader.Length; i++)
{
if (originalHeader[i] != comparisonHeader[i])
return false;
}
return true;
}

if (!Directory.Exists(DecryptedPath))
Directory.CreateDirectory(DecryptedPath);
private static byte GenerateKey(byte key, OperationType operationType)
{
return (byte)(
key
^ (
operationType == OperationType.Encrypt
? UNITYFS_ENCRYPTED_ID[0]
: UNITYFS_ID[0]
)
);
}

static byte[] DecryptDataChunk(byte[] chunk, byte key)
private static string GetOutputPath(string assetPath, string assetFileName)
{
byte[] decryptedChunk = new byte[chunk.Length];
string directory = Path.GetDirectoryName(assetPath) ?? string.Empty;

for (int i = 0; i < chunk.Length; i++)
decryptedChunk[i] = (byte)(chunk[i] ^ key);
if (assetFileName.Contains("_MOD"))
return Path.Combine(directory, assetFileName.Replace("_MOD", "_DEC"));

return decryptedChunk;
if (assetFileName.Contains("_DEC"))
return Path.Combine(directory, assetFileName.Replace("_DEC", "_MOD"));

return Path.Combine(
directory,
$"{Path.GetFileNameWithoutExtension(assetPath)}_DEC{Path.GetExtension(assetPath)}"
);
}

public static void DecryptFile(string inputPath, string outputPath)
private static void ProcessFile(
string inputPath,
string outputPath,
OperationType operationType
)
{
byte[] inputData = File.ReadAllBytes(inputPath);
byte key = (byte)(inputData[0] ^ DECRYPTION_KEY); // generate xor key from the first byte
Console.WriteLine($"XOR Key: {key}");
try
{
byte[] inputData = File.ReadAllBytes(inputPath);
byte key = GenerateKey(inputData[0], operationType);

byte[] comparisonHeader =
operationType == OperationType.Encrypt ? UNITYFS_ID : UNITYFS_ENCRYPTED_ID;

if (!TestHeader(inputData, comparisonHeader))
{
Console.WriteLine("Invalid asset bundle file!");
return;
}

if (key != (byte)(inputData[1] ^ VERIFICATION_KEY)) // verify key with the second byte
throw new Exception("Invalid key");
Console.WriteLine($"Operation: {operationType}");
Console.WriteLine(
$"Saving Xor-ed asset bundle {Path.GetFileName(inputPath)} as {outputPath}."
);
byte[] xoredData = XorDataChunk(inputData, key);
File.WriteAllBytes(outputPath, xoredData);
}
catch (Exception ex)
{
Console.WriteLine($"Error! {ex.Message}. Skipping...");
}
}

byte[] decryptedData = DecryptDataChunk(inputData, key); // decrypt the entire data
File.WriteAllBytes(outputPath, decryptedData);
public static void XorBundleFile(string inputPath, string outputPath)
{
string assetFileName = Path.GetFileNameWithoutExtension(inputPath);
OperationType operationType = assetFileName[^4..] switch
{
"_DEC" => OperationType.Encrypt,
_ => OperationType.Decrypt,
};
ProcessFile(inputPath, outputPath, operationType);
}

public (TimeSpan Duration, int FilesDecrypted) DecryptBundles(string bundlesPath)
public static (TimeSpan Duration, int FilesXored) XorBundleAssets(string[] assetPaths)
{
DateTime startTime = DateTime.Now;
int filesDecrypted = 0;
int filesXored = 0;

Parallel.ForEach(
Directory.EnumerateFiles(bundlesPath, "*.dat", SearchOption.AllDirectories),
assetPaths,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
filePath =>
assetPath =>
{
string outputPath = Path.Combine(DecryptedPath, Path.GetFileName(filePath));
DecryptFile(filePath, outputPath);
filesDecrypted++;
Console.WriteLine(
$"Decrypted {Path.GetFileName(filePath)} to {outputPath}"
);
try
{
string assetFileName = Path.GetFileName(assetPath);
string outputPath = GetOutputPath(assetPath, assetFileName);

XorBundleFile(assetPath, outputPath);
filesXored++;
}
catch { }
}
);

TimeSpan duration = DateTime.Now - startTime;
return (duration, filesDecrypted);
return (duration, filesXored);
}
}

static void Main()
private static void PrintHelp()
{
Console.Title = "Reverse: 1999 - Anarchist";
Console.WriteLine(
"Reverse: 1999 - Anarchist is an asset encryptor & decryptor for Reverse: 1999 game by BLUEPOCH."
);
Console.WriteLine(
"For more information, visit: https://github.com/kiraio-moe/Reverse1999-Anarchist"
);
Console.WriteLine($"Version: {VERSION}");
Console.WriteLine(
"Usage: Asset bundle WITHOUT any suffix/has '_MOD' suffix will be DECRYPTED | '_DEC' suffix will be ENCRYPTED"
);
Console.WriteLine();
}

private static void Main(string[] args)
{
string? cwd =
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
string? bundlesPath = Path.Combine(cwd, BUNDLES_DIR);
PrintHelp();

Console.WriteLine($"Path: {bundlesPath}");
string? cwd = Path.GetDirectoryName(AppContext.BaseDirectory);
string[]? assetsPath = args;

FileDecryptor decryptor = new(bundlesPath);
(TimeSpan duration, int filesDecrypted) = decryptor.DecryptBundles(bundlesPath);
PickFile:
if (args.Length < 1)
{
Console.WriteLine(
"Press SPACE BAR to perform encryption/decryption operation, X to exit."
);
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

switch (keyInfo.Key)
{
case ConsoleKey.Spacebar:
Console.WriteLine("Opening file dialog...");
DialogResult filePicker = Dialog.FileOpenMultiple(
"dat",
Path.GetDirectoryName(BadApple.GetLastOpenedFile()) ?? cwd
);

if (filePicker.IsCancelled)
{
Console.WriteLine("Canceled.");
goto PickFile;
}

assetsPath = filePicker.Paths.ToArray();
BadApple.SaveLastOpenedFile(assetsPath[0]);
break;

case ConsoleKey.X:
return;

default:
goto PickFile;
}
}

(TimeSpan duration, int filesDecrypted) = BundleDecryptor.XorBundleAssets(assetsPath);
double rps = filesDecrypted / duration.TotalSeconds;
Console.WriteLine($"Decryption completed in {duration}. Rate: {rps:F2} files/sec");

Console.WriteLine("Press any key to exit");
Console.ReadLine();
Console.WriteLine($"Xor-ing completed in {duration}. Rate: {rps:F2} files/sec");
Console.WriteLine();
goto PickFile;
}
}
}
41 changes: 41 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/bash

# Define the target operating systems and architectures
VERSION="1.0.0"
TARGET_OS_ARCHITECTURES=("win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
TARGET_FRAMEWORKS=("net6.0" "net7.0" "net8.0")

echo "Building Reverse: 1999 - Anarchist..."
# Build the project for each target OS architecture
for os_arch in "${TARGET_OS_ARCHITECTURES[@]}"
do
for framework in "${TARGET_FRAMEWORKS[@]}"
do
echo "Building for $os_arch architecture..."
dotnet publish -p:PublishSingleFile=true -c Release -f "$framework" -r "$os_arch" --no-self-contained

# Check if build was successful
if [ $? -eq 0 ]; then
echo "Build for $os_arch completed successfully."
else
echo "Build for $os_arch failed."
exit 1 # Exit the script with an error code
fi
done
done

echo "Build process completed for all target OS architectures."
echo "Creating ZIP archive for every architecture..."
for framework in "${TARGET_FRAMEWORKS[@]}"
do
for os_arch in "${TARGET_OS_ARCHITECTURES[@]}"
do
PUBLISH_PATH="bin/Release/${framework}/${os_arch}/publish"
ZIP_OUTPUT="Reverse1999-Anarchist-v${VERSION}-${framework}-${os_arch}.zip"

# Make a zip file for every architecture to be distributed
cd "${PUBLISH_PATH}"
zip -r "../../../${ZIP_OUTPUT}" * # /bin/Releases directory
cd "../../../../../" # build.sh directory
done
done

0 comments on commit 2853013

Please sign in to comment.