Add NcaReader and some related NCA classes

This commit is contained in:
Alex Barney 2022-03-02 11:13:37 -07:00
parent cd2b8edfa8
commit b9e2e0863b
17 changed files with 1184 additions and 54 deletions

View file

@ -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<T>
{
public const int Length = 96;
private Array80<T> _0;
private Array16<T> _80;
public ref T this[int i] => ref Items[i];
public Span<T> Items
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => SpanHelpers.CreateSpan(ref MemoryMarshal.GetReference(_0.Items), Length);
}
public readonly ReadOnlySpan<T> ItemsRo
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => SpanHelpers.CreateSpan(ref MemoryMarshal.GetReference(_0.ItemsRo), Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<T>(in Array96<T> value) => value.ItemsRo;
}

View file

@ -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<byte> signature, ReadOnlySpan<byte> modulus,
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message) =>
VerifyRsa2048Sha256(signature, modulus, exponent, message, RSASignaturePadding.Pss);

21
src/LibHac/Fs/Common.cs Normal file
View file

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

View file

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

View file

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

View file

@ -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<byte> destination, ReadOnlySpan<byte> source);
public delegate DecompressorFunction GetDecompressorFunction(CompressionType compressionType);
public static class CompressionTypeUtility
{
public static bool IsBlockAlignmentRequired(CompressionType type)

View file

@ -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<Array256<byte>> Header1SignKeyModuli;
public Array3<byte> Header1SignKeyPublicExponent;
public Array3<Array16<byte>> KeyAreaEncryptionKeySources;
public Array16<byte> HeaderEncryptionKeySource;
public Array2<Array16<byte>> 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
}

View file

@ -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<byte> Signature1;
public Array256<byte> 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<byte> Reserved222;
public Array3<uint> Reserved224;
public Array16<byte> RightsId;
public Array4<FsInfo> FsInfos;
public Array4<Hash> FsHeaderHashes;
public Array256<byte> 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<byte> IndirectHeader;
public long AesCtrExOffset;
public long AesCtrExSize;
public Array16<byte> AesCtrExHeader;
public readonly bool HasIndirectTable() => IndirectSize != 0;
public readonly bool HasAesCtrExTable() => AesCtrExSize != 0;
}
public struct NcaSparseInfo
{
public long MetaOffset;
public long MetaSize;
public Array16<byte> MetaHeader;
public long PhysicalOffset;
public ushort Generation;
public Array6<byte> 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<byte> 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<byte> Reserved;
public HashData HashDataValue;
public NcaPatchInfo PatchInfo;
public NcaAesCtrUpperIv AesCtrUpperIv;
public NcaSparseInfo SparseInfo;
public NcaCompressionInfo CompressionInfo;
public Array96<byte> 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<Region> 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<HierarchicalIntegrityVerificationLevelInformation> Layers;
public SignatureSalt Salt;
public struct HierarchicalIntegrityVerificationLevelInformation
{
public Fs.Int64 Offset;
public Fs.Int64 Size;
public int OrderBlock;
public Array4<byte> Reserved;
}
public struct SignatureSalt
{
public Array32<byte> Value;
}
}
}
}
}

View file

@ -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<byte> destKey, ReadOnlySpan<byte> sourceKey, int keyType, in NcaCryptoConfiguration config);
public delegate Result DecryptAesCtrFunction(Span<byte> dest, int keyType, ReadOnlySpan<byte> encryptedKey, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> source);
/// <summary>
/// Handles reading information from an NCA file's header.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
public class NcaReader : IDisposable
{
private const uint SdkAddonVersionMin = 0xB0000;
private NcaHeader _header;
private Array5<Array16<byte>> _decryptionKeys;
private SharedRef<IStorage> _bodyStorage;
private UniqueRef<IStorage> _headerStorage;
private Array16<byte> _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<IStorage> 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<IStorage>();
// Generate the keys for decrypting the NCA header.
Unsafe.SkipInit(out Array2<Array16<byte>> 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<byte> 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<byte> signature = _header.Signature1;
ReadOnlySpan<byte> modulus = cryptoConfig.Header1SignKeyModuli[_header.Header1SignatureKeyGeneration];
ReadOnlySpan<byte> exponent = cryptoConfig.Header1SignKeyPublicExponent;
ReadOnlySpan<byte> 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<byte> 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<byte> encryptedKeyCtr = _header.EncryptedKeys.ItemsRo.Slice((int)NcaHeader.DecryptionKey.AesCtr * Aes.KeySize128, Aes.KeySize128);
ReadOnlySpan<byte> 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<NcaHeader>() + Unsafe.SizeOf<NcaFsHeader>() * index;
return _headerStorage.Get.Read(offset, SpanHelpers.AsByteSpan(ref outHeader));
}
public void GetHeaderSign2(Span<byte> outBuffer)
{
Assert.SdkRequiresEqual(NcaHeader.HeaderSignSize, outBuffer.Length);
_header.Signature2.ItemsRo.CopyTo(outBuffer);
}
public void GetHeaderSign2TargetHash(Span<byte> outBuffer)
{
Assert.SdkRequiresNotNull(_hashGeneratorFactory);
Assert.SdkRequiresEqual(IHash256Generator.HashSize, outBuffer.Length);
int signTargetOffset = NcaHeader.HeaderSignSize * NcaHeader.HeaderSignCount;
int signTargetSize = NcaHeader.Size - signTargetOffset;
ReadOnlySpan<byte> signTarget =
SpanHelpers.AsReadOnlyByteSpan(in _header).Slice(signTargetOffset, signTargetSize);
_hashGeneratorFactory.GenerateHash(outBuffer, signTarget);
}
public SharedRef<IStorage> GetSharedBodyStorage()
{
Assert.SdkRequiresNotNull(_bodyStorage);
return SharedRef<IStorage>.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<byte> 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<byte> outBuffer)
{
Assert.SdkRequiresNotNull(_bodyStorage);
Assert.SdkRequiresGreaterEqual(outBuffer.Length, NcaHeader.EncryptedKeyAreaSize);
_header.EncryptedKeys.ItemsRo.CopyTo(outBuffer);
}
public ReadOnlySpan<byte> GetDecryptionKey(int index)
{
Assert.SdkRequiresNotNull(_bodyStorage);
Assert.SdkRequiresInRange(index, 0, (int)NcaHeader.DecryptionKey.Count);
return _decryptionKeys[index];
}
public bool HasValidInternalKey()
{
Array16<byte> 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<byte> zeroKey = default;
return !CryptoUtil.IsSameBytes(zeroKey, GetDecryptionKey((int)NcaHeader.DecryptionKey.AesCtrHw),
Array16<byte>.Length);
}
public bool IsSwAesPrioritized()
{
return _isSoftwareAesPrioritized;
}
public void PrioritizeSwAes()
{
_isSoftwareAesPrioritized = true;
}
public void SetExternalDecryptionKey(ReadOnlySpan<byte> key)
{
Assert.SdkRequiresEqual(_externalDataDecryptionKey.ItemsRo.Length, key.Length);
key.CopyTo(_externalDataDecryptionKey.Items);
}
public ReadOnlySpan<byte> GetExternalDecryptionKey()
{
return _externalDataDecryptionKey.ItemsRo;
}
public bool HasExternalDecryptionKey()
{
Array16<byte> zeroKey = default;
return !CryptoUtil.IsSameBytes(zeroKey, GetExternalDecryptionKey(), Array16<byte>.Length);
}
public void GetRawData(Span<byte> outBuffer)
{
Assert.SdkRequires(_bodyStorage.HasValue);
Assert.SdkRequiresLessEqual(Unsafe.SizeOf<NcaHeader>(), 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;
}
}
/// <summary>
/// Handles reading information from the <see cref="NcaFsHeader"/> of a file system inside an NCA file.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
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<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 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<byte> outBuffer)
{
Assert.SdkRequires(IsInitialized());
Assert.SdkRequiresLessEqual(Unsafe.SizeOf<NcaFsHeader>(), outBuffer.Length);
SpanHelpers.AsReadOnlyByteSpan(in _header).CopyTo(outBuffer);
}
}

View file

@ -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<byte> MetaHeader;
public long PhysicalOffset;
public ushort Generation;
public Array6<byte> 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<byte> 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
{

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Int64AlignmentTest>());
Assert.Equal(0, GetOffset(in s, in s.A));
Assert.Equal(4, GetOffset(in s, in s.B));
}
}

View file

@ -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<Hash>());
Assert.Equal(0x0, GetOffset(in s, in s.Value));
}
[Fact]
public static void NcaFsHeader_Layout()
{
NcaFsHeader s = default;
Assert.Equal(0x200, Unsafe.SizeOf<NcaFsHeader>());
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<NcaFsHeader.Region>());
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<NcaFsHeader.HashData>());
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<NcaFsHeader.HashData.HierarchicalSha256Data>());
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<NcaFsHeader.HashData.IntegrityMetaInfo>());
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<NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash>());
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<NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.HierarchicalIntegrityVerificationLevelInformation>());
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<NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.SignatureSalt>());
Assert.Equal(0, GetOffset(in s, in s.Value));
}
[Fact]
public static void NcaPatchInfo_Layout()
{
NcaPatchInfo s = default;
Assert.Equal(0x40, Unsafe.SizeOf<NcaPatchInfo>());
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<NcaSparseInfo>());
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<NcaCompressionInfo>());
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<NcaAesCtrUpperIv>());
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<NcaHeader>());
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<NcaHeader>());
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<NcaHeader.FsInfo>());
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);
}
}