|
1 | | -using System.Reflection; |
| 1 | +using NativeFileDialogSharp; |
2 | 2 |
|
3 | 3 | namespace Reverse1999 |
4 | 4 | { |
5 | | - public class Timekeeper |
| 5 | + internal class Timekeeper |
6 | 6 | { |
7 | | - const byte DECRYPTION_KEY = 0x55; |
8 | | - const byte VERIFICATION_KEY = 0x6E; |
9 | | - const string DECRYPTED_BUNDLES_DIR = "bundles_decoded"; |
10 | | - const string ENCRYPTED_BUNDLES_DIR = "bundles_encoded"; |
11 | | - const string BUNDLES_DIR = "bundles"; |
| 7 | + const string VERSION = "1.0.0"; |
| 8 | + private static readonly byte[] UNITYFS_ID = { 0x55, 0x6E, 0x69, 0x74, 0x79, 0x46, 0x53 }; |
| 9 | + private static readonly byte[] UNITYFS_ENCRYPTED_ID = |
| 10 | + { |
| 11 | + 0xDF, |
| 12 | + 0xE4, |
| 13 | + 0xE3, |
| 14 | + 0xFE, |
| 15 | + 0xF3, |
| 16 | + 0xCC, |
| 17 | + 0xD9 |
| 18 | + }; |
| 19 | + |
| 20 | + private enum OperationType |
| 21 | + { |
| 22 | + Decrypt, |
| 23 | + Encrypt |
| 24 | + } |
12 | 25 |
|
13 | | - class FileDecryptor |
| 26 | + private class BundleDecryptor |
14 | 27 | { |
15 | | - public string DecryptedPath { get; } |
| 28 | + private static byte[] XorDataChunk(byte[] chunk, byte key) |
| 29 | + { |
| 30 | + byte[] xoredChunk = new byte[chunk.Length]; |
| 31 | + |
| 32 | + for (int i = 0; i < chunk.Length; i++) |
| 33 | + xoredChunk[i] = (byte)(chunk[i] ^ key); |
| 34 | + |
| 35 | + return xoredChunk; |
| 36 | + } |
16 | 37 |
|
17 | | - public FileDecryptor(string bundlesPath) |
| 38 | + private static bool TestHeader(byte[] originalHeader, byte[] comparisonHeader) |
18 | 39 | { |
19 | | - DecryptedPath = Path.Combine(bundlesPath, DECRYPTED_BUNDLES_DIR); |
| 40 | + for (int i = 0; i < comparisonHeader.Length; i++) |
| 41 | + { |
| 42 | + if (originalHeader[i] != comparisonHeader[i]) |
| 43 | + return false; |
| 44 | + } |
| 45 | + return true; |
| 46 | + } |
20 | 47 |
|
21 | | - if (!Directory.Exists(DecryptedPath)) |
22 | | - Directory.CreateDirectory(DecryptedPath); |
| 48 | + private static byte GenerateKey(byte key, OperationType operationType) |
| 49 | + { |
| 50 | + return (byte)( |
| 51 | + key |
| 52 | + ^ ( |
| 53 | + operationType == OperationType.Encrypt |
| 54 | + ? UNITYFS_ENCRYPTED_ID[0] |
| 55 | + : UNITYFS_ID[0] |
| 56 | + ) |
| 57 | + ); |
23 | 58 | } |
24 | 59 |
|
25 | | - static byte[] DecryptDataChunk(byte[] chunk, byte key) |
| 60 | + private static string GetOutputPath(string assetPath, string assetFileName) |
26 | 61 | { |
27 | | - byte[] decryptedChunk = new byte[chunk.Length]; |
| 62 | + string directory = Path.GetDirectoryName(assetPath) ?? string.Empty; |
28 | 63 |
|
29 | | - for (int i = 0; i < chunk.Length; i++) |
30 | | - decryptedChunk[i] = (byte)(chunk[i] ^ key); |
| 64 | + if (assetFileName.Contains("_MOD")) |
| 65 | + return Path.Combine(directory, assetFileName.Replace("_MOD", "_DEC")); |
31 | 66 |
|
32 | | - return decryptedChunk; |
| 67 | + if (assetFileName.Contains("_DEC")) |
| 68 | + return Path.Combine(directory, assetFileName.Replace("_DEC", "_MOD")); |
| 69 | + |
| 70 | + return Path.Combine( |
| 71 | + directory, |
| 72 | + $"{Path.GetFileNameWithoutExtension(assetPath)}_DEC{Path.GetExtension(assetPath)}" |
| 73 | + ); |
33 | 74 | } |
34 | 75 |
|
35 | | - public static void DecryptFile(string inputPath, string outputPath) |
| 76 | + private static void ProcessFile( |
| 77 | + string inputPath, |
| 78 | + string outputPath, |
| 79 | + OperationType operationType |
| 80 | + ) |
36 | 81 | { |
37 | | - byte[] inputData = File.ReadAllBytes(inputPath); |
38 | | - byte key = (byte)(inputData[0] ^ DECRYPTION_KEY); // generate xor key from the first byte |
39 | | - Console.WriteLine($"XOR Key: {key}"); |
| 82 | + try |
| 83 | + { |
| 84 | + byte[] inputData = File.ReadAllBytes(inputPath); |
| 85 | + byte key = GenerateKey(inputData[0], operationType); |
| 86 | + |
| 87 | + byte[] comparisonHeader = |
| 88 | + operationType == OperationType.Encrypt ? UNITYFS_ID : UNITYFS_ENCRYPTED_ID; |
| 89 | + |
| 90 | + if (!TestHeader(inputData, comparisonHeader)) |
| 91 | + { |
| 92 | + Console.WriteLine("Invalid asset bundle file!"); |
| 93 | + return; |
| 94 | + } |
40 | 95 |
|
41 | | - if (key != (byte)(inputData[1] ^ VERIFICATION_KEY)) // verify key with the second byte |
42 | | - throw new Exception("Invalid key"); |
| 96 | + Console.WriteLine($"Operation: {operationType}"); |
| 97 | + Console.WriteLine( |
| 98 | + $"Saving Xor-ed asset bundle {Path.GetFileName(inputPath)} as {outputPath}." |
| 99 | + ); |
| 100 | + byte[] xoredData = XorDataChunk(inputData, key); |
| 101 | + File.WriteAllBytes(outputPath, xoredData); |
| 102 | + } |
| 103 | + catch (Exception ex) |
| 104 | + { |
| 105 | + Console.WriteLine($"Error! {ex.Message}. Skipping..."); |
| 106 | + } |
| 107 | + } |
43 | 108 |
|
44 | | - byte[] decryptedData = DecryptDataChunk(inputData, key); // decrypt the entire data |
45 | | - File.WriteAllBytes(outputPath, decryptedData); |
| 109 | + public static void XorBundleFile(string inputPath, string outputPath) |
| 110 | + { |
| 111 | + string assetFileName = Path.GetFileNameWithoutExtension(inputPath); |
| 112 | + OperationType operationType = assetFileName[^4..] switch |
| 113 | + { |
| 114 | + "_DEC" => OperationType.Encrypt, |
| 115 | + _ => OperationType.Decrypt, |
| 116 | + }; |
| 117 | + ProcessFile(inputPath, outputPath, operationType); |
46 | 118 | } |
47 | 119 |
|
48 | | - public (TimeSpan Duration, int FilesDecrypted) DecryptBundles(string bundlesPath) |
| 120 | + public static (TimeSpan Duration, int FilesXored) XorBundleAssets(string[] assetPaths) |
49 | 121 | { |
50 | 122 | DateTime startTime = DateTime.Now; |
51 | | - int filesDecrypted = 0; |
| 123 | + int filesXored = 0; |
52 | 124 |
|
53 | 125 | Parallel.ForEach( |
54 | | - Directory.EnumerateFiles(bundlesPath, "*.dat", SearchOption.AllDirectories), |
| 126 | + assetPaths, |
55 | 127 | new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, |
56 | | - filePath => |
| 128 | + assetPath => |
57 | 129 | { |
58 | | - string outputPath = Path.Combine(DecryptedPath, Path.GetFileName(filePath)); |
59 | | - DecryptFile(filePath, outputPath); |
60 | | - filesDecrypted++; |
61 | | - Console.WriteLine( |
62 | | - $"Decrypted {Path.GetFileName(filePath)} to {outputPath}" |
63 | | - ); |
| 130 | + try |
| 131 | + { |
| 132 | + string assetFileName = Path.GetFileName(assetPath); |
| 133 | + string outputPath = GetOutputPath(assetPath, assetFileName); |
| 134 | + |
| 135 | + XorBundleFile(assetPath, outputPath); |
| 136 | + filesXored++; |
| 137 | + } |
| 138 | + catch { } |
64 | 139 | } |
65 | 140 | ); |
66 | 141 |
|
67 | 142 | TimeSpan duration = DateTime.Now - startTime; |
68 | | - return (duration, filesDecrypted); |
| 143 | + return (duration, filesXored); |
69 | 144 | } |
70 | 145 | } |
71 | 146 |
|
72 | | - static void Main() |
| 147 | + private static void PrintHelp() |
| 148 | + { |
| 149 | + Console.Title = "Reverse: 1999 - Anarchist"; |
| 150 | + Console.WriteLine( |
| 151 | + "Reverse: 1999 - Anarchist is an asset encryptor & decryptor for Reverse: 1999 game by BLUEPOCH." |
| 152 | + ); |
| 153 | + Console.WriteLine( |
| 154 | + "For more information, visit: https://github.com/kiraio-moe/Reverse1999-Anarchist" |
| 155 | + ); |
| 156 | + Console.WriteLine($"Version: {VERSION}"); |
| 157 | + Console.WriteLine( |
| 158 | + "Usage: Asset bundle WITHOUT any suffix/has '_MOD' suffix will be DECRYPTED | '_DEC' suffix will be ENCRYPTED" |
| 159 | + ); |
| 160 | + Console.WriteLine(); |
| 161 | + } |
| 162 | + |
| 163 | + private static void Main(string[] args) |
73 | 164 | { |
74 | | - string? cwd = |
75 | | - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty; |
76 | | - string? bundlesPath = Path.Combine(cwd, BUNDLES_DIR); |
| 165 | + PrintHelp(); |
77 | 166 |
|
78 | | - Console.WriteLine($"Path: {bundlesPath}"); |
| 167 | + string? cwd = Path.GetDirectoryName(AppContext.BaseDirectory); |
| 168 | + string[]? assetsPath = args; |
79 | 169 |
|
80 | | - FileDecryptor decryptor = new(bundlesPath); |
81 | | - (TimeSpan duration, int filesDecrypted) = decryptor.DecryptBundles(bundlesPath); |
| 170 | + PickFile: |
| 171 | + if (args.Length < 1) |
| 172 | + { |
| 173 | + Console.WriteLine( |
| 174 | + "Press SPACE BAR to perform encryption/decryption operation, X to exit." |
| 175 | + ); |
| 176 | + ConsoleKeyInfo keyInfo = Console.ReadKey(true); |
| 177 | + |
| 178 | + switch (keyInfo.Key) |
| 179 | + { |
| 180 | + case ConsoleKey.Spacebar: |
| 181 | + Console.WriteLine("Opening file dialog..."); |
| 182 | + DialogResult filePicker = Dialog.FileOpenMultiple( |
| 183 | + "dat", |
| 184 | + Path.GetDirectoryName(BadApple.GetLastOpenedFile()) ?? cwd |
| 185 | + ); |
| 186 | + |
| 187 | + if (filePicker.IsCancelled) |
| 188 | + { |
| 189 | + Console.WriteLine("Canceled."); |
| 190 | + goto PickFile; |
| 191 | + } |
| 192 | + |
| 193 | + assetsPath = filePicker.Paths.ToArray(); |
| 194 | + BadApple.SaveLastOpenedFile(assetsPath[0]); |
| 195 | + break; |
| 196 | + |
| 197 | + case ConsoleKey.X: |
| 198 | + return; |
| 199 | + |
| 200 | + default: |
| 201 | + goto PickFile; |
| 202 | + } |
| 203 | + } |
82 | 204 |
|
| 205 | + (TimeSpan duration, int filesDecrypted) = BundleDecryptor.XorBundleAssets(assetsPath); |
83 | 206 | double rps = filesDecrypted / duration.TotalSeconds; |
84 | | - Console.WriteLine($"Decryption completed in {duration}. Rate: {rps:F2} files/sec"); |
85 | 207 |
|
86 | | - Console.WriteLine("Press any key to exit"); |
87 | | - Console.ReadLine(); |
| 208 | + Console.WriteLine($"Xor-ing completed in {duration}. Rate: {rps:F2} files/sec"); |
| 209 | + Console.WriteLine(); |
| 210 | + goto PickFile; |
88 | 211 | } |
89 | 212 | } |
90 | 213 | } |
0 commit comments