diff --git a/src/LibHac/Common/FixedArrays/Array96.cs b/src/LibHac/Common/FixedArrays/Array96.cs new file mode 100644 index 00000000..ca4fbad4 --- /dev/null +++ b/src/LibHac/Common/FixedArrays/Array96.cs @@ -0,0 +1,31 @@ +#pragma warning disable CS0169, CS0649, IDE0051 // Field is never used, Field is never assigned to, Remove unused private members +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.Common.FixedArrays; + +public struct Array96 +{ + public const int Length = 96; + + private Array80 _0; + private Array16 _80; + + public ref T this[int i] => ref Items[i]; + + public Span Items + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => SpanHelpers.CreateSpan(ref MemoryMarshal.GetReference(_0.Items), Length); + } + + public readonly ReadOnlySpan ItemsRo + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => SpanHelpers.CreateSpan(ref MemoryMarshal.GetReference(_0.ItemsRo), Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ReadOnlySpan(in Array96 value) => value.ItemsRo; +} \ No newline at end of file diff --git a/src/LibHac/Crypto/Rsa.cs b/src/LibHac/Crypto/Rsa.cs index 672a5463..c2af9a5e 100644 --- a/src/LibHac/Crypto/Rsa.cs +++ b/src/LibHac/Crypto/Rsa.cs @@ -6,6 +6,9 @@ namespace LibHac.Crypto; public static class Rsa { + public static readonly int ModulusSize2048Pss = 256; + public static readonly int MaximumExponentSize2048Pss = 3; + public static bool VerifyRsa2048PssSha256(ReadOnlySpan signature, ReadOnlySpan modulus, ReadOnlySpan exponent, ReadOnlySpan message) => VerifyRsa2048Sha256(signature, modulus, exponent, message, RSASignaturePadding.Pss); diff --git a/src/LibHac/Fs/Common.cs b/src/LibHac/Fs/Common.cs new file mode 100644 index 00000000..97f28384 --- /dev/null +++ b/src/LibHac/Fs/Common.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; + +namespace LibHac.Fs; + +[StructLayout(LayoutKind.Sequential, Pack = 4)] +public struct Int64 +{ + private long _value; + + public void Set(long value) + { + _value = value; + } + + public readonly long Get() + { + return _value; + } + + public static implicit operator long(in Int64 value) => value.Get(); +} \ No newline at end of file diff --git a/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs b/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs index bf2ec6da..31f1eeb1 100644 --- a/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs +++ b/src/LibHac/FsSrv/FsCreator/StorageOnNcaCreator.cs @@ -7,6 +7,7 @@ using LibHac.FsSystem; using LibHac.FsSystem.Impl; using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; +using NcaFsHeader = LibHac.Tools.FsSystem.NcaUtils.NcaFsHeader; namespace LibHac.FsSrv.FsCreator; diff --git a/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs b/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs index db7be714..0e9a4362 100644 --- a/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs +++ b/src/LibHac/FsSrv/NcaFileSystemServiceImpl.cs @@ -13,6 +13,7 @@ using LibHac.Os; using LibHac.Spl; using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Util; +using NcaFsHeader = LibHac.Tools.FsSystem.NcaUtils.NcaFsHeader; using RightsId = LibHac.Fs.RightsId; using Utility = LibHac.FsSystem.Utility; diff --git a/src/LibHac/FsSrv/SaveDataInfoFilterReader.cs b/src/LibHac/FsSrv/SaveDataInfoFilter.cs similarity index 100% rename from src/LibHac/FsSrv/SaveDataInfoFilterReader.cs rename to src/LibHac/FsSrv/SaveDataInfoFilter.cs diff --git a/src/LibHac/FsSystem/CompressionCommon.cs b/src/LibHac/FsSystem/CompressionCommon.cs index 8670e46c..e0d77183 100644 --- a/src/LibHac/FsSystem/CompressionCommon.cs +++ b/src/LibHac/FsSystem/CompressionCommon.cs @@ -1,4 +1,6 @@ -namespace LibHac.FsSystem; +using System; + +namespace LibHac.FsSystem; public enum CompressionType : byte { @@ -8,6 +10,9 @@ public enum CompressionType : byte Unknown = 4 } +public delegate Result DecompressorFunction(Span destination, ReadOnlySpan source); +public delegate DecompressorFunction GetDecompressorFunction(CompressionType compressionType); + public static class CompressionTypeUtility { public static bool IsBlockAlignmentRequired(CompressionType type) diff --git a/src/LibHac/FsSystem/NcaFileSystemDriver.cs b/src/LibHac/FsSystem/NcaFileSystemDriver.cs new file mode 100644 index 00000000..adda8dae --- /dev/null +++ b/src/LibHac/FsSystem/NcaFileSystemDriver.cs @@ -0,0 +1,63 @@ +using LibHac.Common.FixedArrays; +using LibHac.Crypto; + +namespace LibHac.FsSystem; + +public struct NcaCryptoConfiguration +{ + public static readonly int Rsa2048KeyModulusSize = Rsa.ModulusSize2048Pss; + public static readonly int Rsa2048KeyPublicExponentSize = Rsa.MaximumExponentSize2048Pss; + public static readonly int Rsa2048KeyPrivateExponentSize = Rsa2048KeyModulusSize; + + public static readonly int Aes128KeySize = Aes.KeySize128; + + public static readonly int Header1SignatureKeyGenerationMax = 1; + + public static readonly int KeyAreaEncryptionKeyIndexCount = 3; + public static readonly int HeaderEncryptionKeyCount = 2; + + public static readonly int KeyGenerationMax = 32; + public static readonly int KeyAreaEncryptionKeyCount = KeyAreaEncryptionKeyIndexCount * KeyGenerationMax; + + public Array2> Header1SignKeyModuli; + public Array3 Header1SignKeyPublicExponent; + public Array3> KeyAreaEncryptionKeySources; + public Array16 HeaderEncryptionKeySource; + public Array2> HeaderEncryptedEncryptionKeys; + public GenerateKeyFunction GenerateKey; + public DecryptAesCtrFunction DecryptAesCtr; + public DecryptAesCtrFunction DecryptAesCtrForExternalKey; + public bool IsDev; +} + +public struct NcaCompressionConfiguration +{ + public GetDecompressorFunction GetDecompressorFunc; +} + +public static class NcaKeyFunctions +{ + public static bool IsInvalidKeyTypeValue(int keyType) + { + return keyType < 0; + } + + public static int GetKeyTypeValue(byte keyIndex, byte keyGeneration) + { + const int invalidKeyTypeValue = -1; + + if (keyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount) + return invalidKeyTypeValue; + + return NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount * keyGeneration + keyIndex; + } +} + +public enum KeyType +{ + NcaHeaderKey = 0x60, + NcaExternalKey = 0x61, + SaveDataDeviceUniqueMac = 0x62, + SaveDataSeedUniqueMac = 0x63, + SaveDataTransferMac = 0x64 +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaHeader.cs b/src/LibHac/FsSystem/NcaHeader.cs new file mode 100644 index 00000000..6de0e074 --- /dev/null +++ b/src/LibHac/FsSystem/NcaHeader.cs @@ -0,0 +1,238 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using LibHac.Common.FixedArrays; + +namespace LibHac.FsSystem; + +public struct NcaHeader +{ + public enum ContentType : byte + { + Program = 0, + Meta = 1, + Control = 2, + Manual = 3, + Data = 4, + PublicData = 5 + } + + public enum DistributionType : byte + { + Download = 0, + GameCard = 1 + } + + public enum EncryptionType : byte + { + Auto = 0, + None = 1 + } + + public enum DecryptionKey : byte + { + AesXts = 0, + AesXts1 = 0, + AesXts2 = 1, + AesCtr = 2, + AesCtrEx = 3, + AesCtrHw = 4, + Count + } + + public struct FsInfo + { + public uint StartSector; + public uint EndSector; + public uint HashSectors; + public uint Reserved; + } + + public static readonly uint Magic0 = 0x3041434E; // NCA0 + public static readonly uint Magic1 = 0x3141434E; // NCA1 + public static readonly uint Magic2 = 0x3241434E; // NCA2 + public static readonly uint Magic3 = 0x3341434E; // NCA3 + + public static readonly uint CurrentMagic = Magic3; + + public static readonly int Size = 0x400; + public static readonly int FsCountMax = 4; + public static readonly int HeaderSignCount = 2; + public static readonly int HeaderSignSize = 0x100; + public static readonly int EncryptedKeyAreaSize = 0x100; + public static readonly int SectorSize = 0x200; + public static readonly int SectorShift = 9; + public static readonly int RightsIdSize = 0x10; + public static readonly int XtsBlockSize = 0x200; + public static readonly int CtrBlockSize = 0x10; + + public Array256 Signature1; + public Array256 Signature2; + public uint Magic; + public DistributionType DistributionTypeValue; + public ContentType ContentTypeValue; + public byte KeyGeneration1; + public byte KeyAreaEncryptionKeyIndex; + public ulong ContentSize; + public ulong ProgramId; + public uint ContentIndex; + public uint SdkAddonVersion; + public byte KeyGeneration2; + public byte Header1SignatureKeyGeneration; + public Array2 Reserved222; + public Array3 Reserved224; + public Array16 RightsId; + public Array4 FsInfos; + public Array4 FsHeaderHashes; + public Array256 EncryptedKeys; + + public static ulong SectorToByte(uint sectorIndex) => sectorIndex << SectorShift; + public static uint ByteToSector(ulong byteIndex) => (uint)(byteIndex >> SectorShift); + + public readonly byte GetProperKeyGeneration() => Math.Max(KeyGeneration1, KeyGeneration2); +} + +public struct NcaPatchInfo +{ + public long IndirectOffset; + public long IndirectSize; + public Array16 IndirectHeader; + public long AesCtrExOffset; + public long AesCtrExSize; + public Array16 AesCtrExHeader; + + public readonly bool HasIndirectTable() => IndirectSize != 0; + public readonly bool HasAesCtrExTable() => AesCtrExSize != 0; +} + +public struct NcaSparseInfo +{ + public long MetaOffset; + public long MetaSize; + public Array16 MetaHeader; + public long PhysicalOffset; + public ushort Generation; + public Array6 Reserved; + + public readonly uint GetGeneration() => (uint)(Generation << 16); + public readonly long GetPhysicalSize() => MetaOffset + MetaSize; + + public readonly NcaAesCtrUpperIv MakeAesCtrUpperIv(NcaAesCtrUpperIv upperIv) + { + NcaAesCtrUpperIv sparseUpperIv = upperIv; + sparseUpperIv.Generation = GetGeneration(); + return sparseUpperIv; + } +} + +public struct NcaCompressionInfo +{ + public long TableOffset; + public long TableSize; + public Array16 TableHeader; + public ulong Reserved; +} + +[StructLayout(LayoutKind.Explicit)] +public struct NcaAesCtrUpperIv +{ + [FieldOffset(0)] public ulong Value; + + [FieldOffset(0)] public uint Generation; + [FieldOffset(4)] public uint SecureValue; + + internal NcaAesCtrUpperIv(ulong value) + { + Unsafe.SkipInit(out Generation); + Unsafe.SkipInit(out SecureValue); + Value = value; + } +} + +public struct NcaFsHeader +{ + public ushort Version; + public FsType FsTypeValue; + public HashType HashTypeValue; + public EncryptionType EncryptionTypeValue; + public Array3 Reserved; + public HashData HashDataValue; + public NcaPatchInfo PatchInfo; + public NcaAesCtrUpperIv AesCtrUpperIv; + public NcaSparseInfo SparseInfo; + public NcaCompressionInfo CompressionInfo; + public Array96 Padding; + + public enum FsType : byte + { + RomFs = 0, + PartitionFs = 1 + } + + public enum EncryptionType : byte + { + Auto = 0, + None = 1, + AesXts = 2, + AesCtr = 3, + AesCtrEx = 4 + } + + public enum HashType : byte + { + Auto = 0, + None = 1, + HierarchicalSha256Hash = 2, + HierarchicalIntegrityHash = 3 + } + + public struct Region + { + public long Offset; + public long Size; + } + + [StructLayout(LayoutKind.Explicit, Size = 0xF8)] + public struct HashData + { + [FieldOffset(0)] public HierarchicalSha256Data HierarchicalSha256; + [FieldOffset(0)] public IntegrityMetaInfo IntegrityMeta; + + public struct HierarchicalSha256Data + { + public Hash MasterHash; + public int BlockSize; + public int LayerCount; + public Array5 LayerRegions; + } + + public struct IntegrityMetaInfo + { + public uint Magic; + public uint Version; + public uint MasterHashSize; + public InfoLevelHash LevelHashInfo; + public Hash MasterHash; + + public struct InfoLevelHash + { + public int MaxLayers; + public Array6 Layers; + public SignatureSalt Salt; + + public struct HierarchicalIntegrityVerificationLevelInformation + { + public Fs.Int64 Offset; + public Fs.Int64 Size; + public int OrderBlock; + public Array4 Reserved; + } + + public struct SignatureSalt + { + public Array32 Value; + } + } + } + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaReader.cs b/src/LibHac/FsSystem/NcaReader.cs new file mode 100644 index 00000000..a8a081f0 --- /dev/null +++ b/src/LibHac/FsSystem/NcaReader.cs @@ -0,0 +1,561 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Common.FixedArrays; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; + +namespace LibHac.FsSystem; + +public delegate Result GenerateKeyFunction(Span destKey, ReadOnlySpan sourceKey, int keyType, in NcaCryptoConfiguration config); +public delegate Result DecryptAesCtrFunction(Span dest, int keyType, ReadOnlySpan encryptedKey, ReadOnlySpan iv, ReadOnlySpan source); + +/// +/// Handles reading information from an NCA file's header. +/// +/// Based on FS 13.1.0 (nnSdk 13.4.0) +public class NcaReader : IDisposable +{ + private const uint SdkAddonVersionMin = 0xB0000; + + private NcaHeader _header; + private Array5> _decryptionKeys; + private SharedRef _bodyStorage; + private UniqueRef _headerStorage; + private Array16 _externalDataDecryptionKey; + private DecryptAesCtrFunction _decryptAesCtr; + private DecryptAesCtrFunction _decryptAesCtrForExternalKey; + private bool _isSoftwareAesPrioritized; + private NcaHeader.EncryptionType _headerEncryptionType; + private GetDecompressorFunction _getDecompressorFunc; + private IHash256GeneratorFactory _hashGeneratorFactory; + + public void Dispose() + { + _headerStorage.Destroy(); + _bodyStorage.Destroy(); + } + + public Result Initialize(ref SharedRef baseStorage, in NcaCryptoConfiguration cryptoConfig, + in NcaCompressionConfiguration compressionConfig, IHash256GeneratorFactorySelector hashGeneratorFactorySelector) + { + Assert.SdkRequiresNotNull(in baseStorage); + Assert.SdkRequiresNotNull(hashGeneratorFactorySelector); + Assert.SdkRequiresNull(in _bodyStorage); + + if (cryptoConfig.GenerateKey is null) + return ResultFs.InvalidArgument.Log(); + + using var headerStorage = new UniqueRef(); + + // Generate the keys for decrypting the NCA header. + Unsafe.SkipInit(out Array2> commonDecryptionKeys); + for (int i = 0; i < NcaCryptoConfiguration.HeaderEncryptionKeyCount; i++) + { + cryptoConfig.GenerateKey(commonDecryptionKeys[i].Items, cryptoConfig.HeaderEncryptedEncryptionKeys[i], 0x60, + in cryptoConfig); + } + + // Create an XTS storage to read the encrypted header. + Array16 headerIv = default; + headerStorage.Reset(new AesXtsStorage(baseStorage.Get, commonDecryptionKeys[0], commonDecryptionKeys[1], + headerIv, NcaHeader.XtsBlockSize)); + + if (!headerStorage.HasValue) + return ResultFs.AllocationMemoryFailedInNcaReaderA.Log(); + + // Read the decrypted header. + Result rc = headerStorage.Get.Read(0, SpanHelpers.AsByteSpan(ref _header)); + if (rc.IsFailure()) return rc.Miss(); + + // Check if the NCA magic value is correct. + Result signatureResult = CheckSignature(in _header); + if (signatureResult.IsFailure()) + { + // If the magic value is not correct the header might not be encrypted. + if (cryptoConfig.IsDev) + { + // Read the header without decrypting it and check the magic value again. + rc = baseStorage.Get.Read(0, SpanHelpers.AsByteSpan(ref _header)); + if (rc.IsFailure()) return rc.Miss(); + + rc = CheckSignature(in _header); + if (rc.IsFailure()) + return signatureResult.Miss(); + + // We have a plaintext header. Get an IStorage of just the header. + rc = baseStorage.Get.GetSize(out long baseStorageSize); + if (rc.IsFailure()) return rc.Miss(); + + headerStorage.Reset(new SubStorage(in baseStorage, 0, baseStorageSize)); + + if (!headerStorage.HasValue) + return ResultFs.AllocationMemoryFailedInNcaReaderA.Log(); + + _headerEncryptionType = NcaHeader.EncryptionType.None; + } + else + { + return signatureResult.Miss(); + } + } + + // Validate the fixed key signature. + if (_header.Header1SignatureKeyGeneration > NcaCryptoConfiguration.Header1SignatureKeyGenerationMax) + return ResultFs.InvalidNcaHeader1SignatureKeyGeneration.Log(); + + int signMessageOffset = NcaHeader.HeaderSignSize * NcaHeader.HeaderSignCount; + int signMessageSize = NcaHeader.Size - signMessageOffset; + ReadOnlySpan signature = _header.Signature1; + ReadOnlySpan modulus = cryptoConfig.Header1SignKeyModuli[_header.Header1SignatureKeyGeneration]; + ReadOnlySpan exponent = cryptoConfig.Header1SignKeyPublicExponent; + ReadOnlySpan message = SpanHelpers.AsReadOnlyByteSpan(in _header).Slice(signMessageOffset, signMessageSize); + + if (!Rsa.VerifyRsa2048PssSha256(signature, modulus, exponent, message)) + return ResultFs.NcaHeaderSignature1VerificationFailed.Log(); + + // Validate the sdk version. + if (_header.SdkAddonVersion < SdkAddonVersionMin) + return ResultFs.UnsupportedSdkVersion.Log(); + + // Validate the key index. + if (_header.KeyAreaEncryptionKeyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount) + return ResultFs.InvalidNcaKeyIndex.Log(); + + // Get keys from the key area if the NCA doesn't have a rights ID. + Array16 zeroRightsId = default; + if (CryptoUtil.IsSameBytes(zeroRightsId, _header.RightsId, NcaHeader.RightsIdSize)) + { + // If we don't have a rights ID we need to generate decryption keys. + int keyType = NcaKeyFunctions.GetKeyTypeValue(_header.KeyAreaEncryptionKeyIndex, _header.GetProperKeyGeneration()); + ReadOnlySpan encryptedKeyCtr = _header.EncryptedKeys.ItemsRo.Slice((int)NcaHeader.DecryptionKey.AesCtr * Aes.KeySize128, Aes.KeySize128); + ReadOnlySpan keyCtrHw = _header.EncryptedKeys.ItemsRo.Slice((int)NcaHeader.DecryptionKey.AesCtrHw * Aes.KeySize128, Aes.KeySize128); + + cryptoConfig.GenerateKey(_decryptionKeys[(int)NcaHeader.DecryptionKey.AesCtr].Items, encryptedKeyCtr, keyType, in cryptoConfig); + + // Copy the plaintext hardware key. + keyCtrHw.CopyTo(_decryptionKeys[(int)NcaHeader.DecryptionKey.AesCtrHw].Items); + } + + _externalDataDecryptionKey.Items.Clear(); + + // Copy the configuration to the NcaReader. + _decryptAesCtr = cryptoConfig.DecryptAesCtr; + _decryptAesCtrForExternalKey = cryptoConfig.DecryptAesCtrForExternalKey; + _getDecompressorFunc = compressionConfig.GetDecompressorFunc; + _hashGeneratorFactory = hashGeneratorFactorySelector.GetFactory(); + Assert.SdkRequiresNotNull(_hashGeneratorFactory); + + _bodyStorage.SetByMove(ref baseStorage); + _headerStorage.Set(ref headerStorage.Ref()); + + return Result.Success; + + static Result CheckSignature(in NcaHeader header) + { + if (header.Magic == NcaHeader.Magic0 || + header.Magic == NcaHeader.Magic1 || + header.Magic == NcaHeader.Magic2) + { + return ResultFs.UnsupportedSdkVersion.Log(); + } + + if (header.Magic != NcaHeader.CurrentMagic) + return ResultFs.InvalidNcaSignature.Log(); + + return Result.Success; + } + } + + public Result ReadHeader(out NcaFsHeader outHeader, int index) + { + UnsafeHelpers.SkipParamInit(out outHeader); + + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + long offset = Unsafe.SizeOf() + Unsafe.SizeOf() * index; + return _headerStorage.Get.Read(offset, SpanHelpers.AsByteSpan(ref outHeader)); + } + + public void GetHeaderSign2(Span outBuffer) + { + Assert.SdkRequiresEqual(NcaHeader.HeaderSignSize, outBuffer.Length); + + _header.Signature2.ItemsRo.CopyTo(outBuffer); + } + + public void GetHeaderSign2TargetHash(Span outBuffer) + { + Assert.SdkRequiresNotNull(_hashGeneratorFactory); + Assert.SdkRequiresEqual(IHash256Generator.HashSize, outBuffer.Length); + + int signTargetOffset = NcaHeader.HeaderSignSize * NcaHeader.HeaderSignCount; + int signTargetSize = NcaHeader.Size - signTargetOffset; + ReadOnlySpan signTarget = + SpanHelpers.AsReadOnlyByteSpan(in _header).Slice(signTargetOffset, signTargetSize); + + _hashGeneratorFactory.GenerateHash(outBuffer, signTarget); + } + + public SharedRef GetSharedBodyStorage() + { + Assert.SdkRequiresNotNull(_bodyStorage); + + return SharedRef.CreateCopy(in _bodyStorage); + } + + public uint GetSignature() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.Magic; + } + + public NcaHeader.DistributionType GetDistributionType() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.DistributionTypeValue; + } + + public NcaHeader.ContentType GetContentType() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.ContentTypeValue; + } + + public byte GetKeyGeneration() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.GetProperKeyGeneration(); + } + + public byte GetKeyIndex() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.KeyAreaEncryptionKeyIndex; + } + + public ulong GetContentSize() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.ContentSize; + } + + public ulong GetProgramId() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.ProgramId; + } + + public uint GetContentIndex() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.ContentIndex; + } + + public uint GetSdkAddonVersion() + { + Assert.SdkRequiresNotNull(_bodyStorage); + return _header.SdkAddonVersion; + } + + public void GetRightsId(Span outBuffer) + { + Assert.SdkRequiresGreaterEqual(outBuffer.Length, NcaHeader.RightsIdSize); + + _header.RightsId.ItemsRo.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 int GetFsCount() + { + Assert.SdkRequiresNotNull(_bodyStorage); + + for (int i = 0; i < NcaHeader.FsCountMax; i++) + { + if (!HasFsInfo(i)) + { + return i; + } + } + + return NcaHeader.FsCountMax; + } + + public NcaHeader.EncryptionType GetEncryptionType() + { + return _headerEncryptionType; + } + + public ref readonly Hash GetFsHeaderHash(int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return ref _header.FsHeaderHashes[index]; + } + + public void GetFsHeaderHash(out Hash outHash, int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + outHash = _header.FsHeaderHashes[index]; + } + + public void GetFsInfo(out NcaHeader.FsInfo outFsInfo, int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + outFsInfo = _header.FsInfos[index]; + } + + public ulong GetFsOffset(int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].StartSector); + } + + public ulong GetFsEndOffset(int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector); + } + + public ulong GetFsSize(int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, NcaHeader.FsCountMax); + + return NcaHeader.SectorToByte(_header.FsInfos[index].EndSector - _header.FsInfos[index].StartSector); + } + + public void GetEncryptedKey(Span outBuffer) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresGreaterEqual(outBuffer.Length, NcaHeader.EncryptedKeyAreaSize); + + _header.EncryptedKeys.ItemsRo.CopyTo(outBuffer); + } + + public ReadOnlySpan GetDecryptionKey(int index) + { + Assert.SdkRequiresNotNull(_bodyStorage); + Assert.SdkRequiresInRange(index, 0, (int)NcaHeader.DecryptionKey.Count); + + return _decryptionKeys[index]; + } + + public bool HasValidInternalKey() + { + Array16 zeroKey = default; + + for (int i = 0; i < (int)NcaHeader.DecryptionKey.Count; i++) + { + if (!CryptoUtil.IsSameBytes(zeroKey, + _header.EncryptedKeys.ItemsRo.Slice(i * Aes.KeySize128, Aes.KeySize128), Aes.KeySize128)) + { + return true; + } + } + + return false; + } + + public bool HasInternalDecryptionKeyForAesHw() + { + Array16 zeroKey = default; + return !CryptoUtil.IsSameBytes(zeroKey, GetDecryptionKey((int)NcaHeader.DecryptionKey.AesCtrHw), + Array16.Length); + } + + public bool IsSwAesPrioritized() + { + return _isSoftwareAesPrioritized; + } + + public void PrioritizeSwAes() + { + _isSoftwareAesPrioritized = true; + } + + public void SetExternalDecryptionKey(ReadOnlySpan key) + { + Assert.SdkRequiresEqual(_externalDataDecryptionKey.ItemsRo.Length, key.Length); + + key.CopyTo(_externalDataDecryptionKey.Items); + } + + public ReadOnlySpan GetExternalDecryptionKey() + { + return _externalDataDecryptionKey.ItemsRo; + } + + public bool HasExternalDecryptionKey() + { + Array16 zeroKey = default; + return !CryptoUtil.IsSameBytes(zeroKey, GetExternalDecryptionKey(), Array16.Length); + } + + public void GetRawData(Span outBuffer) + { + Assert.SdkRequires(_bodyStorage.HasValue); + Assert.SdkRequiresLessEqual(Unsafe.SizeOf(), outBuffer.Length); + + SpanHelpers.AsReadOnlyByteSpan(_header).CopyTo(outBuffer); + } + + public DecryptAesCtrFunction GetExternalDecryptAesCtrFunction() + { + Assert.SdkRequiresNotNull(_decryptAesCtr); + return _decryptAesCtr; + } + + public DecryptAesCtrFunction GetExternalDecryptAesCtrFunctionForExternalKey() + { + Assert.SdkRequiresNotNull(_decryptAesCtrForExternalKey); + return _decryptAesCtrForExternalKey; + } + + public GetDecompressorFunction GetDecompressor() + { + Assert.SdkRequiresNotNull(_getDecompressorFunc); + return _getDecompressorFunc; + } + + public IHash256GeneratorFactory GetHashGeneratorFactory() + { + Assert.SdkRequiresNotNull(_hashGeneratorFactory); + return _hashGeneratorFactory; + } +} + +/// +/// Handles reading information from the of a file system inside an NCA file. +/// +/// Based on FS 13.1.0 (nnSdk 13.4.0) +public class NcaFsHeaderReader +{ + private NcaFsHeader _header; + private int _fsIndex; + + public NcaFsHeaderReader() + { + _fsIndex = -1; + } + + public bool IsInitialized() + { + return _fsIndex >= 0; + } + + public Result Initialize(NcaReader reader, int index) + { + _fsIndex = -1; + + Result rc = reader.ReadHeader(out _header, index); + if (rc.IsFailure()) return rc.Miss(); + + Unsafe.SkipInit(out Hash hash); + reader.GetHashGeneratorFactory().GenerateHash(hash.Value.Items, SpanHelpers.AsReadOnlyByteSpan(in _header)); + + if (!CryptoUtil.IsSameBytes(reader.GetFsHeaderHash(index).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 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 void GetRawData(Span outBuffer) + { + Assert.SdkRequires(IsInitialized()); + Assert.SdkRequiresLessEqual(Unsafe.SizeOf(), outBuffer.Length); + + SpanHelpers.AsReadOnlyByteSpan(in _header).CopyTo(outBuffer); + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaStructs.cs b/src/LibHac/FsSystem/NcaStructs.cs index a5bcd0c0..206295ee 100644 --- a/src/LibHac/FsSystem/NcaStructs.cs +++ b/src/LibHac/FsSystem/NcaStructs.cs @@ -1,51 +1,4 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using LibHac.Common.FixedArrays; - -namespace LibHac.FsSystem; - -public struct NcaSparseInfo -{ - public long MetaOffset; - public long MetaSize; - public Array16 MetaHeader; - public long PhysicalOffset; - public ushort Generation; - public Array6 Reserved; - - public readonly uint GetGeneration() => (uint)(Generation << 16); - public readonly long GetPhysicalSize() => MetaOffset + MetaSize; - - public readonly NcaAesCtrUpperIv MakeAesCtrUpperIv(NcaAesCtrUpperIv upperIv) - { - NcaAesCtrUpperIv sparseUpperIv = upperIv; - sparseUpperIv.Generation = GetGeneration(); - return sparseUpperIv; - } -} - -public struct NcaCompressionInfo -{ - public long MetaOffset; - public long MetaSize; - public Array16 MetaHeader; -} - -[StructLayout(LayoutKind.Explicit)] -public struct NcaAesCtrUpperIv -{ - [FieldOffset(0)] public ulong Value; - - [FieldOffset(0)] public uint Generation; - [FieldOffset(4)] public uint SecureValue; - - internal NcaAesCtrUpperIv(ulong value) - { - Unsafe.SkipInit(out Generation); - Unsafe.SkipInit(out SecureValue); - Value = value; - } -} +namespace LibHac.FsSystem; public enum NcaSectionType { diff --git a/src/LibHac/Tools/Fs/SwitchFs.cs b/src/LibHac/Tools/Fs/SwitchFs.cs index be307a04..0be82524 100644 --- a/src/LibHac/Tools/Fs/SwitchFs.cs +++ b/src/LibHac/Tools/Fs/SwitchFs.cs @@ -15,6 +15,7 @@ using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.FsSystem.Save; using LibHac.Tools.Ncm; using LibHac.Util; +using KeyType = LibHac.Common.Keys.KeyType; namespace LibHac.Tools.Fs; diff --git a/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs b/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs index a7532784..b4765e8d 100644 --- a/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs +++ b/src/LibHac/Tools/FsSystem/NcaUtils/Nca.cs @@ -13,6 +13,7 @@ using LibHac.FsSystem; using LibHac.Spl; using LibHac.Tools.Crypto; using LibHac.Tools.FsSystem.RomFs; +using KeyType = LibHac.Common.Keys.KeyType; namespace LibHac.Tools.FsSystem.NcaUtils; @@ -416,13 +417,13 @@ public class Nca ref NcaCompressionInfo compressionInfo = ref header.GetCompressionInfo(); Unsafe.SkipInit(out BucketTree.Header bucketTreeHeader); - compressionInfo.MetaHeader.ItemsRo.CopyTo(SpanHelpers.AsByteSpan(ref bucketTreeHeader)); + compressionInfo.TableHeader.ItemsRo.CopyTo(SpanHelpers.AsByteSpan(ref bucketTreeHeader)); bucketTreeHeader.Verify().ThrowIfFailure(); long nodeStorageSize = CompressedStorage.QueryNodeStorageSize(bucketTreeHeader.EntryCount); long entryStorageSize = CompressedStorage.QueryEntryStorageSize(bucketTreeHeader.EntryCount); - long tableOffset = compressionInfo.MetaOffset; - long tableSize = compressionInfo.MetaSize; + long tableOffset = compressionInfo.TableOffset; + long tableSize = compressionInfo.TableSize; if (entryStorageSize + nodeStorageSize > tableSize) throw new HorizonResultException(ResultFs.NcaInvalidCompressionInfo.Value); diff --git a/src/LibHac/Tools/FsSystem/NcaUtils/NcaFsHeader.cs b/src/LibHac/Tools/FsSystem/NcaUtils/NcaFsHeader.cs index 2ba5f2ae..a2d452ed 100644 --- a/src/LibHac/Tools/FsSystem/NcaUtils/NcaFsHeader.cs +++ b/src/LibHac/Tools/FsSystem/NcaUtils/NcaFsHeader.cs @@ -81,7 +81,7 @@ public struct NcaFsHeader public bool ExistsCompressionLayer() { - return GetCompressionInfo().MetaOffset != 0 && GetCompressionInfo().MetaSize != 0; + return GetCompressionInfo().TableOffset != 0 && GetCompressionInfo().TableSize != 0; } public ulong Counter diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index 55dc4e85..cf1a913b 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -11,6 +11,7 @@ using LibHac.Tools.FsSystem; using LibHac.Tools.FsSystem.NcaUtils; using LibHac.Tools.Npdm; using static hactoolnet.Print; +using NcaFsHeader = LibHac.Tools.FsSystem.NcaUtils.NcaFsHeader; namespace hactoolnet; diff --git a/tests/LibHac.Tests/Fs/TypeLayoutTests.cs b/tests/LibHac.Tests/Fs/TypeLayoutTests.cs index e7bb54cc..ad328e4e 100644 --- a/tests/LibHac.Tests/Fs/TypeLayoutTests.cs +++ b/tests/LibHac.Tests/Fs/TypeLayoutTests.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using LibHac.Fs; using LibHac.Fs.Impl; using Xunit; @@ -471,4 +472,22 @@ public class TypeLayoutTests Assert.Equal(0, GetOffset(in s, in s.Value)); } + + [StructLayout(LayoutKind.Sequential)] + private struct Int64AlignmentTest + { + public int A; + public Int64 B; + } + + [Fact] + public static void Int64Test_Layout() + { + var s = new Int64AlignmentTest(); + + Assert.Equal(12, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.A)); + Assert.Equal(4, GetOffset(in s, in s.B)); + } } \ No newline at end of file diff --git a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs index 2a31de87..5d0746ea 100644 --- a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs +++ b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs @@ -10,10 +10,241 @@ public class TypeLayoutTests [Fact] public static void Hash_Layout() { - var s = new Hash(); + Hash s = default; Assert.Equal(0x20, Unsafe.SizeOf()); Assert.Equal(0x0, GetOffset(in s, in s.Value)); } + + [Fact] + public static void NcaFsHeader_Layout() + { + NcaFsHeader s = default; + + Assert.Equal(0x200, Unsafe.SizeOf()); + + Assert.Equal(0x000, GetOffset(in s, in s.Version)); + Assert.Equal(0x002, GetOffset(in s, in s.FsTypeValue)); + Assert.Equal(0x003, GetOffset(in s, in s.HashTypeValue)); + Assert.Equal(0x004, GetOffset(in s, in s.EncryptionTypeValue)); + Assert.Equal(0x005, GetOffset(in s, in s.Reserved)); + Assert.Equal(0x008, GetOffset(in s, in s.HashDataValue)); + Assert.Equal(0x100, GetOffset(in s, in s.PatchInfo)); + Assert.Equal(0x140, GetOffset(in s, in s.AesCtrUpperIv)); + Assert.Equal(0x148, GetOffset(in s, in s.SparseInfo)); + Assert.Equal(0x178, GetOffset(in s, in s.CompressionInfo)); + Assert.Equal(0x1A0, GetOffset(in s, in s.Padding)); + } + + [Fact] + public static void NcaFsHeaderRegion_Layout() + { + NcaFsHeader.Region s = default; + + Assert.Equal(0x10, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.Offset)); + Assert.Equal(8, GetOffset(in s, in s.Size)); + } + + [Fact] + public static void HashData_Layout() + { + NcaFsHeader.HashData s = default; + + Assert.Equal(0xF8, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.HierarchicalSha256)); + Assert.Equal(0, GetOffset(in s, in s.IntegrityMeta)); + } + + [Fact] + public static void HierarchicalSha256Data_Layout() + { + NcaFsHeader.HashData.HierarchicalSha256Data s = default; + + Assert.Equal(0x78, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.MasterHash)); + Assert.Equal(0x20, GetOffset(in s, in s.BlockSize)); + Assert.Equal(0x24, GetOffset(in s, in s.LayerCount)); + Assert.Equal(0x28, GetOffset(in s, in s.LayerRegions)); + } + + [Fact] + public static void IntegrityMetaInfo_Layout() + { + NcaFsHeader.HashData.IntegrityMetaInfo s = default; + + Assert.Equal(0xE0, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.Magic)); + Assert.Equal(0x04, GetOffset(in s, in s.Version)); + Assert.Equal(0x08, GetOffset(in s, in s.MasterHashSize)); + Assert.Equal(0x0C, GetOffset(in s, in s.LevelHashInfo)); + Assert.Equal(0xC0, GetOffset(in s, in s.MasterHash)); + } + + [Fact] + public static void InfoLevelHash_Layout() + { + NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash s = default; + + Assert.Equal(0xB4, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.MaxLayers)); + Assert.Equal(0x04, GetOffset(in s, in s.Layers)); + Assert.Equal(0x94, GetOffset(in s, in s.Salt)); + } + + [Fact] + public static void HierarchicalIntegrityVerificationLevelInformation_Layout() + { + NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.HierarchicalIntegrityVerificationLevelInformation s = default; + + Assert.Equal(0x18, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.Offset)); + Assert.Equal(0x08, GetOffset(in s, in s.Size)); + Assert.Equal(0x10, GetOffset(in s, in s.OrderBlock)); + Assert.Equal(0x14, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void SignatureSalt_Layout() + { + NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.SignatureSalt s = default; + + Assert.Equal(0x20, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.Value)); + } + + [Fact] + public static void NcaPatchInfo_Layout() + { + NcaPatchInfo s = default; + + Assert.Equal(0x40, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.IndirectOffset)); + Assert.Equal(0x08, GetOffset(in s, in s.IndirectSize)); + Assert.Equal(0x10, GetOffset(in s, in s.IndirectHeader)); + Assert.Equal(0x20, GetOffset(in s, in s.AesCtrExOffset)); + Assert.Equal(0x28, GetOffset(in s, in s.AesCtrExSize)); + Assert.Equal(0x30, GetOffset(in s, in s.AesCtrExHeader)); + } + + [Fact] + public static void NcaSparseInfo_Layout() + { + NcaSparseInfo s = default; + + Assert.Equal(0x30, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.MetaOffset)); + Assert.Equal(0x08, GetOffset(in s, in s.MetaSize)); + Assert.Equal(0x10, GetOffset(in s, in s.MetaHeader)); + Assert.Equal(0x20, GetOffset(in s, in s.PhysicalOffset)); + Assert.Equal(0x28, GetOffset(in s, in s.Generation)); + Assert.Equal(0x2A, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void NcaCompressionInfo_Layout() + { + NcaCompressionInfo s = default; + + Assert.Equal(0x28, Unsafe.SizeOf()); + + Assert.Equal(0x00, GetOffset(in s, in s.TableOffset)); + Assert.Equal(0x08, GetOffset(in s, in s.TableSize)); + Assert.Equal(0x10, GetOffset(in s, in s.TableHeader)); + Assert.Equal(0x20, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void NcaAesCtrUpperIv_Layout() + { + NcaAesCtrUpperIv s = default; + + Assert.Equal(8, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.Value)); + Assert.Equal(0, GetOffset(in s, in s.Generation)); + Assert.Equal(4, GetOffset(in s, in s.SecureValue)); + } + + [Fact] + public static void NcaHeader_Layout() + { + NcaHeader s = default; + + Assert.Equal(0x400, Unsafe.SizeOf()); + + Assert.Equal(0x000, GetOffset(in s, in s.Signature1)); + Assert.Equal(0x100, GetOffset(in s, in s.Signature2)); + Assert.Equal(0x200, GetOffset(in s, in s.Magic)); + Assert.Equal(0x204, GetOffset(in s, in s.DistributionTypeValue)); + Assert.Equal(0x205, GetOffset(in s, in s.ContentTypeValue)); + Assert.Equal(0x206, GetOffset(in s, in s.KeyGeneration1)); + Assert.Equal(0x207, GetOffset(in s, in s.KeyAreaEncryptionKeyIndex)); + Assert.Equal(0x208, GetOffset(in s, in s.ContentSize)); + Assert.Equal(0x210, GetOffset(in s, in s.ProgramId)); + Assert.Equal(0x218, GetOffset(in s, in s.ContentIndex)); + Assert.Equal(0x21C, GetOffset(in s, in s.SdkAddonVersion)); + Assert.Equal(0x220, GetOffset(in s, in s.KeyGeneration2)); + Assert.Equal(0x221, GetOffset(in s, in s.Header1SignatureKeyGeneration)); + Assert.Equal(0x222, GetOffset(in s, in s.Reserved222)); + Assert.Equal(0x224, GetOffset(in s, in s.Reserved224)); + Assert.Equal(0x230, GetOffset(in s, in s.RightsId)); + Assert.Equal(0x240, GetOffset(in s, in s.FsInfos)); + Assert.Equal(0x280, GetOffset(in s, in s.FsHeaderHashes)); + Assert.Equal(0x300, GetOffset(in s, in s.EncryptedKeys)); + + Assert.Equal(NcaHeader.Size, Unsafe.SizeOf()); + Assert.Equal(NcaHeader.SectorSize, 1 << NcaHeader.SectorShift); + + Assert.Equal(NcaHeader.HeaderSignSize, s.Signature1.ItemsRo.Length); + Assert.Equal(NcaHeader.HeaderSignSize, s.Signature2.ItemsRo.Length); + Assert.Equal(NcaHeader.RightsIdSize, s.RightsId.ItemsRo.Length); + Assert.Equal(NcaHeader.FsCountMax, s.FsInfos.ItemsRo.Length); + Assert.Equal(NcaHeader.FsCountMax, s.FsHeaderHashes.ItemsRo.Length); + Assert.Equal(NcaHeader.EncryptedKeyAreaSize, s.EncryptedKeys.ItemsRo.Length); + } + + [Fact] + public static void NcaHeader_FsInfo_Layout() + { + NcaHeader.FsInfo s = default; + + Assert.Equal(0x10, Unsafe.SizeOf()); + + Assert.Equal(0x0, GetOffset(in s, in s.StartSector)); + Assert.Equal(0x4, GetOffset(in s, in s.EndSector)); + Assert.Equal(0x8, GetOffset(in s, in s.HashSectors)); + Assert.Equal(0xC, GetOffset(in s, in s.Reserved)); + } + + [Fact] + public static void KeyType_Layout() + { + NcaCryptoConfiguration s = default; + + Assert.Equal(NcaCryptoConfiguration.Header1SignatureKeyGenerationMax + 1, s.Header1SignKeyModuli.ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.Rsa2048KeyModulusSize, s.Header1SignKeyModuli.ItemsRo[0].ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.Rsa2048KeyPublicExponentSize, s.Header1SignKeyPublicExponent.ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount, s.KeyAreaEncryptionKeySources.ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.Aes128KeySize, s.KeyAreaEncryptionKeySources.ItemsRo[0].ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.Aes128KeySize, s.HeaderEncryptionKeySource.ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.HeaderEncryptionKeyCount, s.HeaderEncryptedEncryptionKeys.ItemsRo.Length); + Assert.Equal(NcaCryptoConfiguration.Aes128KeySize, s.HeaderEncryptedEncryptionKeys.ItemsRo[0].ItemsRo.Length); + + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 0, (int)KeyType.NcaHeaderKey); + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 1, (int)KeyType.NcaExternalKey); + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 2, (int)KeyType.SaveDataDeviceUniqueMac); + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 3, (int)KeyType.SaveDataSeedUniqueMac); + Assert.Equal(NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 4, (int)KeyType.SaveDataTransferMac); + } } \ No newline at end of file