diff --git a/LightvnTools.csproj b/LightvnTools.csproj index fddf7d3..5c72cd5 100644 --- a/LightvnTools.csproj +++ b/LightvnTools.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/Program.cs b/Program.cs index 612259e..4c30e56 100644 --- a/Program.cs +++ b/Program.cs @@ -1,11 +1,11 @@ -using System.IO.Compression; -using System.Text; +using System.Text; +using ICSharpCode.SharpZipLib.Zip; namespace LightvnTools { public class Program { - static readonly string VERSION = "1.0.0"; + static readonly string VERSION = "1.1.0"; // PKZip signature static readonly byte[] PKZIP = { 0x50, 0x4B, 0x03, 0x04 }; @@ -34,70 +34,172 @@ namespace LightvnTools } string input = args[0]; + string zipPassword = Encoding.UTF8.GetString(KEY); - // Unpack .vndat if (File.Exists(input)) - { - if (!IsZip(input)) - { - Console.WriteLine("Not a .vndat (zip) file!"); - Console.ReadLine(); - return; - } + Unpack(input, Path.GetFileNameWithoutExtension(input), zipPassword); - string outputFolder = Path.GetFileNameWithoutExtension(input); - - if (!Directory.Exists(outputFolder)) - Directory.CreateDirectory(outputFolder); - - // Extract .vndat file - Console.WriteLine($"Extracting {Path.GetFileName(input)} to ./{outputFolder}/..."); - ZipFile.ExtractToDirectory(input, outputFolder, true); - - // Decrypt .vndat file contents - string[] files = GetFilesRecursive(outputFolder); - - foreach (string file in files) - { - Console.WriteLine($"Decrypting {file}..."); - XORFile(file); - } - - Console.WriteLine("Done."); - } - - // Repack if (Directory.Exists(input)) - { - string[] files = GetFilesRecursive(input); - - // Encrypting the files back - foreach (string file in files) - { - Console.WriteLine($"Encrypting {Path.GetFileName(file)}..."); - XORFile(file); - } - - // Archiving to .vndat - string fileName = $"{input}.vndat"; - - Console.WriteLine($"Archiving as {fileName}..."); - File.Copy(fileName, $"{fileName}.bak"); - File.Delete(fileName); - ZipFile.CreateFromDirectory(input, fileName, CompressionLevel.Optimal, false); - - Console.WriteLine("Done."); - } + Repack(input, zipPassword); Console.ReadLine(); } /// - /// Check if the given file is Zip or not. + /// Extract `.vndat` file. + /// + /// + /// + /// + static void Unpack(string vndatFile, string outputFolder, string? password = "") + { + if (!IsVndat(vndatFile)) + { + Console.WriteLine($"{Path.GetFileName(vndatFile)} isn\'t a `.vndat` (zip) file!"); + return; + } + + bool usePassword = IsPasswordProtectedZip(vndatFile); + + using ZipFile zipFile = new(vndatFile); + Directory.CreateDirectory(outputFolder); + + // Old Light.vn encrypt the `.vndat` file with `KEY` as the password. + if (usePassword) + { + Console.WriteLine($"{Path.GetFileName(vndatFile)} are password protected. Using `{password}` as the password."); + zipFile.Password = password; + } + + if (zipFile.Count > 0) + { + Console.WriteLine($"Extracting {Path.GetFileName(vndatFile)}..."); + + foreach (ZipEntry entry in zipFile) + { + string? entryPath = Path.Combine(outputFolder, entry.Name); + Directory.CreateDirectory(Path.GetDirectoryName(entryPath)); + + if (!entry.IsDirectory) + { + try + { + Console.WriteLine($"Writing {entryPath}..."); + + using Stream inputStream = zipFile.GetInputStream(entry); + using FileStream outputStream = File.Create(entryPath); + inputStream.CopyTo(outputStream); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to write {entryPath}! {ex.Message}"); + } + } + } + + Console.WriteLine("Done."); + } + + // Only XOR `.vndat` contents that's not password protected. + if (!usePassword) + { + string[] files = GetFilesRecursive(outputFolder); + + if (files.Length > 0) + { + foreach (string file in files) + { + Console.WriteLine($"Decrypting {file}..."); + XorVndatContent(file); + } + + Console.WriteLine("Done."); + } + } + } + + /// + /// Archive folder as `.vndat` file. + /// + /// + /// + static void Repack(string sourceFolder, string? password = "") + { + string outputFile = $"{Path.GetFileName(sourceFolder)}.vndat"; + string[] files = GetFilesRecursive(sourceFolder); + string? tempFolder = $"{sourceFolder}_temp"; + + // Only backup original file once + string backupFile = $"{outputFile}.bak"; + if (!File.Exists(backupFile)) + { + Console.WriteLine($"Backup the original file as {Path.GetFileName(backupFile)}..."); + File.Copy(outputFile, backupFile); + } + + bool usePassword = IsPasswordProtectedZip(backupFile); + + using ZipOutputStream zipStream = new(File.Create(outputFile)); + + // Uses the backup file to check if it's encrypted to bypass + // the file is being used by another process exception. + if (usePassword) + { + Console.WriteLine($"Encrypting {Path.GetFileName(outputFile)} using `{password}` as the password..."); + zipStream.Password = password; + } + else + { + Console.WriteLine($"Creating a temporary copy of {Path.GetFileName(sourceFolder)} to perform XOR encryption..."); + + CopyFolder(sourceFolder, tempFolder); + files = GetFilesRecursive(tempFolder); + + foreach (string file in files) + { + Console.WriteLine($"Encrypting {Path.GetRelativePath(sourceFolder, file)}..."); + XorVndatContent(file); + } + } + + Console.WriteLine($"Creating {outputFile} archive..."); + + foreach (string filePath in files) + { + FileInfo file = new(filePath); + // Keep file structure by including the folder + string entryName = filePath[usePassword ? sourceFolder.Length.. : tempFolder.Length..].TrimStart('\\'); + ZipEntry entry = new(entryName) + { + DateTime = DateTime.Now, + Size = file.Length + }; + zipStream.PutNextEntry(entry); + + using FileStream fileStream = file.OpenRead(); + byte[] buffer = new byte[4096]; // Optimum size + int bytesRead; + while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) + { + zipStream.Write(buffer, 0, bytesRead); + } + } + + if (!usePassword) + { + Console.WriteLine("Cleaning up temporary files..."); + Directory.Delete(tempFolder, true); + } + + Console.WriteLine("Done."); + } + + /// + /// Check if the given file is `.vndat` file (Zip) or not. /// /// /// - static bool IsZip(string filePath) + static bool IsVndat(string filePath) { try { @@ -122,10 +224,37 @@ namespace LightvnTools } /// - /// XOR the given file. + /// Check if the ZIP file is password protected. /// /// - static void XORFile(string filePath) + /// + static bool IsPasswordProtectedZip(string filePath) + { + try + { + using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read); + using ZipInputStream zipStream = new(fileStream); + + ZipEntry entry; + while ((entry = zipStream.GetNextEntry()) != null) + { + if (entry.IsCrypted) + return true; + } + + return false; + } + catch + { + return false; + } + } + + /// + /// Encrypt (XOR) `.vndat` file content. + /// + /// + static void XorVndatContent(string filePath) { try { @@ -181,5 +310,30 @@ namespace LightvnTools { return Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories); } + + /// + /// Copy entire files in a folder. + /// + /// + /// + static void CopyFolder(string sourceDirectory, string destinationDirectory) + { + if (!Directory.Exists(destinationDirectory)) + Directory.CreateDirectory(destinationDirectory); + + string[] files = GetFilesRecursive(sourceDirectory); + + foreach (string sourceFilePath in files) + { + string relativePath = sourceFilePath[sourceDirectory.Length..].TrimStart('\\'); + string destinationFilePath = Path.Combine(destinationDirectory, relativePath); + + string destinationFileDirectory = Path.GetDirectoryName(destinationFilePath); + if (!Directory.Exists(destinationFileDirectory)) + Directory.CreateDirectory(destinationFileDirectory); + + File.Copy(sourceFilePath, destinationFilePath, true); + } + } } }