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 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 :.", 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 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 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 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 EnumerateFiles(IEnumerable 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(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(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; } } }