|
@@ -0,0 +1,284 @@
|
|
|
|
+using System;
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
+using System.IO;
|
|
|
|
+using System.IO.Abstractions;
|
|
|
|
+using System.Linq;
|
|
|
|
+using System.Text;
|
|
|
|
+using CommandLine;
|
|
|
|
+using COM3D2.Toolkit.Arc;
|
|
|
|
+using Ganss.IO;
|
|
|
|
+
|
|
|
|
+namespace ArcToolkitCLI
|
|
|
|
+{
|
|
|
|
+ internal interface IOptions
|
|
|
|
+ {
|
|
|
|
+ [Value(0, MetaName = "input", HelpText = "Input ARC files")]
|
|
|
|
+ IEnumerable<string> Input { get; set; }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal interface IDecryptionOptions
|
|
|
|
+ {
|
|
|
|
+ [Option("arc-search-dir", Required = false, HelpText = "Directory where to search ARC file to decrypt the WARP file")]
|
|
|
|
+ string ArcDirectory { get; set; }
|
|
|
|
+
|
|
|
|
+ [Option('k', "key", HelpText = "Decryption key as a base64 string (applied to all inputs)", Required = false)]
|
|
|
|
+ string DecryptionKey { get; set; }
|
|
|
|
+
|
|
|
|
+ [Option("key-file",
|
|
|
|
+ HelpText = "A file with decryption keys on each line. Format of the file is <decryption arc name>:<key in base64>.",
|
|
|
|
+ Required = false)]
|
|
|
|
+ string KeyFile { get; set; }
|
|
|
|
+
|
|
|
|
+ [Option("warc", Required = false, HelpText = "WARC file to use for decryption")]
|
|
|
|
+ string WarcFile { get; set; }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ [Verb("extract", HelpText = "Extract the contents of the given ARC files")]
|
|
|
|
+ internal class ExtractOptions : IOptions, IDecryptionOptions
|
|
|
|
+ {
|
|
|
|
+ [Option('o', "output", HelpText = "Output directory", Required = true)]
|
|
|
|
+ public string Output { get; set; }
|
|
|
|
+
|
|
|
|
+ public string ArcDirectory { get; set; }
|
|
|
|
+ public string DecryptionKey { get; set; }
|
|
|
|
+ public string KeyFile { get; set; }
|
|
|
|
+
|
|
|
|
+ public string WarcFile { get; set; }
|
|
|
|
+ public IEnumerable<string> Input { get; set; }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ [Verb("info", HelpText = "Display information about the given ARC files")]
|
|
|
|
+ internal class InfoOptions : IOptions
|
|
|
|
+ {
|
|
|
|
+ [Option("only-key",
|
|
|
|
+ HelpText = "If the archive is a WARC file, output only the decryption key as a base64 string",
|
|
|
|
+ Required = false)]
|
|
|
|
+ public bool OnlyKey { get; set; }
|
|
|
|
+
|
|
|
|
+ public IEnumerable<string> Input { get; set; }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ [Verb("decrypt", HelpText = "Decrypts the provided WARP files")]
|
|
|
|
+ internal class DecryptOptions : IOptions, IDecryptionOptions
|
|
|
|
+ {
|
|
|
|
+ [Option('o', "output", HelpText = "Output directory", Default = ".")]
|
|
|
|
+ public string Output { get; set; }
|
|
|
|
+
|
|
|
|
+ public string ArcDirectory { get; set; }
|
|
|
|
+ public string DecryptionKey { get; set; }
|
|
|
|
+ public string KeyFile { get; set; }
|
|
|
|
+ public string WarcFile { get; set; }
|
|
|
|
+ public IEnumerable<string> Input { get; set; }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal static class Errors
|
|
|
|
+ {
|
|
|
|
+ public enum ErrorCodes
|
|
|
|
+ {
|
|
|
|
+ NotAFile = 100,
|
|
|
|
+ UnknownArcType,
|
|
|
|
+ KeyNotFound,
|
|
|
|
+ InvalidKeyFile,
|
|
|
|
+ NoCorrectKey
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public static readonly string[] ErrorMessages =
|
|
|
|
+ {
|
|
|
|
+ "{0} is not a file",
|
|
|
|
+ "{0} is not a known ARC file",
|
|
|
|
+ "No key specified or the key file does not exist",
|
|
|
|
+ "The provided keyfile is not valid",
|
|
|
|
+ "The ARC {0} needs a key from {1}"
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ public static int Error(ErrorCodes code, params object[] args)
|
|
|
|
+ {
|
|
|
|
+ Console.Error.WriteLine(ErrorMessages[code - ErrorCodes.NotAFile], args);
|
|
|
|
+ return (int) code;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal static class Util
|
|
|
|
+ {
|
|
|
|
+ private static readonly char[] GlobCharacters = {'*', '{', '}', '[', ']', '?'};
|
|
|
|
+
|
|
|
|
+ public static IEnumerable<FileSystemInfoBase> EnumerateFiles(IEnumerable<string> patterns)
|
|
|
|
+ {
|
|
|
|
+ foreach (string pattern in patterns)
|
|
|
|
+ if (IsGlob(pattern))
|
|
|
|
+ foreach (var fileSystemInfoBase in Glob.Expand(pattern))
|
|
|
|
+ yield return fileSystemInfoBase;
|
|
|
|
+ else
|
|
|
|
+ yield return new FileInfoWrapper(new FileSystem(), new FileInfo(pattern));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static bool IsGlob(string path)
|
|
|
|
+ {
|
|
|
|
+ return path.IndexOfAny(GlobCharacters) >= 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ internal class Program
|
|
|
|
+ {
|
|
|
|
+ private static int Main(string[] args)
|
|
|
|
+ {
|
|
|
|
+ return Parser.Default.ParseArguments<InfoOptions, ExtractOptions, DecryptOptions>(args)
|
|
|
|
+ .MapResult((InfoOptions opts) => DisplayInfo(opts),
|
|
|
|
+ (ExtractOptions opts) => Extract(opts),
|
|
|
|
+ (DecryptOptions opts) => Decrypt(opts),
|
|
|
|
+ errs => 1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static int DisplayInfo(InfoOptions opts)
|
|
|
|
+ {
|
|
|
|
+ bool first = true;
|
|
|
|
+ foreach (var file in Util.EnumerateFiles(opts.Input))
|
|
|
|
+ {
|
|
|
|
+ if (!file.Exists)
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
|
|
|
|
+
|
|
|
|
+ if (!first && !opts.OnlyKey)
|
|
|
|
+ Console.WriteLine();
|
|
|
|
+ first = false;
|
|
|
|
+
|
|
|
|
+ using (var stream = File.OpenRead(file.FullName))
|
|
|
|
+ {
|
|
|
|
+ var header = new byte[4];
|
|
|
|
+ stream.Read(header, 0, header.Length);
|
|
|
|
+ string headerString = Encoding.ASCII.GetString(header);
|
|
|
|
+ stream.Position = 0;
|
|
|
|
+
|
|
|
|
+ if (headerString == "warc")
|
|
|
|
+ {
|
|
|
|
+ var key = new byte[2048];
|
|
|
|
+ stream.Read(key, 0, key.Length);
|
|
|
|
+ string keyBase64 = Convert.ToBase64String(key);
|
|
|
|
+ if (opts.OnlyKey)
|
|
|
|
+ {
|
|
|
|
+ Console.WriteLine($"{Path.GetFileName(file.FullName)}:{keyBase64}");
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ stream.Position = 0;
|
|
|
|
+ using (var warc = new WarcArc(stream))
|
|
|
|
+ {
|
|
|
|
+ Console.WriteLine($"File name: {file.Name}");
|
|
|
|
+ Console.WriteLine("ARC type: WARC");
|
|
|
|
+ Console.WriteLine($"ARC Name: {warc.Name}");
|
|
|
|
+ Console.WriteLine($"File count: {warc.Entries.Count()}");
|
|
|
|
+ Console.WriteLine($"Decryption key: {keyBase64}");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if (headerString == "warp")
|
|
|
|
+ {
|
|
|
|
+ if (opts.OnlyKey)
|
|
|
|
+ continue;
|
|
|
|
+
|
|
|
|
+ Console.WriteLine($"File name: {file.Name}");
|
|
|
|
+ Console.WriteLine("ARC type: WARP");
|
|
|
|
+ using (var br = new BinaryReader(stream))
|
|
|
|
+ Console.WriteLine($"Needs a decryption key from the following ARC: {WarpArc.GetKeyWarpName(br)}");
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static int Extract(ExtractOptions opts)
|
|
|
|
+ {
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static byte[] ReadKeyFromFile(string filename)
|
|
|
|
+ {
|
|
|
|
+ using (var br = new BinaryReader(File.OpenRead(filename)))
|
|
|
|
+ return br.ReadBytes(2048);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private static int Decrypt(DecryptOptions opts)
|
|
|
|
+ {
|
|
|
|
+ var keysDict = new Dictionary<string, byte[]>(StringComparer.InvariantCultureIgnoreCase);
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(opts.KeyFile))
|
|
|
|
+ {
|
|
|
|
+ if (!File.Exists(opts.KeyFile))
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.KeyNotFound);
|
|
|
|
+
|
|
|
|
+ foreach (string line in File.ReadAllLines(opts.KeyFile))
|
|
|
|
+ {
|
|
|
|
+ var parts = line.Split(':');
|
|
|
|
+ if (parts.Length != 2)
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.InvalidKeyFile);
|
|
|
|
+ keysDict[parts[0].Trim()] = Convert.FromBase64String(parts[1].Trim());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if (!string.IsNullOrWhiteSpace(opts.DecryptionKey))
|
|
|
|
+ {
|
|
|
|
+ keysDict["*"] = Convert.FromBase64String(opts.DecryptionKey);
|
|
|
|
+ }
|
|
|
|
+ else if (string.IsNullOrWhiteSpace(opts.ArcDirectory) && string.IsNullOrWhiteSpace(opts.WarcFile))
|
|
|
|
+ {
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.KeyNotFound);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Directory.CreateDirectory(opts.Output);
|
|
|
|
+
|
|
|
|
+ foreach (var file in Util.EnumerateFiles(opts.Input))
|
|
|
|
+ {
|
|
|
|
+ if (!file.Exists)
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.NotAFile, file.Name);
|
|
|
|
+
|
|
|
|
+ using (var stream = File.OpenRead(file.FullName))
|
|
|
|
+ {
|
|
|
|
+ var header = new byte[4];
|
|
|
|
+ stream.Read(header, 0, header.Length);
|
|
|
|
+ string headerString = Encoding.ASCII.GetString(header);
|
|
|
|
+ stream.Position = 0;
|
|
|
|
+ if (headerString == "warc")
|
|
|
|
+ {
|
|
|
|
+ Console.WriteLine($"{file.Name} is a WARC file, skipping...");
|
|
|
|
+ }
|
|
|
|
+ else if (headerString == "warp")
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ var arcStream = WarpArc.DecryptWarp(stream,
|
|
|
|
+ requestedFile =>
|
|
|
|
+ {
|
|
|
|
+ if (keysDict.TryGetValue("*", out var key))
|
|
|
|
+ return key;
|
|
|
|
+ if (keysDict.TryGetValue(requestedFile, out key))
|
|
|
|
+ return key;
|
|
|
|
+ if (Directory.Exists(opts.ArcDirectory)
|
|
|
|
+ && File.Exists(Path.Combine(opts.ArcDirectory, requestedFile)))
|
|
|
|
+ return ReadKeyFromFile(Path.Combine(opts.ArcDirectory, requestedFile));
|
|
|
|
+ if (File.Exists(opts.WarcFile))
|
|
|
|
+ return ReadKeyFromFile(opts.WarcFile);
|
|
|
|
+ throw new FileNotFoundException("No key found for the requested ARC",
|
|
|
|
+ requestedFile);
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ using (var output =
|
|
|
|
+ File.Create(Path.Combine(Path.GetDirectoryName(opts.Output), Path.GetFileNameWithoutExtension(file.Name))))
|
|
|
|
+ arcStream.CopyTo(output);
|
|
|
|
+ }
|
|
|
|
+ catch (FileNotFoundException fe)
|
|
|
|
+ {
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.NoCorrectKey, file.Name, fe.FileName);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ return Errors.Error(Errors.ErrorCodes.UnknownArcType, file.Name);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|