Secure Transacted File System (STFS) Support (#78)

* Initial STFS support

* Fix build errors

* Fix more build errors

* Final fix

* Cleanup printer

* Fix indent
This commit is contained in:
Deterous
2026-04-08 11:14:39 +09:00
committed by GitHub
parent adedc502e3
commit 7485a43364
23 changed files with 1475 additions and 2 deletions

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// Signature signed by console, for "CON " format STFS files
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class ConsoleSignature : Signature
{
/// <summary>
/// Public Key Certificate Size
/// </summary>
/// <remarks>Big-endian</remarks>
public ushort CertificateSize { get; set; }
/// <summary>
/// Certificate Owner Console ID
/// </summary>
/// <remarks>5 bytes</remarks>
public byte[] ConsoleID { get; set; } = new byte[5];
/// <summary>
/// Certificate Owner Console Part Number
/// </summary>
/// <remarks>20 bytes, ASCII string</remarks>
public byte[] PartNumber { get; set; } = new byte[20];
/// <summary>
/// Certificate Owner Console Type (1 for devkit, 2 for retail)
/// </summary>
public byte ConsoleType { get; set; }
/// <summary>
/// Certificate Date of Generation
/// </summary>
/// <remarks>8 bytes, ASCII string</remarks>
public byte[] CertificateDate { get; set; } = new byte[8];
/// <summary>
/// Public Exponent
/// </summary>
/// <remarks>4 bytes</remarks>
public byte[] PublicExponent { get; set; } = new byte[4];
/// <summary>
/// Public Modulus
/// </summary>
/// <remarks>128 bytes</remarks>
public byte[] PublicModulus { get; set; } = new byte[128];
/// <summary>
/// Certificate Signature
/// </summary>
/// <remarks>256 bytes</remarks>
public byte[] CertificateSignature { get; set; } = new byte[256];
/// <summary>
/// Signature
/// </summary>
/// <remarks>128 bytes</remarks>
public byte[] Signature { get; set; } = new byte[128];
}
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public static class Constants
{
/// <summary>
/// STFS LIVE magic number ("LIVE")
/// </summary>
public static readonly byte[] MagicBytesLIVE = [0x4C, 0x49, 0x56, 0x45];
/// <summary>
/// STFS LIVE magic string ("LIVE")
/// </summary>
public const string MagicStringLIVE = "LIVE";
/// <summary>
/// STFS PIRS magic number ("PIRS")
/// </summary>
public static readonly byte[] MagicBytesPIRS = [0x50, 0x49, 0x52, 0x53];
/// <summary>
/// STFS PIRS magic string ("PIRS")
/// </summary>
public const string MagicStringPIRS = "PIRS";
/// <summary>
/// STFS CON magic number ("CON ")
/// </summary>
public static readonly byte[] MagicBytesCON = [0x43, 0x4F, 0x4E, 0x20];
/// <summary>
/// STFS CON magic string ("CON")
/// </summary>
public const string MagicStringCON = "CON ";
/// <summary>
/// Standard length of an STFS header
/// </summary>
public const uint StandardHeaderSize = 0xB000;
}
}

View File

@@ -0,0 +1,61 @@
using System;
namespace SabreTools.Data.Models.XenonExecutable
{
/// <summary>
/// STFS Volume Content Type possible values
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public enum ContentType : int
{
SAVED_GAME = 0x00000001,
MARKETPLACE_CONTENT = 0x00000002,
PUBLISHER = 0x00000003,
XBOX_360_TITLE = 0x00001000,
IPTV_PAUSE_BUFFER = 0x00002000,
INSTALLED_GAME = 0x00004000,
XBOX_ORIGINAL_GAME = 0x00005000,
XBOX_TITLE = 0x00006000,
GAME_ON_DEMAND = 0x00007000,
AVATAR_ITEM = 0x00009000,
PROFILE = 0x00010000,
GAMER_PICTURE = 0x00020000,
THEME = 0x00030000,
CACHE_FILE = 0x00040000,
STORAGE_DOWNLOAD = 0x00050000,
XBOX_SAVED_GAME = 0x00060000,
XBOX_DOWNLOAD = 0x00070000,
GAME_DEMO = 0x00080000,
VIDEO = 0x00090000,
GAME_TITLE = 0x000A0000,
INSTALLER = 0x000B0000,
GAME_TRAILER = 0x000C0000,
ARCADE_TITLE = 0x000D0000,
XNA = 0x000E0000,
LICENSE_STORE = 0x000F0000,
MOVIE = 0x00100000,
TV = 0x00200000,
MUSIC_VIDEO = 0x00300000,
GAME_VIDEO = 0x00400000,
PODCAST_VIDEO = 0x00500000,
VIRAL_VIDEO = 0x00600000,
COMMUNITY_GAME = 0x02000000,
}
/// <summary>
/// STFS Transfer Flags
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
[Flags]
public enum TransferFlags : uint
{
NONE1 = 0x00000001,
NONE2 = 0x00000002,
DEEP_LINK_SUPPORTED = 0x00000004,
DISABLE_NETWORK_STORAGE = 0x00000008,
KINECT_ENABLED = 0x00000010,
MOVE_ONLY_TRANSFER = 0x00000020,
DEVICE_ID_TRANSFER = 0x00000040,
PROFILE_ID_TRANSFER = 0x00000080,
}
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Hash Table in a Hash Table Block
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class HashTable
{
/// <summary>
/// 170 hash table entries in a single table/block
/// </summary>
public HashTableEntry[] HashTableEntries { get; set; } = new HashTableEntry[170];
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Hash Table Entry in a Hash Block's Hash Table
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class HashTableEntry
{
/// <summary>
/// SHA-1 hash of the block
/// </summary>
public byte[] Hash { get; set; } = new byte[20];
/// <summary>
/// Status of the block
/// 0x00 = Unused block
/// 0x40 = Free block (previously used, now freed)
/// 0x80 = Used block
/// 0xC0 = Newly allocated block
/// </summary>
public byte Status { get; set; }
/// <summary>
/// Block number corresponding to the hash
/// FFFFFF = Block 1 (starting 0xB000)
/// </summary>
/// <remarks>Big-endian, 3-byte uint24</remarks>
public UInt24 BlockNumber { get; set; } = new();
}
}

View File

@@ -0,0 +1,280 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// Secure Transacted File System, Header format
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class Header
{
/// <summary>
/// Magic bytes indicating the format
/// Possible values are "LIVE", "PIRS, and "CON "
/// </summary>
/// <remarks>4 bytes</remarks>
public byte[] MagicBytes { get; set; } = new byte[4];
/// <summary>
/// Signature Block
/// Not optional, but abstract class
/// </summary>
/// <remarks>552 bytes</remarks>
public Signature? Signature { get; set; }
/// <summary>
/// Used to check package owner
/// 16 license entries, 16 bytes each
/// </summary>
/// <remarks>256 bytes</remarks>
public LicenseEntry[] LicensingData { get; set; } = new LicenseEntry[16];
/// <summary>
/// SHA-1 Integrity Hash of the header (from ContentType/0x344 to first hash table)
/// </summary>
/// <remarks>20 bytes</remarks>
public byte[] HeaderHash { get; set; } = new byte[20];
/// <summary>
/// Size of the header, in bytes (from ??? to ???)
/// The actual end of header is padded and zeroed up until next multiple of 4096 bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public uint HeaderSize { get; set; }
/// <summary>
/// Indication of the content in the STFS
/// See Enum.ContentType
/// </summary>
/// <remarks>Big-endian</remarks>
public int ContentType { get; set; }
/// <summary>
/// Intended meaning of some of the below fields
/// Known values are 0, 1 and 2
/// 0 = A bunch of fields will be zeroed (Seen in system updates)
/// 1 = All but the below fields are set
/// 2 = The following new fields are set:
/// SeriesID, SeasonID, SeasonNumber, EpisodeNumber,
/// AdditionalDisplayNames, AdditionalDisplayDescriptions
/// </summary>
/// <remarks>Big-endian</remarks>
public int MetadataVersion { get; set; }
/// <summary>
/// Size of content in bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public long ContentSize { get; set; }
/// <summary>
/// Media ID
/// </summary>
/// <remarks>Big-endian</remarks>
public uint MediaID { get; set; }
/// <summary>
/// Version of System/Title Updates
/// </summary>
/// <remarks>Big-endian</remarks>
public int Version { get; set; }
/// <summary>
/// Base Version of System/Title Updates
/// </summary>
/// <remarks>Big-endian</remarks>
public int BaseVersion { get; set; }
/// <summary>
/// Title ID
/// </summary>
/// <remarks>Big-endian</remarks>
public uint TitleID { get; set; }
/// <summary>
/// Intended platform for content
/// 0 = ???, 2 = Xbox 360, 4 = PC
/// </summary>
public byte Platform { get; set; }
/// <summary>
/// Intended platform for content
/// Xbox 360 = 2, PC = 4
/// </summary>
public byte ExecutableType { get; set; }
/// <summary>
/// Disc Number
/// </summary>
public byte DiscNumber { get; set; }
/// <summary>
/// Disc In Set
/// </summary>
public byte DiscInSet { get; set; }
/// <summary>
/// Save Game ID
/// </summary>
/// <remarks>Big-endian</remarks>
public uint SaveGameID { get; set; }
/// <summary>
/// Console ID
/// </summary>
/// <remarks>5 bytes</remarks>
public byte[] ConsoleID { get; set; } = new byte[5];
/// <summary>
/// Profile ID
/// </summary>
/// <remarks>8 bytes</remarks>
public byte[] ProfileID { get; set; } = new byte[8];
/// <summary>
/// STFS Volume Descriptor
/// Not optional, but abstract class
/// </summary>
public VolumeDescriptor? VolumeDescriptor { get; set; }
/// <summary>
/// Data File Count
/// </summary>
/// <remarks>Big-endian</remarks>
public int DataFileCount { get; set; }
/// <summary>
/// Data File Combined Size, in bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public long DataFileCombinedSize { get; set; }
/// <summary>
/// Descriptor Type
/// 0 = STFS, 1 = SVOD
/// </summary>
/// <remarks>Big-endian</remarks>
public uint DescriptorType { get; set; }
/// <summary>
/// Reserved bytes, should be all zeroed
/// </summary>
/// <remarks>Big-endian</remarks>
public uint Reserved { get; set; }
/// <summary>
/// Series ID
/// Zeroed for MetadataVersion = 1
/// </summary>
/// <remarks>16 bytes</remarks>
public byte[]? SeriesID { get; set; } = new byte[16];
/// <summary>
/// Season ID
/// Zeroed for MetadataVersion = 1
/// </summary>
/// <remarks>16 bytes</remarks>
public byte[]? SeasonID { get; set; } = new byte[16];
/// <summary>
/// Season Number
/// Zeroed for MetadataVersion = 1
/// </summary>
/// <remarks>Big-endian</remarks>
public short? SeasonNumber { get; set; }
/// <summary>
/// Season Number
/// Zeroed for MetadataVersion = 1
/// </summary>
/// <remarks>Big-endian</remarks>
public short? EpisodeNumber { get; set; }
/// <summary>
/// Padding bytes
/// If MetadataVersion is 2, there are 40 bytes
/// Otherwise, there are 76 bytes
/// </summary>
/// <remarks>40 bytes</remarks>
public byte[] Padding { get; set; } = [];
/// <summary>
/// Device ID
/// </summary>
/// <remarks>20 bytes</remarks>
public byte[] DeviceID { get; set; } = new byte[20];
/// <summary>
/// Display Name, UTF-8 string
/// 128 bytes per locale, 18 different locales
/// </summary>
/// <remarks>2304 bytes, UTF-8 string</remarks>
public byte[] DisplayName { get; set; } = new byte[2304];
/// <summary>
/// Display Description, UTF-8 string
/// 128 bytes per locale, 18 different locales
/// </summary>
/// <remarks>2304 bytes, UTF-8 string</remarks>
public byte[] DisplayDescription { get; set; } = new byte[2304];
/// <summary>
/// Publisher Name, UTF-8 string
/// </summary>
/// <remarks>128 bytes, UTF-8 string</remarks>
public byte[] PublisherName { get; set; } = new byte[128];
/// <summary>
/// Title Name, UTF-8 string
/// </summary>
/// <remarks>128 bytes, UTF-8 string</remarks>
public byte[] TitleName { get; set; } = new byte[128];
/// <summary>
/// Transfer Flags, see Constants.TransferFlags
/// </summary>
public byte TransferFlags { get; set; }
/// <summary>
/// Size of the thumbnail image, in bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public int ThumbnailImageSize { get; set; }
/// <summary>
/// Size of the title thumbnail image, in bytes
/// </summary>
/// <remarks>Big-endian</remarks>
public int TitleThumbnailImageSize { get; set; }
/// <summary>
/// Thumbnail image
/// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
/// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
/// </summary>
public byte[] ThumbnailImage { get; set; } = [];
/// <summary>
/// Additional Display Names, UTF-8 string
/// 128 bytes per locale, 6 different locales
/// Only present if MetadataVersion = 2
/// </summary>
/// <remarks>If present, 768 bytes, UTF-8 string</remarks>
public byte[]? AdditionalDisplayNames { get; set; }
/// <summary>
/// Title thumbnail image
/// If Metadata version = 1, 0x4000 bytes allocated, padded with zeroes
/// If Metadata version = 2, 0x3D00 bytes allocated, padded with zeroes
/// </summary>
public byte[] TitleThumbnailImage { get; set; } = [];
/// <summary>
/// Additional Display Descriptions, UTF-8 string
/// 128 bytes per locale, 6 different locales
/// Only present if MetadataVersion = 2
/// </summary>
/// <remarks>If present, 768 bytes, UTF-8 string</remarks>
public byte[]? AdditionalDisplayDescriptions { get; set; }
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// Secure Transacted File System (STFS) License Entry format
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class LicenseEntry
{
/// <summary>
/// License ID
/// </summary>
public long LicenseID { get; set; }
/// <summary>
/// License Bits
/// </summary>
public int LicenseBits { get; set; }
/// <summary>
/// License Flags
/// </summary>
public int LicenseFlags { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// Signature block signed by Microsoft, for LIVE and PIRS format STFS files
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class MicrosoftSignature : Signature
{
/// <summary>
/// Signature remotely signed by Microsoft
/// </summary>
/// <remarks>256 bytes</remarks>
public byte[] PackageSignature { get; set; } = new byte[256];
/// <summary>
/// Zeroed padding
/// </summary>
/// <remarks>296 bytes</remarks>
public byte[] Padding { get; set; } = new byte[296];
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Volume Descriptor, for STFS packages
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class STFSDescriptor : VolumeDescriptor
{
/// <summary>
/// Volume descriptor size (Should be 0x24)
/// </summary>
public byte VolumeDescriptorSize { get; set; }
/// <summary>
/// Reserved (Should be 0x00)
/// </summary>
public byte Reserved { get; set; }
/// <summary>
/// Block Separation
/// </summary>
public byte BlockSeparation { get; set; }
/// <summary>
/// File Table Block Count
/// </summary>
/// <remarks>Big-endian</remarks>
public short FileTableBlockCount { get; set; }
/// <summary>
/// File Table Block Number
/// </summary>
/// <remarks>Big-endian, 3-byte int24</remarks>
public Int24 FileTableBlockNumber { get; set; } = new();
/// <summary>
/// Top Hash Table Hash
/// </summary>
/// <remarks>20 bytes</remarks>
public byte[] TopHashTableHash { get; set; } = new byte[20];
/// <summary>
/// Total Allocated Block Count
/// </summary>
/// <remarks>Big-endian</remarks>
public int TotalAllocatedBlockCount { get; set; }
/// <summary>
/// Total Unallocated Block Count
/// </summary>
/// <remarks>Big-endian</remarks>
public int TotalUnallocatedBlockCount { get; set; }
}
}

View File

@@ -0,0 +1,56 @@
using System.Collections.Generic;
using SabreTools.Numerics;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Volume Descriptor, for STFS packages
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class SVODDescriptor : VolumeDescriptor
{
/// <summary>
/// Volume descriptor size (Should be 0x24)
/// </summary>
public byte VolumeDescriptorSize { get; set; }
/// <summary>
/// Block Cache Element Count
/// </summary>
public byte BlockCacheElementCount { get; set; }
/// <summary>
/// Worker Thread Processor
/// </summary>
public byte WorkerThreadProcessor { get; set; }
/// <summary>
/// Worker Thread Priority
/// </summary>
public byte WorkerThreadPriority { get; set; }
/// <summary>
/// Hash
/// </summary>
/// <remarks>20 bytes</remarks>
public byte[] Hash { get; set; } = new byte[20];
/// <summary>
/// Data Block Count
/// </summary>
/// <remarks>Big-endian, 3-byte uint24</remarks>
public UInt24 DataBlockCount { get; set; } = new();
/// <summary>
/// Data Block Offset
/// </summary>
/// <remarks>Big-endian, 3-byte uint24</remarks>
public UInt24 DataBlockOffset { get; set; } = new();
/// <summary>
/// Padding, should be zeroed
/// </summary>
/// <remarks>5 bytes</remarks>
public byte[] Padding { get; set; } = new byte[5];
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Signature, differs for remotely signed (PIRS/LIVE) and console signed ("CON ") formats
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public abstract class Signature
{
// Filled in by child class, MicrosoftSignature or ConsoleSignature
}
}

View File

@@ -0,0 +1,37 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// Secure Transacted File System, used by Xbox 360
/// There are three formats: "LIVE", "PIRS", "CON "
/// LIVE/PIRS are read-only signed by Microsoft, "CON " is read/write signed by console
/// LIVE files are only distributed via Xbox Live, PIRS can be found elsewhere (e.g. system updates)
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public class Volume
{
/// <summary>
/// STFS Header data
/// Should be 0xA000 bytes (10 blocks)
/// </summary>
public Header Header { get; set; } = new();
/// <summary>
/// An STFS volume contains blocks dedicated to integrity hashes
/// Each 4096-byte block has 170 integrity hashes for other blocks
/// A new hash table block exists after every 170 data blocks
/// i.e. First hash block is at 0x0B000 then next is at 0xB7000
/// </summary>
/// <remarks>Reader does not fill this in yet</remarks>
public HashTable[]? HashTables { get; set; }
/// <summary>
/// Data in the STFS, arranged in blocks of 4096-bytes
/// The Hash Table Blocks are interleaved, and ignored when numbering
/// i.e. Block 170 is not adjacent to Block 171
/// </summary>
/// <remarks>Too large to read into memory, left in model for posterity</remarks>
public byte[]? Data { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace SabreTools.Data.Models.STFS
{
/// <summary>
/// STFS Volume Descriptor, differs for STFS and SVOD packages
/// </summary>
/// <see href="https://free60.org/System-Software/Formats/STFS/"/>
public abstract class VolumeDescriptor
{
// Filled in by child class, SFTSDescriptor or SVODDescriptor
}
}