diff --git a/src/LibHac/Fs/QueryRangeInfo.cs b/src/LibHac/Fs/QueryRangeInfo.cs index d44cfbf8..3be7c655 100644 --- a/src/LibHac/Fs/QueryRangeInfo.cs +++ b/src/LibHac/Fs/QueryRangeInfo.cs @@ -19,12 +19,12 @@ public struct QueryRangeInfo AesCtrKeyType |= other.AesCtrKeyType; SpeedEmulationType |= other.SpeedEmulationType; } +} - [Flags] - public enum AesCtrKeyTypeFlag - { - InternalKeyForSoftwareAes = 1 << 0, - InternalKeyForHardwareAes = 1 << 1, - ExternalKeyForHardwareAes = 1 << 2 - } +[Flags] +public enum AesCtrKeyTypeFlag +{ + InternalKeyForSoftwareAes = 1 << 0, + InternalKeyForHardwareAes = 1 << 1, + ExternalKeyForHardwareAes = 1 << 2 } \ No newline at end of file diff --git a/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs b/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs index 45d0cddc..f9449a51 100644 --- a/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs +++ b/src/LibHac/FsSystem/AesCtrCounterExtendedStorage.cs @@ -404,8 +404,8 @@ public class AesCtrCounterExtendedStorage : IStorage Unsafe.SkipInit(out QueryRangeInfo info); info.Clear(); info.AesCtrKeyType = (int)(_decryptor.Get.HasExternalDecryptionKey() - ? QueryRangeInfo.AesCtrKeyTypeFlag.ExternalKeyForHardwareAes - : QueryRangeInfo.AesCtrKeyTypeFlag.InternalKeyForHardwareAes); + ? AesCtrKeyTypeFlag.ExternalKeyForHardwareAes + : AesCtrKeyTypeFlag.InternalKeyForHardwareAes); outInfo.Merge(in info); diff --git a/src/LibHac/FsSystem/AesCtrStorage.cs b/src/LibHac/FsSystem/AesCtrStorage.cs index 5fed0307..13eb5b22 100644 --- a/src/LibHac/FsSystem/AesCtrStorage.cs +++ b/src/LibHac/FsSystem/AesCtrStorage.cs @@ -215,7 +215,7 @@ public class AesCtrStorage : IStorage Unsafe.SkipInit(out QueryRangeInfo info); info.Clear(); - info.AesCtrKeyType = (int)QueryRangeInfo.AesCtrKeyTypeFlag.InternalKeyForSoftwareAes; + info.AesCtrKeyType = (int)AesCtrKeyTypeFlag.InternalKeyForSoftwareAes; outInfo.Merge(in info); diff --git a/src/LibHac/FsSystem/BucketTree.cs b/src/LibHac/FsSystem/BucketTree.cs index f18e619a..c454f602 100644 --- a/src/LibHac/FsSystem/BucketTree.cs +++ b/src/LibHac/FsSystem/BucketTree.cs @@ -128,7 +128,7 @@ file struct StorageNode /// Based on nnSdk 16.2.0 (FS 16.0.0) public partial class BucketTree : IDisposable { - private const uint Signature = 0x52544B42; // BKTR + public const uint Signature = 0x52544B42; // BKTR private const int MaxVersion = 1; private const int NodeSizeMin = 1024; diff --git a/src/LibHac/FsSystem/CompressionCommon.cs b/src/LibHac/FsSystem/CompressionCommon.cs index e0d77183..8ed4b7ef 100644 --- a/src/LibHac/FsSystem/CompressionCommon.cs +++ b/src/LibHac/FsSystem/CompressionCommon.cs @@ -10,7 +10,14 @@ public enum CompressionType : byte Unknown = 4 } -public delegate Result DecompressorFunction(Span destination, ReadOnlySpan source); +public ref struct DecompressionTask +{ + public Span Destination; + public ReadOnlySpan Source; +} + +public delegate Result DecompressorFunction(DecompressionTask task); + public delegate DecompressorFunction GetDecompressorFunction(CompressionType compressionType); public static class CompressionTypeUtility diff --git a/src/LibHac/FsSystem/IAesCtrDecryptor.cs b/src/LibHac/FsSystem/IAesCtrDecryptor.cs new file mode 100644 index 00000000..b1a1709a --- /dev/null +++ b/src/LibHac/FsSystem/IAesCtrDecryptor.cs @@ -0,0 +1,13 @@ +using System; +using LibHac.Fs; +using LibHac.Spl; + +namespace LibHac.FsSystem; + +public interface IAesCtrDecryptor : IDisposable +{ + Result Decrypt(Span destination, ReadOnlySpan iv, ReadOnlySpan source); + void PrioritizeSw(); + void SetExternalKeySource(in Spl.AccessKey keySource); + AesCtrKeyTypeFlag GetKeyTypeFlag(); +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaHeader.cs b/src/LibHac/FsSystem/NcaHeader.cs index 4efacc51..9b696052 100644 --- a/src/LibHac/FsSystem/NcaHeader.cs +++ b/src/LibHac/FsSystem/NcaHeader.cs @@ -1,12 +1,51 @@ using System; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using LibHac.Common; using LibHac.Common.FixedArrays; +using LibHac.Crypto; using LibHac.Fs; +using LibHac.Util; +using static LibHac.FsSystem.Anonymous; namespace LibHac.FsSystem; +file static class Anonymous +{ + public static bool IsZero(ReadOnlySpan value) where T : unmanaged + { + ReadOnlySpan valueBytes = MemoryMarshal.Cast(value); + Span zero = stackalloc byte[valueBytes.Length]; + zero.Clear(); + + return CryptoUtil.IsSameBytes(valueBytes, zero, valueBytes.Length); + } + + public static bool IsZero(in T value) where T : unmanaged + { + ReadOnlySpan valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value); + Span zero = stackalloc byte[Unsafe.SizeOf()]; + zero.Clear(); + + return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf()); + } + + public static bool IsZero(in T value, int startOffset) where T : unmanaged + { + ReadOnlySpan valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value).Slice(startOffset); + Span zero = stackalloc byte[Unsafe.SizeOf() - startOffset]; + zero.Clear(); + + return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf() - startOffset); + } + + public static bool IsIncluded(T value, T min, T max) where T : IComparisonOperators + { + return min <= value && value <= max; + } +} + /// /// The structure used as the header for an NCA file. /// @@ -96,6 +135,76 @@ public struct NcaHeader public static uint ByteToSector(ulong byteIndex) => (uint)(byteIndex >> SectorShift); public readonly byte GetProperKeyGeneration() => Math.Max(KeyGeneration1, KeyGeneration2); + + public readonly Result Verify() + { + const uint magicBodyMask = 0xFFFFFF; + const uint magicVersionMask = 0xFF000000; + const uint magicBodyValue = 0x41434E; // NCA + const uint magicVersionMax = 0x33000000; // \0\0\03 + + if ((Magic & magicBodyMask) != magicBodyValue || (Magic & magicVersionMask) > magicVersionMax) + return ResultFs.InvalidNcaHeader.Log(); + + if (!IsIncluded((int)DistributionTypeValue, (int)DistributionType.Download, (int)DistributionType.GameCard)) + return ResultFs.InvalidNcaHeader.Log(); + + if (!IsIncluded((int)ContentTypeValue, (int)ContentType.Program, (int)ContentType.PublicData)) + return ResultFs.InvalidNcaHeader.Log(); + + if (KeyAreaEncryptionKeyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount && + KeyAreaEncryptionKeyIndex != NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexZeroKey) + { + return ResultFs.InvalidNcaHeader.Log(); + } + + if (ProgramId == 0) + return ResultFs.InvalidNcaHeader.Log(); + + if (SdkAddonVersion == 0) + return ResultFs.InvalidNcaHeader.Log(); + + if (!IsZero(in Reserved222)) + return ResultFs.InvalidNcaHeader.Log(); + + if (!IsZero(in Reserved224)) + return ResultFs.InvalidNcaHeader.Log(); + + long es = long.MaxValue; + + for (int i = 0; i < FsCountMax; i++) + { + if (FsInfos[i].StartSector != 0 || FsInfos[i].EndSector != 0) + { + if (es == long.MaxValue) + es = FsInfos[i].EndSector; + + if (es < FsInfos[i].EndSector || FsInfos[i].StartSector >= FsInfos[i].EndSector) + return ResultFs.InvalidNcaHeader.Log(); + + es = FsInfos[i].StartSector; + + if (FsInfos[i].HashSectors != ByteToSector(0x200)) + return ResultFs.InvalidNcaHeader.Log(); + } + else if (FsInfos[i].HashSectors != 0) + { + return ResultFs.InvalidNcaHeader.Log(); + } + + if (FsInfos[i].Reserved != 0) + return ResultFs.InvalidNcaHeader.Log(); + } + + const int offset = (int)DecryptionKey.Count * Aes.KeySize128; + if (!IsZero(EncryptedKeys[..][offset..])) + return ResultFs.InvalidNcaHeader.Log(); + + if (!IsZero(EncryptedKeys[offset..])) + return ResultFs.InvalidNcaHeader.Log(); + + return Result.Success; + } } public struct NcaPatchInfo @@ -107,8 +216,15 @@ public struct NcaPatchInfo public long AesCtrExSize; public Array16 AesCtrExHeader; - public readonly bool HasIndirectTable() => IndirectSize != 0; - public readonly bool HasAesCtrExTable() => AesCtrExSize != 0; + public readonly bool HasIndirectTable() + { + return Unsafe.As, uint>(ref Unsafe.AsRef(in IndirectHeader)) == BucketTree.Signature; + } + + public readonly bool HasAesCtrExTable() + { + return Unsafe.As, uint>(ref Unsafe.AsRef(in AesCtrExHeader)) == BucketTree.Signature; + } } public struct NcaSparseInfo @@ -129,6 +245,11 @@ public struct NcaSparseInfo sparseUpperIv.Generation = GetGeneration(); return sparseUpperIv; } + + public readonly bool HasSparseTable() + { + return Unsafe.As, uint>(ref Unsafe.AsRef(in MetaHeader)) == BucketTree.Signature; + } } public struct NcaCompressionInfo @@ -219,7 +340,8 @@ public struct NcaFsHeader public enum MetaDataHashType : byte { None = 0, - HierarchicalIntegrity = 1 + HierarchicalIntegrity = 1, + HierarchicalIntegritySha3 = 2 } public struct Region @@ -240,10 +362,45 @@ public struct NcaFsHeader public int BlockSize; public int LayerCount; public Array5 LayerRegions; + + public readonly Result Verify() + { + if (IsZero(in MasterHash)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (BlockSize <= 0 || !BitUtil.IsPowerOfTwo(BlockSize)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (LayerCount != 2) + return ResultFs.InvalidNcaFsHeader.Log(); + + long currentOffset = 0; + + for (int i = 0; i < LayerCount; i++) + { + if (currentOffset > LayerRegions[i].Offset) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (LayerRegions[i].Size <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + currentOffset = LayerRegions[i].Offset + LayerRegions[i].Size; + } + + for (int i = LayerCount; i < 5; i++) + { + if (!IsZero(in LayerRegions[i])) + return ResultFs.InvalidNcaFsHeader.Log(); + } + + return Result.Success; + } } public struct IntegrityMetaInfo { + public const int HashSize = Sha256Generator.HashSize; + public uint Magic; public uint Version; public uint MasterHashSize; @@ -269,9 +426,194 @@ public struct NcaFsHeader public Array32 Value; } } + + public readonly Result Verify() + { + if (Magic != HierarchicalIntegrityVerificationStorage.IntegrityVerificationStorageMagic) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (Version != HierarchicalIntegrityVerificationStorage.IntegrityVerificationStorageVersion) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (MasterHashSize != HashSize) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsIncluded(LevelHashInfo.MaxLayers, Constants.IntegrityMinLayerCount, Constants.IntegrityMaxLayerCount)) + return ResultFs.InvalidNcaFsHeader.Log(); + + long currentOffset = 0; + + for (int i = 0; i < LevelHashInfo.MaxLayers - 1; i++) + { + ref readonly InfoLevelHash.HierarchicalIntegrityVerificationLevelInformation layer = ref LevelHashInfo.Layers[i]; + + if (layer.OrderBlock <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (currentOffset > layer.Offset || !Alignment.IsAligned(layer.Offset.Get(), (ulong)(1 << layer.OrderBlock))) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (layer.Size <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsZero(layer.Reserved)) + return ResultFs.InvalidNcaFsHeader.Log(); + + currentOffset = layer.Offset + layer.Size; + } + + for (int i = LevelHashInfo.MaxLayers - 1; i < 6; i++) + { + if (!IsZero(in LevelHashInfo.Layers[i])) + return ResultFs.InvalidNcaFsHeader.Log(); + } + + return Result.Success; + } } } + public Result Verify() + { + if (Version != 2) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsIncluded((int)FsTypeValue, (int)FsType.RomFs, (int)FsType.PartitionFs)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsIncluded((int)HashTypeValue, (int)HashType.None, (int)HashType.HierarchicalIntegritySha3Hash) || HashTypeValue == HashType.AutoSha3) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsIncluded((int)EncryptionTypeValue, (int)EncryptionType.None, (int)EncryptionType.AesCtrExSkipLayerHash)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsIncluded((int)MetaDataHashTypeValue, (int)MetaDataHashType.None, (int)MetaDataHashType.HierarchicalIntegritySha3)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsZero(in Reserved)) + return ResultFs.InvalidNcaFsHeader.Log(); + + switch (HashTypeValue) + { + case HashType.HierarchicalSha256Hash: + case HashType.HierarchicalSha3256Hash: + { + Result res = HashDataValue.HierarchicalSha256.Verify(); + if (res.IsFailure()) return res.Miss(); + + if (!IsZero(in HashDataValue, Unsafe.SizeOf())) + return ResultFs.InvalidNcaFsHeader.Log(); + + break; + } + case HashType.HierarchicalIntegrityHash: + case HashType.HierarchicalIntegritySha3Hash: + { + Result res = HashDataValue.IntegrityMeta.Verify(); + if (res.IsFailure()) return res.Miss(); + + if (!IsZero(in HashDataValue, Unsafe.SizeOf())) + return ResultFs.InvalidNcaFsHeader.Log(); + + break; + } + default: + { + if (!IsZero(in HashDataValue)) + return ResultFs.InvalidNcaFsHeader.Log(); + + break; + } + } + + if (EncryptionTypeValue == EncryptionType.AesCtrEx || EncryptionTypeValue == EncryptionType.AesCtrExSkipLayerHash) + { + if (PatchInfo.IndirectOffset < 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (PatchInfo.IndirectSize <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (IsZero(in PatchInfo.IndirectHeader)) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (PatchInfo.AesCtrExOffset < 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (MetaDataHashTypeValue == MetaDataHashType.None) + { + if (PatchInfo.AesCtrExSize <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + } + else if (PatchInfo.IndirectOffset == 0) + { + if (PatchInfo.AesCtrExSize != 0) + return ResultFs.InvalidNcaFsHeader.Log(); + } + else if (PatchInfo.AesCtrExSize <= 0) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + if (IsZero(in PatchInfo.AesCtrExHeader)) + return ResultFs.InvalidNcaFsHeader.Log(); + } + else if (EncryptionTypeValue != EncryptionType.None && !IsZero(in PatchInfo)) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + if (EncryptionTypeValue != EncryptionType.AesCtr + && EncryptionTypeValue != EncryptionType.AesCtrEx + && EncryptionTypeValue != EncryptionType.AesCtrSkipLayerHash + && EncryptionTypeValue != EncryptionType.AesCtrExSkipLayerHash + && !IsZero(in AesCtrUpperIv)) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + if (SparseInfo.Generation != 0) + { + if (SparseInfo.MetaOffset < 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (SparseInfo.MetaSize < 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (SparseInfo.PhysicalOffset < 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (!IsZero(SparseInfo.Reserved)) + return ResultFs.InvalidNcaFsHeader.Log(); + + var header = SpanHelpers.AsStruct(SparseInfo.MetaHeader); + Result res = header.Verify(); + if (res.IsFailure()) return res.Miss(); + } + else if (!IsZero(in SparseInfo)) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + if (MetaDataHashTypeValue != MetaDataHashType.None) + { + if (MetaDataHashDataInfo.Offset <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + if (MetaDataHashDataInfo.Size <= 0) + return ResultFs.InvalidNcaFsHeader.Log(); + } + else if (!IsZero(MetaDataHashDataInfo)) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + if (!IsZero(Padding)) + return ResultFs.InvalidNcaFsHeader.Log(); + + return Result.Success; + } + public readonly Result GetHashTargetOffset(out long outOffset) { UnsafeHelpers.SkipParamInit(out outOffset); diff --git a/src/LibHac/FsSystem/NcaReader17.cs b/src/LibHac/FsSystem/NcaReader17.cs new file mode 100644 index 00000000..b04ecd1a --- /dev/null +++ b/src/LibHac/FsSystem/NcaReader17.cs @@ -0,0 +1,454 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Spl; + +namespace LibHac.FsSystem; + +/// +/// Handles reading information from an NCA's header. +/// +/// Based on nnSdk 17.5.0 (FS 17.0.0) +public class NcaReader17 : IDisposable +{ + private RuntimeNcaHeader _header; + private SharedRef _bodyStorage; + private SharedRef _headerStorage; + private SharedRef _aesCtrDecryptor; + private GetDecompressorFunction _getDecompressorFunc; + private IHash256GeneratorFactorySelector _hashGeneratorFactorySelector; + + public NcaReader17(in RuntimeNcaHeader runtimeNcaHeader, ref readonly SharedRef notVerifiedHeaderStorage, + ref readonly SharedRef bodyStorage, ref readonly SharedRef aesCtrDecryptor, + in NcaCompressionConfiguration compressionConfig, IHash256GeneratorFactorySelector hashGeneratorFactorySelector) + { + Assert.SdkRequiresNotNull(in notVerifiedHeaderStorage); + Assert.SdkRequiresNotNull(in bodyStorage); + Assert.SdkRequiresNotNull(hashGeneratorFactorySelector); + + _header = runtimeNcaHeader; + + _headerStorage = SharedRef.CreateCopy(in notVerifiedHeaderStorage); + _bodyStorage = SharedRef.CreateCopy(in bodyStorage); + _aesCtrDecryptor = SharedRef.CreateCopy(in aesCtrDecryptor); + + _getDecompressorFunc = compressionConfig.GetDecompressorFunc; + _hashGeneratorFactorySelector = hashGeneratorFactorySelector; + } + + public void Dispose() + { + _bodyStorage.Destroy(); + _headerStorage.Destroy(); + _aesCtrDecryptor.Destroy(); + } + + public Result ReadHeader(out NcaFsHeader outHeader, int index) + { + UnsafeHelpers.SkipParamInit(out outHeader); + + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + long offset = _header.FsHeadersOffset + Unsafe.SizeOf() * (long)index; + return _headerStorage.Get.Read(offset, SpanHelpers.AsByteSpan(ref outHeader)).Ret(); + } + + public Result GetHeaderSign2(Span outBuffer) + { + Assert.SdkRequiresGreaterEqual((uint)outBuffer.Length, _header.Header2SignInfo.Size); + + return _headerStorage.Get + .Read(_header.Header2SignInfo.Size, outBuffer.Slice(0, (int)_header.Header2SignInfo.Size)).Ret(); + } + + public void GetHeaderSign2TargetHash(Span outBuffer) + { + Assert.SdkRequiresEqual(outBuffer.Length, Unsafe.SizeOf()); + + _header.Header2SignInfo.Hash.Value[..].CopyTo(outBuffer); + } + + public SharedRef GetSharedBodyStorage() + { + Assert.SdkRequiresNotNull(_bodyStorage); + + return SharedRef.CreateCopy(in _bodyStorage); + } + + public NcaHeader.DistributionType GetDistributionType() + { + return _header.DistributionType; + } + + public NcaHeader.ContentType GetContentType() + { + return _header.ContentType; + } + + public byte GetKeyGeneration() + { + return _header.KeyGeneration; + } + + public ulong GetProgramId() + { + return _header.ProgramId; + } + + public void GetRightsId(Span outBuffer) + { + Assert.SdkRequiresGreaterEqual(outBuffer.Length, NcaHeader.RightsIdSize); + + _header.RightsId[..].CopyTo(outBuffer); + } + + public bool HasFsInfo(int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + return _header.FsInfos[index].StartSector != 0 || _header.FsInfos[index].EndSector != 0; + } + + public void GetFsHeaderHash(out Hash outHash, int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + outHash = _header.FsInfos[index].Hash; + } + + public void GetFsInfo(out NcaHeader.FsInfo outFsInfo, int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + outFsInfo = new NcaHeader.FsInfo + { + StartSector = _header.FsInfos[index].StartSector, + EndSector = _header.FsInfos[index].EndSector, + HashSectors = _header.FsInfos[index].HashSectors, + Reserved = 0 + }; + } + + public ulong GetFsOffset(int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].StartSector); + } + + public ulong GetFsEndOffset(int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector); + } + + public ulong GetFsSize(int index) + { + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector - _header.FsInfos[index].StartSector); + } + + public void PrioritizeSwAes() + { + if (_aesCtrDecryptor.HasValue) + { + _aesCtrDecryptor.Get.PrioritizeSw(); + } + } + + public void SetExternalDecryptionKey(in AccessKey keySource) + { + if (_aesCtrDecryptor.HasValue) + { + _aesCtrDecryptor.Get.SetExternalKeySource(in keySource); + } + } + + public RuntimeNcaHeader GetHeader() + { + return _header; + } + + public SharedRef GetDecryptor() + { + return SharedRef.CreateCopy(in _aesCtrDecryptor); + } + + public GetDecompressorFunction GetDecompressor() + { + Assert.SdkRequiresNotNull(_getDecompressorFunc); + return _getDecompressorFunc; + } + + public IHash256GeneratorFactorySelector GetHashGeneratorFactorySelector() + { + Assert.SdkRequiresNotNull(_hashGeneratorFactorySelector); + return _hashGeneratorFactorySelector; + } + + public Result Verify() + { + Assert.SdkRequiresNotNull(_bodyStorage); + + for (int fsIndex = 0; fsIndex < NcaHeader.FsCountMax; fsIndex++) + { + var reader = new NcaFsHeaderReader17(); + if (HasFsInfo(fsIndex)) + { + Result res = reader.Initialize(this, fsIndex); + if (res.IsFailure()) return res.Miss(); + + res = reader.Verify(_header.ContentType); + if (res.IsFailure()) return res.Miss(); + } + else + { + Result res = ReadHeader(out NcaFsHeader header, fsIndex); + if (res.IsFailure()) return res.Miss(); + + NcaFsHeader zero = default; + if (!CryptoUtil.IsSameBytes(SpanHelpers.AsReadOnlyByteSpan(in header), + SpanHelpers.AsReadOnlyByteSpan(in zero), Unsafe.SizeOf())) + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + } + } + + return Result.Success; + } +} + +/// +/// Handles reading information from the of a file system inside an NCA file. +/// +/// Based on nnSdk 17.5.0 (FS 17.0.0) +public class NcaFsHeaderReader17 +{ + private NcaFsHeader _header; + private int _fsIndex; + + public NcaFsHeaderReader17() + { + _fsIndex = -1; + } + + public bool IsInitialized() + { + return _fsIndex >= 0; + } + + public Result Initialize(NcaReader17 reader, int index) + { + _fsIndex = -1; + + Result res = reader.ReadHeader(out _header, index); + if (res.IsFailure()) return res.Miss(); + + Unsafe.SkipInit(out Hash hash); + IHash256GeneratorFactory generator = reader.GetHashGeneratorFactorySelector().GetFactory(HashAlgorithmType.Sha2); + generator.GenerateHash(hash.Value, SpanHelpers.AsReadOnlyByteSpan(in _header)); + + reader.GetFsHeaderHash(out Hash fsHeaderHash, index); + + if (!CryptoUtil.IsSameBytes(fsHeaderHash.Value, hash.Value, Unsafe.SizeOf())) + { + return ResultFs.NcaFsHeaderHashVerificationFailed.Log(); + } + + _fsIndex = index; + return Result.Success; + } + + public ref readonly NcaFsHeader.HashData GetHashData() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.HashDataValue; + } + + public ushort GetVersion() + { + Assert.SdkRequires(IsInitialized()); + return _header.Version; + } + + public int GetFsIndex() + { + Assert.SdkRequires(IsInitialized()); + return _fsIndex; + } + + public NcaFsHeader.FsType GetFsType() + { + Assert.SdkRequires(IsInitialized()); + return _header.FsTypeValue; + } + + public NcaFsHeader.HashType GetHashType() + { + Assert.SdkRequires(IsInitialized()); + return _header.HashTypeValue; + } + + public NcaFsHeader.EncryptionType GetEncryptionType() + { + Assert.SdkRequires(IsInitialized()); + return _header.EncryptionTypeValue; + } + + public NcaFsHeader.MetaDataHashType GetPatchMetaHashType() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashTypeValue; + } + + public NcaFsHeader.MetaDataHashType GetSparseMetaHashType() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashTypeValue; + } + + public Result GetHashTargetOffset(out long outOffset) + { + Assert.SdkRequires(IsInitialized()); + + Result res = _header.GetHashTargetOffset(out outOffset); + if (res.IsFailure()) return res.Miss(); + + return Result.Success; + } + + public bool IsSkipLayerHashEncryption() + { + Assert.SdkRequires(IsInitialized()); + return _header.IsSkipLayerHashEncryption(); + } + + public ref readonly NcaPatchInfo GetPatchInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.PatchInfo; + } + + public NcaAesCtrUpperIv GetAesCtrUpperIv() + { + Assert.SdkRequires(IsInitialized()); + return _header.AesCtrUpperIv; + } + + public bool ExistsSparseLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.SparseInfo.Generation != 0; + } + + public ref readonly NcaSparseInfo GetSparseInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.SparseInfo; + } + + public bool ExistsCompressionLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.CompressionInfo.TableOffset != 0 && _header.CompressionInfo.TableSize != 0; + } + + public ref readonly NcaCompressionInfo GetCompressionInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.CompressionInfo; + } + + public bool ExistsPatchMetaHashLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashDataInfo.Size != 0 && GetPatchInfo().HasIndirectTable(); + } + + public bool ExistsSparseMetaHashLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashDataInfo.Size != 0 && ExistsSparseLayer(); + } + + public ref readonly NcaMetaDataHashDataInfo GetPatchMetaDataHashDataInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.MetaDataHashDataInfo; + } + + public ref readonly NcaMetaDataHashDataInfo GetSparseMetaDataHashDataInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.MetaDataHashDataInfo; + } + + public void GetRawData(Span outBuffer) + { + Assert.SdkRequires(IsInitialized()); + Assert.SdkRequiresLessEqual(Unsafe.SizeOf(), outBuffer.Length); + + SpanHelpers.AsReadOnlyByteSpan(in _header).CopyTo(outBuffer); + } + + public Result Verify(NcaHeader.ContentType contentType) + { + Assert.SdkRequires(IsInitialized()); + Assert.SdkRequiresWithinMinMax((int)contentType, (int)NcaHeader.ContentType.Program, (int)NcaHeader.ContentType.PublicData); + + Result res = _header.Verify(); + if (res.IsFailure()) return res.Miss(); + + const uint programSecureValue = 1; + const uint dataSecureValue = 2; + const uint htmlDocumentSecureValue = 4; + const uint legalInformationSecureValue = 5; + + // Mask out the program index part of the secure value + uint secureValue = _header.AesCtrUpperIv.SecureValue & 0xFFFFFF; + + if (GetEncryptionType() == NcaFsHeader.EncryptionType.None) + { + if (secureValue != 0) + return ResultFs.InvalidNcaFsHeader.Log(); + + return Result.Success; + } + + switch (contentType) + { + case NcaHeader.ContentType.Program: + switch (_fsIndex) + { + case 0: + if (secureValue != programSecureValue) + return ResultFs.InvalidNcaFsHeader.Log(); + break; + case 1: + if (secureValue != dataSecureValue) + return ResultFs.InvalidNcaFsHeader.Log(); + break; + default: + if (secureValue != 0) + return ResultFs.InvalidNcaFsHeader.Log(); + break; + } + + break; + case NcaHeader.ContentType.Manual: + if (secureValue != htmlDocumentSecureValue && secureValue != legalInformationSecureValue) + return ResultFs.InvalidNcaFsHeader.Log(); + break; + default: + if (secureValue != 0) + return ResultFs.InvalidNcaFsHeader.Log(); + break; + } + + return Result.Success; + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/RuntimeNcaHeader.cs b/src/LibHac/FsSystem/RuntimeNcaHeader.cs new file mode 100644 index 00000000..c07021a0 --- /dev/null +++ b/src/LibHac/FsSystem/RuntimeNcaHeader.cs @@ -0,0 +1,49 @@ +using System; +using LibHac.Common.FixedArrays; + +namespace LibHac.FsSystem; + +public struct RuntimeKeySourceInfo +{ + public uint Offset; + public uint Size; + public Hash Hash; +} + +public struct RuntimeNcaHeader +{ + public struct FsInfo + { + public uint StartSector; + public uint EndSector; + public uint HashSectors; + public Hash Hash; + } + + public struct SignInfo + { + public uint Offset; + public uint Size; + public Hash Hash; + } + + public NcaHeader.DistributionType DistributionType; + public NcaHeader.ContentType ContentType; + public byte KeyGeneration; + public ulong ProgramId; + public Array16 RightsId; + public uint FsHeadersOffset; + public Array4 FsInfos; + public SignInfo Header2SignInfo; + public RuntimeKeySourceInfo KeySourceInfo; + + public static Result CheckUnsupportedVersion(uint magicValue) + { + throw new NotImplementedException(); + } + + public Result InitializeCommonForV3(in NcaHeader header, IHash256GeneratorFactorySelector hashGeneratorFactorySelector) + { + throw new NotImplementedException(); + } +} \ No newline at end of file