Skip to content

Commit 563a657

Browse files
committed
add 'extract-embedded-xvd' command, move commands into Commands/ subfolder
1 parent 14088ac commit 563a657

File tree

12 files changed

+418
-283
lines changed

12 files changed

+418
-283
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ Commands supported for both local and streamed types:
77
- `info`
88
- Lets you view detailed information (headers, regions, segments, files) for a given file.
99
- `extract`
10-
- Lets you extract and decrypted the embedded files contained within a XVC.
10+
- Lets you decrypt and extract the embedded files contained within a XVC.
1111
*Note: Only supports the newer type of XVC which do not just contain a disk partition. (SegmentMetadata.bin)*
12+
- `extract-embedded-xvd`
13+
- Lets you extract the (encrypted) embedded XVD for Xbox XVCs.
1214

1315
Commands only supported by local files:
1416
- `verify`
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System.Diagnostics;
2+
using Spectre.Console;
3+
using Spectre.Console.Cli;
4+
5+
namespace XvdTool.Streaming.Commands;
6+
7+
internal abstract class CryptoCommand<T> : XvdCommand<T> where T : CryptoCommandSettings
8+
{
9+
protected KeyManager KeyManager = default!;
10+
11+
protected bool Initialize(CryptoCommandSettings settings, out KeyEntry entry)
12+
{
13+
Initialize(settings, requiresWriting: true);
14+
15+
Debug.Assert(XvdFile != null, "XvdFile != null");
16+
17+
entry = default;
18+
19+
KeyManager = new KeyManager();
20+
21+
if (settings.DeviceKey != null)
22+
{
23+
KeyManager.LoadDeviceKey(Convert.FromHexString(settings.DeviceKey));
24+
}
25+
26+
if (settings.CikPath != null)
27+
{
28+
entry = KeyManager.LoadCik(settings.CikPath);
29+
}
30+
else
31+
{
32+
KeyManager.LoadCachedKeys();
33+
34+
var keyId = XvdFile.GetKeyId();
35+
if (keyId != Guid.Empty)
36+
{
37+
if (!KeyManager.TryGetKey(keyId, out entry))
38+
{
39+
ConsoleLogger.WriteErrLine($"Could not find key [bold]{keyId}[/] loaded in key storage.");
40+
41+
return false;
42+
}
43+
}
44+
}
45+
46+
return true;
47+
}
48+
49+
public override ValidationResult Validate(CommandContext context, T settings)
50+
{
51+
var result = base.Validate(context, settings);
52+
53+
if (!result.Successful)
54+
return result;
55+
56+
if (settings.CikPath != null && !File.Exists(settings.CikPath))
57+
return ValidationResult.Error("Provided .cik file does not exist.");
58+
59+
if (settings.DeviceKey != null && (settings.DeviceKey.Length != 32 ||
60+
settings.DeviceKey.All("0123456789ABCDEFabcdef".Contains)))
61+
return ValidationResult.Error("Provided device key is invalid. Must be 32 hex characters long.");
62+
63+
return ValidationResult.Success();
64+
}
65+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.ComponentModel;
2+
using Spectre.Console.Cli;
3+
4+
namespace XvdTool.Streaming.Commands;
5+
6+
internal abstract class CryptoCommandSettings : XvdCommandSettings
7+
{
8+
[Description("Path to the .cik file to be used regardless of the header key ID.")]
9+
[CommandOption("-c|--cik")]
10+
public string? CikPath { get; init; }
11+
12+
[Description("Device key used to decrypt UWP licenses.")]
13+
[CommandOption("-d|--device-key")]
14+
public string? DeviceKey { get; init; }
15+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using Spectre.Console;
4+
using Spectre.Console.Cli;
5+
6+
namespace XvdTool.Streaming.Commands;
7+
8+
internal sealed class DecryptCommand : CryptoCommand<DecryptCommand.Settings>
9+
{
10+
public sealed class Settings : CryptoCommandSettings
11+
{
12+
[Description("Skips recalculating the hashes after decryption.\nSpeeds up the process, but makes subsequent hash checks on the file fail.")]
13+
[CommandOption("-n|--no-hash-calc")]
14+
public bool SkipHashCalculation { get; init; }
15+
}
16+
17+
public override int Execute(CommandContext context, Settings settings)
18+
{
19+
if (!Initialize(settings, out var keyEntry))
20+
{
21+
return -1;
22+
}
23+
24+
using (XvdFile)
25+
{
26+
XvdFile.DecryptData(keyEntry, false);
27+
}
28+
29+
return 0;
30+
}
31+
32+
public override ValidationResult Validate(CommandContext context, Settings settings)
33+
{
34+
base.Validate(context, settings);
35+
36+
Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");
37+
38+
if (!settings.SkipHashCalculation)
39+
return ValidationResult.Error(
40+
"Hash recalculation is not yet supported. Please use the 'extract' command instead, or specify '--no-hash-calc' to skip recomputing the hash table.");
41+
42+
if (settings.XvcPath.StartsWith("http"))
43+
return ValidationResult.Error("Only local files are supported for integrity verification.");
44+
45+
if (!File.Exists(settings.XvcPath))
46+
return ValidationResult.Error("Provided file does not exist.");
47+
48+
return ValidationResult.Success();
49+
}
50+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using Spectre.Console;
4+
using Spectre.Console.Cli;
5+
6+
namespace XvdTool.Streaming.Commands;
7+
8+
internal sealed class ExtractCommand : CryptoCommand<ExtractCommand.Settings>
9+
{
10+
public sealed class Settings : CryptoCommandSettings
11+
{
12+
[DefaultValue("output")]
13+
[Description("Output directory to extract the files into.")]
14+
[CommandOption("-o|--output")]
15+
public string? OutputDirectory { get; init; }
16+
17+
[Description("List of regions to skip downloading. Defaults to none.")]
18+
[CommandOption("-b|--skip-region")]
19+
public uint[]? SkipRegions { get; init; }
20+
21+
[Description("List of regions to download. Defaults to all.")]
22+
[CommandOption("-w|--download-region")]
23+
public uint[]? DownloadRegions { get; init; }
24+
25+
[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!")]
26+
[CommandOption("-n|--no-hash-check")]
27+
public bool SkipHashCheck { get; init; }
28+
}
29+
30+
public override int Execute(CommandContext context, Settings settings)
31+
{
32+
Debug.Assert(settings.OutputDirectory != null, "settings.OutputDirectory != null");
33+
34+
if (!Initialize(settings, out var keyEntry))
35+
{
36+
return -1;
37+
}
38+
39+
var outputPath = Path.GetFullPath(settings.OutputDirectory);
40+
41+
var hashStatus = settings.SkipHashCheck ? "[red]disabled[/]" : "[green]enabled[/]";
42+
43+
ConsoleLogger.WriteInfoLine($"Extracting files into [green bold]{outputPath}[/]. (Hash check {hashStatus})");
44+
45+
using (XvdFile)
46+
{
47+
XvdFile.ExtractFiles(outputPath, keyEntry, settings.SkipHashCheck, settings.SkipRegions, settings.DownloadRegions);
48+
}
49+
50+
ConsoleLogger.WriteInfoLine("[green bold]Successfully[/] extracted files.");
51+
52+
return 0;
53+
}
54+
55+
public override ValidationResult Validate(CommandContext context, Settings settings)
56+
{
57+
if (settings is { DownloadRegions: not null, SkipRegions: not null })
58+
return ValidationResult.Error("'--skip-region' and '--download-region' cannot be used together.");
59+
60+
return ValidationResult.Success();
61+
}
62+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using Spectre.Console;
4+
using Spectre.Console.Cli;
5+
6+
namespace XvdTool.Streaming.Commands;
7+
8+
internal sealed class ExtractEmbeddedXvdCommand : XvdCommand<ExtractEmbeddedXvdCommand.Settings>
9+
{
10+
internal sealed class Settings : XvdCommandSettings
11+
{
12+
[CommandOption("-o|--output")]
13+
[Description("Output path of the embedded XVD.")]
14+
[DefaultValue("embedded.xvd")]
15+
public string EmbeddedXvdOutputPath { get; set; } = null!;
16+
}
17+
18+
public override int Execute(CommandContext context, Settings settings)
19+
{
20+
Initialize(settings, requiresWriting: false);
21+
22+
Debug.Assert(XvdFile != null, "XvdFile != null");
23+
24+
var directory = Path.GetDirectoryName(settings.EmbeddedXvdOutputPath);
25+
if (!string.IsNullOrEmpty(directory))
26+
Directory.CreateDirectory(directory);
27+
28+
using (XvdFile)
29+
{
30+
XvdFile.ExtractEmbeddedXvd(settings.EmbeddedXvdOutputPath);
31+
}
32+
33+
return 0;
34+
}
35+
36+
public override ValidationResult Validate(CommandContext context, Settings settings)
37+
{
38+
if (Directory.Exists(settings.EmbeddedXvdOutputPath))
39+
return ValidationResult.Error("The embedded XVD output path is a directory.");
40+
41+
return base.Validate(context, settings);
42+
}
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics;
3+
using Spectre.Console.Cli;
4+
5+
namespace XvdTool.Streaming.Commands;
6+
7+
internal sealed class InfoCommand : XvdCommand<InfoCommand.Settings>
8+
{
9+
public sealed class Settings : XvdCommandSettings
10+
{
11+
[Description("File path to save the output into.")]
12+
[CommandOption("-o|--output")]
13+
public string? OutputPath { get; init; }
14+
15+
[Description("If all files should be printed.\nIf unset, only the first 4096 files will be printed.")]
16+
[CommandOption("-a|--show-all-files")]
17+
public bool ShowAllFiles { get; set; }
18+
}
19+
20+
public override int Execute(CommandContext context, Settings settings)
21+
{
22+
Initialize(settings, requiresWriting: false);
23+
24+
Debug.Assert(XvdFile != null, "XvdFile != null");
25+
26+
using (XvdFile)
27+
{
28+
var infoOutput = XvdFile.PrintInfo(settings.ShowAllFiles);
29+
if (settings.OutputPath != null)
30+
{
31+
var directory = Path.GetDirectoryName(settings.OutputPath);
32+
if (!string.IsNullOrEmpty(directory))
33+
Directory.CreateDirectory(directory);
34+
35+
File.WriteAllText(settings.OutputPath, infoOutput);
36+
}
37+
}
38+
39+
return 0;
40+
}
41+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Diagnostics;
2+
using Spectre.Console;
3+
using Spectre.Console.Cli;
4+
5+
namespace XvdTool.Streaming.Commands;
6+
7+
internal sealed class VerifyCommand : XvdCommand<VerifyCommand.Settings>
8+
{
9+
public sealed class Settings : XvdCommandSettings;
10+
11+
public override int Execute(CommandContext context, Settings settings)
12+
{
13+
Initialize(settings, requiresWriting: false);
14+
15+
Debug.Assert(XvdFile != null, "XvdFile != null");
16+
17+
using (XvdFile)
18+
{
19+
var result = XvdFile.VerifyDataHashes();
20+
21+
ConsoleLogger.WriteInfoLine(result
22+
? "Integrity check [green bold]successful[/]."
23+
: "Integrity check [red bold]failed[/].");
24+
}
25+
26+
return 0;
27+
}
28+
29+
public override ValidationResult Validate(CommandContext context, Settings settings)
30+
{
31+
base.Validate(context, settings);
32+
33+
Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");
34+
35+
if (settings.XvcPath.StartsWith("http"))
36+
return ValidationResult.Error("Only local files are supported for integrity verification.");
37+
38+
if (!File.Exists(settings.XvcPath))
39+
return ValidationResult.Error("Provided file does not exist.");
40+
41+
return ValidationResult.Success();
42+
}
43+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Diagnostics;
2+
using Spectre.Console;
3+
using Spectre.Console.Cli;
4+
5+
namespace XvdTool.Streaming.Commands;
6+
7+
internal abstract class XvdCommand<T> : Command<T> where T : XvdCommandSettings
8+
{
9+
protected StreamedXvdFile XvdFile = default!;
10+
11+
protected void Initialize(XvdCommandSettings settings, bool requiresWriting)
12+
{
13+
Debug.Assert(settings.XvcPath != null, "settings.XvcPath != null");
14+
15+
var path = settings.XvcPath;
16+
17+
XvdFile = path.StartsWith("http")
18+
? StreamedXvdFile.OpenFromUrl(path)
19+
: StreamedXvdFile.OpenFromFile(path, requiresWriting);
20+
21+
XvdFile.Parse();
22+
}
23+
24+
public override ValidationResult Validate(CommandContext context, T settings)
25+
{
26+
if (settings.XvcPath != null && !settings.XvcPath.StartsWith("http") && !File.Exists(settings.XvcPath))
27+
return ValidationResult.Error("Provided file does not exist.");
28+
29+
return ValidationResult.Success();
30+
}
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.ComponentModel;
2+
using Spectre.Console.Cli;
3+
4+
namespace XvdTool.Streaming.Commands;
5+
6+
internal abstract class XvdCommandSettings : CommandSettings
7+
{
8+
[Description("File Path / URL to the XVC.")]
9+
[CommandArgument(0, "<path/url>")]
10+
public string? XvcPath { get; init; }
11+
}

0 commit comments

Comments
 (0)