Implement NcaReader from FS 17.0.0

This commit is contained in:
Alex Barney 2024-03-12 20:05:58 -07:00
parent 8027310320
commit b88d283fbc
9 changed files with 880 additions and 15 deletions

View file

@ -19,6 +19,7 @@ public struct QueryRangeInfo
AesCtrKeyType |= other.AesCtrKeyType;
SpeedEmulationType |= other.SpeedEmulationType;
}
}
[Flags]
public enum AesCtrKeyTypeFlag
@ -27,4 +28,3 @@ public struct QueryRangeInfo
InternalKeyForHardwareAes = 1 << 1,
ExternalKeyForHardwareAes = 1 << 2
}
}

View file

@ -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);

View file

@ -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);

View file

@ -128,7 +128,7 @@ file struct StorageNode
/// <remarks>Based on nnSdk 16.2.0 (FS 16.0.0)</remarks>
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;

View file

@ -10,7 +10,14 @@ public enum CompressionType : byte
Unknown = 4
}
public delegate Result DecompressorFunction(Span<byte> destination, ReadOnlySpan<byte> source);
public ref struct DecompressionTask
{
public Span<byte> Destination;
public ReadOnlySpan<byte> Source;
}
public delegate Result DecompressorFunction(DecompressionTask task);
public delegate DecompressorFunction GetDecompressorFunction(CompressionType compressionType);
public static class CompressionTypeUtility

View file

@ -0,0 +1,13 @@
using System;
using LibHac.Fs;
using LibHac.Spl;
namespace LibHac.FsSystem;
public interface IAesCtrDecryptor : IDisposable
{
Result Decrypt(Span<byte> destination, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> source);
void PrioritizeSw();
void SetExternalKeySource(in Spl.AccessKey keySource);
AesCtrKeyTypeFlag GetKeyTypeFlag();
}

View file

@ -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<T>(ReadOnlySpan<T> value) where T : unmanaged
{
ReadOnlySpan<byte> valueBytes = MemoryMarshal.Cast<T, byte>(value);
Span<byte> zero = stackalloc byte[valueBytes.Length];
zero.Clear();
return CryptoUtil.IsSameBytes(valueBytes, zero, valueBytes.Length);
}
public static bool IsZero<T>(in T value) where T : unmanaged
{
ReadOnlySpan<byte> valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value);
Span<byte> zero = stackalloc byte[Unsafe.SizeOf<T>()];
zero.Clear();
return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf<T>());
}
public static bool IsZero<T>(in T value, int startOffset) where T : unmanaged
{
ReadOnlySpan<byte> valueBytes = SpanHelpers.AsReadOnlyByteSpan(in value).Slice(startOffset);
Span<byte> zero = stackalloc byte[Unsafe.SizeOf<T>() - startOffset];
zero.Clear();
return CryptoUtil.IsSameBytes(valueBytes, zero, Unsafe.SizeOf<T>() - startOffset);
}
public static bool IsIncluded<T>(T value, T min, T max) where T : IComparisonOperators<T, T, bool>
{
return min <= value && value <= max;
}
}
/// <summary>
/// The structure used as the header for an NCA file.
/// </summary>
@ -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<byte> AesCtrExHeader;
public readonly bool HasIndirectTable() => IndirectSize != 0;
public readonly bool HasAesCtrExTable() => AesCtrExSize != 0;
public readonly bool HasIndirectTable()
{
return Unsafe.As<Array16<byte>, uint>(ref Unsafe.AsRef(in IndirectHeader)) == BucketTree.Signature;
}
public readonly bool HasAesCtrExTable()
{
return Unsafe.As<Array16<byte>, 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<Array16<byte>, 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<Region> 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,7 +426,192 @@ public struct NcaFsHeader
public Array32<byte> 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<byte>(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<HashData.HierarchicalSha256Data>()))
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<HashData.IntegrityMetaInfo>()))
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<BucketTree.Header>(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)

View file

@ -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;
/// <summary>
/// Handles reading information from an NCA's header.
/// </summary>
/// <remarks>Based on nnSdk 17.5.0 (FS 17.0.0)</remarks>
public class NcaReader17 : IDisposable
{
private RuntimeNcaHeader _header;
private SharedRef<IStorage> _bodyStorage;
private SharedRef<IStorage> _headerStorage;
private SharedRef<IAesCtrDecryptor> _aesCtrDecryptor;
private GetDecompressorFunction _getDecompressorFunc;
private IHash256GeneratorFactorySelector _hashGeneratorFactorySelector;
public NcaReader17(in RuntimeNcaHeader runtimeNcaHeader, ref readonly SharedRef<IStorage> notVerifiedHeaderStorage,
ref readonly SharedRef<IStorage> bodyStorage, ref readonly SharedRef<IAesCtrDecryptor> aesCtrDecryptor,
in NcaCompressionConfiguration compressionConfig, IHash256GeneratorFactorySelector hashGeneratorFactorySelector)
{
Assert.SdkRequiresNotNull(in notVerifiedHeaderStorage);
Assert.SdkRequiresNotNull(in bodyStorage);
Assert.SdkRequiresNotNull(hashGeneratorFactorySelector);
_header = runtimeNcaHeader;
_headerStorage = SharedRef<IStorage>.CreateCopy(in notVerifiedHeaderStorage);
_bodyStorage = SharedRef<IStorage>.CreateCopy(in bodyStorage);
_aesCtrDecryptor = SharedRef<IAesCtrDecryptor>.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<NcaFsHeader>() * (long)index;
return _headerStorage.Get.Read(offset, SpanHelpers.AsByteSpan(ref outHeader)).Ret();
}
public Result GetHeaderSign2(Span<byte> 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<byte> outBuffer)
{
Assert.SdkRequiresEqual(outBuffer.Length, Unsafe.SizeOf<Hash>());
_header.Header2SignInfo.Hash.Value[..].CopyTo(outBuffer);
}
public SharedRef<IStorage> GetSharedBodyStorage()
{
Assert.SdkRequiresNotNull(_bodyStorage);
return SharedRef<IStorage>.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<byte> 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<IAesCtrDecryptor> GetDecryptor()
{
return SharedRef<IAesCtrDecryptor>.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<NcaFsHeader>()))
{
return ResultFs.InvalidNcaFsHeader.Log();
}
}
}
return Result.Success;
}
}
/// <summary>
/// Handles reading information from the <see cref="NcaFsHeader"/> of a file system inside an NCA file.
/// </summary>
/// <remarks>Based on nnSdk 17.5.0 (FS 17.0.0)</remarks>
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<Hash>()))
{
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<byte> outBuffer)
{
Assert.SdkRequires(IsInitialized());
Assert.SdkRequiresLessEqual(Unsafe.SizeOf<NcaFsHeader>(), 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;
}
}

View file

@ -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<byte> RightsId;
public uint FsHeadersOffset;
public Array4<FsInfo> 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();
}
}