Skip to content

Commit 2853013

Browse files
committed
Add encryption operation & refine UX
Signed-off-by: Bayu Satiyo <itsyuukunz@gmail.com>
1 parent 2659e48 commit 2853013

3 files changed

Lines changed: 261 additions & 47 deletions

File tree

BadApple.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace Reverse1999
2+
{
3+
internal class BadApple
4+
{
5+
const string LAST_OPEN_FILE = "last_open.txt";
6+
7+
internal static void SaveLastOpenedFile(string filePath)
8+
{
9+
try
10+
{
11+
// Write the last opened directory to a text file
12+
using StreamWriter writer = new(LAST_OPEN_FILE);
13+
writer.WriteLine($"last_opened={filePath}");
14+
}
15+
catch (Exception ex)
16+
{
17+
throw new Exception(ex.Message);
18+
}
19+
}
20+
21+
internal static string GetLastOpenedFile()
22+
{
23+
string lastOpenedDirectory = string.Empty;
24+
25+
if (!File.Exists(LAST_OPEN_FILE))
26+
return lastOpenedDirectory;
27+
28+
try
29+
{
30+
// Read the last opened directory from the text file
31+
using StreamReader reader = new(LAST_OPEN_FILE);
32+
string? line;
33+
while ((line = reader.ReadLine()) != null)
34+
{
35+
if (line.StartsWith("last_opened="))
36+
{
37+
lastOpenedDirectory = line["last_opened=".Length..];
38+
break;
39+
}
40+
}
41+
}
42+
catch (Exception ex)
43+
{
44+
throw new Exception(ex.Message);
45+
}
46+
47+
return lastOpenedDirectory;
48+
}
49+
}
50+
}

Timekeeper.cs

Lines changed: 170 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,213 @@
1-
using System.Reflection;
1+
using NativeFileDialogSharp;
22

33
namespace Reverse1999
44
{
5-
public class Timekeeper
5+
internal class Timekeeper
66
{
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+
}
1225

13-
class FileDecryptor
26+
private class BundleDecryptor
1427
{
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+
}
1637

17-
public FileDecryptor(string bundlesPath)
38+
private static bool TestHeader(byte[] originalHeader, byte[] comparisonHeader)
1839
{
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+
}
2047

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+
);
2358
}
2459

25-
static byte[] DecryptDataChunk(byte[] chunk, byte key)
60+
private static string GetOutputPath(string assetPath, string assetFileName)
2661
{
27-
byte[] decryptedChunk = new byte[chunk.Length];
62+
string directory = Path.GetDirectoryName(assetPath) ?? string.Empty;
2863

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"));
3166

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+
);
3374
}
3475

35-
public static void DecryptFile(string inputPath, string outputPath)
76+
private static void ProcessFile(
77+
string inputPath,
78+
string outputPath,
79+
OperationType operationType
80+
)
3681
{
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+
}
4095

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+
}
43108

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);
46118
}
47119

48-
public (TimeSpan Duration, int FilesDecrypted) DecryptBundles(string bundlesPath)
120+
public static (TimeSpan Duration, int FilesXored) XorBundleAssets(string[] assetPaths)
49121
{
50122
DateTime startTime = DateTime.Now;
51-
int filesDecrypted = 0;
123+
int filesXored = 0;
52124

53125
Parallel.ForEach(
54-
Directory.EnumerateFiles(bundlesPath, "*.dat", SearchOption.AllDirectories),
126+
assetPaths,
55127
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
56-
filePath =>
128+
assetPath =>
57129
{
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 { }
64139
}
65140
);
66141

67142
TimeSpan duration = DateTime.Now - startTime;
68-
return (duration, filesDecrypted);
143+
return (duration, filesXored);
69144
}
70145
}
71146

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)
73164
{
74-
string? cwd =
75-
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty;
76-
string? bundlesPath = Path.Combine(cwd, BUNDLES_DIR);
165+
PrintHelp();
77166

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

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+
}
82204

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

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;
88211
}
89212
}
90213
}

build.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
3+
# Define the target operating systems and architectures
4+
VERSION="1.0.0"
5+
TARGET_OS_ARCHITECTURES=("win-x64" "win-arm64" "linux-x64" "linux-arm64" "osx-x64" "osx-arm64")
6+
TARGET_FRAMEWORKS=("net6.0" "net7.0" "net8.0")
7+
8+
echo "Building Reverse: 1999 - Anarchist..."
9+
# Build the project for each target OS architecture
10+
for os_arch in "${TARGET_OS_ARCHITECTURES[@]}"
11+
do
12+
for framework in "${TARGET_FRAMEWORKS[@]}"
13+
do
14+
echo "Building for $os_arch architecture..."
15+
dotnet publish -p:PublishSingleFile=true -c Release -f "$framework" -r "$os_arch" --no-self-contained
16+
17+
# Check if build was successful
18+
if [ $? -eq 0 ]; then
19+
echo "Build for $os_arch completed successfully."
20+
else
21+
echo "Build for $os_arch failed."
22+
exit 1 # Exit the script with an error code
23+
fi
24+
done
25+
done
26+
27+
echo "Build process completed for all target OS architectures."
28+
echo "Creating ZIP archive for every architecture..."
29+
for framework in "${TARGET_FRAMEWORKS[@]}"
30+
do
31+
for os_arch in "${TARGET_OS_ARCHITECTURES[@]}"
32+
do
33+
PUBLISH_PATH="bin/Release/${framework}/${os_arch}/publish"
34+
ZIP_OUTPUT="Reverse1999-Anarchist-v${VERSION}-${framework}-${os_arch}.zip"
35+
36+
# Make a zip file for every architecture to be distributed
37+
cd "${PUBLISH_PATH}"
38+
zip -r "../../../${ZIP_OUTPUT}" * # /bin/Releases directory
39+
cd "../../../../../" # build.sh directory
40+
done
41+
done

0 commit comments

Comments
 (0)