Add new encryption type

This encryption type is come from the Soul Engine
[http://soulengineproject.com/] (is this beta version of Light.vn?)
that I found in HIMAWARI game (https://www.freem.ne.jp/win/game/17154).
Thanks to Zakamutt (https://forums.fuwanovel.net/profile/1088-zakamutt/)
for reporting this.

How does this encryption work?
The `.vndat` file are password protected with the same key as Light.vn
`d6c5fKI3GgBWpZF3Tz6ia3kF0` while the contents of `.vndat` file isn't
encrypted at all. Because of this, I need to rewrite the entire unpack
and repack logic.

Signed-off-by: Bayu Satiyo <itsyuukunz@gmail.com>
This commit is contained in:
Bayu Satiyo 2023-08-19 11:08:47 +07:00
parent 33848ae084
commit ea9a31d407
No known key found for this signature in database
GPG Key ID: 3C98876626C85FE3
2 changed files with 215 additions and 57 deletions

View File

@ -7,4 +7,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
</Project> </Project>

View File

@ -1,11 +1,11 @@
using System.IO.Compression; using System.Text;
using System.Text; using ICSharpCode.SharpZipLib.Zip;
namespace LightvnTools namespace LightvnTools
{ {
public class Program public class Program
{ {
static readonly string VERSION = "1.0.0"; static readonly string VERSION = "1.1.0";
// PKZip signature // PKZip signature
static readonly byte[] PKZIP = { 0x50, 0x4B, 0x03, 0x04 }; static readonly byte[] PKZIP = { 0x50, 0x4B, 0x03, 0x04 };
@ -34,70 +34,172 @@ namespace LightvnTools
} }
string input = args[0]; string input = args[0];
string zipPassword = Encoding.UTF8.GetString(KEY);
// Unpack .vndat
if (File.Exists(input)) if (File.Exists(input))
{ Unpack(input, Path.GetFileNameWithoutExtension(input), zipPassword);
if (!IsZip(input))
{
Console.WriteLine("Not a .vndat (zip) file!");
Console.ReadLine();
return;
}
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)) if (Directory.Exists(input))
{ Repack(input, zipPassword);
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.");
}
Console.ReadLine(); Console.ReadLine();
} }
/// <summary> /// <summary>
/// Check if the given file is Zip or not. /// Extract `.vndat` file.
/// </summary>
/// <param name="vndatFile"></param>
/// <param name="outputFolder"></param>
/// <param name="password"></param>
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.");
}
}
}
/// <summary>
/// Archive folder as `.vndat` file.
/// </summary>
/// <param name="sourceFolder"></param>
/// <param name="password"></param>
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.");
}
/// <summary>
/// Check if the given file is `.vndat` file (Zip) or not.
/// </summary> /// </summary>
/// <param name="filePath"></param> /// <param name="filePath"></param>
/// <returns></returns> /// <returns></returns>
static bool IsZip(string filePath) static bool IsVndat(string filePath)
{ {
try try
{ {
@ -122,10 +224,37 @@ namespace LightvnTools
} }
/// <summary> /// <summary>
/// XOR the given file. /// Check if the ZIP file is password protected.
/// </summary> /// </summary>
/// <param name="filePath"></param> /// <param name="filePath"></param>
static void XORFile(string filePath) /// <returns></returns>
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;
}
}
/// <summary>
/// Encrypt (XOR) `.vndat` file content.
/// </summary>
/// <param name="filePath"></param>
static void XorVndatContent(string filePath)
{ {
try try
{ {
@ -181,5 +310,30 @@ namespace LightvnTools
{ {
return Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories); return Directory.GetFiles(sourceFolder, "*.*", SearchOption.AllDirectories);
} }
/// <summary>
/// Copy entire files in a folder.
/// </summary>
/// <param name="sourceDirectory"></param>
/// <param name="destinationDirectory"></param>
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);
}
}
} }
} }