From 28530137f30bea20f3242cac6b3c54b48bf5725f Mon Sep 17 00:00:00 2001 From: Bayu Satiyo Date: Mon, 25 Dec 2023 17:37:46 +0700 Subject: [PATCH] Add encryption operation & refine UX Signed-off-by: Bayu Satiyo --- BadApple.cs | 50 ++++++++++++ Timekeeper.cs | 217 +++++++++++++++++++++++++++++++++++++++----------- build.sh | 41 ++++++++++ 3 files changed, 261 insertions(+), 47 deletions(-) create mode 100644 BadApple.cs create mode 100644 build.sh diff --git a/BadApple.cs b/BadApple.cs new file mode 100644 index 0000000..6831c63 --- /dev/null +++ b/BadApple.cs @@ -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; + } + } +} diff --git a/Timekeeper.cs b/Timekeeper.cs index 24e3b44..6a9d86c 100644 --- a/Timekeeper.cs +++ b/Timekeeper.cs @@ -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; } } } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e1d7b27 --- /dev/null +++ b/build.sh @@ -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