Skip to content

Commit

Permalink
add 'extract-embedded-xvd' command, move commands into Commands/ subf…
Browse files Browse the repository at this point in the history
…older
  • Loading branch information
LukeFZ committed Aug 4, 2024
1 parent 14088ac commit 563a657
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 283 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Commands supported for both local and streamed types:
- `info`
- Lets you view detailed information (headers, regions, segments, files) for a given file.
- `extract`
- Lets you extract and decrypted the embedded files contained within a XVC.
- Lets you decrypt and extract the embedded files contained within a XVC.
*Note: Only supports the newer type of XVC which do not just contain a disk partition. (SegmentMetadata.bin)*
- `extract-embedded-xvd`
- Lets you extract the (encrypted) embedded XVD for Xbox XVCs.

Commands only supported by local files:
- `verify`
Expand Down
65 changes: 65 additions & 0 deletions XvdTool.Streaming/Commands/CryptoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal abstract class CryptoCommand<T> : XvdCommand<T> where T : CryptoCommandSettings
{
protected KeyManager KeyManager = default!;

protected bool Initialize(CryptoCommandSettings settings, out KeyEntry entry)
{
Initialize(settings, requiresWriting: true);

Debug.Assert(XvdFile != null, "XvdFile != null");

entry = default;

KeyManager = new KeyManager();

if (settings.DeviceKey != null)
{
KeyManager.LoadDeviceKey(Convert.FromHexString(settings.DeviceKey));
}

if (settings.CikPath != null)
{
entry = KeyManager.LoadCik(settings.CikPath);
}
else
{
KeyManager.LoadCachedKeys();

var keyId = XvdFile.GetKeyId();
if (keyId != Guid.Empty)
{
if (!KeyManager.TryGetKey(keyId, out entry))
{
ConsoleLogger.WriteErrLine($"Could not find key [bold]{keyId}[/] loaded in key storage.");

return false;
}
}
}

return true;
}

public override ValidationResult Validate(CommandContext context, T settings)
{
var result = base.Validate(context, settings);

if (!result.Successful)
return result;

if (settings.CikPath != null && !File.Exists(settings.CikPath))
return ValidationResult.Error("Provided .cik file does not exist.");

if (settings.DeviceKey != null && (settings.DeviceKey.Length != 32 ||
settings.DeviceKey.All("0123456789ABCDEFabcdef".Contains)))
return ValidationResult.Error("Provided device key is invalid. Must be 32 hex characters long.");

return ValidationResult.Success();
}
}
15 changes: 15 additions & 0 deletions XvdTool.Streaming/Commands/CryptoCommandSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal abstract class CryptoCommandSettings : XvdCommandSettings
{
[Description("Path to the .cik file to be used regardless of the header key ID.")]
[CommandOption("-c|--cik")]
public string? CikPath { get; init; }

[Description("Device key used to decrypt UWP licenses.")]
[CommandOption("-d|--device-key")]
public string? DeviceKey { get; init; }
}
50 changes: 50 additions & 0 deletions XvdTool.Streaming/Commands/DecryptCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.ComponentModel;
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal sealed class DecryptCommand : CryptoCommand<DecryptCommand.Settings>
{
public sealed class Settings : CryptoCommandSettings
{
[Description("Skips recalculating the hashes after decryption.\nSpeeds up the process, but makes subsequent hash checks on the file fail.")]
[CommandOption("-n|--no-hash-calc")]
public bool SkipHashCalculation { get; init; }
}

public override int Execute(CommandContext context, Settings settings)
{
if (!Initialize(settings, out var keyEntry))
{
return -1;
}

using (XvdFile)
{
XvdFile.DecryptData(keyEntry, false);
}

return 0;
}

public override ValidationResult Validate(CommandContext context, Settings settings)
{
base.Validate(context, settings);

Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");

if (!settings.SkipHashCalculation)
return ValidationResult.Error(
"Hash recalculation is not yet supported. Please use the 'extract' command instead, or specify '--no-hash-calc' to skip recomputing the hash table.");

if (settings.XvcPath.StartsWith("http"))
return ValidationResult.Error("Only local files are supported for integrity verification.");

if (!File.Exists(settings.XvcPath))
return ValidationResult.Error("Provided file does not exist.");

return ValidationResult.Success();
}
}
62 changes: 62 additions & 0 deletions XvdTool.Streaming/Commands/ExtractCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.ComponentModel;
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal sealed class ExtractCommand : CryptoCommand<ExtractCommand.Settings>
{
public sealed class Settings : CryptoCommandSettings
{
[DefaultValue("output")]
[Description("Output directory to extract the files into.")]
[CommandOption("-o|--output")]
public string? OutputDirectory { get; init; }

[Description("List of regions to skip downloading. Defaults to none.")]
[CommandOption("-b|--skip-region")]
public uint[]? SkipRegions { get; init; }

[Description("List of regions to download. Defaults to all.")]
[CommandOption("-w|--download-region")]
public uint[]? DownloadRegions { get; init; }

[Description("Skips performing hash verification on the pages prior to decryption.\nMassively improves performance at the cost of integrity.\nOnly use this if you know the file is not corrupt!")]
[CommandOption("-n|--no-hash-check")]
public bool SkipHashCheck { get; init; }
}

public override int Execute(CommandContext context, Settings settings)
{
Debug.Assert(settings.OutputDirectory != null, "settings.OutputDirectory != null");

if (!Initialize(settings, out var keyEntry))
{
return -1;
}

var outputPath = Path.GetFullPath(settings.OutputDirectory);

var hashStatus = settings.SkipHashCheck ? "[red]disabled[/]" : "[green]enabled[/]";

ConsoleLogger.WriteInfoLine($"Extracting files into [green bold]{outputPath}[/]. (Hash check {hashStatus})");

using (XvdFile)
{
XvdFile.ExtractFiles(outputPath, keyEntry, settings.SkipHashCheck, settings.SkipRegions, settings.DownloadRegions);
}

ConsoleLogger.WriteInfoLine("[green bold]Successfully[/] extracted files.");

return 0;
}

public override ValidationResult Validate(CommandContext context, Settings settings)
{
if (settings is { DownloadRegions: not null, SkipRegions: not null })
return ValidationResult.Error("'--skip-region' and '--download-region' cannot be used together.");

return ValidationResult.Success();
}
}
43 changes: 43 additions & 0 deletions XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.ComponentModel;
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal sealed class ExtractEmbeddedXvdCommand : XvdCommand<ExtractEmbeddedXvdCommand.Settings>
{
internal sealed class Settings : XvdCommandSettings
{
[CommandOption("-o|--output")]
[Description("Output path of the embedded XVD.")]
[DefaultValue("embedded.xvd")]
public string EmbeddedXvdOutputPath { get; set; } = null!;
}

public override int Execute(CommandContext context, Settings settings)
{
Initialize(settings, requiresWriting: false);

Debug.Assert(XvdFile != null, "XvdFile != null");

var directory = Path.GetDirectoryName(settings.EmbeddedXvdOutputPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);

using (XvdFile)
{
XvdFile.ExtractEmbeddedXvd(settings.EmbeddedXvdOutputPath);
}

return 0;
}

public override ValidationResult Validate(CommandContext context, Settings settings)
{
if (Directory.Exists(settings.EmbeddedXvdOutputPath))
return ValidationResult.Error("The embedded XVD output path is a directory.");

return base.Validate(context, settings);
}
}
41 changes: 41 additions & 0 deletions XvdTool.Streaming/Commands/InfoCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.ComponentModel;
using System.Diagnostics;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal sealed class InfoCommand : XvdCommand<InfoCommand.Settings>
{
public sealed class Settings : XvdCommandSettings
{
[Description("File path to save the output into.")]
[CommandOption("-o|--output")]
public string? OutputPath { get; init; }

[Description("If all files should be printed.\nIf unset, only the first 4096 files will be printed.")]
[CommandOption("-a|--show-all-files")]
public bool ShowAllFiles { get; set; }
}

public override int Execute(CommandContext context, Settings settings)
{
Initialize(settings, requiresWriting: false);

Debug.Assert(XvdFile != null, "XvdFile != null");

using (XvdFile)
{
var infoOutput = XvdFile.PrintInfo(settings.ShowAllFiles);
if (settings.OutputPath != null)
{
var directory = Path.GetDirectoryName(settings.OutputPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);

File.WriteAllText(settings.OutputPath, infoOutput);
}
}

return 0;
}
}
43 changes: 43 additions & 0 deletions XvdTool.Streaming/Commands/VerifyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal sealed class VerifyCommand : XvdCommand<VerifyCommand.Settings>
{
public sealed class Settings : XvdCommandSettings;

public override int Execute(CommandContext context, Settings settings)
{
Initialize(settings, requiresWriting: false);

Debug.Assert(XvdFile != null, "XvdFile != null");

using (XvdFile)
{
var result = XvdFile.VerifyDataHashes();

ConsoleLogger.WriteInfoLine(result
? "Integrity check [green bold]successful[/]."
: "Integrity check [red bold]failed[/].");
}

return 0;
}

public override ValidationResult Validate(CommandContext context, Settings settings)
{
base.Validate(context, settings);

Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");

if (settings.XvcPath.StartsWith("http"))
return ValidationResult.Error("Only local files are supported for integrity verification.");

if (!File.Exists(settings.XvcPath))
return ValidationResult.Error("Provided file does not exist.");

return ValidationResult.Success();
}
}
31 changes: 31 additions & 0 deletions XvdTool.Streaming/Commands/XvdCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Diagnostics;
using Spectre.Console;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal abstract class XvdCommand<T> : Command<T> where T : XvdCommandSettings
{
protected StreamedXvdFile XvdFile = default!;

protected void Initialize(XvdCommandSettings settings, bool requiresWriting)
{
Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");

var path = settings.XvcPath;

XvdFile = path.StartsWith("http")
? StreamedXvdFile.OpenFromUrl(path)
: StreamedXvdFile.OpenFromFile(path, requiresWriting);

XvdFile.Parse();
}

public override ValidationResult Validate(CommandContext context, T settings)
{
if (settings.XvcPath != null && !settings.XvcPath.StartsWith("http") && !File.Exists(settings.XvcPath))
return ValidationResult.Error("Provided file does not exist.");

return ValidationResult.Success();
}
}
11 changes: 11 additions & 0 deletions XvdTool.Streaming/Commands/XvdCommandSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel;
using Spectre.Console.Cli;

namespace XvdTool.Streaming.Commands;

internal abstract class XvdCommandSettings : CommandSettings
{
[Description("File Path / URL to the XVC.")]
[CommandArgument(0, "<path/url>")]
public string? XvcPath { get; init; }
}
Loading

0 comments on commit 563a657

Please sign in to comment.