From 6ee59cf305b75d139b2f19d8ac89cfc635e3de93 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Thu, 7 May 2026 21:00:22 +0200 Subject: [PATCH 01/11] bump to .net 10 + fix typo --- .github/workflows/build.yml | 2 +- LibXboxOne.Tests/LibXboxOne.Tests.csproj | 2 +- LibXboxOne/LibXboxOne.csproj | 3 ++- XvdTool.Streaming/StreamedXvdFile.cs | 2 +- XvdTool.Streaming/XvdTool.Streaming.csproj | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56dcd4e..9523c70 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: [ '9.0.x' ] + dotnet-version: [ '10.0.x' ] rid: ['win-x64', 'win-x86', 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64'] steps: diff --git a/LibXboxOne.Tests/LibXboxOne.Tests.csproj b/LibXboxOne.Tests/LibXboxOne.Tests.csproj index d0d252a..a8d41aa 100644 --- a/LibXboxOne.Tests/LibXboxOne.Tests.csproj +++ b/LibXboxOne.Tests/LibXboxOne.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 true false false diff --git a/LibXboxOne/LibXboxOne.csproj b/LibXboxOne/LibXboxOne.csproj index 7a7903f..f664556 100644 --- a/LibXboxOne/LibXboxOne.csproj +++ b/LibXboxOne/LibXboxOne.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 @@ -10,6 +10,7 @@ + diff --git a/XvdTool.Streaming/StreamedXvdFile.cs b/XvdTool.Streaming/StreamedXvdFile.cs index 36bbb17..a3a3075 100644 --- a/XvdTool.Streaming/StreamedXvdFile.cs +++ b/XvdTool.Streaming/StreamedXvdFile.cs @@ -277,7 +277,7 @@ private void ParseNtfsPartition() if (partitionTable == null) { - ConsoleLogger.WriteErrLine("Failed to drive contents as either GPT or MBR."); + ConsoleLogger.WriteErrLine("Failed to parse drive contents as either GPT or MBR."); return; } diff --git a/XvdTool.Streaming/XvdTool.Streaming.csproj b/XvdTool.Streaming/XvdTool.Streaming.csproj index 3c710d5..b998392 100644 --- a/XvdTool.Streaming/XvdTool.Streaming.csproj +++ b/XvdTool.Streaming/XvdTool.Streaming.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable preview From 0b1f53e0fd1487557f37f23ab8282a4ca8eda124 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 01:07:36 +0200 Subject: [PATCH 02/11] initial implementation of XboxPackage.cbor parsing --- LibXboxOne/XVC2/BoxIndex.cs | 6 + LibXboxOne/XVC2/CborExtensions.cs | 125 +++++++++++ LibXboxOne/XVC2/CborTagEx.cs | 28 +++ LibXboxOne/XVC2/PackagingCompression.cs | 7 + .../XVC2/PackagingDerivationAlgorithm.cs | 7 + .../XVC2/PackagingEncryptionAlgorithm.cs | 9 + LibXboxOne/XVC2/PackagingHash.cs | 8 + LibXboxOne/XVC2/PackagingHashAlgorithm.cs | 9 + LibXboxOne/XVC2/PackagingIV.cs | 52 +++++ LibXboxOne/XVC2/README.md | 1 + LibXboxOne/XVC2/SegmentationAlgorithm.cs | 8 + .../XVC2/SerializedModel/BoxReference.cs | 41 ++++ LibXboxOne/XVC2/SerializedModel/Chunk.cs | 204 ++++++++++++++++++ LibXboxOne/XVC2/SerializedModel/FileFormat.cs | 86 ++++++++ .../XVC2/SerializedModel/IRootSerialize.cs | 7 + LibXboxOne/XVC2/SerializedModel/ISerialize.cs | 8 + LibXboxOne/XVC2/SerializedModel/Package.cs | 191 ++++++++++++++++ LibXboxOne/XVC2/SerializedModel/PackageKey.cs | 30 +++ .../XVC2/SerializedModel/PackageKeySource.cs | 106 +++++++++ .../XVC2/SerializedModel/SegmentReference.cs | 169 +++++++++++++++ .../XVC2/SerializedModel/Segmentation.cs | 60 ++++++ .../SerializedModel/SerializedAlgorithm.cs | 16 ++ .../SerializedModel/SerializedKeyPurpose.cs | 8 + .../XVC2/SerializedModel/SerializedLabel.cs | 69 ++++++ .../SerializedModel/SerializedPlatform.cs | 9 + LibXboxOne/XVC2/SerializedModel/Version.cs | 80 +++++++ .../XVC2/Specifiers/IPackagingSpecifier.cs | 3 + .../XVC2/Specifiers/LogicalSpecifierType.cs | 7 + .../Specifiers/PackagingLogicalSpecifier.cs | 5 + .../Specifiers/PackagingSpecifierValue.cs | 3 + 30 files changed, 1362 insertions(+) create mode 100644 LibXboxOne/XVC2/BoxIndex.cs create mode 100644 LibXboxOne/XVC2/CborExtensions.cs create mode 100644 LibXboxOne/XVC2/CborTagEx.cs create mode 100644 LibXboxOne/XVC2/PackagingCompression.cs create mode 100644 LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs create mode 100644 LibXboxOne/XVC2/PackagingEncryptionAlgorithm.cs create mode 100644 LibXboxOne/XVC2/PackagingHash.cs create mode 100644 LibXboxOne/XVC2/PackagingHashAlgorithm.cs create mode 100644 LibXboxOne/XVC2/PackagingIV.cs create mode 100644 LibXboxOne/XVC2/README.md create mode 100644 LibXboxOne/XVC2/SegmentationAlgorithm.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/BoxReference.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/Chunk.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/FileFormat.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/IRootSerialize.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/ISerialize.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/Package.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/PackageKey.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/SegmentReference.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/Segmentation.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/SerializedAlgorithm.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/SerializedKeyPurpose.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/SerializedLabel.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/Version.cs create mode 100644 LibXboxOne/XVC2/Specifiers/IPackagingSpecifier.cs create mode 100644 LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs create mode 100644 LibXboxOne/XVC2/Specifiers/PackagingLogicalSpecifier.cs create mode 100644 LibXboxOne/XVC2/Specifiers/PackagingSpecifierValue.cs diff --git a/LibXboxOne/XVC2/BoxIndex.cs b/LibXboxOne/XVC2/BoxIndex.cs new file mode 100644 index 0000000..a2108bb --- /dev/null +++ b/LibXboxOne/XVC2/BoxIndex.cs @@ -0,0 +1,6 @@ +namespace LibXboxOne.XVC2; + +public readonly record struct BoxIndex(int Value) +{ + public override string ToString() => $"box:{Value}"; +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/CborExtensions.cs b/LibXboxOne/XVC2/CborExtensions.cs new file mode 100644 index 0000000..b6c9de3 --- /dev/null +++ b/LibXboxOne/XVC2/CborExtensions.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; +using System.IO; + +namespace LibXboxOne.XVC2; + +public static class CborExtensions +{ + extension(CborWriter writer) + { + public void WriteSelfDescribeTag(CborTagEx tag) + { + writer.WriteTag(CborTag.SelfDescribeCbor); + writer.WriteTag((CborTag)tag); + } + + public void WriteHash(PackagingHash hash) + { + var tag = hash.Algorithm switch + { + PackagingHashAlgorithm.SHA256 => 0x486C, + PackagingHashAlgorithm.SHA384 => 0x4851, + PackagingHashAlgorithm.SHA512 => 0x4850, + _ => throw new UnreachableException() + }; + + writer.WriteTag((CborTag)tag); + writer.WriteByteString(hash.Hash); + } + + public void WriteMap(Dictionary map) + { + writer.WriteStartMap(map.Count); + + foreach (var (key, value) in map) + { + writer.WriteInt32((int)(object)key); + + switch (value) + { + case string str: + writer.WriteTextString(str); + break; + case byte[] bytes: + writer.WriteByteString(bytes); + break; + case int i32: + writer.WriteInt32(i32); + break; + case long i64: + writer.WriteInt64(i64); + break; + case uint u32: + writer.WriteUInt32(u32); + break; + case ulong u64: + writer.WriteUInt64(u64); + break; + case bool boolean: + writer.WriteBoolean(boolean); + break; + default: + Debug.Assert(false); + break; + } + } + + writer.WriteEndMap(); + } + } + + extension(CborReader reader) + { + public void ReadSelfDescribeTag(CborTagEx tag) + { + var tag0 = reader.ReadTag(); + if (tag0 != CborTag.SelfDescribeCbor) + throw new InvalidDataException(); + + var tag1 = reader.ReadTag(); + if ((CborTagEx)tag1 != tag) + throw new InvalidDataException(); + } + + public PackagingHash ReadHash() + { + var tagType = reader.ReadTag(); + var type = (int)tagType switch + { + 0x486C => PackagingHashAlgorithm.SHA256, + 0x4851 => PackagingHashAlgorithm.SHA384, + 0x4850 => PackagingHashAlgorithm.SHA512, + _ => throw new UnreachableException() + }; + + var hash = reader.ReadByteString(); + return new PackagingHash(type, hash); + } + + public Dictionary ReadMap() + { + var count = reader.ReadStartMap(); + + var dict = new Dictionary(); + while (count-- != 0) + { + var key = (TKey)(object)reader.ReadInt32(); + dict[key] = reader.PeekState() switch + { + CborReaderState.TextString => reader.ReadTextString(), + CborReaderState.ByteString => reader.ReadByteString(), + CborReaderState.UnsignedInteger => reader.ReadUInt64(), + CborReaderState.NegativeInteger => reader.ReadInt64(), + CborReaderState.Boolean => reader.ReadBoolean(), + _ => throw new UnreachableException() + }; + } + + reader.ReadEndMap(); + + return dict; + } + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/CborTagEx.cs b/LibXboxOne/XVC2/CborTagEx.cs new file mode 100644 index 0000000..8ecd4ae --- /dev/null +++ b/LibXboxOne/XVC2/CborTagEx.cs @@ -0,0 +1,28 @@ +namespace LibXboxOne.XVC2; + +public enum CborTagEx +{ + SHA512 = 18512, + SHA384 = 18513, + SHA256 = 18540, + Enum0 = 121, + Enum1 = 122, + Enum2 = 123, + Enum3 = 124, + Enum4 = 125, + Enum5 = 126, + Enum6 = 127, + LogicalNone = 32870, + LogicalAny = 32871, + LogicalAll = 32872, + XVC2 = 1482048306, + XVCB = 1482048322, + XVCC = 1482048323, + XVCD = 1482048324, + XVCE = 1482048325, + XVCF = 1482048326, + XVCI = 1482048329, + XVCP = 1482048336, + XVCS = 1482048339, + XVCZ = 1482048346, +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingCompression.cs b/LibXboxOne/XVC2/PackagingCompression.cs new file mode 100644 index 0000000..6c232f8 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingCompression.cs @@ -0,0 +1,7 @@ +namespace LibXboxOne.XVC2; + +public enum PackagingCompression +{ + None = 0, + Brotli = 1 // TODO: maybe +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs b/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs new file mode 100644 index 0000000..3e35b26 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs @@ -0,0 +1,7 @@ +namespace LibXboxOne.XVC2; + +public enum PackagingDerivationAlgorithm +{ + None = 0, + SP800_100_HMAC_SHA256 = 1 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingEncryptionAlgorithm.cs b/LibXboxOne/XVC2/PackagingEncryptionAlgorithm.cs new file mode 100644 index 0000000..48dbad8 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingEncryptionAlgorithm.cs @@ -0,0 +1,9 @@ +namespace LibXboxOne.XVC2; + +public enum PackagingEncryptionAlgorithm +{ + None = 0, + Automatic = 1, + AES_256_CBC = 2, + AES_256_KW = 3, +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingHash.cs b/LibXboxOne/XVC2/PackagingHash.cs new file mode 100644 index 0000000..ae93856 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingHash.cs @@ -0,0 +1,8 @@ +using System.Buffers.Text; + +namespace LibXboxOne.XVC2; + +public readonly record struct PackagingHash(PackagingHashAlgorithm Algorithm, byte[] Hash) +{ + public override string ToString() => $"{Algorithm.ToString().ToLower()}:{Base64Url.EncodeToString(Hash)}"; +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingHashAlgorithm.cs b/LibXboxOne/XVC2/PackagingHashAlgorithm.cs new file mode 100644 index 0000000..41b997a --- /dev/null +++ b/LibXboxOne/XVC2/PackagingHashAlgorithm.cs @@ -0,0 +1,9 @@ +namespace LibXboxOne.XVC2; + +public enum PackagingHashAlgorithm +{ + None = 0, + SHA256 = 1, + SHA384 = 2, + SHA512 = 3 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingIV.cs b/LibXboxOne/XVC2/PackagingIV.cs new file mode 100644 index 0000000..e5b714c --- /dev/null +++ b/LibXboxOne/XVC2/PackagingIV.cs @@ -0,0 +1,52 @@ +using Org.BouncyCastle.Utilities; +using System; +using System.Buffers.Binary; +using System.Buffers.Text; + +namespace LibXboxOne.XVC2; + +public struct PackagingIV +{ + private ulong _counter0; + private ulong _counter1; + + public PackagingIV Increment() + { + var copy = new PackagingIV + { + _counter0 = _counter0, + _counter1 = _counter1 + }; + + var prev = copy._counter0++; + if (prev + 1 != copy._counter0) + copy._counter1++; + + return copy; + } + + public byte[] ToArray() + { + var array = new byte[16]; + BinaryPrimitives.WriteUInt64BigEndian(array, _counter0); + BinaryPrimitives.WriteUInt64BigEndian(array.AsSpan(8), _counter1); + return array; + } + + public override string ToString() => Base64Url.EncodeToString(ToArray()); + + public static PackagingIV FromBase64UrlString(string value) + { + var bytes = Base64Url.DecodeFromChars(value); + return FromBytes(bytes); + } + + public static PackagingIV FromBytes(byte[] value) + { + return new PackagingIV + { + _counter0 = BinaryPrimitives.ReadUInt64BigEndian(value), + _counter1 = BinaryPrimitives.ReadUInt64BigEndian(value.AsSpan(8)) + }; + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/README.md b/LibXboxOne/XVC2/README.md new file mode 100644 index 0000000..c20626a --- /dev/null +++ b/LibXboxOne/XVC2/README.md @@ -0,0 +1 @@ +This directory contains information about the new MSIXVC2 format, introduced in the April 2026 GDK. \ No newline at end of file diff --git a/LibXboxOne/XVC2/SegmentationAlgorithm.cs b/LibXboxOne/XVC2/SegmentationAlgorithm.cs new file mode 100644 index 0000000..23a2186 --- /dev/null +++ b/LibXboxOne/XVC2/SegmentationAlgorithm.cs @@ -0,0 +1,8 @@ +namespace LibXboxOne.XVC2; + +public enum SegmentationAlgorithm +{ + None = 0, + FastCDC = 1, + Fixed = 2 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/BoxReference.cs b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs new file mode 100644 index 0000000..e17d16b --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs @@ -0,0 +1,41 @@ +#nullable enable +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record BoxReference(string Name) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(1); + writer.WriteInt32(25); + writer.WriteTextString(Name); + writer.WriteEndMap(); + } + + public static BoxReference Deserialize(CborReader reader) + { + string? name = null; + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 25: + name = reader.ReadTextString(); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + Debug.Assert(name != null); + return new BoxReference(name); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs new file mode 100644 index 0000000..0647374 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -0,0 +1,204 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; +using LibXboxOne.XVC2.Specifiers; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record Chunk( + IPackagingSpecifier? Specifier0, + IPackagingSpecifier? Specifier1, + IPackagingSpecifier? Specifier2, + int Value0, + long Value1, + bool? Unknown0, + bool? Unknown1, + int Unknown2, + int Unknown3, + SegmentReference SegmentReference +) : ISerialize +{ + private static void SerializeSpecifier(CborWriter writer, IPackagingSpecifier specifier) + { + if (specifier is PackagingSpecifierValue packagingSpecifierValue) + { + writer.WriteTextString(packagingSpecifierValue.Value); + return; + } + + if (specifier is PackagingLogicalSpecifier logicalSpecifierValue) + { + writer.WriteTag((CborTag)(0x8067 + logicalSpecifierValue.Type)); + + writer.WriteStartArray(logicalSpecifierValue.Specifiers.Count); + foreach (var subSpecifier in logicalSpecifierValue.Specifiers) + { + SerializeSpecifier(writer, subSpecifier); + } + writer.WriteEndArray(); + return; + } + + Debug.Assert(false); + } + + private static IPackagingSpecifier DeserializeSpecifier(CborReader reader) + { + var nextTag = reader.PeekState(); + if (nextTag == CborReaderState.TextString) + { + return new PackagingSpecifierValue(reader.ReadTextString()); + } + + if (nextTag == CborReaderState.Tag) + { + var tag = (uint)reader.ReadTag(); + if (tag is 0x8067 or 0x8068) + { + var logicalSpecifier = (LogicalSpecifierType)(tag - 0x8067); + var subSpecifiers = new List(); + + var count = reader.ReadStartArray(); + while (count-- != 0) + { + subSpecifiers.Add(DeserializeSpecifier(reader)); + } + reader.ReadEndArray(); + + return new PackagingLogicalSpecifier(logicalSpecifier, subSpecifiers); + } + } + + Debug.Assert(false); + throw new InvalidOperationException("Invalid packaging specifier"); + } + + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(3 + + (Specifier0 != null ? 1 : 0) + + (Specifier1 != null ? 1 : 0) + + (Specifier2 != null ? 1 : 0) + + (Unknown0.HasValue ? 1 : 0) + + (Unknown1.HasValue ? 1 : 0) + + (Unknown2 != 0 ? 1 : 0) + + (Unknown3 != 0 ? 1 : 0)); + + writer.WriteInt32(24); + writer.WriteInt32(Value0); + + if (Unknown0 is { } unknown0) + { + writer.WriteInt32(36); + writer.WriteBoolean(unknown0); + } + + if (Specifier0 != null) + { + writer.WriteInt32(30); + SerializeSpecifier(writer, Specifier0); + } + + if (Specifier1 != null) + { + writer.WriteInt32(31); + SerializeSpecifier(writer, Specifier1); + } + + if (Specifier2 != null) + { + writer.WriteInt32(32); + SerializeSpecifier(writer, Specifier2); + } + + if (Unknown1 is { } unknown1) + { + writer.WriteInt32(33); + writer.WriteBoolean(unknown1); + } + + if (Unknown2 != 0) + { + writer.WriteInt32(34); + writer.WriteInt32(Unknown2); + } + + writer.WriteInt32(2); + writer.WriteInt64(Value1); + + if (Unknown3 != 0) + { + writer.WriteInt32(11); + writer.WriteInt32(Unknown3); + } + + writer.WriteInt32(26); + SegmentReference.Serialize(writer); + + writer.WriteEndMap(); + } + + public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) + { + IPackagingSpecifier? specifier0 = default; + IPackagingSpecifier? specifier1 = default; + IPackagingSpecifier? specifier2 = default; + bool? unknown0 = default; + bool? unknown1 = default; + int value0 = default; + int unknown2 = default; + long value1 = default; + int unknown3 = default; + SegmentReference? segmentReference = default; + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 24: + value0 = reader.ReadInt32(); + break; + case 36: + unknown0 = reader.ReadBoolean(); + break; + case 30: + specifier0 = DeserializeSpecifier(reader); + break; + case 31: + specifier1 = DeserializeSpecifier(reader); + break; + case 32: + specifier2 = DeserializeSpecifier(reader); + break; + case 33: + unknown1 = reader.ReadBoolean(); + break; + case 34: + unknown2 = reader.ReadInt32(); + break; + case 2: + value1 = reader.ReadInt64(); + break; + case 11: + unknown3 = reader.ReadInt32(); + break; + case 26: + segmentReference = SegmentReference.Deserialize(reader, ref initialIV); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + Debug.Assert(segmentReference != null); + return new Chunk(specifier0, specifier1, specifier2, value0, value1, unknown0, unknown1, unknown2, unknown3, + segmentReference); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs new file mode 100644 index 0000000..5c365e8 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs @@ -0,0 +1,86 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public record FileFormat(List Tags, int MajorVersion, int MinorVersion, int Patch) : ISerialize +{ + public System.Version Version => new(MajorVersion, MinorVersion); + + public override string ToString() + => $"{MajorVersion}.{MinorVersion}.{Patch};"; + + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(2 + (Patch > 0 ? 1 : 0) + (Tags.Count > 0 ? 1 : 0)); + + writer.WriteInt32(257); + writer.WriteInt32(MajorVersion); + + writer.WriteInt32(258); + writer.WriteInt32(MinorVersion); + + if (Patch > 0) + { + writer.WriteInt32(267); + writer.WriteInt32(Patch); + } + + if (Tags.Count > 0) + { + writer.WriteInt32(290); + writer.WriteStartArray(Tags.Count); + foreach (var tag in Tags) + writer.WriteTextString(tag); + + writer.WriteEndArray(); + } + + writer.WriteEndMap(); + } + + public static FileFormat Deserialize(CborReader reader) + { + int? major = null, minor = null; + var patch = 0; + List tags = []; + + var remaining = reader.ReadStartMap(); + while (remaining-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 257: + major = reader.ReadInt32(); + break; + case 258: + minor = reader.ReadInt32(); + break; + case 267: + patch = reader.ReadInt32(); + break; + case 290: + tags = []; + var count = reader.ReadStartArray(); + while (count-- != 0) + { + tags.Add(reader.ReadTextString()); + } + + reader.ReadEndArray(); + break; + default: + reader.SkipValue(); + break; + } + } + + reader.ReadEndMap(); + + Debug.Assert(major != null && minor != null); + return new FileFormat(tags, major.Value, minor.Value, patch); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/IRootSerialize.cs b/LibXboxOne/XVC2/SerializedModel/IRootSerialize.cs new file mode 100644 index 0000000..42070b5 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/IRootSerialize.cs @@ -0,0 +1,7 @@ +namespace LibXboxOne.XVC2.SerializedModel; + +public interface IRootSerialize : ISerialize +{ + string OpcPath { get; } + string OpcRelationship { get; } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/ISerialize.cs b/LibXboxOne/XVC2/SerializedModel/ISerialize.cs new file mode 100644 index 0000000..fd154e3 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/ISerialize.cs @@ -0,0 +1,8 @@ +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public interface ISerialize +{ + void Serialize(CborWriter writer); +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Package.cs b/LibXboxOne/XVC2/SerializedModel/Package.cs new file mode 100644 index 0000000..253464b --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Package.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public record Package( + FileFormat FileFormat, + Guid ContentId, + Version Version, + PackagingIV? InitialIV, + List Keys, + Segmentation Segmentation, + List Boxes, + List Chunks, + Guid FulfillmentContentId, + Guid ProductId, + Version MinimumSystemVersion, + string StoreId, + SerializedPlatform SupportedPlatforms +) : IRootSerialize +{ + public string OpcPath => "/XbocPackage.cbor"; + public string OpcRelationship => "http://xbox.com/MSIXVC2/Package"; + + public void Serialize(CborWriter writer) + { + writer.WriteSelfDescribeTag(CborTagEx.XVCP); + + writer.WriteStartMap(11 + (InitialIV != null ? 1 : 0) + (Keys.Count > 0 ? 1 : 0)); + + writer.WriteInt32(256); + FileFormat.Serialize(writer); + + if (InitialIV is {} initialIV) + { + writer.WriteInt32(27); + writer.WriteByteString(initialIV.ToArray()); + } + + writer.WriteInt32(260); + writer.WriteTextString(ContentId.ToString()); + + writer.WriteInt32(279); + writer.WriteTextString(FulfillmentContentId.ToString()); + + writer.WriteInt32(280); + writer.WriteTextString(ProductId.ToString()); + + writer.WriteInt32(261); + Version.Serialize(writer); + + writer.WriteInt32(281); + MinimumSystemVersion.Serialize(writer); + + writer.WriteInt32(282); + writer.WriteTextString(StoreId); + + writer.WriteInt32(283); + writer.WriteUInt32((uint)SupportedPlatforms); + + writer.WriteInt32(263); + Segmentation.Serialize(writer); + + if (Keys.Count > 0) + { + writer.WriteInt32(262); + writer.WriteStartArray(Keys.Count); + foreach (var key in Keys) + { + key.Serialize(writer); + } + writer.WriteEndArray(); + } + + writer.WriteInt32(264); + writer.WriteStartArray(Boxes.Count); + foreach (var box in Boxes) + { + box.Serialize(writer); + } + writer.WriteEndArray(); + + writer.WriteInt32(265); + writer.WriteStartArray(Chunks.Count); + foreach (var chunk in Chunks) + { + chunk.Serialize(writer); + } + writer.WriteEndArray(); + + writer.WriteEndMap(); + } + + public static Package Deserialize(CborReader reader) + { + FileFormat? fileFormat = default; + Guid contentId = default; + Version version = default; + PackagingIV? initialIV = default; + List keys = []; + Segmentation? segmentation = default; + List? boxes = default; + List? chunks = default; + Guid fulfillmentContentId = default; + Guid productId = default; + Version minimumSystemVersion = default; + string? storeId = default; + SerializedPlatform supportedPlatforms = 0; + + reader.ReadSelfDescribeTag(CborTagEx.XVCP); + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 256: + fileFormat = FileFormat.Deserialize(reader); + break; + case 27: + initialIV = PackagingIV.FromBytes(reader.ReadByteString()); + break; + case 260: + contentId = Guid.Parse(reader.ReadTextString()); + break; + case 279: + fulfillmentContentId = Guid.Parse(reader.ReadTextString()); + break; + case 280: + productId = Guid.Parse(reader.ReadTextString()); + break; + case 261: + version = Version.Deserialize(reader); + break; + case 281: + minimumSystemVersion = Version.Deserialize(reader); + break; + case 282: + storeId = reader.ReadTextString(); + break; + case 283: + supportedPlatforms = (SerializedPlatform)reader.ReadUInt32(); + break; + case 263: + segmentation = Segmentation.Deserialize(reader); + break; + case 262: + var keyCount = reader.ReadStartArray(); + keys = new List(keyCount ?? 0); + while (keyCount-- != 0) + { + keys.Add(PackageKey.Deserialize(reader)); + } + reader.ReadEndArray(); + break; + case 264: + var boxCount = reader.ReadStartArray(); + boxes = new List(boxCount ?? 0); + while (boxCount-- != 0) + { + boxes.Add(BoxReference.Deserialize(reader)); + } + reader.ReadEndArray(); + break; + case 265: + var chunkCount = reader.ReadStartArray(); + chunks = new List(chunkCount ?? 0); + while (chunkCount-- != 0) + { + chunks.Add(Chunk.Deserialize(reader, ref initialIV)); + } + reader.ReadEndArray(); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + Debug.Assert(fileFormat != null && segmentation != null && boxes != null && chunks != null && storeId != null); + return new Package(fileFormat, contentId, version, initialIV, keys, segmentation, boxes, chunks, + fulfillmentContentId, productId, minimumSystemVersion, storeId, supportedPlatforms); + + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/PackageKey.cs b/LibXboxOne/XVC2/SerializedModel/PackageKey.cs new file mode 100644 index 0000000..38f494b --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/PackageKey.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record PackageKey(List Sources) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartArray(Sources.Count); + foreach (var source in Sources) + { + source.Serialize(writer); + } + writer.WriteEndArray(); + } + + public static PackageKey Deserialize(CborReader reader) + { + var count = reader.ReadStartArray(); + var sources = new List(count ?? 0); + while (count-- != 0) + { + sources.Add(PackageKeySource.Deserialize(reader)); + } + reader.ReadEndArray(); + + return new PackageKey(sources); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs new file mode 100644 index 0000000..3eee394 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs @@ -0,0 +1,106 @@ +#nullable enable +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record PackageKeySource( + string SourceKeyId, + int SourcePurpose, + int DerivationAlgorithm, + byte[] KdfContext, + int EncryptionAlgorithm, + byte[]? Unknown1, + byte[] WrappedKey, + int PackagingEncryptionAlgorithm2 + +) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(7 + (Unknown1 != null ? 1 : 0)); + + writer.WriteInt32(287); + writer.WriteInt32(SourcePurpose); + + writer.WriteInt32(288); + writer.WriteTextString(SourceKeyId); + + writer.WriteInt32(284); + writer.WriteInt32(DerivationAlgorithm); + + writer.WriteInt32(286); + writer.WriteByteString(KdfContext); + + writer.WriteInt32(285); + writer.WriteInt32(EncryptionAlgorithm); + + if (Unknown1 != null) + { + writer.WriteInt32(7); + writer.WriteByteString(Unknown1); + } + + writer.WriteInt32(6); + writer.WriteByteString(WrappedKey); + + writer.WriteInt32(259); + writer.WriteInt32(PackagingEncryptionAlgorithm2); + + writer.WriteEndMap(); + } + + public static PackageKeySource Deserialize(CborReader reader) + { + string? sourceKeyId = null; + var keyPurpose = 0; + var derivationAlgorithm = 0; + var encryptionAlgorithm = 0; + var encryptionAlgorithm2 = 0; + byte[]? kdfContext = null; + byte[]? unknown1 = null; + byte[]? wrappedKey = null; + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 287: + keyPurpose = reader.ReadInt32(); + break; + case 288: + sourceKeyId = reader.ReadTextString(); + break; + case 284: + derivationAlgorithm = reader.ReadInt32(); + break; + case 286: + kdfContext = reader.ReadByteString(); + break; + case 285: + encryptionAlgorithm = reader.ReadInt32(); + break; + case 7: + unknown1 = reader.ReadByteString(); + break; + case 6: + wrappedKey = reader.ReadByteString(); + break; + case 259: + encryptionAlgorithm2 = reader.ReadInt32(); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + reader.ReadEndMap(); + + Debug.Assert(sourceKeyId != null && kdfContext != null && wrappedKey != null); + return new PackageKeySource(sourceKeyId, keyPurpose, derivationAlgorithm, kdfContext, encryptionAlgorithm, + unknown1, wrappedKey, encryptionAlgorithm2); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs new file mode 100644 index 0000000..8670465 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs @@ -0,0 +1,169 @@ +#nullable enable +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record SegmentReference( + PackagingHash Hash, + int Length, + PackagingCompression Compression, + int CompressedLength, + byte[]? EncryptionKey, + byte[]? WrappedKey, + PackagingIV? WrapIV, + PackagingHash? BoxHash, + BoxIndex? BoxIndex, + int BoxOffset, + int BoxLength, + bool Secondary +) +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(2 + + (Compression != 0 ? 2 : 0) + + (EncryptionKey != null ? 1 : 0) + + (WrappedKey != null ? 1 : 0) + + (BoxHash.HasValue ? 1 : 0) + + (BoxIndex.HasValue ? 1 : 0) + + (BoxOffset != 0 ? 1 : 0) + + (BoxLength != 0 ? 1 : 0) + + (Secondary ? 1 : 0)); + + writer.WriteInt32(1); + writer.WriteHash(Hash); + + writer.WriteInt32(2); + writer.WriteInt32(Length); + + if (Compression != 0) + { + writer.WriteInt32(3); + writer.WriteInt32((int)Compression); + + writer.WriteInt32(4); + writer.WriteInt32(CompressedLength); + } + + if (EncryptionKey != null) + { + writer.WriteInt32(5); + writer.WriteByteString(EncryptionKey); + } + + if (WrappedKey != null) + { + writer.WriteInt32(6); + writer.WriteByteString(WrappedKey); + } + + // 7 is probably WrapIV? is this used? + + if (BoxHash is { } boxHash) + { + writer.WriteInt32(8); + writer.WriteHash(boxHash); + } + + if (BoxIndex is { } boxIndex) + { + writer.WriteInt32(9); + writer.WriteInt32(boxIndex.Value); + } + + if (BoxOffset != 0) + { + writer.WriteInt32(10); + writer.WriteInt32(BoxOffset); + } + + if (BoxLength != 0) + { + writer.WriteInt32(11); + writer.WriteInt32(BoxLength); + } + + if (Secondary) + { + writer.WriteInt32(12); + writer.WriteBoolean(Secondary); + } + + writer.WriteEndMap(); + } + + public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? initialIV) + { + PackagingHash hash = default; + int length = default; + PackagingCompression compression = default; + int compressedLength = default; + byte[]? encryptionKey = default; + byte[]? wrappedKey = default; + PackagingIV? wrapIV = default; + PackagingHash? boxHash = default; + BoxIndex? boxIndex = default; + int boxOffset = default; + int boxLength = default; + bool secondary = default; + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 1: + hash = reader.ReadHash(); + break; + case 2: + length = reader.ReadInt32(); + break; + case 3: + compression = (PackagingCompression)reader.ReadInt32(); + break; + case 4: + compressedLength = reader.ReadInt32(); + break; + case 5: + encryptionKey = reader.ReadByteString(); + break; + case 6: + wrappedKey = reader.ReadByteString(); + + Debug.Assert(initialIV.HasValue); + wrapIV = initialIV; + initialIV = wrapIV?.Increment(); + break; + //case 7: + // // This is not set in normal references - the iv is gotten from the initial iv + // wrapIV = PackagingIV.FromBytes(reader.ReadByteString()); + // break; + case 8: + boxHash = reader.ReadHash(); + break; + case 9: + boxIndex = new BoxIndex(reader.ReadInt32()); + break; + case 10: + boxOffset = reader.ReadInt32(); + break; + case 11: + boxLength = reader.ReadInt32(); + break; + case 12: + secondary = reader.ReadBoolean(); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + + reader.ReadEndMap(); + return new SegmentReference(hash, length, compression, compressedLength, encryptionKey, wrappedKey, wrapIV, + boxHash, boxIndex, boxOffset, boxLength, secondary); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Segmentation.cs b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs new file mode 100644 index 0000000..a215283 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record Segmentation(SegmentationAlgorithm Algorithm, Dictionary Labels, int Algorithm2) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(2 + (Algorithm2 != 0x301 ? 1 : 0)); + + writer.WriteInt32(259); + writer.WriteInt32((int)Algorithm); + + writer.WriteInt32(266); + writer.WriteMap(Labels); + + if (Algorithm2 != 0x301) + { + writer.WriteInt32(291); + writer.WriteInt32(Algorithm2); + } + + writer.WriteEndMap(); + } + + public static Segmentation Deserialize(CborReader reader) + { + SegmentationAlgorithm algorithm = 0; + var labels = new Dictionary(); + var algorithm2 = 0x301; + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 259: + algorithm = (SegmentationAlgorithm)reader.ReadInt32(); + break; + case 266: + labels = reader.ReadMap(); + break; + case 291: + algorithm2 = reader.ReadInt32(); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + + reader.ReadEndMap(); + + return new Segmentation(algorithm, labels, algorithm2); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SerializedAlgorithm.cs b/LibXboxOne/XVC2/SerializedModel/SerializedAlgorithm.cs new file mode 100644 index 0000000..f0b9d0c --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/SerializedAlgorithm.cs @@ -0,0 +1,16 @@ +namespace LibXboxOne.XVC2.SerializedModel; + +public enum SerializedAlgorithm +{ + None = 0, + Deflate = 1, + Brotli = 2, + AES_256_CBC = 256, + AES_256_KW = 257, + FastCDC = 512, + Fixed = 513, + SHA256 = 768, + SHA384 = 769, + SHA512 = 770, + SP800_108_HMAC_SHA256 = 1024, +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SerializedKeyPurpose.cs b/LibXboxOne/XVC2/SerializedModel/SerializedKeyPurpose.cs new file mode 100644 index 0000000..7d1b6e0 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/SerializedKeyPurpose.cs @@ -0,0 +1,8 @@ +namespace LibXboxOne.XVC2.SerializedModel; + +public enum SerializedKeyPurpose +{ + Content = 0, + Version = 1, + PackageData = 2, +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SerializedLabel.cs b/LibXboxOne/XVC2/SerializedModel/SerializedLabel.cs new file mode 100644 index 0000000..54a89b9 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/SerializedLabel.cs @@ -0,0 +1,69 @@ +namespace LibXboxOne.XVC2.SerializedModel; + +public enum SerializedLabel +{ + Unknown = 0, + Hash = 1, + Length = 2, + Compression = 3, + CompressedLength = 4, + EncryptionKey = 5, + WrappedKey = 6, + WrapIV = 7, + BoxHash = 8, + BoxIndex = 9, + BoxOffset = 10, + BoxLength = 11, + Secondary = 12, + Id = 24, + Name = 25, + SecretReference = 26, + InitialIV = 27, + Files = 28, + Segments = 29, + Tags = 30, + Languages = 31, + Devices = 32, + RequiredToLaunch = 33, + KeyIndex = 34, + ChunkId = 35, + OnDemand = 36, + ReadProtected = 37, + FileFormat = 256, + MajorVersion = 257, + MinorVersion = 258, + Algorithm = 259, + ContentId = 260, + Version = 261, + Keys = 262, + Segmentation = 263, + Boxes = 264, + Chunks = 265, + Options = 266, + Build = 267, + Revision = 268, + BuildId = 269, + ContentKeyId = 270, + VersionKeyId = 271, + WrappedPackageKey = 272, + Min = 273, + Avg = 274, + Max = 275, + PackageUri = 276, + FileName = 277, + UserDataName = 278, + FulfillmentContentId = 279, + ProductId = 280, + MinimumSystemVersion = 281, + StoreId = 282, + SupportedPlatforms = 283, + DerivationAlgorithm = 284, + WrapAlgorithm = 285, + KdfContext = 286, + SourcePurpose = 287, + SourceKeyId = 288, + Target = 289, + WrittenBy = 290, + HashAlgorithm = 291, + OriginalBuildId = 292, +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs b/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs new file mode 100644 index 0000000..4c9e9d9 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs @@ -0,0 +1,9 @@ +namespace LibXboxOne.XVC2.SerializedModel; + +public enum SerializedPlatform +{ + None = 0, + PC = 1, + ConsoleGen8 = 2, + ConsoleGen9 = 3 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Version.cs b/LibXboxOne/XVC2/SerializedModel/Version.cs new file mode 100644 index 0000000..725b15d --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Version.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public record struct Version(ushort Major, ushort Minor, ushort Patch, ushort Build, Guid Id, Guid? Unknown) : ISerialize +{ + public override string ToString() => $"{Major}.{Minor}.{Patch}.{Build}.{Id}"; + + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(5 + (Unknown.HasValue ? 1 : 0)); + + writer.WriteInt32(257); + writer.WriteUInt32(Major); + + writer.WriteInt32(258); + writer.WriteUInt32(Minor); + + writer.WriteInt32(267); + writer.WriteUInt32(Patch); + + writer.WriteInt32(268); + writer.WriteUInt32(Build); + + writer.WriteInt32(269); + writer.WriteTextString(Id.ToString()); + + if (Unknown is { } value) + { + writer.WriteInt32(292); + writer.WriteTextString(value.ToString()); + } + + writer.WriteEndMap(); + } + + public static Version Deserialize(CborReader reader) + { + ushort major = 0, minor = 0, patch = 0, build = 0; + Guid id = default; + Guid? unknown = null; + + var remaining = reader.ReadStartMap(); + while (remaining-- != 0) + { + var key = reader.ReadInt32(); + switch (key) + { + case 257: + major = (ushort)reader.ReadUInt32(); + break; + case 258: + minor = (ushort)reader.ReadUInt32(); + break; + case 267: + patch = (ushort)reader.ReadUInt32(); + break; + case 268: + build = (ushort)reader.ReadUInt32(); + break; + case 269: + id = Guid.Parse(reader.ReadTextString()); + break; + case 292: + unknown = Guid.Parse(reader.ReadTextString()); + break; + default: + Debug.Assert(false); + reader.SkipValue(); + break; + } + } + + reader.ReadEndMap(); + + return new Version(major, minor, patch, build, id, unknown); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/Specifiers/IPackagingSpecifier.cs b/LibXboxOne/XVC2/Specifiers/IPackagingSpecifier.cs new file mode 100644 index 0000000..ef76ff7 --- /dev/null +++ b/LibXboxOne/XVC2/Specifiers/IPackagingSpecifier.cs @@ -0,0 +1,3 @@ +namespace LibXboxOne.XVC2.Specifiers; + +public interface IPackagingSpecifier; \ No newline at end of file diff --git a/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs b/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs new file mode 100644 index 0000000..a6492a5 --- /dev/null +++ b/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs @@ -0,0 +1,7 @@ +namespace LibXboxOne.XVC2.Specifiers; + +public enum LogicalSpecifierType +{ + Unknown0 = 0, + Unknown1 = 1 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/Specifiers/PackagingLogicalSpecifier.cs b/LibXboxOne/XVC2/Specifiers/PackagingLogicalSpecifier.cs new file mode 100644 index 0000000..274f109 --- /dev/null +++ b/LibXboxOne/XVC2/Specifiers/PackagingLogicalSpecifier.cs @@ -0,0 +1,5 @@ +using System.Collections.Generic; + +namespace LibXboxOne.XVC2.Specifiers; + +public sealed record PackagingLogicalSpecifier(LogicalSpecifierType Type, List Specifiers) : IPackagingSpecifier; \ No newline at end of file diff --git a/LibXboxOne/XVC2/Specifiers/PackagingSpecifierValue.cs b/LibXboxOne/XVC2/Specifiers/PackagingSpecifierValue.cs new file mode 100644 index 0000000..6c5f790 --- /dev/null +++ b/LibXboxOne/XVC2/Specifiers/PackagingSpecifierValue.cs @@ -0,0 +1,3 @@ +namespace LibXboxOne.XVC2.Specifiers; + +public sealed record PackagingSpecifierValue(string Value) : IPackagingSpecifier; \ No newline at end of file From 540a73b51b2ca54ccc40c08de0a3bbb7fb0da9e3 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 02:08:30 +0200 Subject: [PATCH 03/11] Improve code quality, add more enums, add wrapper file class --- LibXboxOne/XVC2/BoxIndex.cs | 14 ++- LibXboxOne/XVC2/CborExtensions.cs | 40 ++++++- LibXboxOne/XVC2/Msixvc2File.cs | 47 ++++++++ LibXboxOne/XVC2/PackagingCompression.cs | 3 +- .../XVC2/PackagingDerivationAlgorithm.cs | 2 +- LibXboxOne/XVC2/PackagingIV.cs | 11 ++ LibXboxOne/XVC2/PackagingKeyPurpose.cs | 8 ++ .../XVC2/SerializedModel/BoxReference.cs | 3 +- LibXboxOne/XVC2/SerializedModel/Chunk.cs | 33 +++--- LibXboxOne/XVC2/SerializedModel/FileFormat.cs | 14 +-- LibXboxOne/XVC2/SerializedModel/Package.cs | 21 ++-- .../XVC2/SerializedModel/PackageKeySource.cs | 101 +++++++++++++----- .../XVC2/SerializedModel/SegmentReference.cs | 29 +++-- .../XVC2/SerializedModel/Segmentation.cs | 21 +++- .../SerializedModel/SerializedPlatform.cs | 2 +- LibXboxOne/XVC2/SerializedModel/Version.cs | 28 +++-- .../XVC2/Specifiers/LogicalSpecifierType.cs | 4 +- 17 files changed, 289 insertions(+), 92 deletions(-) create mode 100644 LibXboxOne/XVC2/Msixvc2File.cs create mode 100644 LibXboxOne/XVC2/PackagingKeyPurpose.cs diff --git a/LibXboxOne/XVC2/BoxIndex.cs b/LibXboxOne/XVC2/BoxIndex.cs index a2108bb..839b086 100644 --- a/LibXboxOne/XVC2/BoxIndex.cs +++ b/LibXboxOne/XVC2/BoxIndex.cs @@ -1,6 +1,16 @@ -namespace LibXboxOne.XVC2; +using System.Formats.Cbor; +using LibXboxOne.XVC2.SerializedModel; -public readonly record struct BoxIndex(int Value) +namespace LibXboxOne.XVC2; + +public readonly record struct BoxIndex(int Value) : ISerialize { public override string ToString() => $"box:{Value}"; + + public void Serialize(CborWriter writer) + { + writer.WriteInt32(Value); + } + + public static BoxIndex Deserialize(CborReader reader) => new(reader.ReadInt32()); } \ No newline at end of file diff --git a/LibXboxOne/XVC2/CborExtensions.cs b/LibXboxOne/XVC2/CborExtensions.cs index b6c9de3..5a756fe 100644 --- a/LibXboxOne/XVC2/CborExtensions.cs +++ b/LibXboxOne/XVC2/CborExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Formats.Cbor; using System.IO; @@ -9,10 +10,15 @@ public static class CborExtensions { extension(CborWriter writer) { + public void WriteTagEx(CborTagEx tag) + { + writer.WriteTag((CborTag)tag); + } + public void WriteSelfDescribeTag(CborTagEx tag) { writer.WriteTag(CborTag.SelfDescribeCbor); - writer.WriteTag((CborTag)tag); + writer.WriteTagEx(tag); } public void WriteHash(PackagingHash hash) @@ -68,18 +74,31 @@ public void WriteMap(Dictionary map) writer.WriteEndMap(); } + + public void WriteGuid(Guid value) + { + writer.WriteTextString(value.ToString()); + } + + public void WriteEnum(T value) where T : Enum + { + writer.WriteInt32((int)(object)value); + } } extension(CborReader reader) { + public CborTagEx ReadTagEx() + => (CborTagEx)reader.ReadTag(); + public void ReadSelfDescribeTag(CborTagEx tag) { var tag0 = reader.ReadTag(); if (tag0 != CborTag.SelfDescribeCbor) throw new InvalidDataException(); - var tag1 = reader.ReadTag(); - if ((CborTagEx)tag1 != tag) + var tag1 = reader.ReadTagEx(); + if (tag1 != tag) throw new InvalidDataException(); } @@ -121,5 +140,18 @@ public Dictionary ReadMap() return dict; } + + public Guid ReadGuid() + { + return Guid.Parse(reader.ReadTextString()); + } + + public T ReadEnum() where T : Enum => (T)(object)reader.ReadInt32(); + + public void AssertInvalidValue() + { + Debug.Assert(false); + reader.SkipValue(); + } } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs new file mode 100644 index 0000000..2255c7b --- /dev/null +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -0,0 +1,47 @@ +using System; +using System.Formats.Cbor; +using System.IO; +using System.IO.Compression; +using LibXboxOne.XVC2.SerializedModel; + +namespace LibXboxOne.XVC2; + +public sealed class Msixvc2File : IDisposable +{ + private readonly ZipArchive _archive; + private readonly Package _package; + + + public Msixvc2File(Stream stream, bool leaveOpen = false) + { + _archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen); + _package = ReadPackageMetadata(); + } + + public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); + + private Package ReadPackageMetadata() + { + var metadataCbor = GetEntryContent("XboxPackage.cbor"); + var reader = new CborReader(metadataCbor); + return Package.Deserialize(reader); + } + + private byte[] GetEntryContent(string entryPath) + { + var packageEntry = _archive.GetEntry(entryPath); + if (packageEntry == null) + throw new InvalidOperationException($"Failed to find entry {entryPath} in MSIXVC2"); + + using var packageDataStream = packageEntry.Open(); + var ms = new MemoryStream(); + packageDataStream.CopyTo(ms); + + return ms.GetBuffer(); + } + + public void Dispose() + { + _archive.Dispose(); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingCompression.cs b/LibXboxOne/XVC2/PackagingCompression.cs index 6c232f8..c9e71e6 100644 --- a/LibXboxOne/XVC2/PackagingCompression.cs +++ b/LibXboxOne/XVC2/PackagingCompression.cs @@ -3,5 +3,6 @@ public enum PackagingCompression { None = 0, - Brotli = 1 // TODO: maybe + Deflate = 1, + Brotli = 2 } \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs b/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs index 3e35b26..c228db8 100644 --- a/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs +++ b/LibXboxOne/XVC2/PackagingDerivationAlgorithm.cs @@ -3,5 +3,5 @@ public enum PackagingDerivationAlgorithm { None = 0, - SP800_100_HMAC_SHA256 = 1 + SP800_108_HMAC_SHA256 = 1 } \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingIV.cs b/LibXboxOne/XVC2/PackagingIV.cs index e5b714c..f70b9e3 100644 --- a/LibXboxOne/XVC2/PackagingIV.cs +++ b/LibXboxOne/XVC2/PackagingIV.cs @@ -2,6 +2,7 @@ using System; using System.Buffers.Binary; using System.Buffers.Text; +using System.Formats.Cbor; namespace LibXboxOne.XVC2; @@ -49,4 +50,14 @@ public static PackagingIV FromBytes(byte[] value) _counter1 = BinaryPrimitives.ReadUInt64BigEndian(value.AsSpan(8)) }; } + + public void Serialize(CborWriter writer) + { + writer.WriteByteString(ToArray()); + } + + public static PackagingIV Deserialize(CborReader reader) + { + return FromBytes(reader.ReadByteString()); + } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingKeyPurpose.cs b/LibXboxOne/XVC2/PackagingKeyPurpose.cs new file mode 100644 index 0000000..5d9eb60 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingKeyPurpose.cs @@ -0,0 +1,8 @@ +namespace LibXboxOne.XVC2; + +public enum PackagingKeyPurpose +{ + Content = 0, + Version = 1, + PackageData = 2 +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/BoxReference.cs b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs index e17d16b..163f1fe 100644 --- a/LibXboxOne/XVC2/SerializedModel/BoxReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs @@ -28,8 +28,7 @@ public static BoxReference Deserialize(CborReader reader) name = reader.ReadTextString(); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs index 0647374..2c5bdfa 100644 --- a/LibXboxOne/XVC2/SerializedModel/Chunk.cs +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -11,7 +11,7 @@ public sealed record Chunk( IPackagingSpecifier? Specifier0, IPackagingSpecifier? Specifier1, IPackagingSpecifier? Specifier2, - int Value0, + int Id, long Value1, bool? Unknown0, bool? Unknown1, @@ -30,7 +30,12 @@ private static void SerializeSpecifier(CborWriter writer, IPackagingSpecifier sp if (specifier is PackagingLogicalSpecifier logicalSpecifierValue) { - writer.WriteTag((CborTag)(0x8067 + logicalSpecifierValue.Type)); + writer.WriteTagEx(logicalSpecifierValue.Type switch + { + LogicalSpecifierType.Any => CborTagEx.LogicalAny, + LogicalSpecifierType.All => CborTagEx.LogicalAll, + _ => throw new UnreachableException() + }); writer.WriteStartArray(logicalSpecifierValue.Specifiers.Count); foreach (var subSpecifier in logicalSpecifierValue.Specifiers) @@ -54,10 +59,16 @@ private static IPackagingSpecifier DeserializeSpecifier(CborReader reader) if (nextTag == CborReaderState.Tag) { - var tag = (uint)reader.ReadTag(); - if (tag is 0x8067 or 0x8068) + var tag = reader.ReadTagEx(); + if (tag is CborTagEx.LogicalAny or CborTagEx.LogicalAll) { - var logicalSpecifier = (LogicalSpecifierType)(tag - 0x8067); + var logicalSpecifier = tag switch + { + CborTagEx.LogicalAny => LogicalSpecifierType.Any, + CborTagEx.LogicalAll => LogicalSpecifierType.All, + _ => throw new UnreachableException() + }; + var subSpecifiers = new List(); var count = reader.ReadStartArray(); @@ -71,7 +82,6 @@ private static IPackagingSpecifier DeserializeSpecifier(CborReader reader) } } - Debug.Assert(false); throw new InvalidOperationException("Invalid packaging specifier"); } @@ -87,7 +97,7 @@ public void Serialize(CborWriter writer) + (Unknown3 != 0 ? 1 : 0)); writer.WriteInt32(24); - writer.WriteInt32(Value0); + writer.WriteInt32(Id); if (Unknown0 is { } unknown0) { @@ -147,7 +157,7 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) IPackagingSpecifier? specifier2 = default; bool? unknown0 = default; bool? unknown1 = default; - int value0 = default; + int id = default; int unknown2 = default; long value1 = default; int unknown3 = default; @@ -160,7 +170,7 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) switch (key) { case 24: - value0 = reader.ReadInt32(); + id = reader.ReadInt32(); break; case 36: unknown0 = reader.ReadBoolean(); @@ -190,15 +200,14 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) segmentReference = SegmentReference.Deserialize(reader, ref initialIV); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } reader.ReadEndMap(); Debug.Assert(segmentReference != null); - return new Chunk(specifier0, specifier1, specifier2, value0, value1, unknown0, unknown1, unknown2, unknown3, + return new Chunk(specifier0, specifier1, specifier2, id, value1, unknown0, unknown1, unknown2, unknown3, segmentReference); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs index 5c365e8..16bacc5 100644 --- a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs +++ b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs @@ -10,7 +10,7 @@ public record FileFormat(List Tags, int MajorVersion, int MinorVersion, public System.Version Version => new(MajorVersion, MinorVersion); public override string ToString() - => $"{MajorVersion}.{MinorVersion}.{Patch};"; + => $"{MajorVersion}.{MinorVersion}.{Patch}"; public void Serialize(CborWriter writer) { @@ -33,8 +33,9 @@ public void Serialize(CborWriter writer) writer.WriteInt32(290); writer.WriteStartArray(Tags.Count); foreach (var tag in Tags) + { writer.WriteTextString(tag); - + } writer.WriteEndArray(); } @@ -43,8 +44,9 @@ public void Serialize(CborWriter writer) public static FileFormat Deserialize(CborReader reader) { - int? major = null, minor = null; - var patch = 0; + int major = default; + int minor = default; + int patch = default; List tags = []; var remaining = reader.ReadStartMap(); @@ -69,7 +71,6 @@ public static FileFormat Deserialize(CborReader reader) { tags.Add(reader.ReadTextString()); } - reader.ReadEndArray(); break; default: @@ -80,7 +81,6 @@ public static FileFormat Deserialize(CborReader reader) reader.ReadEndMap(); - Debug.Assert(major != null && minor != null); - return new FileFormat(tags, major.Value, minor.Value, patch); + return new FileFormat(tags, major, minor, patch); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Package.cs b/LibXboxOne/XVC2/SerializedModel/Package.cs index 253464b..940d7b8 100644 --- a/LibXboxOne/XVC2/SerializedModel/Package.cs +++ b/LibXboxOne/XVC2/SerializedModel/Package.cs @@ -37,17 +37,17 @@ public void Serialize(CborWriter writer) if (InitialIV is {} initialIV) { writer.WriteInt32(27); - writer.WriteByteString(initialIV.ToArray()); + initialIV.Serialize(writer); } writer.WriteInt32(260); - writer.WriteTextString(ContentId.ToString()); + writer.WriteGuid(ContentId); writer.WriteInt32(279); - writer.WriteTextString(FulfillmentContentId.ToString()); + writer.WriteGuid(FulfillmentContentId); writer.WriteInt32(280); - writer.WriteTextString(ProductId.ToString()); + writer.WriteGuid(ProductId); writer.WriteInt32(261); Version.Serialize(writer); @@ -59,7 +59,7 @@ public void Serialize(CborWriter writer) writer.WriteTextString(StoreId); writer.WriteInt32(283); - writer.WriteUInt32((uint)SupportedPlatforms); + writer.WriteEnum(SupportedPlatforms); writer.WriteInt32(263); Segmentation.Serialize(writer); @@ -108,7 +108,7 @@ public static Package Deserialize(CborReader reader) Guid productId = default; Version minimumSystemVersion = default; string? storeId = default; - SerializedPlatform supportedPlatforms = 0; + SerializedPlatform supportedPlatforms = default; reader.ReadSelfDescribeTag(CborTagEx.XVCP); @@ -125,13 +125,13 @@ public static Package Deserialize(CborReader reader) initialIV = PackagingIV.FromBytes(reader.ReadByteString()); break; case 260: - contentId = Guid.Parse(reader.ReadTextString()); + contentId = reader.ReadGuid(); break; case 279: - fulfillmentContentId = Guid.Parse(reader.ReadTextString()); + fulfillmentContentId = reader.ReadGuid(); break; case 280: - productId = Guid.Parse(reader.ReadTextString()); + productId = reader.ReadGuid(); break; case 261: version = Version.Deserialize(reader); @@ -176,8 +176,7 @@ public static Package Deserialize(CborReader reader) reader.ReadEndArray(); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } diff --git a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs index 3eee394..7440385 100644 --- a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs +++ b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs @@ -6,60 +6,106 @@ namespace LibXboxOne.XVC2.SerializedModel; public sealed record PackageKeySource( string SourceKeyId, - int SourcePurpose, - int DerivationAlgorithm, + PackagingKeyPurpose SourcePurpose, + PackagingDerivationAlgorithm DerivationAlgorithm, byte[] KdfContext, - int EncryptionAlgorithm, - byte[]? Unknown1, + PackagingEncryptionAlgorithm EncryptionAlgorithm, + byte[]? EncryptionKey, // unverified byte[] WrappedKey, - int PackagingEncryptionAlgorithm2 + PackagingEncryptionAlgorithm PackagingEncryptionAlgorithm2 ) : ISerialize { + private static PackagingDerivationAlgorithm ToDerivationAlgorithm(SerializedAlgorithm algorithm) => algorithm switch + { + SerializedAlgorithm.None => PackagingDerivationAlgorithm.None, + SerializedAlgorithm.SP800_108_HMAC_SHA256 => PackagingDerivationAlgorithm.SP800_108_HMAC_SHA256, + _ => throw new UnreachableException() + }; + + private static PackagingEncryptionAlgorithm ToEncryptionAlgorithm(SerializedAlgorithm algorithm) => algorithm switch + { + SerializedAlgorithm.None => PackagingEncryptionAlgorithm.None, + SerializedAlgorithm.AES_256_CBC => PackagingEncryptionAlgorithm.AES_256_CBC, + SerializedAlgorithm.AES_256_KW => PackagingEncryptionAlgorithm.AES_256_KW, + _ => throw new UnreachableException() + }; + + private static PackagingKeyPurpose ToKeyPurpose(SerializedKeyPurpose keyPurpose) => keyPurpose switch + { + SerializedKeyPurpose.Content => PackagingKeyPurpose.Content, + SerializedKeyPurpose.Version => PackagingKeyPurpose.Version, + SerializedKeyPurpose.PackageData => PackagingKeyPurpose.PackageData, + _ => throw new UnreachableException() + }; + + private static SerializedAlgorithm ToSerialized(PackagingDerivationAlgorithm algorithm) => algorithm switch + { + PackagingDerivationAlgorithm.None => SerializedAlgorithm.None, + PackagingDerivationAlgorithm.SP800_108_HMAC_SHA256 => SerializedAlgorithm.SP800_108_HMAC_SHA256, + _ => throw new UnreachableException() + }; + + private static SerializedAlgorithm ToSerialized(PackagingEncryptionAlgorithm algorithm) => algorithm switch + { + PackagingEncryptionAlgorithm.None => SerializedAlgorithm.None, + PackagingEncryptionAlgorithm.AES_256_CBC => SerializedAlgorithm.AES_256_CBC, + PackagingEncryptionAlgorithm.AES_256_KW => SerializedAlgorithm.AES_256_KW, + _ => throw new UnreachableException() + }; + + private static SerializedKeyPurpose ToSerialized(PackagingKeyPurpose keyPurpose) => keyPurpose switch + { + PackagingKeyPurpose.Content => SerializedKeyPurpose.Content, + PackagingKeyPurpose.Version => SerializedKeyPurpose.Version, + PackagingKeyPurpose.PackageData => SerializedKeyPurpose.PackageData, + _ => throw new UnreachableException() + }; + public void Serialize(CborWriter writer) { - writer.WriteStartMap(7 + (Unknown1 != null ? 1 : 0)); + writer.WriteStartMap(7 + (EncryptionKey != null ? 1 : 0)); writer.WriteInt32(287); - writer.WriteInt32(SourcePurpose); + writer.WriteEnum(ToSerialized(SourcePurpose)); writer.WriteInt32(288); writer.WriteTextString(SourceKeyId); writer.WriteInt32(284); - writer.WriteInt32(DerivationAlgorithm); + writer.WriteEnum(ToSerialized(DerivationAlgorithm)); writer.WriteInt32(286); writer.WriteByteString(KdfContext); writer.WriteInt32(285); - writer.WriteInt32(EncryptionAlgorithm); + writer.WriteEnum(ToSerialized(EncryptionAlgorithm)); - if (Unknown1 != null) + if (EncryptionKey != null) { writer.WriteInt32(7); - writer.WriteByteString(Unknown1); + writer.WriteByteString(EncryptionKey); } writer.WriteInt32(6); writer.WriteByteString(WrappedKey); writer.WriteInt32(259); - writer.WriteInt32(PackagingEncryptionAlgorithm2); + writer.WriteEnum(ToSerialized(PackagingEncryptionAlgorithm2)); writer.WriteEndMap(); } public static PackageKeySource Deserialize(CborReader reader) { - string? sourceKeyId = null; - var keyPurpose = 0; - var derivationAlgorithm = 0; - var encryptionAlgorithm = 0; - var encryptionAlgorithm2 = 0; - byte[]? kdfContext = null; - byte[]? unknown1 = null; - byte[]? wrappedKey = null; + string? sourceKeyId = default; + PackagingKeyPurpose keyPurpose = default; + PackagingDerivationAlgorithm derivationAlgorithm = default; + PackagingEncryptionAlgorithm encryptionAlgorithm = default; + PackagingEncryptionAlgorithm encryptionAlgorithm2 = default; + byte[]? kdfContext = default; + byte[]? encryptionKey = default; + byte[]? wrappedKey = default; var count = reader.ReadStartMap(); while (count-- != 0) @@ -68,32 +114,31 @@ public static PackageKeySource Deserialize(CborReader reader) switch (key) { case 287: - keyPurpose = reader.ReadInt32(); + keyPurpose = ToKeyPurpose(reader.ReadEnum()); break; case 288: sourceKeyId = reader.ReadTextString(); break; case 284: - derivationAlgorithm = reader.ReadInt32(); + derivationAlgorithm = ToDerivationAlgorithm(reader.ReadEnum()); break; case 286: kdfContext = reader.ReadByteString(); break; case 285: - encryptionAlgorithm = reader.ReadInt32(); + encryptionAlgorithm = ToEncryptionAlgorithm(reader.ReadEnum()); break; case 7: - unknown1 = reader.ReadByteString(); + encryptionKey = reader.ReadByteString(); break; case 6: wrappedKey = reader.ReadByteString(); break; case 259: - encryptionAlgorithm2 = reader.ReadInt32(); + encryptionAlgorithm2 = ToEncryptionAlgorithm(reader.ReadEnum()); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } @@ -101,6 +146,6 @@ public static PackageKeySource Deserialize(CborReader reader) Debug.Assert(sourceKeyId != null && kdfContext != null && wrappedKey != null); return new PackageKeySource(sourceKeyId, keyPurpose, derivationAlgorithm, kdfContext, encryptionAlgorithm, - unknown1, wrappedKey, encryptionAlgorithm2); + encryptionKey, wrappedKey, encryptionAlgorithm2); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs index 8670465..f90672f 100644 --- a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs @@ -19,6 +19,22 @@ public sealed record SegmentReference( bool Secondary ) { + private static SerializedAlgorithm ToSerialized(PackagingCompression compression) => compression switch + { + PackagingCompression.None => SerializedAlgorithm.None, + PackagingCompression.Deflate => SerializedAlgorithm.Deflate, + PackagingCompression.Brotli => SerializedAlgorithm.Brotli, + _ => throw new UnreachableException() + }; + + private static PackagingCompression ToCompression(SerializedAlgorithm algorithm) => algorithm switch + { + SerializedAlgorithm.None => PackagingCompression.None, + SerializedAlgorithm.Deflate => PackagingCompression.Deflate, + SerializedAlgorithm.Brotli => PackagingCompression.Brotli, + _ => throw new UnreachableException() + }; + public void Serialize(CborWriter writer) { writer.WriteStartMap(2 @@ -40,7 +56,7 @@ public void Serialize(CborWriter writer) if (Compression != 0) { writer.WriteInt32(3); - writer.WriteInt32((int)Compression); + writer.WriteEnum(ToSerialized(Compression)); writer.WriteInt32(4); writer.WriteInt32(CompressedLength); @@ -69,7 +85,7 @@ public void Serialize(CborWriter writer) if (BoxIndex is { } boxIndex) { writer.WriteInt32(9); - writer.WriteInt32(boxIndex.Value); + boxIndex.Serialize(writer); } if (BoxOffset != 0) @@ -121,7 +137,7 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i length = reader.ReadInt32(); break; case 3: - compression = (PackagingCompression)reader.ReadInt32(); + compression = ToCompression(reader.ReadEnum()); break; case 4: compressedLength = reader.ReadInt32(); @@ -134,7 +150,7 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i Debug.Assert(initialIV.HasValue); wrapIV = initialIV; - initialIV = wrapIV?.Increment(); + initialIV = wrapIV.Value.Increment(); break; //case 7: // // This is not set in normal references - the iv is gotten from the initial iv @@ -144,7 +160,7 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i boxHash = reader.ReadHash(); break; case 9: - boxIndex = new BoxIndex(reader.ReadInt32()); + boxIndex = XVC2.BoxIndex.Deserialize(reader); break; case 10: boxOffset = reader.ReadInt32(); @@ -156,8 +172,7 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i secondary = reader.ReadBoolean(); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } diff --git a/LibXboxOne/XVC2/SerializedModel/Segmentation.cs b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs index a215283..2519127 100644 --- a/LibXboxOne/XVC2/SerializedModel/Segmentation.cs +++ b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs @@ -6,12 +6,26 @@ namespace LibXboxOne.XVC2.SerializedModel; public sealed record Segmentation(SegmentationAlgorithm Algorithm, Dictionary Labels, int Algorithm2) : ISerialize { + private static SerializedAlgorithm ToSerialized(SegmentationAlgorithm algorithm) => algorithm switch + { + SegmentationAlgorithm.FastCDC => SerializedAlgorithm.FastCDC, + SegmentationAlgorithm.Fixed => SerializedAlgorithm.Fixed, + _ => throw new UnreachableException() + }; + + private static SegmentationAlgorithm ToSegmentationAlgorithm(SerializedAlgorithm algorithm) => algorithm switch + { + SerializedAlgorithm.FastCDC => SegmentationAlgorithm.FastCDC, + SerializedAlgorithm.Fixed => SegmentationAlgorithm.Fixed, + _ => throw new UnreachableException() + }; + public void Serialize(CborWriter writer) { writer.WriteStartMap(2 + (Algorithm2 != 0x301 ? 1 : 0)); writer.WriteInt32(259); - writer.WriteInt32((int)Algorithm); + writer.WriteEnum(ToSerialized(Algorithm)); writer.WriteInt32(266); writer.WriteMap(Labels); @@ -38,7 +52,7 @@ public static Segmentation Deserialize(CborReader reader) switch (key) { case 259: - algorithm = (SegmentationAlgorithm)reader.ReadInt32(); + algorithm = ToSegmentationAlgorithm(reader.ReadEnum()); break; case 266: labels = reader.ReadMap(); @@ -47,8 +61,7 @@ public static Segmentation Deserialize(CborReader reader) algorithm2 = reader.ReadInt32(); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } diff --git a/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs b/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs index 4c9e9d9..a496d75 100644 --- a/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs +++ b/LibXboxOne/XVC2/SerializedModel/SerializedPlatform.cs @@ -1,6 +1,6 @@ namespace LibXboxOne.XVC2.SerializedModel; -public enum SerializedPlatform +public enum SerializedPlatform : uint { None = 0, PC = 1, diff --git a/LibXboxOne/XVC2/SerializedModel/Version.cs b/LibXboxOne/XVC2/SerializedModel/Version.cs index 725b15d..23c2354 100644 --- a/LibXboxOne/XVC2/SerializedModel/Version.cs +++ b/LibXboxOne/XVC2/SerializedModel/Version.cs @@ -1,10 +1,16 @@ using System; -using System.Diagnostics; using System.Formats.Cbor; namespace LibXboxOne.XVC2.SerializedModel; -public record struct Version(ushort Major, ushort Minor, ushort Patch, ushort Build, Guid Id, Guid? Unknown) : ISerialize +public record struct Version( + ushort Major, + ushort Minor, + ushort Patch, + ushort Build, + Guid Id, + Guid? Unknown +) : ISerialize { public override string ToString() => $"{Major}.{Minor}.{Patch}.{Build}.{Id}"; @@ -25,12 +31,12 @@ public void Serialize(CborWriter writer) writer.WriteUInt32(Build); writer.WriteInt32(269); - writer.WriteTextString(Id.ToString()); + writer.WriteGuid(Id); if (Unknown is { } value) { writer.WriteInt32(292); - writer.WriteTextString(value.ToString()); + writer.WriteGuid(value); } writer.WriteEndMap(); @@ -38,9 +44,12 @@ public void Serialize(CborWriter writer) public static Version Deserialize(CborReader reader) { - ushort major = 0, minor = 0, patch = 0, build = 0; + ushort major = default; + ushort minor = default; + ushort patch = default; + ushort build = default; Guid id = default; - Guid? unknown = null; + Guid? unknown = default; var remaining = reader.ReadStartMap(); while (remaining-- != 0) @@ -61,14 +70,13 @@ public static Version Deserialize(CborReader reader) build = (ushort)reader.ReadUInt32(); break; case 269: - id = Guid.Parse(reader.ReadTextString()); + id = reader.ReadGuid(); break; case 292: - unknown = Guid.Parse(reader.ReadTextString()); + unknown = reader.ReadGuid(); break; default: - Debug.Assert(false); - reader.SkipValue(); + reader.AssertInvalidValue(); break; } } diff --git a/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs b/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs index a6492a5..6493742 100644 --- a/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs +++ b/LibXboxOne/XVC2/Specifiers/LogicalSpecifierType.cs @@ -2,6 +2,6 @@ public enum LogicalSpecifierType { - Unknown0 = 0, - Unknown1 = 1 + Any = 0, + All = 1 } \ No newline at end of file From c2966d760172e359243a847ac2a8e04fae02c94e Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 03:01:02 +0200 Subject: [PATCH 04/11] Add chunk detail loading --- LibXboxOne/XVC2/CborExtensions.cs | 28 ++-- LibXboxOne/XVC2/Msixvc2File.cs | 17 +++ LibXboxOne/XVC2/SerializedModel/Box.cs | 74 ++++++++++ .../XVC2/SerializedModel/ChunkDetails.cs | 82 +++++++++++ LibXboxOne/XVC2/SerializedModel/File.cs | 132 ++++++++++++++++++ LibXboxOne/XVC2/SerializedModel/Seal.cs | 50 +++++++ 6 files changed, 369 insertions(+), 14 deletions(-) create mode 100644 LibXboxOne/XVC2/SerializedModel/Box.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/File.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/Seal.cs diff --git a/LibXboxOne/XVC2/CborExtensions.cs b/LibXboxOne/XVC2/CborExtensions.cs index 5a756fe..4175b8b 100644 --- a/LibXboxOne/XVC2/CborExtensions.cs +++ b/LibXboxOne/XVC2/CborExtensions.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Formats.Cbor; using System.IO; +using LibXboxOne.XVC2.SerializedModel; namespace LibXboxOne.XVC2; @@ -11,9 +12,7 @@ public static class CborExtensions extension(CborWriter writer) { public void WriteTagEx(CborTagEx tag) - { - writer.WriteTag((CborTag)tag); - } + => writer.WriteTag((CborTag)tag); public void WriteSelfDescribeTag(CborTagEx tag) { @@ -75,15 +74,14 @@ public void WriteMap(Dictionary map) writer.WriteEndMap(); } - public void WriteGuid(Guid value) - { - writer.WriteTextString(value.ToString()); - } + public void WriteGuid(Guid value) + => writer.WriteTextString(value.ToString()); public void WriteEnum(T value) where T : Enum - { - writer.WriteInt32((int)(object)value); - } + => writer.WriteInt32((int)(object)value); + + public void WriteLabel(SerializedLabel label) + => writer.WriteInt32((int)label); } extension(CborReader reader) @@ -142,16 +140,18 @@ public Dictionary ReadMap() } public Guid ReadGuid() - { - return Guid.Parse(reader.ReadTextString()); - } + => Guid.Parse(reader.ReadTextString()); - public T ReadEnum() where T : Enum => (T)(object)reader.ReadInt32(); + public T ReadEnum() where T : Enum + => (T)(object)reader.ReadInt32(); public void AssertInvalidValue() { Debug.Assert(false); reader.SkipValue(); } + + public SerializedLabel ReadLabel() + => (SerializedLabel)reader.ReadInt32(); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs index 2255c7b..87023c9 100644 --- a/LibXboxOne/XVC2/Msixvc2File.cs +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Formats.Cbor; using System.IO; using System.IO.Compression; using LibXboxOne.XVC2.SerializedModel; +using File = System.IO.File; namespace LibXboxOne.XVC2; @@ -10,12 +12,19 @@ public sealed class Msixvc2File : IDisposable { private readonly ZipArchive _archive; private readonly Package _package; + private readonly Dictionary _chunks; public Msixvc2File(Stream stream, bool leaveOpen = false) { _archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen); _package = ReadPackageMetadata(); + + _chunks = []; + foreach (var chunk in _package.Chunks) + { + _chunks[chunk.Id] = ReadChunkDetails(chunk); + } } public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); @@ -27,6 +36,14 @@ private Package ReadPackageMetadata() return Package.Deserialize(reader); } + private ChunkDetails ReadChunkDetails(Chunk chunk) + { + var path = $"Chunks/{chunk.Id}.cbor"; + var cbor = GetEntryContent(path); + var reader = new CborReader(cbor); + return ChunkDetails.Deserialize(reader); + } + private byte[] GetEntryContent(string entryPath) { var packageEntry = _archive.GetEntry(entryPath); diff --git a/LibXboxOne/XVC2/SerializedModel/Box.cs b/LibXboxOne/XVC2/SerializedModel/Box.cs new file mode 100644 index 0000000..9f78d47 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Box.cs @@ -0,0 +1,74 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record Box(FileFormat FileFormat, string Name, List Segments) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteSelfDescribeTag(CborTagEx.XVCB); + + writer.WriteStartMap(3); + + writer.WriteLabel(SerializedLabel.FileFormat); + FileFormat.Serialize(writer); + + writer.WriteLabel(SerializedLabel.Name); + writer.WriteTextString(Name); + + writer.WriteLabel(SerializedLabel.Segments); + writer.WriteStartArray(Segments.Count); + foreach (var segment in Segments) + { + segment.Serialize(writer); + } + writer.WriteEndArray(); + + writer.WriteEndMap(); + } + + public static Box Deserialize(CborReader reader) + { + FileFormat? fileFormat = default; + string? name = default; + List? segments = default; + + reader.ReadSelfDescribeTag(CborTagEx.XVCB); + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.FileFormat: + fileFormat = FileFormat.Deserialize(reader); + break; + case SerializedLabel.Name: + name = reader.ReadTextString(); + break; + case SerializedLabel.Segments: + segments = []; + var segmentCount = reader.ReadStartArray(); + PackagingIV? iv = default; + while (segmentCount-- != 0) + { + segments.Add(SegmentReference.Deserialize(reader, ref iv)); + } + reader.ReadEndArray(); + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + + Debug.Assert(fileFormat != null && name != null && segments != null); + return new Box(fileFormat, name, segments); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs b/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs new file mode 100644 index 0000000..3fbe391 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs @@ -0,0 +1,82 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record ChunkDetails(List Files, int Id, PackagingIV? InitialIV) : IRootSerialize +{ + public string OpcPath => $"/Chunks/{Id}.cbor"; + public string OpcRelationship => "http://xbox.com/MSIXVC2/Chunk"; + + public void Serialize(CborWriter writer) + { + writer.WriteSelfDescribeTag(CborTagEx.XVCC); + + writer.WriteStartMap(2 + (InitialIV.HasValue ? 1 : 0)); + + writer.WriteLabel(SerializedLabel.Id); + writer.WriteInt32(Id); + + if (InitialIV is { } initialIV) + { + writer.WriteLabel(SerializedLabel.InitialIV); + initialIV.Serialize(writer); + } + + writer.WriteLabel(SerializedLabel.Files); + writer.WriteStartArray(Files.Count); + + var iv = InitialIV; + foreach (var file in Files) + { + file.Serialize(writer, ref iv, false); + } + writer.WriteEndArray(); + + writer.WriteEndMap(); + } + + public static ChunkDetails Deserialize(CborReader reader) + { + int id = default; + PackagingIV? initialIV = default; + List? files = default; + + reader.ReadSelfDescribeTag(CborTagEx.XVCC); + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.Id: + id = reader.ReadInt32(); + break; + case SerializedLabel.InitialIV: + initialIV = PackagingIV.Deserialize(reader); + break; + case SerializedLabel.Files: + var fileCount = reader.ReadStartArray(); + files = []; + while (fileCount-- != 0) + { + files.Add(File.Deserialize(reader, ref initialIV)); + } + reader.ReadEndArray(); + + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + + Debug.Assert(files != null); + return new ChunkDetails(files, id, initialIV); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/File.cs b/LibXboxOne/XVC2/SerializedModel/File.cs new file mode 100644 index 0000000..bdcfd64 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/File.cs @@ -0,0 +1,132 @@ +#nullable enable +using System.Collections.Generic; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record File( + int Id, + int ChunkId, + PackagingIV? IV, + long Length, + PackagingHash Hash, + bool ReadProtected, + List? Segments +) : IRootSerialize +{ + public string OpcPath => $"/Files/{Id}.cbor"; + public string OpcRelationship => "http://xbox.com/MSIXVC2/File"; + + public void Serialize(CborWriter writer) + { + PackagingIV? iv = default; + Serialize(writer, ref iv, true); + } + + public void Serialize(CborWriter writer, ref PackagingIV? initialIV, bool isStandaloneSerialize) + { + writer.WriteStartMap(3 + + (isStandaloneSerialize && ChunkId != 0 ? 1 : 0) + + (IV.HasValue ? 1 : 0) + + (ReadProtected ? 1 : 0) + + (Segments != null ? 1 : 0)); + + writer.WriteLabel(SerializedLabel.Id); + writer.WriteInt32(Id); + + if (isStandaloneSerialize && ChunkId != 0) + { + writer.WriteLabel(SerializedLabel.ChunkId); + writer.WriteInt32(ChunkId); + } + + if (IV is {} iv) + { + writer.WriteLabel(SerializedLabel.InitialIV); + iv.Serialize(writer); + } + + writer.WriteLabel(SerializedLabel.Length); + writer.WriteInt64(Length); + + writer.WriteLabel(SerializedLabel.Hash); + writer.WriteHash(Hash); + + writer.WriteLabel(SerializedLabel.ReadProtected); + writer.WriteBoolean(ReadProtected); + + if (Segments != null) + { + writer.WriteLabel(SerializedLabel.Segments); + writer.WriteStartArray(Segments.Count); + foreach (var segment in Segments) + { + segment.Serialize(writer); + } + writer.WriteEndArray(); + } + + writer.WriteEndMap(); + } + + public static File Deserialize(CborReader reader, ref PackagingIV? initialIV) + { + int id = default; + int chunkId = default; + PackagingIV? iv = default; + long length = default; + PackagingHash hash = default; + bool readProtected = default; + List? segments = default; + + var count = reader.ReadStartMap(); + + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.Id: + id = reader.ReadInt32(); + break; + case SerializedLabel.ChunkId: + chunkId = reader.ReadInt32(); + break; + case SerializedLabel.InitialIV: + iv = PackagingIV.Deserialize(reader); + break; + case SerializedLabel.Length: + length = reader.ReadInt64(); + break; + case SerializedLabel.Hash: + hash = reader.ReadHash(); + break; + case SerializedLabel.ReadProtected: + readProtected = reader.ReadBoolean(); + break; + case SerializedLabel.Segments: + segments = []; + var segmentCount = reader.ReadStartArray(); + while (segmentCount-- != 0) + { + segments.Add(SegmentReference.Deserialize(reader, ref initialIV)); + } + reader.ReadEndArray(); + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + + if (initialIV != null) + { + initialIV = iv; + } + + return new File(id, chunkId, iv, length, hash, readProtected, segments); + + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Seal.cs b/LibXboxOne/XVC2/SerializedModel/Seal.cs new file mode 100644 index 0000000..28b9e90 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/Seal.cs @@ -0,0 +1,50 @@ +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record Seal(int Target, PackagingHash Hash) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteSelfDescribeTag(CborTagEx.XVCS); + + writer.WriteStartMap(2); + + writer.WriteLabel(SerializedLabel.Target); + writer.WriteInt32(Target); + + writer.WriteLabel(SerializedLabel.Hash); + writer.WriteHash(Hash); + + writer.WriteEndMap(); + } + + public static Seal Deserialize(CborReader reader) + { + int target = default; + PackagingHash hash = default; + + reader.ReadSelfDescribeTag(CborTagEx.XVCS); + + var count = reader.ReadStartMap(); + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.Target: + target = reader.ReadInt32(); + break; + case SerializedLabel.Hash: + hash = reader.ReadHash(); + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + return new Seal(target, hash); + } +} \ No newline at end of file From d8aeb3107b296e66f5d79643c280fac6c2cd2555 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 03:23:18 +0200 Subject: [PATCH 05/11] make all deserializers use ReadLabel and rename prev unknown fields --- .../XVC2/SerializedModel/BoxReference.cs | 6 +- LibXboxOne/XVC2/SerializedModel/Chunk.cs | 147 +++++++++--------- LibXboxOne/XVC2/SerializedModel/FileFormat.cs | 50 +++--- LibXboxOne/XVC2/SerializedModel/Package.cs | 54 +++---- .../XVC2/SerializedModel/PackageKeySource.cs | 66 ++++---- .../XVC2/SerializedModel/SegmentReference.cs | 46 +++--- .../XVC2/SerializedModel/Segmentation.cs | 34 ++-- 7 files changed, 202 insertions(+), 201 deletions(-) diff --git a/LibXboxOne/XVC2/SerializedModel/BoxReference.cs b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs index 163f1fe..df1facc 100644 --- a/LibXboxOne/XVC2/SerializedModel/BoxReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/BoxReference.cs @@ -9,7 +9,7 @@ public sealed record BoxReference(string Name) : ISerialize public void Serialize(CborWriter writer) { writer.WriteStartMap(1); - writer.WriteInt32(25); + writer.WriteLabel(SerializedLabel.Name); writer.WriteTextString(Name); writer.WriteEndMap(); } @@ -21,10 +21,10 @@ public static BoxReference Deserialize(CborReader reader) var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 25: + case SerializedLabel.Name: name = reader.ReadTextString(); break; default: diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs index 2c5bdfa..910e24b 100644 --- a/LibXboxOne/XVC2/SerializedModel/Chunk.cs +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -8,16 +8,16 @@ namespace LibXboxOne.XVC2.SerializedModel; public sealed record Chunk( - IPackagingSpecifier? Specifier0, - IPackagingSpecifier? Specifier1, - IPackagingSpecifier? Specifier2, + IPackagingSpecifier? Tags, + IPackagingSpecifier? Languages, + IPackagingSpecifier? Devices, int Id, - long Value1, - bool? Unknown0, - bool? Unknown1, - int Unknown2, - int Unknown3, - SegmentReference SegmentReference + long Length, + bool? OnDemand, + bool? RequiredToLaunch, + int KeyIndex, + int BoxLength, + SegmentReference SecretReference ) : ISerialize { private static void SerializeSpecifier(CborWriter writer, IPackagingSpecifier specifier) @@ -88,116 +88,116 @@ private static IPackagingSpecifier DeserializeSpecifier(CborReader reader) public void Serialize(CborWriter writer) { writer.WriteStartMap(3 - + (Specifier0 != null ? 1 : 0) - + (Specifier1 != null ? 1 : 0) - + (Specifier2 != null ? 1 : 0) - + (Unknown0.HasValue ? 1 : 0) - + (Unknown1.HasValue ? 1 : 0) - + (Unknown2 != 0 ? 1 : 0) - + (Unknown3 != 0 ? 1 : 0)); - - writer.WriteInt32(24); + + (Tags != null ? 1 : 0) + + (Languages != null ? 1 : 0) + + (Devices != null ? 1 : 0) + + (OnDemand.HasValue ? 1 : 0) + + (RequiredToLaunch.HasValue ? 1 : 0) + + (KeyIndex != 0 ? 1 : 0) + + (BoxLength != 0 ? 1 : 0)); + + writer.WriteLabel(SerializedLabel.Id); writer.WriteInt32(Id); - if (Unknown0 is { } unknown0) + if (OnDemand is { } onDemand) { - writer.WriteInt32(36); - writer.WriteBoolean(unknown0); + writer.WriteLabel(SerializedLabel.OnDemand); + writer.WriteBoolean(onDemand); } - if (Specifier0 != null) + if (Tags != null) { - writer.WriteInt32(30); - SerializeSpecifier(writer, Specifier0); + writer.WriteLabel(SerializedLabel.Tags); + SerializeSpecifier(writer, Tags); } - if (Specifier1 != null) + if (Languages != null) { - writer.WriteInt32(31); - SerializeSpecifier(writer, Specifier1); + writer.WriteLabel(SerializedLabel.Languages); + SerializeSpecifier(writer, Languages); } - if (Specifier2 != null) + if (Devices != null) { - writer.WriteInt32(32); - SerializeSpecifier(writer, Specifier2); + writer.WriteLabel(SerializedLabel.Devices); + SerializeSpecifier(writer, Devices); } - if (Unknown1 is { } unknown1) + if (RequiredToLaunch is { } requiredToLaunch) { - writer.WriteInt32(33); - writer.WriteBoolean(unknown1); + writer.WriteLabel(SerializedLabel.RequiredToLaunch); + writer.WriteBoolean(requiredToLaunch); } - if (Unknown2 != 0) + if (KeyIndex != 0) { - writer.WriteInt32(34); - writer.WriteInt32(Unknown2); + writer.WriteLabel(SerializedLabel.KeyIndex); + writer.WriteInt32(KeyIndex); } - writer.WriteInt32(2); - writer.WriteInt64(Value1); + writer.WriteLabel(SerializedLabel.Length); + writer.WriteInt64(Length); - if (Unknown3 != 0) + if (BoxLength != 0) { - writer.WriteInt32(11); - writer.WriteInt32(Unknown3); + writer.WriteLabel(SerializedLabel.BoxLength); + writer.WriteInt32(BoxLength); } - writer.WriteInt32(26); - SegmentReference.Serialize(writer); + writer.WriteLabel(SerializedLabel.SecretReference); + SecretReference.Serialize(writer); writer.WriteEndMap(); } public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) { - IPackagingSpecifier? specifier0 = default; - IPackagingSpecifier? specifier1 = default; - IPackagingSpecifier? specifier2 = default; - bool? unknown0 = default; - bool? unknown1 = default; + IPackagingSpecifier? tags = default; + IPackagingSpecifier? languages = default; + IPackagingSpecifier? devices = default; + bool? onDemand = default; + bool? requiredToLaunch = default; int id = default; - int unknown2 = default; - long value1 = default; - int unknown3 = default; - SegmentReference? segmentReference = default; + int keyIndex = default; + long length = default; + int boxLength = default; + SegmentReference? secretReference = default; var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 24: + case SerializedLabel.Id: id = reader.ReadInt32(); break; - case 36: - unknown0 = reader.ReadBoolean(); + case SerializedLabel.OnDemand: + onDemand = reader.ReadBoolean(); break; - case 30: - specifier0 = DeserializeSpecifier(reader); + case SerializedLabel.Tags: + tags = DeserializeSpecifier(reader); break; - case 31: - specifier1 = DeserializeSpecifier(reader); + case SerializedLabel.Languages: + languages = DeserializeSpecifier(reader); break; - case 32: - specifier2 = DeserializeSpecifier(reader); + case SerializedLabel.Devices: + devices = DeserializeSpecifier(reader); break; - case 33: - unknown1 = reader.ReadBoolean(); + case SerializedLabel.RequiredToLaunch: + requiredToLaunch = reader.ReadBoolean(); break; - case 34: - unknown2 = reader.ReadInt32(); + case SerializedLabel.KeyIndex: + keyIndex = reader.ReadInt32(); break; - case 2: - value1 = reader.ReadInt64(); + case SerializedLabel.Length: + length = reader.ReadInt64(); break; - case 11: - unknown3 = reader.ReadInt32(); + case SerializedLabel.BoxLength: + boxLength = reader.ReadInt32(); break; - case 26: - segmentReference = SegmentReference.Deserialize(reader, ref initialIV); + case SerializedLabel.SecretReference: + secretReference = SegmentReference.Deserialize(reader, ref initialIV); break; default: reader.AssertInvalidValue(); @@ -207,7 +207,6 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) reader.ReadEndMap(); Debug.Assert(segmentReference != null); - return new Chunk(specifier0, specifier1, specifier2, id, value1, unknown0, unknown1, unknown2, unknown3, - segmentReference); + return new Chunk(tags, languages, devices, id, length, onDemand, requiredToLaunch, keyIndex, boxLength, secretReference); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs index 16bacc5..56dda7a 100644 --- a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs +++ b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs @@ -5,36 +5,38 @@ namespace LibXboxOne.XVC2.SerializedModel; -public record FileFormat(List Tags, int MajorVersion, int MinorVersion, int Patch) : ISerialize +public record FileFormat(List WrittenBy, int MajorVersion, int MinorVersion, int Build) : ISerialize { public System.Version Version => new(MajorVersion, MinorVersion); public override string ToString() - => $"{MajorVersion}.{MinorVersion}.{Patch}"; + => $"{MajorVersion}.{MinorVersion}.{Build} ({WrittenBy})"; public void Serialize(CborWriter writer) { - writer.WriteStartMap(2 + (Patch > 0 ? 1 : 0) + (Tags.Count > 0 ? 1 : 0)); + writer.WriteStartMap(2 + + (Build > 0 ? 1 : 0) + + (WrittenBy.Count > 0 ? 1 : 0)); - writer.WriteInt32(257); + writer.WriteLabel(SerializedLabel.MajorVersion); writer.WriteInt32(MajorVersion); - writer.WriteInt32(258); + writer.WriteLabel(SerializedLabel.MinorVersion); writer.WriteInt32(MinorVersion); - if (Patch > 0) + if (Build > 0) { - writer.WriteInt32(267); - writer.WriteInt32(Patch); + writer.WriteLabel(SerializedLabel.Build); + writer.WriteInt32(Build); } - if (Tags.Count > 0) + if (WrittenBy.Count > 0) { - writer.WriteInt32(290); - writer.WriteStartArray(Tags.Count); - foreach (var tag in Tags) + writer.WriteLabel(SerializedLabel.WrittenBy); + writer.WriteStartArray(WrittenBy.Count); + foreach (var writtenBy in WrittenBy) { - writer.WriteTextString(tag); + writer.WriteTextString(writtenBy); } writer.WriteEndArray(); } @@ -46,30 +48,30 @@ public static FileFormat Deserialize(CborReader reader) { int major = default; int minor = default; - int patch = default; - List tags = []; + int build = default; + List writtenBy = []; var remaining = reader.ReadStartMap(); while (remaining-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 257: + case SerializedLabel.MajorVersion: major = reader.ReadInt32(); break; - case 258: + case SerializedLabel.MinorVersion: minor = reader.ReadInt32(); break; - case 267: - patch = reader.ReadInt32(); + case SerializedLabel.Build: + build = reader.ReadInt32(); break; - case 290: - tags = []; + case SerializedLabel.WrittenBy: + writtenBy = []; var count = reader.ReadStartArray(); while (count-- != 0) { - tags.Add(reader.ReadTextString()); + writtenBy.Add(reader.ReadTextString()); } reader.ReadEndArray(); break; @@ -81,6 +83,6 @@ public static FileFormat Deserialize(CborReader reader) reader.ReadEndMap(); - return new FileFormat(tags, major, minor, patch); + return new FileFormat(writtenBy, major, minor, build); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/Package.cs b/LibXboxOne/XVC2/SerializedModel/Package.cs index 940d7b8..a080f53 100644 --- a/LibXboxOne/XVC2/SerializedModel/Package.cs +++ b/LibXboxOne/XVC2/SerializedModel/Package.cs @@ -31,42 +31,42 @@ public void Serialize(CborWriter writer) writer.WriteStartMap(11 + (InitialIV != null ? 1 : 0) + (Keys.Count > 0 ? 1 : 0)); - writer.WriteInt32(256); + writer.WriteLabel(SerializedLabel.FileFormat); FileFormat.Serialize(writer); if (InitialIV is {} initialIV) { - writer.WriteInt32(27); + writer.WriteLabel(SerializedLabel.InitialIV); initialIV.Serialize(writer); } - writer.WriteInt32(260); + writer.WriteLabel(SerializedLabel.ContentId); writer.WriteGuid(ContentId); - writer.WriteInt32(279); + writer.WriteLabel(SerializedLabel.FulfillmentContentId); writer.WriteGuid(FulfillmentContentId); - writer.WriteInt32(280); + writer.WriteLabel(SerializedLabel.ProductId); writer.WriteGuid(ProductId); - writer.WriteInt32(261); + writer.WriteLabel(SerializedLabel.Version); Version.Serialize(writer); - writer.WriteInt32(281); + writer.WriteLabel(SerializedLabel.MinimumSystemVersion); MinimumSystemVersion.Serialize(writer); - writer.WriteInt32(282); + writer.WriteLabel(SerializedLabel.StoreId); writer.WriteTextString(StoreId); - writer.WriteInt32(283); + writer.WriteLabel(SerializedLabel.SupportedPlatforms); writer.WriteEnum(SupportedPlatforms); - writer.WriteInt32(263); + writer.WriteLabel(SerializedLabel.Segmentation); Segmentation.Serialize(writer); if (Keys.Count > 0) { - writer.WriteInt32(262); + writer.WriteLabel(SerializedLabel.Keys); writer.WriteStartArray(Keys.Count); foreach (var key in Keys) { @@ -75,7 +75,7 @@ public void Serialize(CborWriter writer) writer.WriteEndArray(); } - writer.WriteInt32(264); + writer.WriteLabel(SerializedLabel.Boxes); writer.WriteStartArray(Boxes.Count); foreach (var box in Boxes) { @@ -83,7 +83,7 @@ public void Serialize(CborWriter writer) } writer.WriteEndArray(); - writer.WriteInt32(265); + writer.WriteLabel(SerializedLabel.Chunks); writer.WriteStartArray(Chunks.Count); foreach (var chunk in Chunks) { @@ -115,40 +115,40 @@ public static Package Deserialize(CborReader reader) var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 256: + case SerializedLabel.FileFormat: fileFormat = FileFormat.Deserialize(reader); break; - case 27: + case SerializedLabel.InitialIV: initialIV = PackagingIV.FromBytes(reader.ReadByteString()); break; - case 260: + case SerializedLabel.ContentId: contentId = reader.ReadGuid(); break; - case 279: + case SerializedLabel.FulfillmentContentId: fulfillmentContentId = reader.ReadGuid(); break; - case 280: + case SerializedLabel.ProductId: productId = reader.ReadGuid(); break; - case 261: + case SerializedLabel.Version: version = Version.Deserialize(reader); break; - case 281: + case SerializedLabel.MinimumSystemVersion: minimumSystemVersion = Version.Deserialize(reader); break; - case 282: + case SerializedLabel.StoreId: storeId = reader.ReadTextString(); break; - case 283: + case SerializedLabel.SupportedPlatforms: supportedPlatforms = (SerializedPlatform)reader.ReadUInt32(); break; - case 263: + case SerializedLabel.Segmentation: segmentation = Segmentation.Deserialize(reader); break; - case 262: + case SerializedLabel.Keys: var keyCount = reader.ReadStartArray(); keys = new List(keyCount ?? 0); while (keyCount-- != 0) @@ -157,7 +157,7 @@ public static Package Deserialize(CborReader reader) } reader.ReadEndArray(); break; - case 264: + case SerializedLabel.Boxes: var boxCount = reader.ReadStartArray(); boxes = new List(boxCount ?? 0); while (boxCount-- != 0) @@ -166,7 +166,7 @@ public static Package Deserialize(CborReader reader) } reader.ReadEndArray(); break; - case 265: + case SerializedLabel.Chunks: var chunkCount = reader.ReadStartArray(); chunks = new List(chunkCount ?? 0); while (chunkCount-- != 0) diff --git a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs index 7440385..9e35755 100644 --- a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs +++ b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs @@ -9,10 +9,10 @@ public sealed record PackageKeySource( PackagingKeyPurpose SourcePurpose, PackagingDerivationAlgorithm DerivationAlgorithm, byte[] KdfContext, - PackagingEncryptionAlgorithm EncryptionAlgorithm, - byte[]? EncryptionKey, // unverified + PackagingEncryptionAlgorithm WrapAlgorithm, + byte[]? WrapIV, // unverified byte[] WrappedKey, - PackagingEncryptionAlgorithm PackagingEncryptionAlgorithm2 + PackagingEncryptionAlgorithm Algorithm ) : ISerialize { @@ -64,34 +64,34 @@ PackagingEncryptionAlgorithm PackagingEncryptionAlgorithm2 public void Serialize(CborWriter writer) { - writer.WriteStartMap(7 + (EncryptionKey != null ? 1 : 0)); + writer.WriteStartMap(7 + (WrapIV != null ? 1 : 0)); - writer.WriteInt32(287); + writer.WriteLabel(SerializedLabel.SourcePurpose); writer.WriteEnum(ToSerialized(SourcePurpose)); - writer.WriteInt32(288); + writer.WriteLabel(SerializedLabel.SourceKeyId); writer.WriteTextString(SourceKeyId); - writer.WriteInt32(284); + writer.WriteLabel(SerializedLabel.DerivationAlgorithm); writer.WriteEnum(ToSerialized(DerivationAlgorithm)); - writer.WriteInt32(286); + writer.WriteLabel(SerializedLabel.KdfContext); writer.WriteByteString(KdfContext); - writer.WriteInt32(285); - writer.WriteEnum(ToSerialized(EncryptionAlgorithm)); + writer.WriteLabel(SerializedLabel.WrapAlgorithm); + writer.WriteEnum(ToSerialized(WrapAlgorithm)); - if (EncryptionKey != null) + if (WrapIV != null) { - writer.WriteInt32(7); - writer.WriteByteString(EncryptionKey); + writer.WriteLabel(SerializedLabel.WrapIV); + writer.WriteByteString(WrapIV); } - writer.WriteInt32(6); + writer.WriteLabel(SerializedLabel.WrappedKey); writer.WriteByteString(WrappedKey); - writer.WriteInt32(259); - writer.WriteEnum(ToSerialized(PackagingEncryptionAlgorithm2)); + writer.WriteLabel(SerializedLabel.Algorithm); + writer.WriteEnum(ToSerialized(Algorithm)); writer.WriteEndMap(); } @@ -101,41 +101,41 @@ public static PackageKeySource Deserialize(CborReader reader) string? sourceKeyId = default; PackagingKeyPurpose keyPurpose = default; PackagingDerivationAlgorithm derivationAlgorithm = default; - PackagingEncryptionAlgorithm encryptionAlgorithm = default; - PackagingEncryptionAlgorithm encryptionAlgorithm2 = default; + PackagingEncryptionAlgorithm wrapAlgorithm = default; + PackagingEncryptionAlgorithm algorithm = default; byte[]? kdfContext = default; - byte[]? encryptionKey = default; + byte[]? wrapIV = default; byte[]? wrappedKey = default; var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 287: + case SerializedLabel.SourcePurpose: keyPurpose = ToKeyPurpose(reader.ReadEnum()); break; - case 288: + case SerializedLabel.SourceKeyId: sourceKeyId = reader.ReadTextString(); break; - case 284: + case SerializedLabel.DerivationAlgorithm: derivationAlgorithm = ToDerivationAlgorithm(reader.ReadEnum()); break; - case 286: + case SerializedLabel.KdfContext: kdfContext = reader.ReadByteString(); break; - case 285: - encryptionAlgorithm = ToEncryptionAlgorithm(reader.ReadEnum()); + case SerializedLabel.WrapAlgorithm: + wrapAlgorithm = ToEncryptionAlgorithm(reader.ReadEnum()); break; - case 7: - encryptionKey = reader.ReadByteString(); + case SerializedLabel.WrapIV: + wrapIV = reader.ReadByteString(); break; - case 6: + case SerializedLabel.WrappedKey: wrappedKey = reader.ReadByteString(); break; - case 259: - encryptionAlgorithm2 = ToEncryptionAlgorithm(reader.ReadEnum()); + case SerializedLabel.Algorithm: + algorithm = ToEncryptionAlgorithm(reader.ReadEnum()); break; default: reader.AssertInvalidValue(); @@ -145,7 +145,7 @@ public static PackageKeySource Deserialize(CborReader reader) reader.ReadEndMap(); Debug.Assert(sourceKeyId != null && kdfContext != null && wrappedKey != null); - return new PackageKeySource(sourceKeyId, keyPurpose, derivationAlgorithm, kdfContext, encryptionAlgorithm, - encryptionKey, wrappedKey, encryptionAlgorithm2); + return new PackageKeySource(sourceKeyId, keyPurpose, derivationAlgorithm, kdfContext, wrapAlgorithm, + wrapIV, wrappedKey, algorithm); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs index f90672f..ae2364f 100644 --- a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs @@ -47,30 +47,30 @@ public void Serialize(CborWriter writer) + (BoxLength != 0 ? 1 : 0) + (Secondary ? 1 : 0)); - writer.WriteInt32(1); + writer.WriteLabel(SerializedLabel.Hash); writer.WriteHash(Hash); - writer.WriteInt32(2); + writer.WriteLabel(SerializedLabel.Length); writer.WriteInt32(Length); if (Compression != 0) { - writer.WriteInt32(3); + writer.WriteLabel(SerializedLabel.Compression); writer.WriteEnum(ToSerialized(Compression)); - writer.WriteInt32(4); + writer.WriteLabel(SerializedLabel.CompressedLength); writer.WriteInt32(CompressedLength); } if (EncryptionKey != null) { - writer.WriteInt32(5); + writer.WriteLabel(SerializedLabel.EncryptionKey); writer.WriteByteString(EncryptionKey); } if (WrappedKey != null) { - writer.WriteInt32(6); + writer.WriteLabel(SerializedLabel.WrappedKey); writer.WriteByteString(WrappedKey); } @@ -78,31 +78,31 @@ public void Serialize(CborWriter writer) if (BoxHash is { } boxHash) { - writer.WriteInt32(8); + writer.WriteLabel(SerializedLabel.BoxHash); writer.WriteHash(boxHash); } if (BoxIndex is { } boxIndex) { - writer.WriteInt32(9); + writer.WriteLabel(SerializedLabel.BoxIndex); boxIndex.Serialize(writer); } if (BoxOffset != 0) { - writer.WriteInt32(10); + writer.WriteLabel(SerializedLabel.BoxOffset); writer.WriteInt32(BoxOffset); } if (BoxLength != 0) { - writer.WriteInt32(11); + writer.WriteLabel(SerializedLabel.BoxLength); writer.WriteInt32(BoxLength); } if (Secondary) { - writer.WriteInt32(12); + writer.WriteLabel(SerializedLabel.Secondary); writer.WriteBoolean(Secondary); } @@ -127,25 +127,25 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 1: + case SerializedLabel.Hash: hash = reader.ReadHash(); break; - case 2: + case SerializedLabel.Length: length = reader.ReadInt32(); break; - case 3: + case SerializedLabel.Compression: compression = ToCompression(reader.ReadEnum()); break; - case 4: + case SerializedLabel.CompressedLength: compressedLength = reader.ReadInt32(); break; - case 5: + case SerializedLabel.EncryptionKey: encryptionKey = reader.ReadByteString(); break; - case 6: + case SerializedLabel.WrappedKey: wrappedKey = reader.ReadByteString(); Debug.Assert(initialIV.HasValue); @@ -156,19 +156,19 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i // // This is not set in normal references - the iv is gotten from the initial iv // wrapIV = PackagingIV.FromBytes(reader.ReadByteString()); // break; - case 8: + case SerializedLabel.BoxHash: boxHash = reader.ReadHash(); break; - case 9: + case SerializedLabel.BoxIndex: boxIndex = XVC2.BoxIndex.Deserialize(reader); break; - case 10: + case SerializedLabel.BoxOffset: boxOffset = reader.ReadInt32(); break; - case 11: + case SerializedLabel.BoxLength: boxLength = reader.ReadInt32(); break; - case 12: + case SerializedLabel.Secondary: secondary = reader.ReadBoolean(); break; default: diff --git a/LibXboxOne/XVC2/SerializedModel/Segmentation.cs b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs index 2519127..74dde12 100644 --- a/LibXboxOne/XVC2/SerializedModel/Segmentation.cs +++ b/LibXboxOne/XVC2/SerializedModel/Segmentation.cs @@ -4,7 +4,7 @@ namespace LibXboxOne.XVC2.SerializedModel; -public sealed record Segmentation(SegmentationAlgorithm Algorithm, Dictionary Labels, int Algorithm2) : ISerialize +public sealed record Segmentation(SegmentationAlgorithm Algorithm, Dictionary Options, int HashAlgorithm) : ISerialize { private static SerializedAlgorithm ToSerialized(SegmentationAlgorithm algorithm) => algorithm switch { @@ -22,18 +22,18 @@ public sealed record Segmentation(SegmentationAlgorithm Algorithm, Dictionary(); - var algorithm2 = 0x301; + var options = new Dictionary(); + var hashAlgorithm = 0x301; var count = reader.ReadStartMap(); while (count-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 259: + case SerializedLabel.Algorithm: algorithm = ToSegmentationAlgorithm(reader.ReadEnum()); break; - case 266: - labels = reader.ReadMap(); + case SerializedLabel.Options: + options = reader.ReadMap(); break; - case 291: - algorithm2 = reader.ReadInt32(); + case SerializedLabel.HashAlgorithm: + hashAlgorithm = reader.ReadInt32(); break; default: reader.AssertInvalidValue(); @@ -68,6 +68,6 @@ public static Segmentation Deserialize(CborReader reader) reader.ReadEndMap(); - return new Segmentation(algorithm, labels, algorithm2); + return new Segmentation(algorithm, options, hashAlgorithm); } } \ No newline at end of file From 2333bf9dddcbe615fa393170ca1ef96b2ed6666c Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 03:43:10 +0200 Subject: [PATCH 06/11] add chunk and file secret --- LibXboxOne/XVC2/SerializedModel/Chunk.cs | 2 +- .../SerializedModel/ChunkDetailsSecret.cs | 71 +++++++++++++++++++ LibXboxOne/XVC2/SerializedModel/FileSecret.cs | 45 ++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 LibXboxOne/XVC2/SerializedModel/ChunkDetailsSecret.cs create mode 100644 LibXboxOne/XVC2/SerializedModel/FileSecret.cs diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs index 910e24b..e99bd55 100644 --- a/LibXboxOne/XVC2/SerializedModel/Chunk.cs +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -206,7 +206,7 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) } reader.ReadEndMap(); - Debug.Assert(segmentReference != null); + Debug.Assert(secretReference != null); return new Chunk(tags, languages, devices, id, length, onDemand, requiredToLaunch, keyIndex, boxLength, secretReference); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/ChunkDetailsSecret.cs b/LibXboxOne/XVC2/SerializedModel/ChunkDetailsSecret.cs new file mode 100644 index 0000000..7b3d86c --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/ChunkDetailsSecret.cs @@ -0,0 +1,71 @@ +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record ChunkDetailsSecret(int Id, List Files) : IRootSerialize +{ + public string OpcPath => $"/Chunks/{Id}-secret.cbor"; + public string OpcRelationship => "http://xbox.com/MSIXVC2/ChunkSecret"; + + public void Serialize(CborWriter writer) + { + writer.WriteSelfDescribeTag(CborTagEx.XVCC); + + writer.WriteStartMap(2); + + writer.WriteLabel(SerializedLabel.Id); + writer.WriteInt32(Id); + + writer.WriteLabel(SerializedLabel.Files); + writer.WriteStartArray(Files.Count); + foreach (var file in Files) + { + file.Serialize(writer); + } + writer.WriteEndArray(); + + writer.WriteEndMap(); + } + + public static ChunkDetailsSecret Deserialize(CborReader reader) + { + reader.ReadSelfDescribeTag(CborTagEx.XVCC); + + int id = default; + List? files = default; + + var count = reader.ReadStartMap(); + + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.Id: + id = reader.ReadInt32(); + break; + case SerializedLabel.Files: + files = []; + var fileCount = reader.ReadStartArray(); + while (fileCount-- != 0) + { + files.Add(FileSecret.Deserialize(reader)); + } + reader.ReadEndArray(); + + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + + Debug.Assert(files != null); + return new ChunkDetailsSecret(id, files); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/FileSecret.cs b/LibXboxOne/XVC2/SerializedModel/FileSecret.cs new file mode 100644 index 0000000..f48d149 --- /dev/null +++ b/LibXboxOne/XVC2/SerializedModel/FileSecret.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Diagnostics; +using System.Formats.Cbor; + +namespace LibXboxOne.XVC2.SerializedModel; + +public sealed record FileSecret(string FileName) : ISerialize +{ + public void Serialize(CborWriter writer) + { + writer.WriteStartMap(1); + + // I think this is a typo, and they meant to use SerializedLabel.FileName here + writer.WriteLabel(SerializedLabel.Name); + writer.WriteTextString(FileName); + + writer.WriteEndMap(); + } + + public static FileSecret Deserialize(CborReader reader) + { + string? name = default; + + var count = reader.ReadStartMap(); + + while (count-- != 0) + { + var key = reader.ReadLabel(); + switch (key) + { + case SerializedLabel.Name: + name = reader.ReadTextString(); + break; + default: + reader.AssertInvalidValue(); + break; + } + } + + reader.ReadEndMap(); + + Debug.Assert(name != null); + return new FileSecret(name); + } +} \ No newline at end of file From d1379ecc7cb2edf02e4be9becf7778119f126dc7 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 04:40:10 +0200 Subject: [PATCH 07/11] Implement filename retrieval and file extraction --- LibXboxOne/LibXboxOne.csproj | 1 + LibXboxOne/XVC2/Msixvc2File.Content.cs | 153 ++++++++++++++++++ LibXboxOne/XVC2/Msixvc2File.cs | 29 +++- LibXboxOne/XVC2/PackagingIV.cs | 3 +- LibXboxOne/XVC2/SerializedModel/FileFormat.cs | 1 - .../XVC2/SerializedModel/SegmentReference.cs | 22 +-- 6 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 LibXboxOne/XVC2/Msixvc2File.Content.cs diff --git a/LibXboxOne/LibXboxOne.csproj b/LibXboxOne/LibXboxOne.csproj index f664556..f0ff9d3 100644 --- a/LibXboxOne/LibXboxOne.csproj +++ b/LibXboxOne/LibXboxOne.csproj @@ -2,6 +2,7 @@ net10.0 + True diff --git a/LibXboxOne/XVC2/Msixvc2File.Content.cs b/LibXboxOne/XVC2/Msixvc2File.Content.cs new file mode 100644 index 0000000..55ddc0e --- /dev/null +++ b/LibXboxOne/XVC2/Msixvc2File.Content.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Security.Cryptography; +using LibXboxOne.XVC2.SerializedModel; + +namespace LibXboxOne.XVC2; + +public partial class Msixvc2File +{ + private readonly Dictionary _cachedBoxEntryStreams = []; + + public byte[] GetFileContent(string filePath) + { + var file = _files[filePath]; + var chunk = _chunks[file.ChunkId]; + + var fileContent = new byte[file.Length]; + var currentOffset = 0; + + if (file.Segments != null) + { + foreach (var segment in file.Segments) + { + ReadSegmentContent(segment, chunk.KeyIndex, fileContent.AsSpan(currentOffset, segment.Length)); + currentOffset += segment.Length; + } + } + + if (!ValidateHash(fileContent, file.Hash)) + throw new InvalidDataException("Failed to validate file hash"); + + return fileContent; + } + + public byte[] GetSegmentContent(SegmentReference segment, int keyId) + { + var content = new byte[segment.Length]; + ReadSegmentContent(segment, keyId, content); + return content; + } + + public void ReadSegmentContent(SegmentReference segment, int keyId, Span content) + { + var boxContent = new byte[segment.BoxLength]; + + ReadBoxContent(segment.BoxIndex, segment.BoxOffset, boxContent); + + if (!ValidateHash(boxContent, segment.BoxHash)) + throw new InvalidDataException("Failed to verify box content hash"); + + if (_package.Keys.Count != 0) + { + throw new NotImplementedException("Decryption not yet implemented"); + } + + if (segment.Compression != PackagingCompression.None) + { + DecompressContent(boxContent, content, segment.Compression); + } + else + { + boxContent.AsSpan(0, segment.Length).CopyTo(content); + } + + if (!ValidateHash(content, segment.Hash)) + throw new InvalidDataException("Failed to verify decompressed content hash"); + } + + private static void DecompressContent(ReadOnlySpan compressed, Span decompressed, + PackagingCompression compression) + { + switch (compression) + { + case PackagingCompression.Deflate: + { + unsafe + { + fixed (byte* compressedPtr = compressed, decompressedPtr = decompressed) + { + using var input = new UnmanagedMemoryStream(compressedPtr, compressed.Length); + using var deflateStream = new DeflateStream(input, CompressionMode.Decompress); + using var output = new UnmanagedMemoryStream(decompressedPtr, decompressed.Length); + deflateStream.CopyTo(output); + } + } + break; + } + case PackagingCompression.Brotli: + BrotliDecoder.TryDecompress(compressed, decompressed, out _); + break; + default: + throw new UnreachableException(); + } + } + + private static bool ValidateHash(ReadOnlySpan content, PackagingHash hash) + { + switch (hash.Algorithm) + { + case PackagingHashAlgorithm.None: + return true; + case PackagingHashAlgorithm.SHA256: + return SHA256.HashData(content).SequenceEqual(hash.Hash); + case PackagingHashAlgorithm.SHA384: + return SHA384.HashData(content).SequenceEqual(hash.Hash); + case PackagingHashAlgorithm.SHA512: + return SHA512.HashData(content).SequenceEqual(hash.Hash); + default: + Debug.Assert(false); + return false; + } + } + + private void ReadBoxContent(BoxIndex boxIndex, int offset, Span content) + { + var stream = GetBoxEntryStream(boxIndex); + stream.Seek(offset, SeekOrigin.Begin); + stream.ReadExactly(content); + } + + private Stream GetBoxEntryStream(BoxIndex boxIndex) + { + if (_cachedBoxEntryStreams.TryGetValue(boxIndex, out var cachedStream)) + return cachedStream; + + var boxName = _package.Boxes[boxIndex.Value].Name; + var boxPath = $"Boxes/{boxName}"; + + var boxEntry = _archive.GetEntry(boxPath); + if (boxEntry == null) + throw new InvalidOperationException($"Failed to open box {boxPath}"); + + var boxEntryStream = boxEntry.Open(); + + if (!boxEntryStream.CanSeek) + { + var cachedContent = new byte[boxEntry.Length]; + var stream = new MemoryStream(cachedContent); + + boxEntryStream.CopyTo(stream); + boxEntryStream.Dispose(); + + stream.Position = 0; + boxEntryStream = stream; + } + + _cachedBoxEntryStreams[boxIndex] = boxEntryStream; + return boxEntryStream; + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs index 87023c9..5ee9707 100644 --- a/LibXboxOne/XVC2/Msixvc2File.cs +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -8,11 +8,15 @@ namespace LibXboxOne.XVC2; -public sealed class Msixvc2File : IDisposable +public sealed partial class Msixvc2File : IDisposable { private readonly ZipArchive _archive; private readonly Package _package; - private readonly Dictionary _chunks; + + private readonly Dictionary _chunks = []; + private readonly Dictionary _chunkDetails = []; + private readonly Dictionary _chunkSecrets = []; + private readonly Dictionary _files = []; public Msixvc2File(Stream stream, bool leaveOpen = false) @@ -20,15 +24,32 @@ public Msixvc2File(Stream stream, bool leaveOpen = false) _archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen); _package = ReadPackageMetadata(); - _chunks = []; foreach (var chunk in _package.Chunks) { - _chunks[chunk.Id] = ReadChunkDetails(chunk); + _chunks[chunk.Id] = chunk; + _chunkDetails[chunk.Id] = ReadChunkDetails(chunk); } } public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); + public void LoadFileNames() + { + foreach (var chunk in _chunks.Values) + { + var chunkSecretContent = GetSegmentContent(chunk.SecretReference, chunk.KeyIndex); + var reader = new CborReader(chunkSecretContent); + _chunkSecrets[chunk.Id] = ChunkDetailsSecret.Deserialize(reader); + + for (int i = 0; i < _chunkDetails[chunk.Id].Files.Count; i++) + { + var file = _chunkDetails[chunk.Id].Files[i]; + var fileName = _chunkSecrets[chunk.Id].Files[i].FileName; + _files[fileName] = file with { ChunkId = chunk.Id }; + } + } + } + private Package ReadPackageMetadata() { var metadataCbor = GetEntryContent("XboxPackage.cbor"); diff --git a/LibXboxOne/XVC2/PackagingIV.cs b/LibXboxOne/XVC2/PackagingIV.cs index f70b9e3..c89f3e1 100644 --- a/LibXboxOne/XVC2/PackagingIV.cs +++ b/LibXboxOne/XVC2/PackagingIV.cs @@ -1,5 +1,4 @@ -using Org.BouncyCastle.Utilities; -using System; +using System; using System.Buffers.Binary; using System.Buffers.Text; using System.Formats.Cbor; diff --git a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs index 56dda7a..dac49f9 100644 --- a/LibXboxOne/XVC2/SerializedModel/FileFormat.cs +++ b/LibXboxOne/XVC2/SerializedModel/FileFormat.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.Generic; -using System.Diagnostics; using System.Formats.Cbor; namespace LibXboxOne.XVC2.SerializedModel; diff --git a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs index ae2364f..4eae09a 100644 --- a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs @@ -12,8 +12,8 @@ public sealed record SegmentReference( byte[]? EncryptionKey, byte[]? WrappedKey, PackagingIV? WrapIV, - PackagingHash? BoxHash, - BoxIndex? BoxIndex, + PackagingHash BoxHash, + BoxIndex BoxIndex, int BoxOffset, int BoxLength, bool Secondary @@ -41,8 +41,8 @@ public void Serialize(CborWriter writer) + (Compression != 0 ? 2 : 0) + (EncryptionKey != null ? 1 : 0) + (WrappedKey != null ? 1 : 0) - + (BoxHash.HasValue ? 1 : 0) - + (BoxIndex.HasValue ? 1 : 0) + + (BoxHash != default ? 1 : 0) + + (BoxIndex != default ? 1 : 0) + (BoxOffset != 0 ? 1 : 0) + (BoxLength != 0 ? 1 : 0) + (Secondary ? 1 : 0)); @@ -76,16 +76,16 @@ public void Serialize(CborWriter writer) // 7 is probably WrapIV? is this used? - if (BoxHash is { } boxHash) + if (BoxHash != default) { writer.WriteLabel(SerializedLabel.BoxHash); - writer.WriteHash(boxHash); + writer.WriteHash(BoxHash); } - if (BoxIndex is { } boxIndex) + if (BoxIndex != default) { writer.WriteLabel(SerializedLabel.BoxIndex); - boxIndex.Serialize(writer); + BoxIndex.Serialize(writer); } if (BoxOffset != 0) @@ -118,8 +118,8 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i byte[]? encryptionKey = default; byte[]? wrappedKey = default; PackagingIV? wrapIV = default; - PackagingHash? boxHash = default; - BoxIndex? boxIndex = default; + PackagingHash boxHash = default; + BoxIndex boxIndex = default; int boxOffset = default; int boxLength = default; bool secondary = default; @@ -160,7 +160,7 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i boxHash = reader.ReadHash(); break; case SerializedLabel.BoxIndex: - boxIndex = XVC2.BoxIndex.Deserialize(reader); + boxIndex = BoxIndex.Deserialize(reader); break; case SerializedLabel.BoxOffset: boxOffset = reader.ReadInt32(); From 3903bf57c6f6d3bf9807ffe4603247bc3a10d01d Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 14:50:52 +0200 Subject: [PATCH 08/11] add box manifest verification + parsing, fixup more ints ot enums --- LibXboxOne/XVC2/CborExtensions.cs | 18 +++---- LibXboxOne/XVC2/CborTagEx.cs | 8 +-- LibXboxOne/XVC2/Msixvc2File.cs | 56 +++++++++++++++++++- LibXboxOne/XVC2/SerializedModel/Chunk.cs | 20 ++++---- LibXboxOne/XVC2/SerializedModel/Seal.cs | 12 ++--- LibXboxOne/XVC2/SerializedModel/Version.cs | 60 +++++++++++----------- 6 files changed, 113 insertions(+), 61 deletions(-) diff --git a/LibXboxOne/XVC2/CborExtensions.cs b/LibXboxOne/XVC2/CborExtensions.cs index 4175b8b..8c472df 100644 --- a/LibXboxOne/XVC2/CborExtensions.cs +++ b/LibXboxOne/XVC2/CborExtensions.cs @@ -24,13 +24,13 @@ public void WriteHash(PackagingHash hash) { var tag = hash.Algorithm switch { - PackagingHashAlgorithm.SHA256 => 0x486C, - PackagingHashAlgorithm.SHA384 => 0x4851, - PackagingHashAlgorithm.SHA512 => 0x4850, + PackagingHashAlgorithm.SHA256 => CborTagEx.SHA256, + PackagingHashAlgorithm.SHA384 => CborTagEx.SHA384, + PackagingHashAlgorithm.SHA512 => CborTagEx.SHA512, _ => throw new UnreachableException() }; - writer.WriteTag((CborTag)tag); + writer.WriteTagEx(tag); writer.WriteByteString(hash.Hash); } @@ -102,12 +102,12 @@ public void ReadSelfDescribeTag(CborTagEx tag) public PackagingHash ReadHash() { - var tagType = reader.ReadTag(); - var type = (int)tagType switch + var tagType = reader.ReadTagEx(); + var type = tagType switch { - 0x486C => PackagingHashAlgorithm.SHA256, - 0x4851 => PackagingHashAlgorithm.SHA384, - 0x4850 => PackagingHashAlgorithm.SHA512, + CborTagEx.SHA256 => PackagingHashAlgorithm.SHA256, + CborTagEx.SHA384 => PackagingHashAlgorithm.SHA384, + CborTagEx.SHA512 => PackagingHashAlgorithm.SHA512, _ => throw new UnreachableException() }; diff --git a/LibXboxOne/XVC2/CborTagEx.cs b/LibXboxOne/XVC2/CborTagEx.cs index 8ecd4ae..6fe543f 100644 --- a/LibXboxOne/XVC2/CborTagEx.cs +++ b/LibXboxOne/XVC2/CborTagEx.cs @@ -16,13 +16,13 @@ public enum CborTagEx LogicalAny = 32871, LogicalAll = 32872, XVC2 = 1482048306, - XVCB = 1482048322, - XVCC = 1482048323, + XVCB = 1482048322, // Box + XVCC = 1482048323, // Chunk XVCD = 1482048324, XVCE = 1482048325, XVCF = 1482048326, XVCI = 1482048329, - XVCP = 1482048336, + XVCP = 1482048336, // Package XVCS = 1482048339, - XVCZ = 1482048346, + XVCZ = 1482048346, // Seal } \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs index 5ee9707..157fe3b 100644 --- a/LibXboxOne/XVC2/Msixvc2File.cs +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -3,6 +3,7 @@ using System.Formats.Cbor; using System.IO; using System.IO.Compression; +using System.Runtime.InteropServices; using LibXboxOne.XVC2.SerializedModel; using File = System.IO.File; @@ -17,7 +18,9 @@ public sealed partial class Msixvc2File : IDisposable private readonly Dictionary _chunkDetails = []; private readonly Dictionary _chunkSecrets = []; private readonly Dictionary _files = []; + private readonly Dictionary _boxes = []; + public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); public Msixvc2File(Stream stream, bool leaveOpen = false) { @@ -29,9 +32,13 @@ public Msixvc2File(Stream stream, bool leaveOpen = false) _chunks[chunk.Id] = chunk; _chunkDetails[chunk.Id] = ReadChunkDetails(chunk); } - } - public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); + for (int i = 0; i < _package.Boxes.Count; i++) + { + var boxIndex = new BoxIndex(i); + _boxes[boxIndex] = ReadBoxManifest(boxIndex); + } + } public void LoadFileNames() { @@ -50,6 +57,51 @@ public void LoadFileNames() } } + private Box ReadBoxManifest(BoxIndex index) + { + var entry = GetBoxEntryStream(index); + + entry.Position = 0; + var header = (stackalloc byte[8]); + entry.ReadExactly(header); + + if (!header.SequenceEqual("XBOXBOX\0"u8)) + throw new InvalidDataException("Invalid box header"); + + entry.Position = entry.Length - 8 - 4; + + var manifestOffset = 0uL; + var manifestLength = 0u; + + entry.ReadExactly(MemoryMarshal.AsBytes(new Span(ref manifestOffset))); + entry.ReadExactly(MemoryMarshal.AsBytes(new Span(ref manifestLength))); + + var manifestEndOffset = long.CreateChecked(manifestLength + manifestOffset); + if (manifestEndOffset > entry.Length) + throw new InvalidDataException("Invalid box metadata offset or length"); + + entry.Position = long.CreateChecked(manifestOffset); + var manifestContent = new byte[manifestLength]; + entry.ReadExactly(manifestContent); + + var sealLength = entry.Length - (manifestEndOffset + 8 + 4); + var sealContent = new byte[sealLength]; + entry.ReadExactly(sealContent); + + var sealReader = new CborReader(sealContent); + var seal = Seal.Deserialize(sealReader); + if (seal.Target != CborTagEx.XVCB) + throw new InvalidDataException("Seal did not seal box manifest"); + + if (!ValidateHash(manifestContent, seal.Hash)) + throw new InvalidDataException("Failed to validate box manifest hash"); + + var reader = new CborReader(manifestContent); + var box = Box.Deserialize(reader); + + return box; + } + private Package ReadPackageMetadata() { var metadataCbor = GetEntryContent("XboxPackage.cbor"); diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs index e99bd55..2842ac9 100644 --- a/LibXboxOne/XVC2/SerializedModel/Chunk.cs +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -13,8 +13,8 @@ public sealed record Chunk( IPackagingSpecifier? Devices, int Id, long Length, - bool? OnDemand, - bool? RequiredToLaunch, + bool OnDemand, + bool RequiredToLaunch, int KeyIndex, int BoxLength, SegmentReference SecretReference @@ -91,18 +91,18 @@ public void Serialize(CborWriter writer) + (Tags != null ? 1 : 0) + (Languages != null ? 1 : 0) + (Devices != null ? 1 : 0) - + (OnDemand.HasValue ? 1 : 0) - + (RequiredToLaunch.HasValue ? 1 : 0) + + (OnDemand ? 1 : 0) + + (RequiredToLaunch ? 1 : 0) + (KeyIndex != 0 ? 1 : 0) + (BoxLength != 0 ? 1 : 0)); writer.WriteLabel(SerializedLabel.Id); writer.WriteInt32(Id); - if (OnDemand is { } onDemand) + if (OnDemand) { writer.WriteLabel(SerializedLabel.OnDemand); - writer.WriteBoolean(onDemand); + writer.WriteBoolean(OnDemand); } if (Tags != null) @@ -123,10 +123,10 @@ public void Serialize(CborWriter writer) SerializeSpecifier(writer, Devices); } - if (RequiredToLaunch is { } requiredToLaunch) + if (RequiredToLaunch) { writer.WriteLabel(SerializedLabel.RequiredToLaunch); - writer.WriteBoolean(requiredToLaunch); + writer.WriteBoolean(RequiredToLaunch); } if (KeyIndex != 0) @@ -155,8 +155,8 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) IPackagingSpecifier? tags = default; IPackagingSpecifier? languages = default; IPackagingSpecifier? devices = default; - bool? onDemand = default; - bool? requiredToLaunch = default; + bool onDemand = default; + bool requiredToLaunch = default; int id = default; int keyIndex = default; long length = default; diff --git a/LibXboxOne/XVC2/SerializedModel/Seal.cs b/LibXboxOne/XVC2/SerializedModel/Seal.cs index 28b9e90..a3efb8d 100644 --- a/LibXboxOne/XVC2/SerializedModel/Seal.cs +++ b/LibXboxOne/XVC2/SerializedModel/Seal.cs @@ -2,16 +2,16 @@ namespace LibXboxOne.XVC2.SerializedModel; -public sealed record Seal(int Target, PackagingHash Hash) : ISerialize +public sealed record Seal(CborTagEx Target, PackagingHash Hash) : ISerialize { public void Serialize(CborWriter writer) { - writer.WriteSelfDescribeTag(CborTagEx.XVCS); + writer.WriteSelfDescribeTag(CborTagEx.XVCZ); writer.WriteStartMap(2); writer.WriteLabel(SerializedLabel.Target); - writer.WriteInt32(Target); + writer.WriteEnum(Target); writer.WriteLabel(SerializedLabel.Hash); writer.WriteHash(Hash); @@ -21,10 +21,10 @@ public void Serialize(CborWriter writer) public static Seal Deserialize(CborReader reader) { - int target = default; + CborTagEx target = default; PackagingHash hash = default; - reader.ReadSelfDescribeTag(CborTagEx.XVCS); + reader.ReadSelfDescribeTag(CborTagEx.XVCZ); var count = reader.ReadStartMap(); while (count-- != 0) @@ -33,7 +33,7 @@ public static Seal Deserialize(CborReader reader) switch (key) { case SerializedLabel.Target: - target = reader.ReadInt32(); + target = reader.ReadEnum(); break; case SerializedLabel.Hash: hash = reader.ReadHash(); diff --git a/LibXboxOne/XVC2/SerializedModel/Version.cs b/LibXboxOne/XVC2/SerializedModel/Version.cs index 23c2354..5fb9965 100644 --- a/LibXboxOne/XVC2/SerializedModel/Version.cs +++ b/LibXboxOne/XVC2/SerializedModel/Version.cs @@ -6,36 +6,36 @@ namespace LibXboxOne.XVC2.SerializedModel; public record struct Version( ushort Major, ushort Minor, - ushort Patch, ushort Build, - Guid Id, - Guid? Unknown + ushort Revision, + Guid BuildId, + Guid? OriginalBuildId ) : ISerialize { - public override string ToString() => $"{Major}.{Minor}.{Patch}.{Build}.{Id}"; + public override string ToString() => $"{Major}.{Minor}.{Build}.{Revision}.{BuildId}"; public void Serialize(CborWriter writer) { - writer.WriteStartMap(5 + (Unknown.HasValue ? 1 : 0)); + writer.WriteStartMap(5 + (OriginalBuildId.HasValue ? 1 : 0)); - writer.WriteInt32(257); + writer.WriteLabel(SerializedLabel.MajorVersion); writer.WriteUInt32(Major); - writer.WriteInt32(258); + writer.WriteLabel(SerializedLabel.MinorVersion); writer.WriteUInt32(Minor); - writer.WriteInt32(267); - writer.WriteUInt32(Patch); - - writer.WriteInt32(268); + writer.WriteLabel(SerializedLabel.Build); writer.WriteUInt32(Build); - writer.WriteInt32(269); - writer.WriteGuid(Id); + writer.WriteLabel(SerializedLabel.Revision); + writer.WriteUInt32(Revision); + + writer.WriteLabel(SerializedLabel.BuildId); + writer.WriteGuid(BuildId); - if (Unknown is { } value) + if (OriginalBuildId is { } value) { - writer.WriteInt32(292); + writer.WriteLabel(SerializedLabel.OriginalBuildId); writer.WriteGuid(value); } @@ -46,34 +46,34 @@ public static Version Deserialize(CborReader reader) { ushort major = default; ushort minor = default; - ushort patch = default; ushort build = default; - Guid id = default; - Guid? unknown = default; + ushort revision = default; + Guid buildId = default; + Guid? originalBuildId = default; var remaining = reader.ReadStartMap(); while (remaining-- != 0) { - var key = reader.ReadInt32(); + var key = reader.ReadLabel(); switch (key) { - case 257: + case SerializedLabel.MajorVersion: major = (ushort)reader.ReadUInt32(); break; - case 258: + case SerializedLabel.MinorVersion: minor = (ushort)reader.ReadUInt32(); break; - case 267: - patch = (ushort)reader.ReadUInt32(); - break; - case 268: + case SerializedLabel.Build: build = (ushort)reader.ReadUInt32(); break; - case 269: - id = reader.ReadGuid(); + case SerializedLabel.Revision: + revision = (ushort)reader.ReadUInt32(); + break; + case SerializedLabel.BuildId: + buildId = reader.ReadGuid(); break; - case 292: - unknown = reader.ReadGuid(); + case SerializedLabel.OriginalBuildId: + originalBuildId = reader.ReadGuid(); break; default: reader.AssertInvalidValue(); @@ -83,6 +83,6 @@ public static Version Deserialize(CborReader reader) reader.ReadEndMap(); - return new Version(major, minor, patch, build, id, unknown); + return new Version(major, minor, build, revision, buildId, originalBuildId); } } \ No newline at end of file From e4044870083a201923e4f86c2692a817c5a05bb3 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 17:10:41 +0200 Subject: [PATCH 09/11] Implement support for encrypted MSIXVC2 --- LibXboxOne/XVC2/Msixvc2File.Content.cs | 50 ++++----- LibXboxOne/XVC2/Msixvc2File.Crypto.cs | 48 ++++++++ LibXboxOne/XVC2/Msixvc2File.cs | 10 +- LibXboxOne/XVC2/PackagingDerivationKey.cs | 67 +++++++++++ LibXboxOne/XVC2/PackagingEncryptionKey.cs | 104 ++++++++++++++++++ LibXboxOne/XVC2/PackagingIV.cs | 14 ++- LibXboxOne/XVC2/SerializedModel/Chunk.cs | 4 +- .../XVC2/SerializedModel/ChunkDetails.cs | 16 +-- LibXboxOne/XVC2/SerializedModel/File.cs | 12 +- .../XVC2/SerializedModel/PackageKeySource.cs | 21 ++-- .../XVC2/SerializedModel/SegmentReference.cs | 8 +- 11 files changed, 287 insertions(+), 67 deletions(-) create mode 100644 LibXboxOne/XVC2/Msixvc2File.Crypto.cs create mode 100644 LibXboxOne/XVC2/PackagingDerivationKey.cs create mode 100644 LibXboxOne/XVC2/PackagingEncryptionKey.cs diff --git a/LibXboxOne/XVC2/Msixvc2File.Content.cs b/LibXboxOne/XVC2/Msixvc2File.Content.cs index 55ddc0e..9a19e69 100644 --- a/LibXboxOne/XVC2/Msixvc2File.Content.cs +++ b/LibXboxOne/XVC2/Msixvc2File.Content.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -24,7 +25,7 @@ public byte[] GetFileContent(string filePath) { foreach (var segment in file.Segments) { - ReadSegmentContent(segment, chunk.KeyIndex, fileContent.AsSpan(currentOffset, segment.Length)); + ReadSegmentContent(segment, chunk.KeyIndex, fileContent.AsSpan(currentOffset, segment.Length), PackagingKeyPurpose.Content); currentOffset += segment.Length; } } @@ -35,14 +36,14 @@ public byte[] GetFileContent(string filePath) return fileContent; } - public byte[] GetSegmentContent(SegmentReference segment, int keyId) + public byte[] GetSegmentContent(SegmentReference segment, int keyId, PackagingKeyPurpose purpose) { var content = new byte[segment.Length]; - ReadSegmentContent(segment, keyId, content); + ReadSegmentContent(segment, keyId, content, purpose); return content; } - public void ReadSegmentContent(SegmentReference segment, int keyId, Span content) + public void ReadSegmentContent(SegmentReference segment, int keyId, Span content, PackagingKeyPurpose purpose) { var boxContent = new byte[segment.BoxLength]; @@ -51,14 +52,20 @@ public void ReadSegmentContent(SegmentReference segment, int keyId, Span c if (!ValidateHash(boxContent, segment.BoxHash)) throw new InvalidDataException("Failed to verify box content hash"); - if (_package.Keys.Count != 0) + if (segment.EncryptionKey != null || segment.WrappedKey != null) { - throw new NotImplementedException("Decryption not yet implemented"); + var decrypted = new byte[segment.BoxLength]; + + DecryptContent(boxContent, segment.Hash.Hash, decrypted, keyId, segment.EncryptionKey, segment.WrappedKey, + segment.WrapIV, purpose); + + boxContent = decrypted; } if (segment.Compression != PackagingCompression.None) { - DecompressContent(boxContent, content, segment.Compression); + var compressed = boxContent.AsSpan(0, segment.CompressedLength); + DecompressContent(compressed, content, segment.Compression); } else { @@ -89,7 +96,9 @@ private static void DecompressContent(ReadOnlySpan compressed, Span break; } case PackagingCompression.Brotli: - BrotliDecoder.TryDecompress(compressed, decompressed, out _); + if (!BrotliDecoder.TryDecompress(compressed, decompressed, out _)) + throw new InvalidDataException("Brotli decompression failed"); + break; default: throw new UnreachableException(); @@ -129,25 +138,10 @@ private Stream GetBoxEntryStream(BoxIndex boxIndex) var boxName = _package.Boxes[boxIndex.Value].Name; var boxPath = $"Boxes/{boxName}"; - var boxEntry = _archive.GetEntry(boxPath); - if (boxEntry == null) - throw new InvalidOperationException($"Failed to open box {boxPath}"); - - var boxEntryStream = boxEntry.Open(); - - if (!boxEntryStream.CanSeek) - { - var cachedContent = new byte[boxEntry.Length]; - var stream = new MemoryStream(cachedContent); - - boxEntryStream.CopyTo(stream); - boxEntryStream.Dispose(); - - stream.Position = 0; - boxEntryStream = stream; - } + var content = GetEntryContent(boxPath); + var ms = new MemoryStream(content); - _cachedBoxEntryStreams[boxIndex] = boxEntryStream; - return boxEntryStream; + _cachedBoxEntryStreams[boxIndex] = ms; + return ms; } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.Crypto.cs b/LibXboxOne/XVC2/Msixvc2File.Crypto.cs new file mode 100644 index 0000000..b510599 --- /dev/null +++ b/LibXboxOne/XVC2/Msixvc2File.Crypto.cs @@ -0,0 +1,48 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using LibXboxOne.XVC2.SerializedModel; + +namespace LibXboxOne.XVC2; + +public partial class Msixvc2File +{ + private readonly Dictionary _storedKeyMaterial = []; + + public void SubmitKeyMaterial(Guid keyId, byte[] keyMaterial) + { + _storedKeyMaterial[keyId] = keyMaterial; + } + + private void DecryptContent(ReadOnlySpan encrypted, ReadOnlySpan iv, Span decrypted, int keyId, + byte[]? encryptionKeyMaterial, byte[]? wrappedKey, PackagingIV? wrapIV, PackagingKeyPurpose purpose) + { + var key = _package.Keys[keyId]; + var keySource = key.Sources.First(x => x.SourcePurpose == purpose); + + if (wrappedKey != null) + { + var wrapKey = DeriveWrappingKey(keySource, keySource.WrapAlgorithm); + encryptionKeyMaterial = wrapKey.UnwrapKeyMaterial(wrappedKey, wrapIV); + } + + if (encryptionKeyMaterial != null) + { + var encryptionKey = PackagingEncryptionKey.Create(keySource.Algorithm, encryptionKeyMaterial); + encryptionKey.Decrypt(encrypted, PackagingIV.FromBytes(iv[..PackagingIV.Size]), decrypted); + } + } + + private IPackagingEncryptionKey DeriveWrappingKey(PackageKeySource keySource, PackagingEncryptionAlgorithm algorithm) + { + // Is this really how it's supposed to be done? since .PackageData is only used here + + var wrapKeyMaterial = _storedKeyMaterial[keySource.SourceKeyId]; + var derivationKey = PackagingDerivationKey.Create(keySource.DerivationAlgorithm, wrapKeyMaterial); + var derivedKey = derivationKey.DeriveKey(PackagingKeyPurpose.PackageData, keySource.WrapAlgorithm, keySource.KdfContext); + + var wrappingKey = derivedKey.UnwrapKeyMaterial(keySource.WrappedKey, keySource.WrapIV); + return PackagingEncryptionKey.Create(algorithm, wrappingKey); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs index 157fe3b..6bd3ccc 100644 --- a/LibXboxOne/XVC2/Msixvc2File.cs +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -44,7 +44,9 @@ public void LoadFileNames() { foreach (var chunk in _chunks.Values) { - var chunkSecretContent = GetSegmentContent(chunk.SecretReference, chunk.KeyIndex); + var chunkSecretContent = + GetSegmentContent(chunk.SecretReference, chunk.KeyIndex, PackagingKeyPurpose.Content); + var reader = new CborReader(chunkSecretContent); _chunkSecrets[chunk.Id] = ChunkDetailsSecret.Deserialize(reader); @@ -124,10 +126,12 @@ private byte[] GetEntryContent(string entryPath) throw new InvalidOperationException($"Failed to find entry {entryPath} in MSIXVC2"); using var packageDataStream = packageEntry.Open(); - var ms = new MemoryStream(); + + var buffer = new byte[packageEntry.Length]; + using var ms = new MemoryStream(buffer); packageDataStream.CopyTo(ms); - return ms.GetBuffer(); + return buffer; } public void Dispose() diff --git a/LibXboxOne/XVC2/PackagingDerivationKey.cs b/LibXboxOne/XVC2/PackagingDerivationKey.cs new file mode 100644 index 0000000..81acb97 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingDerivationKey.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace LibXboxOne.XVC2; + +public interface IPackagingDerivationKey +{ + IPackagingEncryptionKey DeriveKey(PackagingKeyPurpose purpose, + PackagingEncryptionAlgorithm algorithm, ReadOnlySpan context); +} + +public static class PackagingDerivationKey +{ + public static IPackagingDerivationKey Create(PackagingDerivationAlgorithm algorithm, ReadOnlySpan key) + { + return algorithm switch + { + PackagingDerivationAlgorithm.None => new PackagingDerivationKeyNone(), + PackagingDerivationAlgorithm.SP800_108_HMAC_SHA256 => new PackagingDerivationKeySp800108(key.ToArray()), + _ => throw new UnreachableException() + }; + } +} + +public class PackagingDerivationKeyNone : IPackagingDerivationKey +{ + public IPackagingEncryptionKey DeriveKey(PackagingKeyPurpose purpose, PackagingEncryptionAlgorithm algorithm, + ReadOnlySpan context) + { + throw new NotImplementedException(); + } +} + +public class PackagingDerivationKeySp800108(byte[] key) : IPackagingDerivationKey +{ + private readonly byte[] _key = key; + + public IPackagingEncryptionKey DeriveKey(PackagingKeyPurpose purpose, + PackagingEncryptionAlgorithm algorithm, ReadOnlySpan context) + { + var label = GetLabel(purpose, algorithm); + var keySize = GetKeySize(algorithm); + + var derivedKey = SP800108HmacCounterKdf.DeriveBytes(_key, HashAlgorithmName.SHA256, label, context, keySize); + return PackagingEncryptionKey.Create(algorithm, derivedKey); + } + + private static int GetKeySize(PackagingEncryptionAlgorithm encryptionAlgorithm) + { + if (encryptionAlgorithm == PackagingEncryptionAlgorithm.AES_256_CBC) + return 32; + + throw new InvalidOperationException($"No key size defined for {encryptionAlgorithm}"); + } + + private static ReadOnlySpan GetLabel(PackagingKeyPurpose keyPurpose, PackagingEncryptionAlgorithm encryptionAlgorithm) + { + if (keyPurpose == PackagingKeyPurpose.PackageData && + encryptionAlgorithm == PackagingEncryptionAlgorithm.AES_256_CBC) + { + return "MSIXVC2:PackageData:AES_256_CBC"u8; + } + + throw new InvalidOperationException($"No label defined for {keyPurpose} in {encryptionAlgorithm}"); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingEncryptionKey.cs b/LibXboxOne/XVC2/PackagingEncryptionKey.cs new file mode 100644 index 0000000..eb7c2b7 --- /dev/null +++ b/LibXboxOne/XVC2/PackagingEncryptionKey.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics; +using System.Security.Cryptography; + +namespace LibXboxOne.XVC2; + +public interface IPackagingEncryptionKey +{ + void Encrypt(ReadOnlySpan input, PackagingIV iv, Span output); + void Decrypt(ReadOnlySpan input, PackagingIV iv, Span output); + byte[] WrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv); + byte[] UnwrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv); +} + +public static class PackagingEncryptionKey +{ + public static IPackagingEncryptionKey Create(PackagingEncryptionAlgorithm algorithm, ReadOnlySpan key) + { + return algorithm switch + { + PackagingEncryptionAlgorithm.Automatic => new PackagingEncryptionAesCbc(key), + PackagingEncryptionAlgorithm.AES_256_CBC => new PackagingEncryptionAesCbc(key), + PackagingEncryptionAlgorithm.AES_256_KW => new PackagingEncryptionAesKw(key), + _ => throw new UnreachableException() + }; + } +} + +public class PackagingEncryptionAesCbc : IPackagingEncryptionKey +{ + private readonly Aes _aes; + + public PackagingEncryptionAesCbc(ReadOnlySpan key) + { + _aes = Aes.Create(); + _aes.Key = key.ToArray(); + } + + public void Encrypt(ReadOnlySpan input, PackagingIV iv, Span output) + { + _aes.EncryptCbc(input, iv.ToArray(), output, PaddingMode.None); + } + + public void Decrypt(ReadOnlySpan input, PackagingIV iv, Span output) + { + _aes.DecryptCbc(input, iv.ToArray(), output, PaddingMode.None); + } + + public byte[] WrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv) + { + if (iv == null) + throw new InvalidOperationException("IV required for CBC key wrapping"); + + var output = new byte[input.Length]; + Encrypt(input, iv.Value, output); + return output; + } + + public byte[] UnwrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv) + { + if (iv == null) + throw new InvalidOperationException("IV required for CBC key unwrapping"); + + var output = new byte[input.Length]; + Decrypt(input, iv.Value, output); + return output; + } +} + +public class PackagingEncryptionAesKw : IPackagingEncryptionKey +{ + private readonly Aes _aes; + + public PackagingEncryptionAesKw(ReadOnlySpan key) + { + _aes = Aes.Create(); + _aes.Key = key.ToArray(); + } + public void Encrypt(ReadOnlySpan input, PackagingIV iv, Span output) + { + throw new NotImplementedException(); + } + + public void Decrypt(ReadOnlySpan input, PackagingIV iv, Span output) + { + throw new NotImplementedException(); + } + + public byte[] WrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv) + { + if (iv != null) + throw new InvalidOperationException("No IV needed for AES-KW"); + + return _aes.EncryptKeyWrapPadded(input); + } + + public byte[] UnwrapKeyMaterial(ReadOnlySpan input, PackagingIV? iv) + { + if (iv != null) + throw new InvalidOperationException("No IV needed for AES-KW"); + + return _aes.DecryptKeyWrapPadded(input); + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/PackagingIV.cs b/LibXboxOne/XVC2/PackagingIV.cs index c89f3e1..923caf7 100644 --- a/LibXboxOne/XVC2/PackagingIV.cs +++ b/LibXboxOne/XVC2/PackagingIV.cs @@ -7,6 +7,8 @@ namespace LibXboxOne.XVC2; public struct PackagingIV { + public const int Size = 16; + private ulong _counter0; private ulong _counter1; @@ -19,7 +21,7 @@ public PackagingIV Increment() }; var prev = copy._counter0++; - if (prev + 1 != copy._counter0) + if (prev + 1 < copy._counter0) copy._counter1++; return copy; @@ -28,8 +30,8 @@ public PackagingIV Increment() public byte[] ToArray() { var array = new byte[16]; - BinaryPrimitives.WriteUInt64BigEndian(array, _counter0); - BinaryPrimitives.WriteUInt64BigEndian(array.AsSpan(8), _counter1); + BinaryPrimitives.WriteUInt64BigEndian(array, _counter1); + BinaryPrimitives.WriteUInt64BigEndian(array.AsSpan(8), _counter0); return array; } @@ -41,12 +43,12 @@ public static PackagingIV FromBase64UrlString(string value) return FromBytes(bytes); } - public static PackagingIV FromBytes(byte[] value) + public static PackagingIV FromBytes(ReadOnlySpan value) { return new PackagingIV { - _counter0 = BinaryPrimitives.ReadUInt64BigEndian(value), - _counter1 = BinaryPrimitives.ReadUInt64BigEndian(value.AsSpan(8)) + _counter1 = BinaryPrimitives.ReadUInt64BigEndian(value), + _counter0 = BinaryPrimitives.ReadUInt64BigEndian(value[8..]) }; } diff --git a/LibXboxOne/XVC2/SerializedModel/Chunk.cs b/LibXboxOne/XVC2/SerializedModel/Chunk.cs index 2842ac9..c1822f2 100644 --- a/LibXboxOne/XVC2/SerializedModel/Chunk.cs +++ b/LibXboxOne/XVC2/SerializedModel/Chunk.cs @@ -150,7 +150,7 @@ public void Serialize(CborWriter writer) writer.WriteEndMap(); } - public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) + public static Chunk Deserialize(CborReader reader, ref PackagingIV? rollingIV) { IPackagingSpecifier? tags = default; IPackagingSpecifier? languages = default; @@ -197,7 +197,7 @@ public static Chunk Deserialize(CborReader reader, ref PackagingIV? initialIV) boxLength = reader.ReadInt32(); break; case SerializedLabel.SecretReference: - secretReference = SegmentReference.Deserialize(reader, ref initialIV); + secretReference = SegmentReference.Deserialize(reader, ref rollingIV); break; default: reader.AssertInvalidValue(); diff --git a/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs b/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs index 3fbe391..d7cb8db 100644 --- a/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs +++ b/LibXboxOne/XVC2/SerializedModel/ChunkDetails.cs @@ -5,7 +5,7 @@ namespace LibXboxOne.XVC2.SerializedModel; -public sealed record ChunkDetails(List Files, int Id, PackagingIV? InitialIV) : IRootSerialize +public sealed record ChunkDetails(List Files, int Id, PackagingIV? IV) : IRootSerialize { public string OpcPath => $"/Chunks/{Id}.cbor"; public string OpcRelationship => "http://xbox.com/MSIXVC2/Chunk"; @@ -14,12 +14,12 @@ public void Serialize(CborWriter writer) { writer.WriteSelfDescribeTag(CborTagEx.XVCC); - writer.WriteStartMap(2 + (InitialIV.HasValue ? 1 : 0)); + writer.WriteStartMap(2 + (IV.HasValue ? 1 : 0)); writer.WriteLabel(SerializedLabel.Id); writer.WriteInt32(Id); - if (InitialIV is { } initialIV) + if (IV is { } initialIV) { writer.WriteLabel(SerializedLabel.InitialIV); initialIV.Serialize(writer); @@ -28,7 +28,7 @@ public void Serialize(CborWriter writer) writer.WriteLabel(SerializedLabel.Files); writer.WriteStartArray(Files.Count); - var iv = InitialIV; + var iv = IV; foreach (var file in Files) { file.Serialize(writer, ref iv, false); @@ -41,7 +41,7 @@ public void Serialize(CborWriter writer) public static ChunkDetails Deserialize(CborReader reader) { int id = default; - PackagingIV? initialIV = default; + PackagingIV? iv = default; List? files = default; reader.ReadSelfDescribeTag(CborTagEx.XVCC); @@ -56,14 +56,14 @@ public static ChunkDetails Deserialize(CborReader reader) id = reader.ReadInt32(); break; case SerializedLabel.InitialIV: - initialIV = PackagingIV.Deserialize(reader); + iv = PackagingIV.Deserialize(reader); break; case SerializedLabel.Files: var fileCount = reader.ReadStartArray(); files = []; while (fileCount-- != 0) { - files.Add(File.Deserialize(reader, ref initialIV)); + files.Add(File.Deserialize(reader, ref iv)); } reader.ReadEndArray(); @@ -77,6 +77,6 @@ public static ChunkDetails Deserialize(CborReader reader) reader.ReadEndMap(); Debug.Assert(files != null); - return new ChunkDetails(files, id, initialIV); + return new ChunkDetails(files, id, iv); } } \ No newline at end of file diff --git a/LibXboxOne/XVC2/SerializedModel/File.cs b/LibXboxOne/XVC2/SerializedModel/File.cs index bdcfd64..e8c37a1 100644 --- a/LibXboxOne/XVC2/SerializedModel/File.cs +++ b/LibXboxOne/XVC2/SerializedModel/File.cs @@ -23,7 +23,7 @@ public void Serialize(CborWriter writer) Serialize(writer, ref iv, true); } - public void Serialize(CborWriter writer, ref PackagingIV? initialIV, bool isStandaloneSerialize) + public void Serialize(CborWriter writer, ref PackagingIV? rollingIV, bool isStandaloneSerialize) { writer.WriteStartMap(3 + (isStandaloneSerialize && ChunkId != 0 ? 1 : 0) @@ -69,11 +69,11 @@ public void Serialize(CborWriter writer, ref PackagingIV? initialIV, bool isStan writer.WriteEndMap(); } - public static File Deserialize(CborReader reader, ref PackagingIV? initialIV) + public static File Deserialize(CborReader reader, ref PackagingIV? rollingIV) { int id = default; int chunkId = default; - PackagingIV? iv = default; + var iv = rollingIV; long length = default; PackagingHash hash = default; bool readProtected = default; @@ -109,7 +109,7 @@ public static File Deserialize(CborReader reader, ref PackagingIV? initialIV) var segmentCount = reader.ReadStartArray(); while (segmentCount-- != 0) { - segments.Add(SegmentReference.Deserialize(reader, ref initialIV)); + segments.Add(SegmentReference.Deserialize(reader, ref iv)); } reader.ReadEndArray(); break; @@ -121,9 +121,9 @@ public static File Deserialize(CborReader reader, ref PackagingIV? initialIV) reader.ReadEndMap(); - if (initialIV != null) + if (rollingIV != null) { - initialIV = iv; + rollingIV = iv; } return new File(id, chunkId, iv, length, hash, readProtected, segments); diff --git a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs index 9e35755..60f8ae9 100644 --- a/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs +++ b/LibXboxOne/XVC2/SerializedModel/PackageKeySource.cs @@ -1,16 +1,17 @@ #nullable enable +using System; using System.Diagnostics; using System.Formats.Cbor; namespace LibXboxOne.XVC2.SerializedModel; public sealed record PackageKeySource( - string SourceKeyId, + Guid SourceKeyId, PackagingKeyPurpose SourcePurpose, PackagingDerivationAlgorithm DerivationAlgorithm, byte[] KdfContext, PackagingEncryptionAlgorithm WrapAlgorithm, - byte[]? WrapIV, // unverified + PackagingIV? WrapIV, // unverified byte[] WrappedKey, PackagingEncryptionAlgorithm Algorithm @@ -70,7 +71,7 @@ public void Serialize(CborWriter writer) writer.WriteEnum(ToSerialized(SourcePurpose)); writer.WriteLabel(SerializedLabel.SourceKeyId); - writer.WriteTextString(SourceKeyId); + writer.WriteGuid(SourceKeyId); writer.WriteLabel(SerializedLabel.DerivationAlgorithm); writer.WriteEnum(ToSerialized(DerivationAlgorithm)); @@ -81,10 +82,10 @@ public void Serialize(CborWriter writer) writer.WriteLabel(SerializedLabel.WrapAlgorithm); writer.WriteEnum(ToSerialized(WrapAlgorithm)); - if (WrapIV != null) + if (WrapIV is {} wrapIV) { writer.WriteLabel(SerializedLabel.WrapIV); - writer.WriteByteString(WrapIV); + wrapIV.Serialize(writer); } writer.WriteLabel(SerializedLabel.WrappedKey); @@ -98,13 +99,13 @@ public void Serialize(CborWriter writer) public static PackageKeySource Deserialize(CborReader reader) { - string? sourceKeyId = default; + Guid sourceKeyId = default; PackagingKeyPurpose keyPurpose = default; PackagingDerivationAlgorithm derivationAlgorithm = default; PackagingEncryptionAlgorithm wrapAlgorithm = default; PackagingEncryptionAlgorithm algorithm = default; byte[]? kdfContext = default; - byte[]? wrapIV = default; + PackagingIV? wrapIV = default; byte[]? wrappedKey = default; var count = reader.ReadStartMap(); @@ -117,7 +118,7 @@ public static PackageKeySource Deserialize(CborReader reader) keyPurpose = ToKeyPurpose(reader.ReadEnum()); break; case SerializedLabel.SourceKeyId: - sourceKeyId = reader.ReadTextString(); + sourceKeyId = reader.ReadGuid(); break; case SerializedLabel.DerivationAlgorithm: derivationAlgorithm = ToDerivationAlgorithm(reader.ReadEnum()); @@ -129,7 +130,7 @@ public static PackageKeySource Deserialize(CborReader reader) wrapAlgorithm = ToEncryptionAlgorithm(reader.ReadEnum()); break; case SerializedLabel.WrapIV: - wrapIV = reader.ReadByteString(); + wrapIV = PackagingIV.Deserialize(reader); break; case SerializedLabel.WrappedKey: wrappedKey = reader.ReadByteString(); @@ -144,7 +145,7 @@ public static PackageKeySource Deserialize(CborReader reader) } reader.ReadEndMap(); - Debug.Assert(sourceKeyId != null && kdfContext != null && wrappedKey != null); + Debug.Assert(kdfContext != null && wrappedKey != null); return new PackageKeySource(sourceKeyId, keyPurpose, derivationAlgorithm, kdfContext, wrapAlgorithm, wrapIV, wrappedKey, algorithm); } diff --git a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs index 4eae09a..8a2ea63 100644 --- a/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs +++ b/LibXboxOne/XVC2/SerializedModel/SegmentReference.cs @@ -109,7 +109,7 @@ public void Serialize(CborWriter writer) writer.WriteEndMap(); } - public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? initialIV) + public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? rollingIV) { PackagingHash hash = default; int length = default; @@ -148,9 +148,9 @@ public static SegmentReference Deserialize(CborReader reader, ref PackagingIV? i case SerializedLabel.WrappedKey: wrappedKey = reader.ReadByteString(); - Debug.Assert(initialIV.HasValue); - wrapIV = initialIV; - initialIV = wrapIV.Value.Increment(); + Debug.Assert(rollingIV.HasValue); + wrapIV = rollingIV; + rollingIV = wrapIV.Value.Increment(); break; //case 7: // // This is not set in normal references - the iv is gotten from the initial iv From cc76bb667ffa2952a0f7e17bc6a8851be3fedddf Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 17:23:55 +0200 Subject: [PATCH 10/11] bump nugets, migrate fully to ltrdata.discutils, fix warnings --- LibXboxOne.Tests/LibXboxOne.Tests.csproj | 6 +++--- LibXboxOne/LibXboxOne.csproj | 7 +++---- LibXboxOne/XVD/XvdFilesystem.cs | 2 +- XvdTool.Streaming/Commands/CryptoCommand.cs | 2 +- XvdTool.Streaming/Commands/DecryptCommand.cs | 4 ++-- XvdTool.Streaming/Commands/ExtractCommand.cs | 4 ++-- XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs | 4 ++-- XvdTool.Streaming/Commands/InfoCommand.cs | 2 +- XvdTool.Streaming/Commands/VerifyCommand.cs | 4 ++-- XvdTool.Streaming/Commands/XvdCommand.cs | 2 +- XvdTool.Streaming/XvdTool.Streaming.csproj | 6 +++--- 11 files changed, 21 insertions(+), 22 deletions(-) diff --git a/LibXboxOne.Tests/LibXboxOne.Tests.csproj b/LibXboxOne.Tests/LibXboxOne.Tests.csproj index a8d41aa..6e093e9 100644 --- a/LibXboxOne.Tests/LibXboxOne.Tests.csproj +++ b/LibXboxOne.Tests/LibXboxOne.Tests.csproj @@ -8,12 +8,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/LibXboxOne/LibXboxOne.csproj b/LibXboxOne/LibXboxOne.csproj index f0ff9d3..48fd92d 100644 --- a/LibXboxOne/LibXboxOne.csproj +++ b/LibXboxOne/LibXboxOne.csproj @@ -11,13 +11,12 @@ + + - + - - - diff --git a/LibXboxOne/XVD/XvdFilesystem.cs b/LibXboxOne/XVD/XvdFilesystem.cs index adeb086..9ccaf8c 100644 --- a/LibXboxOne/XVD/XvdFilesystem.cs +++ b/LibXboxOne/XVD/XvdFilesystem.cs @@ -189,7 +189,7 @@ public bool ExtractFilesystemImage(string targetFile, bool createVhd) for (long offset = 0; offset < _fs.Length; offset += XvdFile.PAGE_SIZE) { - _fs.Read(buffer, 0, buffer.Length); + _fs.ReadExactly(buffer, 0, buffer.Length); destFs.Write(buffer, 0, buffer.Length); } } diff --git a/XvdTool.Streaming/Commands/CryptoCommand.cs b/XvdTool.Streaming/Commands/CryptoCommand.cs index 29db082..53cc347 100644 --- a/XvdTool.Streaming/Commands/CryptoCommand.cs +++ b/XvdTool.Streaming/Commands/CryptoCommand.cs @@ -46,7 +46,7 @@ protected bool Initialize(CryptoCommandSettings settings, out KeyEntry entry) return true; } - public override ValidationResult Validate(CommandContext context, T settings) + protected override ValidationResult Validate(CommandContext context, T settings) { var result = base.Validate(context, settings); diff --git a/XvdTool.Streaming/Commands/DecryptCommand.cs b/XvdTool.Streaming/Commands/DecryptCommand.cs index 090f6cb..f9ca934 100644 --- a/XvdTool.Streaming/Commands/DecryptCommand.cs +++ b/XvdTool.Streaming/Commands/DecryptCommand.cs @@ -14,7 +14,7 @@ public sealed class Settings : CryptoCommandSettings public bool SkipHashCalculation { get; init; } } - public override int Execute(CommandContext context, Settings settings) + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { if (!Initialize(settings, out var keyEntry)) { @@ -29,7 +29,7 @@ public override int Execute(CommandContext context, Settings settings) return 0; } - public override ValidationResult Validate(CommandContext context, Settings settings) + protected override ValidationResult Validate(CommandContext context, Settings settings) { base.Validate(context, settings); diff --git a/XvdTool.Streaming/Commands/ExtractCommand.cs b/XvdTool.Streaming/Commands/ExtractCommand.cs index 6827ef5..f86a90e 100644 --- a/XvdTool.Streaming/Commands/ExtractCommand.cs +++ b/XvdTool.Streaming/Commands/ExtractCommand.cs @@ -27,7 +27,7 @@ public sealed class Settings : CryptoCommandSettings public bool SkipHashCheck { get; init; } } - public override int Execute(CommandContext context, Settings settings) + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { Debug.Assert(settings.OutputDirectory != null, "settings.OutputDirectory != null"); @@ -52,7 +52,7 @@ public override int Execute(CommandContext context, Settings settings) return 0; } - public override ValidationResult Validate(CommandContext context, Settings settings) + protected 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."); diff --git a/XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs b/XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs index 7851a18..9496d6b 100644 --- a/XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs +++ b/XvdTool.Streaming/Commands/ExtractEmbeddedXvdCommand.cs @@ -15,7 +15,7 @@ internal sealed class Settings : XvdCommandSettings public string EmbeddedXvdOutputPath { get; set; } = null!; } - public override int Execute(CommandContext context, Settings settings) + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { Initialize(settings, requiresWriting: false); @@ -33,7 +33,7 @@ public override int Execute(CommandContext context, Settings settings) return 0; } - public override ValidationResult Validate(CommandContext context, Settings settings) + protected override ValidationResult Validate(CommandContext context, Settings settings) { if (Directory.Exists(settings.EmbeddedXvdOutputPath)) return ValidationResult.Error("The embedded XVD output path is a directory."); diff --git a/XvdTool.Streaming/Commands/InfoCommand.cs b/XvdTool.Streaming/Commands/InfoCommand.cs index 0745616..b8714aa 100644 --- a/XvdTool.Streaming/Commands/InfoCommand.cs +++ b/XvdTool.Streaming/Commands/InfoCommand.cs @@ -17,7 +17,7 @@ public sealed class Settings : XvdCommandSettings public bool ShowAllFiles { get; set; } } - public override int Execute(CommandContext context, Settings settings) + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { Initialize(settings, requiresWriting: false); diff --git a/XvdTool.Streaming/Commands/VerifyCommand.cs b/XvdTool.Streaming/Commands/VerifyCommand.cs index 1b15dbd..37b140f 100644 --- a/XvdTool.Streaming/Commands/VerifyCommand.cs +++ b/XvdTool.Streaming/Commands/VerifyCommand.cs @@ -8,7 +8,7 @@ internal sealed class VerifyCommand : XvdCommand { public sealed class Settings : XvdCommandSettings; - public override int Execute(CommandContext context, Settings settings) + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) { Initialize(settings, requiresWriting: false); @@ -26,7 +26,7 @@ public override int Execute(CommandContext context, Settings settings) return 0; } - public override ValidationResult Validate(CommandContext context, Settings settings) + protected override ValidationResult Validate(CommandContext context, Settings settings) { base.Validate(context, settings); diff --git a/XvdTool.Streaming/Commands/XvdCommand.cs b/XvdTool.Streaming/Commands/XvdCommand.cs index dd8547d..8aa11db 100644 --- a/XvdTool.Streaming/Commands/XvdCommand.cs +++ b/XvdTool.Streaming/Commands/XvdCommand.cs @@ -21,7 +21,7 @@ protected void Initialize(XvdCommandSettings settings, bool requiresWriting) XvdFile.Parse(); } - public override ValidationResult Validate(CommandContext context, T settings) + protected 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."); diff --git a/XvdTool.Streaming/XvdTool.Streaming.csproj b/XvdTool.Streaming/XvdTool.Streaming.csproj index b998392..8d7cd91 100644 --- a/XvdTool.Streaming/XvdTool.Streaming.csproj +++ b/XvdTool.Streaming/XvdTool.Streaming.csproj @@ -9,9 +9,9 @@ - - - + + + From 99db4dd3285b80cadcd60f21a311d909121a8944 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Fri, 8 May 2026 20:06:57 +0200 Subject: [PATCH 11/11] add basic cli interface for msixvc2 --- LibXboxOne/XVC2/Msixvc2File.Content.cs | 3 +- LibXboxOne/XVC2/Msixvc2File.Crypto.cs | 20 +++ LibXboxOne/XVC2/Msixvc2File.Streaming.cs | 151 ++++++++++++++++++ LibXboxOne/XVC2/Msixvc2File.cs | 4 + .../Commands/Msixvc2/ExtractCommand.cs | 29 ++++ .../Commands/Msixvc2/InfoCommand.cs | 41 +++++ .../Commands/Msixvc2/Msixvc2Command.cs | 59 +++++++ .../Msixvc2/Msixvc2CommandSettings.cs | 19 +++ XvdTool.Streaming/Program.cs | 15 ++ 9 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 LibXboxOne/XVC2/Msixvc2File.Streaming.cs create mode 100644 XvdTool.Streaming/Commands/Msixvc2/ExtractCommand.cs create mode 100644 XvdTool.Streaming/Commands/Msixvc2/InfoCommand.cs create mode 100644 XvdTool.Streaming/Commands/Msixvc2/Msixvc2Command.cs create mode 100644 XvdTool.Streaming/Commands/Msixvc2/Msixvc2CommandSettings.cs diff --git a/LibXboxOne/XVC2/Msixvc2File.Content.cs b/LibXboxOne/XVC2/Msixvc2File.Content.cs index 9a19e69..c1c1a52 100644 --- a/LibXboxOne/XVC2/Msixvc2File.Content.cs +++ b/LibXboxOne/XVC2/Msixvc2File.Content.cs @@ -43,7 +43,8 @@ public byte[] GetSegmentContent(SegmentReference segment, int keyId, PackagingKe return content; } - public void ReadSegmentContent(SegmentReference segment, int keyId, Span content, PackagingKeyPurpose purpose) + public void ReadSegmentContent(SegmentReference segment, int keyId, Span content, + PackagingKeyPurpose purpose = PackagingKeyPurpose.Content) { var boxContent = new byte[segment.BoxLength]; diff --git a/LibXboxOne/XVC2/Msixvc2File.Crypto.cs b/LibXboxOne/XVC2/Msixvc2File.Crypto.cs index b510599..a9ce42d 100644 --- a/LibXboxOne/XVC2/Msixvc2File.Crypto.cs +++ b/LibXboxOne/XVC2/Msixvc2File.Crypto.cs @@ -15,6 +15,26 @@ public void SubmitKeyMaterial(Guid keyId, byte[] keyMaterial) _storedKeyMaterial[keyId] = keyMaterial; } + public void SubmitKeys(byte[]? contentKey, byte[]? versionKey) + { + if (_package.Keys.Count == 0) + return; + + var sources = _package.Keys[0].Sources; + + if (contentKey != null) + { + var contentKeySource = sources.First(x => x.SourcePurpose == PackagingKeyPurpose.Content); + SubmitKeyMaterial(contentKeySource.SourceKeyId, contentKey); + } + + if (versionKey != null) + { + var versionKeySource = sources.First(x => x.SourcePurpose == PackagingKeyPurpose.Version); + SubmitKeyMaterial(versionKeySource.SourceKeyId, versionKey); + } + } + private void DecryptContent(ReadOnlySpan encrypted, ReadOnlySpan iv, Span decrypted, int keyId, byte[]? encryptionKeyMaterial, byte[]? wrappedKey, PackagingIV? wrapIV, PackagingKeyPurpose purpose) { diff --git a/LibXboxOne/XVC2/Msixvc2File.Streaming.cs b/LibXboxOne/XVC2/Msixvc2File.Streaming.cs new file mode 100644 index 0000000..62d3585 --- /dev/null +++ b/LibXboxOne/XVC2/Msixvc2File.Streaming.cs @@ -0,0 +1,151 @@ +using LibXboxOne.XVC2.SerializedModel; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace LibXboxOne.XVC2; + +public partial class Msixvc2File +{ + // TODO: Make this look nice + optimized + + public string PrintInfo(bool showAllFiles) + { + var sb = new StringBuilder(); + + sb.AppendLine("MSIXVC2 Package"); + sb.AppendLine($" ContentId: {_package.ContentId}"); + sb.AppendLine($" FulfillmentContentId: {_package.FulfillmentContentId}"); + sb.AppendLine($" Version: {_package.Version}"); + sb.AppendLine($" StoreId: {_package.StoreId}"); + sb.AppendLine($" ProductId: {_package.ProductId}"); + sb.AppendLine($" MinimumSystemVersion: {_package.MinimumSystemVersion}"); + sb.AppendLine($" SupportedPlatforms: {_package.SupportedPlatforms}"); + sb.AppendLine(); + + sb.AppendLine("Segmentation:"); + sb.AppendLine($" Algorithm: {_package.Segmentation.Algorithm}"); + sb.AppendLine($" Hash algorithm: 0x{_package.Segmentation.HashAlgorithm:x}"); + sb.AppendLine("Options:"); + foreach (var (key, value) in _package.Segmentation.Options) + { + sb.AppendLine($" {key}: {value}"); + } + sb.AppendLine(); + + sb.AppendLine("Boxes:"); + foreach (var box in _package.Boxes) + { + sb.AppendLine($" Name: {box.Name}"); + } + sb.AppendLine(); + + if (_package.Keys.Count > 0) + { + sb.AppendLine("Encrypted: true"); + sb.AppendLine($"Initial IV: {_package.InitialIV}"); + + sb.AppendLine("Keys:"); + for (var i = 0; i < _package.Keys.Count; i++) + { + sb.AppendLine($" Key #{i} - Sources:"); + var key = _package.Keys[i]; + foreach (var source in key.Sources) + { + sb.AppendLine($" Key ID: {source.SourceKeyId}"); + sb.AppendLine($" Purpose: {source.SourcePurpose}"); + sb.AppendLine($" Derivation: {source.DerivationAlgorithm}"); + sb.AppendLine($" KDF Context: {Convert.ToHexString(source.KdfContext)}"); + sb.AppendLine($" Wrap: {source.WrapAlgorithm}"); + sb.AppendLine($" Wrapping IV: {source.WrapIV}"); + sb.AppendLine($" Wrapped Key: {Convert.ToHexString(source.WrappedKey)}"); + sb.AppendLine($" Encryption: {source.Algorithm}"); + sb.AppendLine(); + } + + sb.AppendLine(); + } + + if (!_loadedFileNames) + { + var remainingFilesToShow = 4096; + foreach (var chunk in _chunkDetails) + { + foreach (var file in chunk.Value.Files) + { + sb.AppendLine($" File {file.Id}:"); + sb.AppendLine($" ChunkId: 0x{chunk.Key:x}"); + sb.AppendLine($" Length: 0x{file.Length:x}"); + sb.AppendLine($" Hash: {file.Hash}"); + sb.AppendLine($" ReadProtected: {file.ReadProtected}"); + sb.AppendLine(); + + if (!showAllFiles && remainingFilesToShow-- == 0) + break; + } + + if (remainingFilesToShow == 0) + break; + } + } + else + { + sb.AppendLine(" Files in package:"); + foreach (var (name, file) in showAllFiles ? _files : _files.Take(Math.Min(4096, _files.Count))) + { + sb.AppendLine($" File {name}:"); + sb.AppendLine($" ChunkId: 0x{file.ChunkId:x}"); + sb.AppendLine($" Length: 0x{file.Length:x}"); + sb.AppendLine($" Hash: {file.Hash}"); + sb.AppendLine($" ReadProtected: {file.ReadProtected}"); + sb.AppendLine(); + } + } + + sb.AppendLine(); + } + + return sb.ToString(); + } + + public void ExtractFiles(string outputDirectory) + { + if (!_loadedFileNames) + { + throw new InvalidOperationException("Cannot extract files without filenames being loaded"); + } + + foreach (var path in _files.Keys) + { + var outputPath = Path.Join(outputDirectory, path); + var dir = Path.GetDirectoryName(outputPath); + if (dir != null) + { + Directory.CreateDirectory(dir); + } + } + + foreach (var (name, file) in _files) + { + Console.WriteLine($"Extracting {name}"); + + var chunk = _chunks[file.ChunkId]; + + var outputPath = Path.Join(outputDirectory, name); + using var output = System.IO.File.OpenWrite(outputPath); + + if (file.Segments == null) + continue; + + var buffer = new byte[file.Segments.Select(x => x.Length).Max()].AsSpan(); + + foreach (var segment in file.Segments) + { + var currentSegmentBuffer = buffer[..segment.Length]; + ReadSegmentContent(segment, chunk.KeyIndex, currentSegmentBuffer); + output.Write(currentSegmentBuffer); + } + } + } +} \ No newline at end of file diff --git a/LibXboxOne/XVC2/Msixvc2File.cs b/LibXboxOne/XVC2/Msixvc2File.cs index 6bd3ccc..0814f28 100644 --- a/LibXboxOne/XVC2/Msixvc2File.cs +++ b/LibXboxOne/XVC2/Msixvc2File.cs @@ -20,6 +20,8 @@ public sealed partial class Msixvc2File : IDisposable private readonly Dictionary _files = []; private readonly Dictionary _boxes = []; + private bool _loadedFileNames; + public static Msixvc2File FromPath(string path) => new(File.OpenRead(path)); public Msixvc2File(Stream stream, bool leaveOpen = false) @@ -57,6 +59,8 @@ public void LoadFileNames() _files[fileName] = file with { ChunkId = chunk.Id }; } } + + _loadedFileNames = true; } private Box ReadBoxManifest(BoxIndex index) diff --git a/XvdTool.Streaming/Commands/Msixvc2/ExtractCommand.cs b/XvdTool.Streaming/Commands/Msixvc2/ExtractCommand.cs new file mode 100644 index 0000000..c88dc08 --- /dev/null +++ b/XvdTool.Streaming/Commands/Msixvc2/ExtractCommand.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.Diagnostics; +using Spectre.Console.Cli; + +namespace XvdTool.Streaming.Commands.Msixvc2; + +internal sealed class ExtractCommand : Msixvc2Command +{ + public sealed class Settings : Msixvc2CommandSettings + { + [Description("File path to save the output into.")] + [CommandOption("-o|--output")] + public string? OutputPath { get; init; } + } + + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) + { + Initialize(settings); + + Debug.Assert(Msixvc2 != null); + + using (Msixvc2) + { + Msixvc2.ExtractFiles(settings.OutputPath); + } + + return 0; + } +} \ No newline at end of file diff --git a/XvdTool.Streaming/Commands/Msixvc2/InfoCommand.cs b/XvdTool.Streaming/Commands/Msixvc2/InfoCommand.cs new file mode 100644 index 0000000..bd19556 --- /dev/null +++ b/XvdTool.Streaming/Commands/Msixvc2/InfoCommand.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using System.Diagnostics; +using Spectre.Console.Cli; + +namespace XvdTool.Streaming.Commands.Msixvc2; + +internal sealed class InfoCommand : Msixvc2Command +{ + public sealed class Settings : Msixvc2CommandSettings + { + [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; } + } + + protected override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken) + { + Initialize(settings); + + Debug.Assert(Msixvc2 != null); + + using (Msixvc2) + { + var infoOutput = Msixvc2.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; + } +} \ No newline at end of file diff --git a/XvdTool.Streaming/Commands/Msixvc2/Msixvc2Command.cs b/XvdTool.Streaming/Commands/Msixvc2/Msixvc2Command.cs new file mode 100644 index 0000000..76f4e75 --- /dev/null +++ b/XvdTool.Streaming/Commands/Msixvc2/Msixvc2Command.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using LibXboxOne.XVC2; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace XvdTool.Streaming.Commands.Msixvc2; + +internal abstract class Msixvc2Command : Command where T : Msixvc2CommandSettings +{ + protected Msixvc2File Msixvc2 = default!; + + private static Stream OpenStream(string path) + => path.StartsWith("http") + ? HttpFileStream.Open(path) + : File.OpenRead(path); + + private static bool CheckIfMsixvc2(string path) + { + using var stream = OpenStream(path); + var header = (stackalloc byte[4]); + stream.ReadExactly(header); + + return header.SequenceEqual("PK\x03\x04"u8); + } + + protected void Initialize(Msixvc2CommandSettings settings) + { + Debug.Assert(settings.XvcPath != null); + + var path = settings.XvcPath; + if (!CheckIfMsixvc2(path)) + { + throw new NotSupportedException("File is not a MSIXVC2."); + } + + Msixvc2 = new Msixvc2File(OpenStream(path)); + + var contentKey = settings.ContentKey != null ? Convert.FromBase64String(settings.ContentKey) : null; + var versionKey = settings.VersionKey != null ? Convert.FromBase64String(settings.VersionKey) : null; + Msixvc2.SubmitKeys(contentKey, versionKey); + + try + { + Msixvc2.LoadFileNames(); + } + catch (Exception) + { + // Attempt to load file names, the info command can be used without this. + } + } + + protected 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(); + } +} \ No newline at end of file diff --git a/XvdTool.Streaming/Commands/Msixvc2/Msixvc2CommandSettings.cs b/XvdTool.Streaming/Commands/Msixvc2/Msixvc2CommandSettings.cs new file mode 100644 index 0000000..4b91ef9 --- /dev/null +++ b/XvdTool.Streaming/Commands/Msixvc2/Msixvc2CommandSettings.cs @@ -0,0 +1,19 @@ +using Spectre.Console.Cli; +using System.ComponentModel; + +namespace XvdTool.Streaming.Commands.Msixvc2; + +internal abstract class Msixvc2CommandSettings : CommandSettings +{ + [Description("File Path / URL to the MSIXVC.")] + [CommandArgument(0, "")] + public string? XvcPath { get; init; } + + [Description("(Encrypted packages only) Base64-encoded content key")] + [CommandOption("-c|--content-key")] + public string? ContentKey { get; init; } + + [Description("(Encrypted packages only, currently unused) Base64-encoded version key")] + [CommandOption("-v|--version-key")] + public string? VersionKey { get; init; } +} \ No newline at end of file diff --git a/XvdTool.Streaming/Program.cs b/XvdTool.Streaming/Program.cs index 67f1bd2..aaabd51 100644 --- a/XvdTool.Streaming/Program.cs +++ b/XvdTool.Streaming/Program.cs @@ -36,6 +36,21 @@ static void Main(string[] args) .WithDescription("Extracts an embedded XVD from a given file.") .WithExample("extract-embedded-xvd", "c:/file.xvc") .WithExample("extract-embedded-xvd", "https://assets1.xboxlive.com/..."); + + config.AddBranch("msixvc2", msixvc2 => + { + msixvc2.AddCommand("info") + .WithDescription("Prints information about a given MSIXVC2.") + .WithExample("info", "c:/file.msixvc") + .WithExample("info", "c:/file.msixvc", "-o log.txt") + .WithExample("info", "https://assets1.xboxlive.com/..."); + + msixvc2.AddCommand("extract") + .WithDescription("Decrypts and extracts the files contained in a given MSIXVC2.") + .WithExample("extract", "c:/file.msixvc") + .WithExample("extract", "c:/file.msixvc", "-o c:/output") + .WithExample("extract", "https://assets1.xboxlive.com/..."); + }); }); app.Run(args);