From d952d0797c08693386f02f9af5456910a8f9ecd7 Mon Sep 17 00:00:00 2001 From: James Deogrades Date: Sat, 4 Jan 2025 01:43:29 -0500 Subject: [PATCH] Add project files. --- CommandLineHandler.cs | 87 ++++++++++++++++++++ EncryptMC.csproj | 25 ++++++ EncryptMC.ico | Bin 0 -> 4286 bytes EncryptMC.sln | 25 ++++++ InputValidator.cs | 55 +++++++++++++ PackEncryption.cs | 154 +++++++++++++++++++++++++++++++++++ Program.cs | 93 +++++++++++++++++++++ README.md | 21 +++++ Utility/EncryptionUtility.cs | 81 ++++++++++++++++++ Utility/ManifestUtility.cs | 90 ++++++++++++++++++++ Utility/PathUtility.cs | 33 ++++++++ 11 files changed, 664 insertions(+) create mode 100644 CommandLineHandler.cs create mode 100644 EncryptMC.csproj create mode 100644 EncryptMC.ico create mode 100644 EncryptMC.sln create mode 100644 InputValidator.cs create mode 100644 PackEncryption.cs create mode 100644 Program.cs create mode 100644 README.md create mode 100644 Utility/EncryptionUtility.cs create mode 100644 Utility/ManifestUtility.cs create mode 100644 Utility/PathUtility.cs diff --git a/CommandLineHandler.cs b/CommandLineHandler.cs new file mode 100644 index 0000000..0d4c673 --- /dev/null +++ b/CommandLineHandler.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; + +namespace EncryptMC +{ + internal static class CommandLineHandler + { + internal static (string inputPath, string outputPath, string contentKey, bool isInteractiveMode) ParseArgs(string[] args) + { + int argCount = args.Length; + bool isInteractive = false; + + string inputPath; + string outputPath; + string contentKey; + + switch (argCount) + { + case 0: // Interactive mode + isInteractive = true; + Console.Clear(); + Console.WriteLine("===== EncryptMC =====\n"); + + Console.Write("Enter the input path: "); + string? temp = Console.ReadLine(); + if (string.IsNullOrEmpty(temp)) + { + ShowError("Invalid input path."); + return (string.Empty, string.Empty, string.Empty, true); + } + inputPath = Path.GetFullPath(temp); + Console.WriteLine($"\nInput Path: {inputPath}\n"); + + Console.Write("Enter the output path: "); + temp = Console.ReadLine(); + if (string.IsNullOrEmpty(temp)) + { + ShowError("Invalid output path."); + return (string.Empty, string.Empty, string.Empty, true); + } + outputPath = Path.GetFullPath(temp); + Console.WriteLine($"\nOutput Path: {outputPath}\n"); + + Console.Write("Enter the content key (must be 32 bytes): "); + temp = Console.ReadLine(); + if (string.IsNullOrEmpty(temp)) + { + ShowError("Invalid content key."); + return (string.Empty, string.Empty, string.Empty, true); + } + contentKey = temp; + Console.WriteLine($"\nContent Key: {contentKey}\n"); + break; + + case 3: // Command line mode + inputPath = Path.GetFullPath(args[0]); + outputPath = Path.GetFullPath(args[1]); + contentKey = args[2]; + + Console.WriteLine($"\nInput Path: {inputPath}\n"); + Console.WriteLine($"\nOutput Path: {outputPath}\n"); + Console.WriteLine($"\nContent Key: {contentKey}\n"); + break; + + default: // Invalid arguments + ShowUsage(); + return (string.Empty, string.Empty, string.Empty, false); + } + + return (inputPath, outputPath, contentKey, isInteractive); + } + + private static void ShowUsage() + { + string exeName = Process.GetCurrentProcess().ProcessName; + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\nUsage: ./{exeName}.exe "); + Console.ResetColor(); + } + + private static void ShowError(string error) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"\n{error}"); + Console.ResetColor(); + } + } +} diff --git a/EncryptMC.csproj b/EncryptMC.csproj new file mode 100644 index 0000000..6378a48 --- /dev/null +++ b/EncryptMC.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + true + true + EncryptMC.Program + EncryptMC.ico + EncryptMC + James Deogrades + © 2025 James Deogrades. Licensed under the MIT License. + + + + + + + + + + + diff --git a/EncryptMC.ico b/EncryptMC.ico new file mode 100644 index 0000000000000000000000000000000000000000..f01ce5c3da237d1f24d8d30b76c0a184baf94202 GIT binary patch literal 4286 zcmc&&TTqne8K&30=}c#O?My46?5<7+*nbxV56dpQB+4F8+bEtu&PZCLCQ$)V5QD}V z1$3gOhImTUsu!Kqv{6LI*z_=sr}Uz+NoQKkK{=Bq^7MJW-5(bdXL47VhwtS(yw7|3 z-ftl#<$L^|G%1B&lTwcTASLBTl+w*_DJio!a+N>%#kC)PKaTiBN{VClDmdnp!;!ZV zj!->PFMNU2KmHd#dGAX&FZW~1D~Hg&vlH#R_G9ZyoyhyyW>~RCc#6LX?}E+9U$qzh ziZ1xe_QF?p5a~UANWJzYCKFS72M}&Ji{j_sLhOZ;29NklpTPeb`Mc2mEAe-I2mX#Y z|EiAf&aW8cu0%#?^<)0`p=I|$?A+0ToiBHxb$bVL7d62hZJ_@|Ok*Mrl)Vd@;4j^S z0J#GzUxBaY)o=RWnDGC_Q^tSioC-MS60^$SD13GTzk~7L)cJ<7Ve6jP(D3pB%z9=k zd76>4h*;Q6d2<3yW^OJy6?bvZS<{X5OZ^k#U$p5AV(U&I${Yw*Y!lG{E3yIZ#Vts? z_`u{p^}SIDvcY+|4~{DXn0DcF6m2|>=-T6mtbY?>j{fpa=A1F7-{PM7&ZkIco|SjQ zozOFxHI?4ekNnk#P{1>R%6$m1PCSg6;AKp$C2dLkBZgmfDEwIGdj}D2JQuGY#!vQ3 zkH3QY?0l1Z^7~&IpH+9ZAF65DO^5h2MX#BVCwmM zNW0WWKX|T(x)OS%b4qVNZG6?_N;%VAjjTUmpqv(MIt}lwVYokf2Y~K!(=_q!In0B5ic&ZnM*v#=g6#@*$s zgkw%6G7D$^Je7oO_1N>)*jF8VR+=L&QQm=PRW|P znOKu7yOJS(?X%CZ4ov+11$h5H3V;2fcz@NsXAOIJZ`^#3EZ$#>b!WJ_cRc1spL8bt zvTMN}ZGWG%u6fy_n!Gv0{>A=L$~TU8S9e}@Z1*p(Vk*C-+qU24vBzZp>^xlnr`+Ob=(hS;_UD@LTs;=i*>qY(H&I+aB2`PSuC>2;=p`HTtg_ z2{)cWl<$Y$?;`e}Y5p44V!3(0Tu=Xkcf3FDzv4x1-@0z(*BvmBtT_{($N80K@r#$a zG)VR*!LR$GJFEU1UVDtas0tbE2m1Wte5(F2e&&nZdYc5vio>_^%YSn}$sZ=a!1ut; zr+R|?SC7)?m*Ne-L4-ZnHMbfW>;;Y)@y{=||JrZrMemKxPg!%8xDt8w-uW5^pD@3- z2jQ*Q1#d}f9FjH1*mysFh#9<}MH^2cvgQc6PhkEpyD|M~_TtbgxB{h^7OQ6r`{9)? z?$7yf82RhF;aANPk~L@aC3u4az&QHH5WF{n;N9UkBx_C>1_J*c!u0TSaPc#X%U5l< z^Uu~G@O$FzN(BC}7Qy>N2o6$b;#}y{QH1U#K-tVEafSxRFahKVB}3~2V<;Q}3jQ<5 z9Ir-3?g|rgXRr)`cd8M(T8m(BC4yJiBGfm8&=7SPy^raVUfMQ+PkWP*%q`jI0t!dT ze|Hov_F+dZbDhT+1j`ZlT@6B)$$zm5!7Jh)iQ8}Mq>jP_j9>PAVhr9NV9XNuVt_sl zO+Zj{M6iDtfluzi6|P0b+~vsdmBKZz6#loXP;jXlfeRHV=v{-*=bzCA#=CD2p$F7; zH%T95&6yY}PDJ*J5(D>JOOV~Y6w{)7g7T<6a@K1ck~OEb;3d^;Ev#b;;y6(Z_u++bGN%qd`$?e8$l1Rx zM)vV2+(#K>+UjVZgN*I74J2#sQT&VHf433^7pqwlRi-wkN6Kjf`vW04UN5Yf3j}L+ z>EAXREc>7ti~rnUY)~8oaT|T~DWLl9slk%JG@#_KO$L%Rr!{eEuXUXEG7p0L7|*jl zbw>ornv1W~ANsWNW~)Kzt!j*#}$3&q&ss z*32I6)fuCk`ptR9S!-sUa`L+NJ(1r&FMj#PJ8t-|C-GA@{9H48zKuU&{{;K!n{A{0 Je}Mnj_&=IeD_Z~n literal 0 HcmV?d00001 diff --git a/EncryptMC.sln b/EncryptMC.sln new file mode 100644 index 0000000..03bab31 --- /dev/null +++ b/EncryptMC.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35312.102 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EncryptMC", "EncryptMC.csproj", "{F6FD4BCA-FB8B-4D96-9272-FFC531B1E604}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F6FD4BCA-FB8B-4D96-9272-FFC531B1E604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6FD4BCA-FB8B-4D96-9272-FFC531B1E604}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6FD4BCA-FB8B-4D96-9272-FFC531B1E604}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6FD4BCA-FB8B-4D96-9272-FFC531B1E604}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D0E17F41-CEE9-4562-86ED-CA31E3F9201D} + EndGlobalSection +EndGlobal diff --git a/InputValidator.cs b/InputValidator.cs new file mode 100644 index 0000000..b411a89 --- /dev/null +++ b/InputValidator.cs @@ -0,0 +1,55 @@ +using EncryptMC.Utility; + +namespace EncryptMC +{ + internal static class InputValidator + { + internal static bool ValidatePathsAndKey(string inputPath, string outputPath, string contentKey, bool isInteractive) + { + // Check input path + if (!Directory.Exists(inputPath)) + { + ShowError("Input path does not exist or is not a directory.\n", isInteractive); + return false; + } + + // Check output path + if (!Directory.Exists(outputPath)) + { + ShowError("Output path does not exist or is not a directory.\n", isInteractive); + return false; + } + + // Check if input path is a subpath of output path + if (PathUtility.IsSubPath(inputPath, outputPath)) + { + ShowError("Input path cannot be a subpath of output path.\n", isInteractive); + return false; + } + + // Check if content key is 32 bytes + if (contentKey.Length != 32) + { + ShowError("Content key must be exactly 32 bytes long.\n", isInteractive); + return false; + } + + return true; + } + + private static void ShowError(string message, bool isInteractive) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + + if (isInteractive) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("Press Enter to exit..."); + Console.ReadKey(true); + Console.ResetColor(); + } + } + } +} diff --git a/PackEncryption.cs b/PackEncryption.cs new file mode 100644 index 0000000..4cb9af8 --- /dev/null +++ b/PackEncryption.cs @@ -0,0 +1,154 @@ +using EncryptMC.Utility; +using Newtonsoft.Json; +using System.Text; + +namespace EncryptMC +{ + internal static class PackEncryption + { + // Paths that should not be encrypted + private static readonly HashSet DoNotEncrypt = + [ + "contents.json", + "manifest.json", + "pack_icon.png", + "texts" + ]; + + internal static void EncryptPack(string inputPath, string outputPath, string contentKey, string uuid) + { + string contentsPath = Path.Combine(outputPath, "contents.json"); + List> contentsList = []; + + // Parallel traversal and encryption + ParallelEncrypt(inputPath, outputPath, contentsList); + + // Prepare contents.json file + var contentsFile = new Dictionary + { + { "version", 1 }, + { "content", contentsList } + }; + + // Serialize contents to JSON + string jsonString = JsonConvert.SerializeObject(contentsFile, Formatting.None); + + // Encrypt JSON + byte[] jsonData = Encoding.UTF8.GetBytes(jsonString); + byte[] encryptedDataForContents = EncryptionUtility.EncryptData(contentKey, jsonData); + + // Write result to contents.json with special header + using (FileStream fileStream = new(contentsPath, FileMode.Create, FileAccess.Write)) + { + // Write header (4 + 4 + 8 = 16 bytes) + fileStream.Write(BitConverter.GetBytes(0), 0, 4); // Zero bytes (4 bytes) + fileStream.Write(BitConverter.GetBytes(0x9BCFB9FC), 0, 4); // Magic number (4 bytes) + fileStream.Write(BitConverter.GetBytes(0L), 0, 8); // Padding (8 bytes) + + // Write UUID length and contents (1 byte for length + actual UUID) + byte[] uuidBytes = Encoding.UTF8.GetBytes(uuid); + fileStream.WriteByte((byte) uuidBytes.Length); + fileStream.Write(uuidBytes, 0, uuidBytes.Length); + + // Add padding to reach required length (0xEF - length of UUID) + int paddingLength = 0xEF - uuidBytes.Length; + fileStream.Write(new byte[paddingLength], 0, paddingLength); + + // Write encrypted data + fileStream.Write(encryptedDataForContents, 0, encryptedDataForContents.Length); + } + } + + private static void ParallelEncrypt(string inputPath, string outputPath, List> contents) + { + bool inputOutputPathsEqual = inputPath.Equals(outputPath, StringComparison.OrdinalIgnoreCase); + + // Create the base directory (just in case) + PathUtility.EnsureOutputFolderExists(outputPath); + + // Create all subdirectories (sequentially) + string[]? allDirectories = Directory.GetDirectories(inputPath, "*", SearchOption.AllDirectories); + foreach (string dir in allDirectories) + { + string relativeFilePath = Path.GetRelativePath(inputPath, dir).Replace("\\", "/"); + string outputDir = Path.Combine(outputPath, relativeFilePath); + + Directory.CreateDirectory(outputDir); + + // Record this directory in final contents list + lock (contents) + { + contents.Add(new Dictionary + { + { "path", relativeFilePath + "/" } + }); + } + } + + // Grab all files + string[]? allFiles = Directory.GetFiles(inputPath, "*", SearchOption.AllDirectories); + + // Process all files in parallel + Parallel.ForEach(allFiles, file => + { + string relativeFilePath = Path.GetRelativePath(inputPath, file).Replace("\\", "/"); + string outputFilePath = Path.Combine(outputPath, relativeFilePath); + bool shouldEncryptFile = ShouldEncrypt(relativeFilePath, DoNotEncrypt); + + if (shouldEncryptFile) + { + string fileContentKey = EncryptionUtility.GenerateContentKey(); + + // Encrypt file contents + byte[] fileContents = File.ReadAllBytes(file); + EncryptionUtility.WriteDataToEncryptedFile(outputFilePath, fileContents, fileContentKey); + + // Add file reference with encryption key + lock (contents) + { + contents.Add(new Dictionary + { + { "key", fileContentKey }, + { "path", relativeFilePath } + }); + } + } + else + { + if (!inputOutputPathsEqual) + { + // Copy file without encryption + Directory.CreateDirectory(Path.GetDirectoryName(outputFilePath)!); + File.Copy(file, outputFilePath, overwrite: true); + } + + // Add file reference without encryption key + lock (contents) + { + contents.Add(new Dictionary + { + { "path", relativeFilePath } + }); + } + } + }); + } + + private static bool ShouldEncrypt(string relativePath, HashSet doNotEncrypt) + { + // Normalize path to forward slashes + string normalizedPath = relativePath.Replace("\\", "/").TrimStart('/'); + + // Check if path is in DoNotEncrypt set or subdirectory + foreach (string excludePath in doNotEncrypt) + { + string? cleanedExclude = excludePath.Replace("\\", "/").TrimStart('/'); + if (normalizedPath.StartsWith(cleanedExclude, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return true; + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..72b6c77 --- /dev/null +++ b/Program.cs @@ -0,0 +1,93 @@ +using EncryptMC.Utility; + +namespace EncryptMC +{ + internal static class Program + { + private static void Main(string[] args) + { + // Parse arguments or prompt for them interactively + var (inputPath, outputPath, contentKey, isInteractiveMode) = CommandLineHandler.ParseArgs(args); + + // Validate user input + if (!InputValidator.ValidatePathsAndKey(inputPath, outputPath, contentKey, isInteractiveMode)) + { + return; + } + + // Retrieve UUID from manifest + string manifestUuid; + try + { + manifestUuid = ManifestUtility.GetManifestUuid(inputPath); + Console.WriteLine($"Manifest UUID: {manifestUuid}\n"); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Failed to get manifest UUID: {ex.Message}\n"); + ExitIfInteractive(isInteractiveMode); + return; + } + + bool inputOutputPathsEqual = inputPath.Equals(outputPath, StringComparison.OrdinalIgnoreCase); + + // Remove existing output directory (if any) to rebuild cleanly + if (Directory.Exists(outputPath) && !inputOutputPathsEqual) + { + try + { + Directory.Delete(outputPath, recursive: true); + Console.WriteLine($"Successfully removed the directory: {outputPath}\n"); + } + catch (Exception ex) + { + throw new IOException($"Failed to remove the directory \"{outputPath}\": {ex.Message}", ex); + } + } + + // Create and encrypt manifest signature + try + { + ManifestUtility.CreateManifestSignature(inputPath, outputPath, contentKey); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Failed to create manifest signature: {ex.Message}\n"); + ExitIfInteractive(isInteractiveMode); + return; + } + + // Encrypt entire pack + try + { + PackEncryption.EncryptPack(inputPath, outputPath, contentKey, manifestUuid); + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Failed to encrypt pack: {ex.Message}\n"); + ExitIfInteractive(isInteractiveMode); + return; + } + + // Success message + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("Encryption completed successfully.\n"); + ExitIfInteractive(isInteractiveMode); + Console.ResetColor(); + } + + private static void ExitIfInteractive(bool isInteractiveMode) + { + if (isInteractiveMode) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write("Press any key to exit..."); + Console.ReadKey(true); + Console.ResetColor(); + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7941eab --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# EncryptMC + +EncryptMC is a utility tool for encrypting Minecraft Bedrock edition resource packs. + +## Features + +- Command line and interactive modes. +- Uses parallel processing for efficiency. + +## Usage + +### Command Line + +```bash +EncryptMC.exe +``` + +### Interactive + +- Run the executable. (without any arguments) +- Enter the input path, output path, and content key when prompted. \ No newline at end of file diff --git a/Utility/EncryptionUtility.cs b/Utility/EncryptionUtility.cs new file mode 100644 index 0000000..3d8c901 --- /dev/null +++ b/Utility/EncryptionUtility.cs @@ -0,0 +1,81 @@ +using System.Security.Cryptography; +using System.Text; + +namespace EncryptMC.Utility +{ + internal static class EncryptionUtility + { + internal static string GenerateContentKey() + { + const string characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var contentKey = new StringBuilder(32); + + byte[] buffer = new byte[1]; + for (int i = 0; i < 32; i++) + { + RandomNumberGenerator.Fill(buffer); + char randomChar = characters[buffer[0] % characters.Length]; + contentKey.Append(randomChar); + } + + return contentKey.ToString(); + } + + internal static void WriteDataToEncryptedFile(string path, byte[] data, string contentKey) + { + byte[] encryptedData = EncryptData(contentKey, data); + + string? directoryPath = Path.GetDirectoryName(path); + if (string.IsNullOrEmpty(directoryPath)) + { + throw new IOException($"Failed to get directory path from file: {path}"); + } + + // Ensure output directory exists + PathUtility.EnsureOutputFolderExists(directoryPath); + + try + { + // Write encrypted data to file + File.WriteAllBytes(path, encryptedData); + } + catch (Exception ex) + { + throw new IOException($"Failed to write encrypted data: {ex.Message}", ex); + } + } + + internal static byte[] EncryptData(string contentKey, byte[] data) + { + byte[] contentKeyBytes = Encoding.UTF8.GetBytes(contentKey); + byte[] iv = contentKeyBytes[..16]; + byte[] ciphertext = Aes256Cfb8Encrypt(contentKeyBytes, iv, data); + return ciphertext; + } + + internal static byte[] Aes256Cfb8Encrypt(byte[] key, byte[] iv, byte[] data) + { + using Aes aes = Aes.Create(); + aes.Mode = CipherMode.CFB; + aes.Padding = PaddingMode.None; + aes.BlockSize = 128; // Explicitly set block size to 128 bits (standard for AES) + aes.KeySize = 256; // Explicitly set key size to 256 bits (standard for AES-256) + + using ICryptoTransform aesEncryptor = aes.CreateEncryptor(key, iv); + using MemoryStream msEncrypt = new(); + using CryptoStream csEncrypt = new(msEncrypt, aesEncryptor, CryptoStreamMode.Write); + + csEncrypt.Write(data, 0, data.Length); + + // Ensure padding to nearest block size (CFB8 mode needs this for alignment) + int paddingNeeded = aes.BlockSize / 8 - (data.Length % (aes.BlockSize / 8)); + if (paddingNeeded > 0) + { + csEncrypt.Write(new byte[paddingNeeded], 0, paddingNeeded); + } + + csEncrypt.FlushFinalBlock(); + return msEncrypt.ToArray(); + } + } +} diff --git a/Utility/ManifestUtility.cs b/Utility/ManifestUtility.cs new file mode 100644 index 0000000..58ba6ca --- /dev/null +++ b/Utility/ManifestUtility.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace EncryptMC.Utility +{ + internal static class ManifestUtility + { + internal static string GetManifestUuid(string inputPath) + { + string manifestPath = Path.Combine(inputPath, "manifest.json"); + + if (!File.Exists(manifestPath)) + { + throw new FileNotFoundException("Manifest file not found.", manifestPath); + } + + string manifestContents = File.ReadAllText(manifestPath); + + // Parse the JSON content + JObject manifest; + try + { + manifest = JObject.Parse(manifestContents); + } + catch (JsonReaderException ex) + { + throw new FormatException($"Invalid JSON formatting in manifest file: {ex.Message}", ex); + } + + // Get "header" object + var header = manifest["header"]; + if (header == null) + { + throw new KeyNotFoundException("Manifest header not found."); + } + + // Get "uuid" property + var uuid = header["uuid"]?.ToString(); + if (string.IsNullOrEmpty(uuid)) + { + throw new KeyNotFoundException("Manifest UUID not found."); + } + + return uuid; + } + + internal static void CreateManifestSignature(string inputPath, string outputPath, string contentKey) + { + string manifestPath = Path.Combine(inputPath, "manifest.json"); + string signaturePath = Path.Combine(outputPath, "signatures.json"); + + // Check if manifest file exists + if (!File.Exists(manifestPath)) + { + throw new FileNotFoundException("Manifest file not found.", manifestPath); + } + + // Read manifest file contents + string manifestContents = File.ReadAllText(manifestPath); + + // Calculate SHA-256 hash of manifest + string manifestHashBase64 = CalculateSha256Hash(manifestContents); + + // Prepare signatures contents + var signaturesContents = new List> + { + new() + { + { "hash", manifestHashBase64 }, + { "path", "manifest.json" } + } + }; + + // Convert signatures contents to JSON string + string contentsString = JsonConvert.SerializeObject(signaturesContents, Formatting.None); + byte[] contentsBytes = Encoding.UTF8.GetBytes(contentsString); + + // Encrypt and write to file + EncryptionUtility.WriteDataToEncryptedFile(signaturePath, contentsBytes, contentKey); + } + + private static string CalculateSha256Hash(string plaintext) + { + byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(plaintext)); + return Convert.ToBase64String(hashBytes); + } + } +} diff --git a/Utility/PathUtility.cs b/Utility/PathUtility.cs new file mode 100644 index 0000000..5120450 --- /dev/null +++ b/Utility/PathUtility.cs @@ -0,0 +1,33 @@ +namespace EncryptMC.Utility +{ + internal static class PathUtility + { + internal static void EnsureOutputFolderExists(string outputPath) + { + try + { + if (!Directory.Exists(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + } + catch (Exception ex) + { + throw new IOException($"Failed to ensure directory exists: {ex.Message}", ex); + } + } + + internal static bool IsSubPath(string inputPath, string outputPath) + { + // Normalize paths to remove relative elements and ensure consistency + string? normalizedInput = Path.GetFullPath(inputPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + string? normalizedOutput = Path.GetFullPath(outputPath) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + bool pathsEqual = string.Equals(normalizedInput, normalizedOutput, StringComparison.OrdinalIgnoreCase); + + // Check if output starts with input + return normalizedOutput.StartsWith(normalizedInput, StringComparison.OrdinalIgnoreCase) && !pathsEqual; + } + } +}