diff --git a/sync/Test/App.config b/sync/Test/App.config index 8e15646..d0feca6 100644 --- a/sync/Test/App.config +++ b/sync/Test/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/sync/Test/Program.cs b/sync/Test/Program.cs index 29c3388..296e7f3 100644 --- a/sync/Test/Program.cs +++ b/sync/Test/Program.cs @@ -1,32 +1,81 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using asardotnet; -using Newtonsoft.Json.Linq; -namespace Test { - class Program { - static void Main(string[] args) { - - AsarArchive asarArhive = new AsarArchive("D:\\Downloads\\app.asar"); - AsarExtractor extractor = new AsarExtractor(); - - extractor.ExtractAll(asarArhive, "G:\\Asardotnet\\out\\"); - - // extractor.Extract(asarArhive, "app/index.js", "G:\\Asardotnet\\out\\index.js"); +namespace asardotnet +{ + class Program + { + static void PrintUsage() + { + Console.WriteLine("\n Usage: Test.exe [command] [options]"); + Console.WriteLine("\n Commands:"); + Console.WriteLine("\n pack|p \n create asar archive"); + Console.WriteLine("\n list|l \n list files of asar archive"); + Console.WriteLine("\n extract-file|ef \n extract one file from asar archive"); + Console.WriteLine("\n extract|e \n extract asar archive"); + Console.WriteLine("\n"); + } - // AsarExtractor extractor = new AsarExtractor(); - // extractor.Extract(asarArhive, "NotificationWindow.js", ""); + static void Main(string[] args) + { + if (args.Length < 2) + { + PrintUsage(); + return; + } - // AsarExtractor asarExtractor = new AsarExtractor(); + AsarArchive asarArchive; + AsarExtractor extractor = new AsarExtractor(); + AsarPacker packer = new AsarPacker(); - //asarExtractor.Extract(asarArhive, "G:\\Asardotnet\\extract\\"); + switch (args[0].ToLower()) + { + case "e": + case "extract": + if (args.Length != 3 || !File.Exists(args[1])) + { - // asarExtractor.ExtractFile(asarArhive, 8528, 6479); + PrintUsage(); + return; + } + asarArchive = new AsarArchive(args[1]); + extractor.ExtractAll(asarArchive, args[2]); + return; + case "ef": + case "extract-file": + if (args.Length != 3) + { + PrintUsage(); + return; + } + asarArchive = new AsarArchive(args[1]); + extractor.Extract(asarArchive, args[1], args[2]); + break; + case "l": + case "list": + if (!File.Exists(args[1])) + { + PrintUsage(); + return; + } + asarArchive = new AsarArchive(args[1]); + extractor.ListAll(asarArchive); + return; + case "p": + case "pack": + if (args.Length != 3) + { + PrintUsage(); + break; + } + packer.Pack(args[1], args[2]); + return; + default: + break; + } } } -} +} \ No newline at end of file diff --git a/sync/Test/Test.csproj b/sync/Test/Test.csproj index f5722ef..2be0b0d 100644 --- a/sync/Test/Test.csproj +++ b/sync/Test/Test.csproj @@ -9,8 +9,9 @@ Properties Test Test - v4.5 + v4.5.1 512 + AnyCPU diff --git a/sync/asardotnet/AsarArchive.cs b/sync/asardotnet/AsarArchive.cs index 461afca..a81c977 100644 --- a/sync/asardotnet/AsarArchive.cs +++ b/sync/asardotnet/AsarArchive.cs @@ -26,40 +26,52 @@ * */ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; -namespace asardotnet { - public class AsarArchive { +namespace asardotnet +{ + public class AsarArchive + { private const int SIZE_UINT = 4; private readonly int _baseOffset; + public int GetBaseOffset() { return _baseOffset; } private readonly byte[] _bytes; + public byte[] GetBytes() { return _bytes; } - private readonly String _filePath; - public String GetFilePath() { return _filePath; } + private readonly string _filePath; + + public string GetFilePath() { return _filePath; } private Header _header; + public Header GetHeader() { return _header; } - public struct Header { + public struct Header + { private readonly byte[] _headerInfo; + public byte[] GetHeaderInfo() { return _headerInfo; } + private readonly int _headerLength; + public int GetHeaderLenth() { return _headerLength; } private readonly byte[] _headerData; + public byte[] GetHeaderData() { return _headerData; } + private readonly JObject _headerJson; + public JObject GetHeaderJson() { return _headerJson; } - public Header(byte[] hinfo, int length, byte[] data, JObject hjson) { + public Header(byte[] hinfo, int length, byte[] data, JObject hjson) + { _headerInfo = hinfo; _headerLength = length; _headerData = data; @@ -67,22 +79,34 @@ public Header(byte[] hinfo, int length, byte[] data, JObject hjson) { } } - public AsarArchive(String filePath) { - if(!File.Exists(filePath)) + public AsarArchive() + { + + } + + public AsarArchive(string filePath) + { + if (!File.Exists(filePath)) throw new AsarExceptions(AsarException.ASAR_FILE_CANT_FIND); _filePath = filePath; - try { + try + { _bytes = File.ReadAllBytes(filePath); - } catch(Exception ex) { + } + catch (Exception ex) + { throw new AsarExceptions(AsarException.ASAR_FILE_CANT_READ, ex.ToString()); } - try { + try + { _header = ReadAsarHeader(ref _bytes); _baseOffset = _header.GetHeaderLenth(); - } catch(Exception _ex) { + } + catch (Exception _ex) + { throw _ex; } } @@ -91,29 +115,33 @@ public AsarArchive(String filePath) { * Exceptions should never be thrown as long as the file * was created with nodejs asar algorithm */ - private static Header ReadAsarHeader(ref byte[] bytes) { - int SIZE_LONG = 2 * SIZE_UINT; - int SIZE_INFO = 2 * SIZE_LONG; + private static Header ReadAsarHeader(ref byte[] bytes) + { + int SIZE_LONG = 2 * SIZE_UINT; // 8 + int SIZE_INFO = 2 * SIZE_LONG; // 16 // Header Info - byte[] headerInfo = bytes.Take(SIZE_INFO).ToArray(); + byte[] headerInfo = bytes.Take(SIZE_INFO).ToArray(); // 16 - if(headerInfo.Length < SIZE_INFO) + if (headerInfo.Length < SIZE_INFO) // 16 throw new AsarExceptions(AsarException.ASAR_INVALID_FILE_SIZE); - byte[] asarFileDescriptor = headerInfo.Take(SIZE_LONG).ToArray(); - byte[] asarPayloadSize = asarFileDescriptor.Take(SIZE_UINT).ToArray(); + byte[] asarFileDescriptor = headerInfo.Take(SIZE_LONG).ToArray(); // 16 + byte[] asarPayloadSize = asarFileDescriptor.Take(SIZE_UINT).ToArray(); // 4 int payloadSize = BitConverter.ToInt32(asarPayloadSize, 0); - int payloadOffset = asarFileDescriptor.Length - payloadSize; + int payloadOffset = asarFileDescriptor.Length - payloadSize; // 16 - 4 = 12 - if(payloadSize != SIZE_UINT && payloadSize != SIZE_LONG) + // payload size should be 4 + if (payloadSize != SIZE_UINT && payloadSize != SIZE_LONG) throw new AsarExceptions(AsarException.ASAR_INVALID_DESCRIPTOR); + // skip to byte 12 and read 4 bytes into headerLength byte[] asarHeaderLength = asarFileDescriptor.Skip(payloadOffset).Take(SIZE_UINT).ToArray(); int headerLength = BitConverter.ToInt32(asarHeaderLength, 0); + // skip 8 and take 8 byte[] asarFileHeader = headerInfo.Skip(SIZE_LONG).Take(SIZE_LONG).ToArray(); byte[] asarHeaderPayloadSize = asarFileHeader.Take(SIZE_UINT).ToArray(); @@ -126,7 +154,7 @@ private static Header ReadAsarHeader(ref byte[] bytes) { // Data Table byte[] hdata = bytes.Skip(SIZE_INFO).Take(dataTableSize).ToArray(); - if(hdata.Length != dataTableSize) + if (hdata.Length != dataTableSize) throw new AsarExceptions(AsarException.ASAR_INVALID_FILE_SIZE); int asarDataOffset = asarFileDescriptor.Length + headerLength; diff --git a/sync/asardotnet/AsarExceptions.cs b/sync/asardotnet/AsarExceptions.cs index 48b85ea..9c5d9e9 100644 --- a/sync/asardotnet/AsarExceptions.cs +++ b/sync/asardotnet/AsarExceptions.cs @@ -27,32 +27,39 @@ using System; -namespace asardotnet { - public enum AsarException { +namespace asardotnet +{ + public enum AsarException + { ASAR_FILE_CANT_FIND, ASAR_FILE_CANT_READ, ASAR_INVALID_DESCRIPTOR, ASAR_INVALID_FILE_SIZE - }; + } - public class AsarExceptions: Exception { + public class AsarExceptions : Exception + { private readonly AsarException _asarException; + private readonly string _asarMessage; public AsarExceptions(AsarException ex) : this(ex, "") { } - public AsarExceptions(AsarException ex, String customMessage) { + public AsarExceptions(AsarException ex, String customMessage) + { _asarException = ex; - if(customMessage.Length > 0) + if (customMessage.Length > 0) _asarMessage = customMessage; else _asarMessage = GetMessage(ex); } - private String GetMessage(AsarException ex) { - String result; + private string GetMessage(AsarException ex) + { + string result; - switch(ex) { + switch (ex) + { case AsarException.ASAR_FILE_CANT_FIND: result = "Error: The specified file couldn't be found."; break; @@ -73,15 +80,18 @@ private String GetMessage(AsarException ex) { return result; } - public AsarException GetExceptionCode() { + public AsarException GetExceptionCode() + { return _asarException; } - public String GetExceptionMessage() { + public string GetExceptionMessage() + { return _asarMessage; } - override public String ToString() { + override public String ToString() + { return "(Code " + GetExceptionCode() + ") " + GetExceptionMessage(); } } diff --git a/sync/asardotnet/AsarExtractor.cs b/sync/asardotnet/AsarExtractor.cs index 9ec1982..14f4500 100644 --- a/sync/asardotnet/AsarExtractor.cs +++ b/sync/asardotnet/AsarExtractor.cs @@ -28,24 +28,22 @@ using System; using System.Collections.Generic; -using System.Data.Odbc; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.Remoting; -using System.Security.Cryptography; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace asardotnet { - public class AsarExtractor { - - public Boolean Extract(AsarArchive archive, String filepath, String destination) { - String[] path = filepath.Split('/'); +namespace asardotnet +{ + public class AsarExtractor + { + public bool Extract(AsarArchive archive, string filepath, string destination) + { + string[] path = filepath.Split('/'); JToken token = archive.GetHeader().GetHeaderJson(); - for(int i = 0; i < path.Length; i++) { + for (int i = 0; i < path.Length; i++) + { token = token["files"][path[i]]; } @@ -59,85 +57,120 @@ public Boolean Extract(AsarArchive archive, String filepath, String destination) return false; } - private List _filesToExtract; - private bool _emptyDir = false; + private List filesToExtract; + + public Dictionary unpackedFiles = new Dictionary(); + + private readonly bool verbose = false; + + private struct AFile + { + private readonly string path; + + public string GetPath() { return path; } + + private readonly int size; + + public int GetSize() { return size; } + + private readonly int offset; + + public int GetOffset() { return offset; } - public Boolean ExtractAll(AsarArchive archive, String destination, bool emptyDir = false) { - _filesToExtract = new List(); + public AFile(string path, int size, int offset) + { + this.path = path; + this.size = size; + this.offset = offset; + } + } + + private void TokenIterator(JObject jObj, string fullPath) + { + foreach (KeyValuePair entry in jObj) + { + if (entry.Value["files"] != null) + { + var newPath = fullPath + entry.Key + Path.DirectorySeparatorChar; + var newDir = new AFile(newPath, -1, -1); + this.filesToExtract.Add(newDir); + TokenIterator((JObject)entry.Value["files"], newPath); + } + if (entry.Value["unpacked"] != null && entry.Value["size"] != null) + { + if (bool.Parse(entry.Value["unpacked"].ToString())) + { + this.unpackedFiles.Add(fullPath + entry.Key, int.Parse(entry.Value["size"].ToString())); + } + } + if (entry.Value["size"] != null && entry.Value["offset"] != null) + { + int size = int.Parse(entry.Value["size"].ToString()); + int offset = int.Parse(entry.Value["offset"].ToString()); + var aFile = new AFile(fullPath + entry.Key, size, offset); + this.filesToExtract.Add(aFile); + } + } + } - /* ENABLE FOR EMPTY FOLDERS (ONLY IF NEEDED) */ - _emptyDir = emptyDir; + public bool ExtractAll(AsarArchive archive, string destination) + { + filesToExtract = new List(); JObject jObject = archive.GetHeader().GetHeaderJson(); - if(jObject.HasValues) - TokenIterator(jObject.First); + + if (jObject.HasValues) + TokenIterator((JObject)jObject["files"], ""); + + Console.WriteLine($"Extracting files to: {destination} .."); byte[] bytes = archive.GetBytes(); - foreach(AFile aFile in _filesToExtract) { + foreach (AFile aFile in filesToExtract) + { + if (verbose) + Console.WriteLine($"Extracting.. {aFile.GetPath()}"); + int size = aFile.GetSize(); + int offset = archive.GetBaseOffset() + aFile.GetOffset(); - if(size > -1) { - byte[] fileBytes = new byte[size]; + if (size > -1) + { + byte[] fileBytes = new byte[size]; Buffer.BlockCopy(bytes, offset, fileBytes, 0, size); - Utilities.WriteFile(fileBytes, destination + aFile.GetPath()); - } else { - if(_emptyDir) - Utilities.CreateDirectory(destination + aFile.GetPath()); + try + { + Utilities.WriteFile(fileBytes, Path.Combine(destination, aFile.GetPath())); + } + catch (PathTooLongException) + { + Console.WriteLine($"Error unpacking {aFile.GetPath()}"); + Console.WriteLine("File name is too long. Try setting current directory to a shorter path (e.g. c:\temp)"); + return false; + } + } + else + { + Utilities.CreateDirectory(Path.Combine(destination, aFile.GetPath())); } } - return false; + return true; } - private struct AFile { - private String _path; - public String GetPath() { return _path; } - private int _size; - public int GetSize() { return _size; } - private int _offset; - public int GetOffset() { return _offset; } - - public AFile(String path, String fileName, int size, int offset) { - path = path.Replace("['", "").Replace("']", ""); - path = path.Substring(0, path.Length - fileName.Length); - path = path.Replace(".files.", "/").Replace("files.", ""); - path += fileName; - - _path = path; - _size = size; - _offset = offset; - } - } + public void ListAll(AsarArchive archive) + { + filesToExtract = new List(); - private void TokenIterator(JToken jToken) { - JProperty jProperty = jToken as JProperty; - - foreach(JProperty prop in jProperty.Value.Children()) { - int size = -1; - int offset = -1; - foreach(JProperty nextProp in prop.Value.Children()) { - if(nextProp.Name == "files") { - /* ENABLE FOR EMPTY FOLDERS (ONLY IF NEEDED) */ - if(_emptyDir) { - AFile afile = new AFile(prop.Path, "", size, offset); - _filesToExtract.Add(afile); - } - - TokenIterator(nextProp); - } else { - if(nextProp.Name == "size") - size = Int32.Parse(nextProp.Value.ToString()); - if(nextProp.Name == "offset") - offset = Int32.Parse(nextProp.Value.ToString()); - } - } + JObject jObject = archive.GetHeader().GetHeaderJson(); - if(size > -1 && offset > -1) { - AFile afile = new AFile(prop.Path, prop.Name, size, offset); - _filesToExtract.Add(afile); - } + if (jObject.HasValues) + TokenIterator((JObject)jObject["files"], ""); + + foreach (var aFile in filesToExtract) + { + Console.WriteLine(aFile.GetPath()); } } } diff --git a/sync/asardotnet/AsarPacker.cs b/sync/asardotnet/AsarPacker.cs new file mode 100644 index 0000000..4802cb4 --- /dev/null +++ b/sync/asardotnet/AsarPacker.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json; + +namespace asardotnet +{ + public class AsarIntegrity + { + public const int blockSize = 0x400000; + + public readonly string algorithm = "SHA256"; + + public string hash; + + public string[] blocks; + + public AsarIntegrity(string filePath) + { + this.hash = GetSha256Sum(filePath); + this.blocks = GetSha256SumBlocks(filePath); + } + + private static string[] GetSha256SumBlocks(string filePath) + { + List hashes = new List(); + byte[] buffer = new byte[blockSize]; + int bytesRead; + using (FileStream fs = File.Open(filePath, FileMode.Open, FileAccess.Read)) + using (BufferedStream bs = new BufferedStream(fs)) + { + while ((bytesRead = bs.Read(buffer, 0, blockSize)) != 0) + { + byte[] chunk = new byte[bytesRead]; + Array.Copy(buffer, chunk, bytesRead); + hashes.Add(GetSha256Sum(chunk)); + } + } + return hashes.ToArray(); + } + + public static string GetSha256Sum(string filename) + { + using (SHA256 sha256 = SHA256.Create()) + using (FileStream stream = File.OpenRead(filename)) + { + return BitConverter.ToString( + sha256.ComputeHash(stream)) + .Replace("-", string.Empty).ToLower(); + } + } + + public static string GetSha256Sum(byte[] input) + { + using (SHA256 sha256 = SHA256.Create()) + { + return BitConverter.ToString(sha256.ComputeHash(input)) + .Replace("-", string.Empty).ToLower(); + } + } + } + + public class UnpackedAsarFile + { + public long size; + + public bool unpacked = true; + } + + public class AsarFile + { + public long size; + + public string offset; + + public AsarIntegrity integrity; + } + + public class AsarFileNoIntegrity + { + public long size; + + public string offset; + + } + + public class AsarDirectory + { + public Dictionary files = new Dictionary(); + } + + public class AsarPacker + { + internal bool skipIntegrity = false; + + private string rootDir; + + private long offset = 0; + + private readonly bool verbose = false; + + private Dictionary unpackedFiles = new Dictionary(); + + private AsarDirectory WalkDirectory(AsarDirectory dir, string sourceDir, MemoryStream ms) + { + foreach (var entry in Directory.GetFileSystemEntries(sourceDir, "*", SearchOption.TopDirectoryOnly)) + { + var attrs = File.GetAttributes(entry); + var fileInfo = new FileInfo(entry); + string curPath = entry.Replace(rootDir, string.Empty).Replace(@"\", "/").TrimStart('/'); + + if (!attrs.HasFlag(FileAttributes.Directory)) + { + // Entry is a file + if (verbose) + Console.WriteLine($"Adding file {Path.GetFullPath(entry)}"); + + if (!skipIntegrity) + { + // create the integrity block + AsarFile asarFile = new AsarFile + { + offset = offset.ToString(), + size = fileInfo.Length, + integrity = new AsarIntegrity(entry), + }; + dir.files.Add(Path.GetFileName(entry), asarFile); + } + else + { + AsarFileNoIntegrity asarFile = new AsarFileNoIntegrity + { + offset = offset.ToString(), + size = fileInfo.Length, + }; + dir.files.Add(Path.GetFileName(entry), asarFile); + } + + using (var fs = new FileStream(entry, FileMode.Open)) + { + fs.CopyTo(ms); + offset += fileInfo.Length; + } + } + else + { + // Entry is a directory + var dirInfo = new DirectoryInfo(entry); + + if (verbose) + Console.WriteLine($"Adding directory {dirInfo.FullName}"); + + AsarDirectory asarDir = new AsarDirectory(); + asarDir = WalkDirectory(asarDir, dirInfo.FullName, ms); + + // Insert any "unpacked" files that belong to this directory (if any) + foreach (var key in unpackedFiles.Keys) + { + var uDir = Path.GetDirectoryName(key); + + var thisDir = dirInfo.FullName.Replace(rootDir, string.Empty).TrimStart('\\'); + + if (uDir == thisDir) + { + // Mark file as unpacked and don't append data to archive + UnpackedAsarFile unpackedFile = new UnpackedAsarFile + { + size = unpackedFiles[key], + unpacked = true + }; + + if (verbose) + Console.WriteLine($"Adding unpacked file {key}"); + + asarDir.files.Add(Path.GetFileName(key), unpackedFile); + } + } + + // Add new directory branch + dir.files.Add(dirInfo.Name, asarDir); + } + } + return dir; + } + + internal static byte[] CreateSHA256(string input) + { + using (SHA256 sha256 = SHA256.Create()) + { + return sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); + } + } + + public void Pack(string sourceDir, string destFile, Dictionary unpackedFiles = null, + bool skipIntegrity = true, bool created = false) + { + if (!Directory.Exists(sourceDir)) + { + Console.WriteLine("Source directory does not exist!"); + return; + } + + Console.WriteLine($"Packing {sourceDir} .."); + + if (unpackedFiles != null) + this.unpackedFiles = unpackedFiles; + + if (skipIntegrity) + this.skipIntegrity = true; + + this.rootDir = sourceDir; + + using (var ms = new MemoryStream()) + { + AsarDirectory root = new AsarDirectory(); + + // First we generate the json table of contents + root = WalkDirectory(root, sourceDir, ms); + + string json = JsonConvert.SerializeObject(root); + + // finally write the header + using (var ms2 = new MemoryStream()) + using (var bw = new BinaryWriter(ms2)) + { + byte[] padding = created ? + CreateSHA256(this.GetType().Namespace).ToArray() : + new byte[] { 0x00, 0x00, 0x00, 0x00 }; + + bw.Write(0x04); + bw.Write(json.Length + padding.Length + 8); + bw.Write(json.Length + padding.Length + 4); + bw.Write(json.Length); + bw.Write(Encoding.UTF8.GetBytes(json)); + bw.Write(padding); + bw.Write(ms.ToArray()); + File.WriteAllBytes(destFile, ms2.ToArray()); + } + } + } + } +} diff --git a/sync/asardotnet/Utilities.cs b/sync/asardotnet/Utilities.cs index c3f80f6..125097b 100644 --- a/sync/asardotnet/Utilities.cs +++ b/sync/asardotnet/Utilities.cs @@ -26,30 +26,30 @@ * */ using System; -using System.Diagnostics; using System.IO; -using System.Security.Cryptography; -using System.Text; -namespace asardotnet { - public class Utilities { - - public static void WriteFile(byte[] bytes, String destination) { +namespace asardotnet +{ + public class Utilities + { + public static void WriteFile(byte[] bytes, string destination) + { // Debug.Print("Writing bytes to : " + destination); - String dirPath = Path.GetDirectoryName(destination); - String filename = Path.GetFileName(destination); + string dirPath = Path.GetDirectoryName(destination); + string filename = Path.GetFileName(destination); Directory.CreateDirectory(dirPath); File.WriteAllBytes(destination, bytes); } - public static void CreateDirectory(String path) { - if(!Directory.Exists(path)) { + public static void CreateDirectory(string path) + { + if (!Directory.Exists(path)) + { Directory.CreateDirectory(path); } } - } } diff --git a/sync/asardotnet/asardotnet.csproj b/sync/asardotnet/asardotnet.csproj index 88594c8..0c48709 100644 --- a/sync/asardotnet/asardotnet.csproj +++ b/sync/asardotnet/asardotnet.csproj @@ -9,8 +9,9 @@ Properties asardotnet asardotnet - v4.5 + v4.5.1 512 + true @@ -46,6 +47,7 @@ +