From 398a142b2722c8c9797e33516e9a775901ad56f2 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 16 Apr 2022 20:55:46 -0700 Subject: [PATCH] Update some Nca classes for 14.0.0 --- src/LibHac/Crypto/Rsa.cs | 4 +- src/LibHac/FsSystem/AesXtsStorageExternal.cs | 304 ++++++++++++++++++ src/LibHac/FsSystem/IHash256Generator.cs | 24 +- src/LibHac/FsSystem/NcaFileSystemDriver.cs | 114 +++++-- src/LibHac/FsSystem/NcaHeader.cs | 71 +++- src/LibHac/FsSystem/NcaReader.cs | 155 +++++++-- tests/LibHac.Tests/Fs/TypeLayoutTests.cs | 2 +- .../LibHac.Tests/FsSystem/TypeLayoutTests.cs | 42 ++- 8 files changed, 634 insertions(+), 82 deletions(-) create mode 100644 src/LibHac/FsSystem/AesXtsStorageExternal.cs diff --git a/src/LibHac/Crypto/Rsa.cs b/src/LibHac/Crypto/Rsa.cs index c2af9a5e..2605f3ee 100644 --- a/src/LibHac/Crypto/Rsa.cs +++ b/src/LibHac/Crypto/Rsa.cs @@ -6,8 +6,8 @@ namespace LibHac.Crypto; public static class Rsa { - public static readonly int ModulusSize2048Pss = 256; - public static readonly int MaximumExponentSize2048Pss = 3; + public const int ModulusSize2048Pss = 256; + public const int MaximumExponentSize2048Pss = 3; public static bool VerifyRsa2048PssSha256(ReadOnlySpan signature, ReadOnlySpan modulus, ReadOnlySpan exponent, ReadOnlySpan message) => diff --git a/src/LibHac/FsSystem/AesXtsStorageExternal.cs b/src/LibHac/FsSystem/AesXtsStorageExternal.cs new file mode 100644 index 00000000..a68d24a0 --- /dev/null +++ b/src/LibHac/FsSystem/AesXtsStorageExternal.cs @@ -0,0 +1,304 @@ +using System; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Common.FixedArrays; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Util; + +namespace LibHac.FsSystem; + +/// +/// Reads and writes to an that's encrypted with AES-XTS-128. +/// All encryption or decryption will be done externally via a provided function. +/// This allows for using hardware decryption where the FS process doesn't has access to the actual keys. +/// +/// Based on FS 14.1.0 (nnSdk 14.3.0) +public class AesXtsStorageExternal : IStorage +{ + public const int AesBlockSize = Aes.BlockSize; + public const int KeySize = Aes.KeySize128; + public const int IvSize = Aes.KeySize128; + + private IStorage _baseStorage; + private Array2> _key; + private Array16 _iv; + private uint _blockSize; + private CryptAesXtsFunction _encryptFunction; + private CryptAesXtsFunction _decryptFunction; + + // The original class uses a template for both the shared and non-shared IStorage which avoids needing this field. + private SharedRef _baseStorageShared; + + public AesXtsStorageExternal(IStorage baseStorage, ReadOnlySpan key1, ReadOnlySpan key2, + ReadOnlySpan iv, uint blockSize, CryptAesXtsFunction encryptFunction, CryptAesXtsFunction decryptFunction) + { + _baseStorage = baseStorage; + _blockSize = blockSize; + _encryptFunction = encryptFunction; + _decryptFunction = decryptFunction; + + Assert.SdkRequires(key1.Length is 0 or KeySize); + Assert.SdkRequires(key2.Length is 0 or KeySize); + Assert.SdkRequiresEqual(IvSize, iv.Length); + Assert.SdkRequiresAligned(blockSize, AesBlockSize); + + if (key1.Length != 0) + key1.CopyTo(_key[0].Items); + + if (key2.Length != 0) + key2.CopyTo(_key[1].Items); + + iv.CopyTo(_iv.Items); + } + + public AesXtsStorageExternal(in SharedRef baseStorage, ReadOnlySpan key1, ReadOnlySpan key2, + ReadOnlySpan iv, uint blockSize, CryptAesXtsFunction encryptFunction, CryptAesXtsFunction decryptFunction) + : this(baseStorage.Get, key1, key2, iv, blockSize, encryptFunction, decryptFunction) + { + _baseStorageShared = SharedRef.CreateCopy(in baseStorage); + } + + public override void Dispose() + { + _baseStorageShared.Destroy(); + + base.Dispose(); + } + + public override Result Read(long offset, Span destination) + { + // Allow zero size. + if (destination.Length == 0) + return Result.Success; + + // Ensure we can decrypt. + if (_decryptFunction is null) + return ResultFs.NullptrArgument.Log(); + + // We can only read at block aligned offsets. + if (!Alignment.IsAlignedPow2(offset, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAlignedPow2(destination.Length, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + + // Read the encrypted data. + Result rc = _baseStorage.Read(offset, destination); + if (rc.IsFailure()) return rc.Miss(); + + // Temporarily increase our thread priority while decrypting. + using var changePriority = new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative); + + // Setup the counter. + Span counter = stackalloc byte[IvSize]; + _iv.ItemsRo.CopyTo(counter); + Utility.AddCounter(counter, (ulong)offset / _blockSize); + + // Handle any unaligned data before the start. + int processedSize = 0; + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (offset % _blockSize != 0) + { + // Determine the size of the pre-data read. + int skipSize = (int)(offset - Alignment.AlignDownPow2(offset, _blockSize)); + int dataSize = (int)Math.Min(destination.Length, _blockSize - skipSize); + + // Decrypt into a pooled buffer. + using (var tmpBuffer = new PooledBuffer((int)_blockSize, (int)_blockSize)) + { + Assert.SdkAssert(tmpBuffer.GetSize() >= _blockSize); + + tmpBuffer.GetBuffer().Slice(0, skipSize).Clear(); + destination.Slice(0, dataSize).CopyTo(tmpBuffer.GetBuffer().Slice(skipSize, dataSize)); + Span decryptionBuffer = tmpBuffer.GetBuffer().Slice(0, (int)_blockSize); + + // Decrypt and copy the partial block to the output buffer. + rc = _decryptFunction(decryptionBuffer, _key[0], _key[1], counter, decryptionBuffer); + if (rc.IsFailure()) return rc.Miss(); + + tmpBuffer.GetBuffer().Slice(skipSize, dataSize).CopyTo(destination); + } + + Utility.AddCounter(counter, 1); + processedSize += dataSize; + Assert.SdkAssert(processedSize == Math.Min(destination.Length, _blockSize - skipSize)); + } + + // Decrypt aligned chunks. + Span currentOutput = destination.Slice(processedSize); + int remainingSize = destination.Length - processedSize; + + while (remainingSize > 0) + { + Span currentBlock = currentOutput.Slice(0, Math.Min((int)_blockSize, remainingSize)); + + rc = _decryptFunction(currentBlock, _key[0], _key[1], counter, currentBlock); + if (rc.IsFailure()) return rc.Miss(); + + remainingSize -= currentBlock.Length; + currentOutput = currentBlock.Slice(currentBlock.Length); + + Utility.AddCounter(counter, 1); + } + + return Result.Success; + } + + public override Result Write(long offset, ReadOnlySpan source) + { + Result rc; + + // Allow zero-size writes. + if (source.Length == 0) + return Result.Success; + + // Ensure we can encrypt. + if (_encryptFunction is null) + return ResultFs.NullptrArgument.Log(); + + // We can only write at block aligned offsets. + if (!Alignment.IsAlignedPow2(offset, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAlignedPow2(source.Length, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + + // Get a pooled buffer. + using var pooledBuffer = new PooledBuffer(); + bool useWorkBuffer = !PooledBufferGlobalMethods.IsDeviceAddress(source); + if (useWorkBuffer) + { + pooledBuffer.Allocate(source.Length, (int)_blockSize); + } + + // Setup the counter. + Span counter = stackalloc byte[IvSize]; + _iv.ItemsRo.CopyTo(counter); + Utility.AddCounter(counter, (ulong)offset / _blockSize); + + // Handle any unaligned data before the start. + int processedSize = 0; + + // Todo: remove when fixed in Resharper + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (offset % _blockSize != 0) + { + // Determine the size of the pre-data write. + int skipSize = (int)(offset - Alignment.AlignDownPow2(offset, _blockSize)); + int dataSize = (int)Math.Min(source.Length, _blockSize - skipSize); + + // Encrypt into a pooled buffer. + // Note: Nintendo allocates a second pooled buffer here despite having one already allocated above. + using (var tmpBuffer = new PooledBuffer((int)_blockSize, (int)_blockSize)) + { + Assert.SdkAssert(tmpBuffer.GetSize() >= _blockSize); + + tmpBuffer.GetBuffer().Slice(0, skipSize).Clear(); + source.Slice(0, dataSize).CopyTo(tmpBuffer.GetBuffer().Slice(skipSize, dataSize)); + Span encryptionBuffer = tmpBuffer.GetBuffer().Slice(0, (int)_blockSize); + + rc = _encryptFunction(encryptionBuffer, _key[0], _key[1], counter, encryptionBuffer); + if (rc.IsFailure()) return rc.Miss(); + + rc = _baseStorage.Write(offset, tmpBuffer.GetBuffer().Slice(skipSize, dataSize)); + if (rc.IsFailure()) return rc.Miss(); + } + + Utility.AddCounter(counter, 1); + processedSize += dataSize; + Assert.SdkAssert(processedSize == Math.Min(source.Length, _blockSize - skipSize)); + } + + // Encrypt aligned chunks. + int remainingSize = source.Length - processedSize; + long currentOffset = offset + processedSize; + + while (remainingSize > 0) + { + // Determine data we're writing and where. + int writeSize = useWorkBuffer ? Math.Min(pooledBuffer.GetSize(), remainingSize) : remainingSize; + + // Encrypt the data with temporarily increased priority. + using (new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative)) + { + int remainingEncryptSize = writeSize; + int encryptOffset = 0; + + // Encrypt one block at a time. + while (remainingEncryptSize > 0) + { + int currentSize = Math.Min(remainingEncryptSize, (int)_blockSize); + ReadOnlySpan encryptSource = source.Slice(processedSize + encryptOffset, currentSize); + + // const_cast the input buffer and encrypt in-place if it's a "device buffer". + Span encryptDest = useWorkBuffer + ? pooledBuffer.GetBuffer().Slice(encryptOffset, currentSize) + : MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(encryptSource), encryptSource.Length); + + rc = _encryptFunction(encryptDest, _key[0], _key[1], counter, encryptSource); + if (rc.IsFailure()) return rc.Miss(); + + Utility.AddCounter(counter, 1); + + encryptOffset += currentSize; + remainingEncryptSize -= currentSize; + } + } + + // Write the encrypted data. + ReadOnlySpan writeBuffer = useWorkBuffer + ? pooledBuffer.GetBuffer().Slice(0, writeSize) + : source.Slice(processedSize, writeSize); + + rc = _baseStorage.Write(currentOffset, writeBuffer); + if (rc.IsFailure()) return rc.Miss(); + + // Advance. + currentOffset += writeSize; + processedSize += writeSize; + remainingSize -= writeSize; + } + + return Result.Success; + } + + public override Result Flush() + { + return _baseStorage.Flush(); + } + + public override Result SetSize(long size) + { + return _baseStorage.SetSize(size); + } + + public override Result GetSize(out long size) + { + return _baseStorage.GetSize(out size); + } + + public override Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + if (operationId != OperationId.InvalidateCache) + { + // Handle the zero size case. + if (size == 0) + return Result.Success; + + // Ensure alignment. + if (!Alignment.IsAlignedPow2(offset, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAlignedPow2(size, AesBlockSize)) + return ResultFs.InvalidArgument.Log(); + } + + Result rc = _baseStorage.OperateRange(outBuffer, operationId, offset, size, inBuffer); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/IHash256Generator.cs b/src/LibHac/FsSystem/IHash256Generator.cs index 3f2e26dc..807ec7d4 100644 --- a/src/LibHac/FsSystem/IHash256Generator.cs +++ b/src/LibHac/FsSystem/IHash256Generator.cs @@ -5,11 +5,17 @@ using LibHac.Diag; namespace LibHac.FsSystem; +public enum HashAlgorithmType : byte +{ + Sha2 = 0, + Sha3 = 1 +} + /// /// Generates a hash for a stream of data. The data can be given to the /// as multiple, smaller sequential blocks of data. /// -/// Based on FS 13.1.0 (nnSdk 13.4.0) +/// Based on FS 14.1.0 (nnSdk 14.3.0) public abstract class IHash256Generator : IDisposable { public static readonly long HashSize = 256 / 8; @@ -41,14 +47,14 @@ public abstract class IHash256Generator : IDisposable /// /// Creates objects and can generate a hash for a single, in-memory block of data. /// -/// Based on FS 13.1.0 (nnSdk 13.4.0) +/// Based on FS 14.1.0 (nnSdk 14.3.0) public abstract class IHash256GeneratorFactory : IDisposable { public virtual void Dispose() { } - public UniqueRef Create() + public Result Create(ref UniqueRef outGenerator) { - return DoCreate(); + return DoCreate(ref outGenerator); } public void GenerateHash(Span hashBuffer, ReadOnlySpan data) @@ -58,22 +64,22 @@ public abstract class IHash256GeneratorFactory : IDisposable DoGenerateHash(hashBuffer, data); } - protected abstract UniqueRef DoCreate(); + protected abstract Result DoCreate(ref UniqueRef outGenerator); protected abstract void DoGenerateHash(Span hashBuffer, ReadOnlySpan data); } /// /// Creates objects. /// -/// Based on FS 13.1.0 (nnSdk 13.4.0) +/// Based on FS 14.1.0 (nnSdk 14.3.0) public abstract class IHash256GeneratorFactorySelector : IDisposable { public virtual void Dispose() { } - public IHash256GeneratorFactory GetFactory() + public IHash256GeneratorFactory GetFactory(HashAlgorithmType type) { - return DoGetFactory(); + return DoGetFactory(type); } - protected abstract IHash256GeneratorFactory DoGetFactory(); + protected abstract IHash256GeneratorFactory DoGetFactory(HashAlgorithmType type); } \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaFileSystemDriver.cs b/src/LibHac/FsSystem/NcaFileSystemDriver.cs index 0f651f37..823483ad 100644 --- a/src/LibHac/FsSystem/NcaFileSystemDriver.cs +++ b/src/LibHac/FsSystem/NcaFileSystemDriver.cs @@ -8,21 +8,27 @@ using LibHac.FsSrv; namespace LibHac.FsSystem; +/// +/// Contains the configuration used for decrypting NCAs. +/// +/// Based on FS 14.1.0 (nnSdk 14.3.0) public struct NcaCryptoConfiguration { - public static readonly int Rsa2048KeyModulusSize = Rsa.ModulusSize2048Pss; - public static readonly int Rsa2048KeyPublicExponentSize = Rsa.MaximumExponentSize2048Pss; - public static readonly int Rsa2048KeyPrivateExponentSize = Rsa2048KeyModulusSize; + public const int Rsa2048KeyModulusSize = Rsa.ModulusSize2048Pss; + public const int Rsa2048KeyPublicExponentSize = Rsa.MaximumExponentSize2048Pss; + public const int Rsa2048KeyPrivateExponentSize = Rsa2048KeyModulusSize; - public static readonly int Aes128KeySize = Aes.KeySize128; + public const int Aes128KeySize = Aes.KeySize128; - public static readonly int Header1SignatureKeyGenerationMax = 1; + public const int Header1SignatureKeyGenerationMax = 1; - public static readonly int KeyAreaEncryptionKeyIndexCount = 3; - public static readonly int HeaderEncryptionKeyCount = 2; + public const int KeyAreaEncryptionKeyIndexCount = 3; + public const int HeaderEncryptionKeyCount = 2; - public static readonly int KeyGenerationMax = 32; - public static readonly int KeyAreaEncryptionKeyCount = KeyAreaEncryptionKeyIndexCount * KeyGenerationMax; + public const byte KeyAreaEncryptionKeyIndexZeroKey = 0xFF; + + public const int KeyGenerationMax = 32; + public const int KeyAreaEncryptionKeyCount = KeyAreaEncryptionKeyIndexCount * KeyGenerationMax; public Array2> Header1SignKeyModuli; public Array3 Header1SignKeyPublicExponent; @@ -30,9 +36,13 @@ public struct NcaCryptoConfiguration public Array16 HeaderEncryptionKeySource; public Array2> HeaderEncryptedEncryptionKeys; public GenerateKeyFunction GenerateKey; + public CryptAesXtsFunction EncryptAesXtsForExternalKey; + public CryptAesXtsFunction DecryptAesXtsForExternalKey; public DecryptAesCtrFunction DecryptAesCtr; public DecryptAesCtrFunction DecryptAesCtrForExternalKey; + public VerifySign1Function VerifySign1; public bool IsDev; + public bool IsAvailableSwKey; } public struct NcaCompressionConfiguration @@ -49,22 +59,30 @@ public static class NcaKeyFunctions public static int GetKeyTypeValue(byte keyIndex, byte keyGeneration) { - const int invalidKeyTypeValue = -1; + if (keyIndex == NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexZeroKey) + { + return (int)KeyType.ZeroKey; + } - if (keyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount) - return invalidKeyTypeValue; + if (keyIndex < NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount) + { + return NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount * keyGeneration + keyIndex; + } - return NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount * keyGeneration + keyIndex; + return (int)KeyType.InvalidKey; } } public enum KeyType { - NcaHeaderKey = 0x60, - NcaExternalKey = 0x61, - SaveDataDeviceUniqueMac = 0x62, - SaveDataSeedUniqueMac = 0x63, - SaveDataTransferMac = 0x64 + ZeroKey = -2, + InvalidKey = -1, + NcaHeaderKey1 = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 0, + NcaHeaderKey2 = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 1, + NcaExternalKey = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 2, + SaveDataDeviceUniqueMac = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 3, + SaveDataSeedUniqueMac = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 4, + SaveDataTransferMac = NcaCryptoConfiguration.KeyAreaEncryptionKeyCount + 5 } public class NcaFileSystemDriver : IDisposable @@ -85,6 +103,8 @@ public class NcaFileSystemDriver : IDisposable public SharedRef FsDataStorage; public SharedRef CompressedStorageMetaStorage; public SharedRef CompressedStorage; + public SharedRef PatchLayerInfoStorage; + public SharedRef SparseLayerInfoStorage; public void Dispose() { @@ -100,6 +120,8 @@ public class NcaFileSystemDriver : IDisposable FsDataStorage.Destroy(); CompressedStorageMetaStorage.Destroy(); CompressedStorage.Destroy(); + PatchLayerInfoStorage.Destroy(); + SparseLayerInfoStorage.Destroy(); } } @@ -184,6 +206,14 @@ public class NcaFileSystemDriver : IDisposable throw new NotImplementedException(); } + private Result CreateSparseStorageMetaStorageWithVerification(ref SharedRef outStorage, + ref SharedRef outLayerInfoStorage, ref SharedRef baseStorage, long offset, + in NcaAesCtrUpperIv upperIv, in NcaSparseInfo sparseInfo, in NcaMetaDataHashDataInfo metaDataHashDataInfo, + IHash256GeneratorFactory hashGeneratorFactory) + { + throw new NotImplementedException(); + } + private Result CreateSparseStorageCore(ref SharedRef outStorage, ref SharedRef baseStorage, long baseStorageSize, ref SharedRef sparseStorageMetaStorage, in NcaSparseInfo sparseInfo, bool hasExternalInfo) @@ -198,8 +228,26 @@ public class NcaFileSystemDriver : IDisposable throw new NotImplementedException(); } + private Result CreateSparseStorageWithVerification(ref SharedRef outStorage, out long outFsDataOffset, + out SharedRef outSparseStorage, ref SharedRef outSparseStorageMetaStorage, + ref SharedRef outLayerInfoStorage, int index, in NcaAesCtrUpperIv upperIv, + in NcaSparseInfo sparseInfo, in NcaMetaDataHashDataInfo metaDataHashDataInfo, + NcaFsHeader.MetaDataHashType metaDataHashType) + { + throw new NotImplementedException(); + } + + private Result CreatePatchMetaStorage(ref SharedRef outAesCtrExMetaStorage, + ref SharedRef outIndirectMetaStorage, ref SharedRef outLayerInfoStorage, + ref SharedRef baseStorage, long offset, in NcaAesCtrUpperIv upperIv, in NcaPatchInfo patchInfo, + in NcaMetaDataHashDataInfo metaDataHashDataInfo, IHash256GeneratorFactory hashGeneratorFactory) + { + throw new NotImplementedException(); + } + private Result CreateAesCtrExStorageMetaStorage(ref SharedRef outStorage, - ref SharedRef baseStorage, long offset, in NcaAesCtrUpperIv upperIv, in NcaPatchInfo patchInfo) + ref SharedRef baseStorage, long offset, NcaFsHeader.EncryptionType encryptionType, + in NcaAesCtrUpperIv upperIv, in NcaPatchInfo patchInfo) { throw new NotImplementedException(); } @@ -227,13 +275,29 @@ public class NcaFileSystemDriver : IDisposable } private Result CreateSha256Storage(ref SharedRef outStorage, ref SharedRef baseStorage, - in NcaFsHeader.HashData.HierarchicalSha256Data sha256Data) + in NcaFsHeader.HashData.HierarchicalSha256Data sha256Data, IHash256GeneratorFactory hashGeneratorFactory) { throw new NotImplementedException(); } - private Result HierarchicalSha256Data(ref SharedRef outStorage, ref SharedRef baseStorage, - in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo) + private Result CreateIntegrityVerificationStorage(ref SharedRef outStorage, + ref SharedRef baseStorage, in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo, + IHash256GeneratorFactory hashGeneratorFactory) + { + throw new NotImplementedException(); + } + + private Result CreateIntegrityVerificationStorageImpl(ref SharedRef outStorage, + ref SharedRef baseStorage, in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo, long layerInfoOffset, + int maxDataCacheEntries, int maxHashCacheEntries, sbyte bufferLevel, + IHash256GeneratorFactory hashGeneratorFactory) + { + throw new NotImplementedException(); + } + + private Result CreateIntegrityVerificationStorageForMeta(ref SharedRef outStorage, + ref SharedRef outLayerInfoStorage, ref SharedRef baseStorage, long offset, + in NcaMetaDataHashDataInfo metaDataHashDataInfo, IHash256GeneratorFactory hashGeneratorFactory) { throw new NotImplementedException(); } @@ -252,4 +316,10 @@ public class NcaFileSystemDriver : IDisposable { throw new NotImplementedException(); } + + public Result CreateRegionSwitchStorage(ref SharedRef outStorage, NcaFsHeaderReader headerReader, + ref SharedRef insideRegionStorage, ref SharedRef outsideRegionStorage) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaHeader.cs b/src/LibHac/FsSystem/NcaHeader.cs index 6de0e074..32c34800 100644 --- a/src/LibHac/FsSystem/NcaHeader.cs +++ b/src/LibHac/FsSystem/NcaHeader.cs @@ -1,10 +1,16 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using LibHac.Common; using LibHac.Common.FixedArrays; +using LibHac.Fs; namespace LibHac.FsSystem; +/// +/// The structure used as the header for an NCA file. +/// +/// Based on FS 14.1.0 (nnSdk 14.3.0) public struct NcaHeader { public enum ContentType : byte @@ -133,6 +139,19 @@ public struct NcaCompressionInfo public ulong Reserved; } +public struct NcaMetaDataHashDataInfo +{ + public long Offset; + public long Size; + public Hash Hash; +} + +public struct NcaMetaDataHashData +{ + public long LayerInfoOffset; + public NcaFsHeader.HashData.IntegrityMetaInfo IntegrityMetaInfo; +} + [StructLayout(LayoutKind.Explicit)] public struct NcaAesCtrUpperIv { @@ -149,19 +168,25 @@ public struct NcaAesCtrUpperIv } } +/// +/// The structure used as the header for an NCA file system. +/// +/// Based on FS 14.1.0 (nnSdk 14.3.0) public struct NcaFsHeader { public ushort Version; public FsType FsTypeValue; public HashType HashTypeValue; public EncryptionType EncryptionTypeValue; - public Array3 Reserved; + public MetaDataHashType MetaDataHashTypeValue; + public Array2 Reserved; public HashData HashDataValue; public NcaPatchInfo PatchInfo; public NcaAesCtrUpperIv AesCtrUpperIv; public NcaSparseInfo SparseInfo; public NcaCompressionInfo CompressionInfo; - public Array96 Padding; + public NcaMetaDataHashDataInfo MetaDataHashDataInfo; + public Array48 Padding; public enum FsType : byte { @@ -175,7 +200,9 @@ public struct NcaFsHeader None = 1, AesXts = 2, AesCtr = 3, - AesCtrEx = 4 + AesCtrEx = 4, + AesCtrSkipLayerHash = 5, + AesCtrExSkipLayerHash = 6 } public enum HashType : byte @@ -183,7 +210,16 @@ public struct NcaFsHeader Auto = 0, None = 1, HierarchicalSha256Hash = 2, - HierarchicalIntegrityHash = 3 + HierarchicalIntegrityHash = 3, + AutoSha3 = 4, + HierarchicalSha3256Hash = 5, + HierarchicalIntegritySha3Hash = 6 + } + + public enum MetaDataHashType : byte + { + None = 0, + HierarchicalIntegrity = 1 } public struct Region @@ -235,4 +271,31 @@ public struct NcaFsHeader } } } + + public readonly Result GetHashTargetOffset(out long outOffset) + { + UnsafeHelpers.SkipParamInit(out outOffset); + + if (HashTypeValue is HashType.HierarchicalIntegrityHash or HashType.HierarchicalIntegritySha3Hash) + { + ref readonly HashData.IntegrityMetaInfo.InfoLevelHash hashInfo = ref HashDataValue.IntegrityMeta.LevelHashInfo; + outOffset = hashInfo.Layers[hashInfo.MaxLayers - 2].Offset; + } + else if (HashTypeValue is HashType.HierarchicalSha256Hash or HashType.HierarchicalSha3256Hash) + { + ref readonly HashData.HierarchicalSha256Data hashInfo = ref HashDataValue.HierarchicalSha256; + outOffset = hashInfo.LayerRegions[hashInfo.LayerCount - 1].Offset; + } + else + { + return ResultFs.InvalidNcaFsHeader.Log(); + } + + return Result.Success; + } + + public readonly bool IsSkipLayerHashEncryption() + { + return EncryptionTypeValue is EncryptionType.AesCtrSkipLayerHash or EncryptionType.AesCtrExSkipLayerHash; + } } \ No newline at end of file diff --git a/src/LibHac/FsSystem/NcaReader.cs b/src/LibHac/FsSystem/NcaReader.cs index a8a081f0..28ba2feb 100644 --- a/src/LibHac/FsSystem/NcaReader.cs +++ b/src/LibHac/FsSystem/NcaReader.cs @@ -8,13 +8,15 @@ 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); +public delegate void GenerateKeyFunction(Span destKey, ReadOnlySpan sourceKey, int keyType); +public delegate Result DecryptAesCtrFunction(Span dest, int keyIndex, int keyGeneration, ReadOnlySpan encryptedKey, ReadOnlySpan iv, ReadOnlySpan source); +public delegate Result CryptAesXtsFunction(Span dest, ReadOnlySpan key1, ReadOnlySpan key2, ReadOnlySpan iv, ReadOnlySpan source); +public delegate bool VerifySign1Function(ReadOnlySpan signature, ReadOnlySpan data, bool isProd, byte generation); /// /// Handles reading information from an NCA file's header. /// -/// Based on FS 13.1.0 (nnSdk 13.4.0) +/// Based on FS 14.1.0 (nnSdk 14.3.0) public class NcaReader : IDisposable { private const uint SdkAddonVersionMin = 0xB0000; @@ -27,9 +29,10 @@ public class NcaReader : IDisposable private DecryptAesCtrFunction _decryptAesCtr; private DecryptAesCtrFunction _decryptAesCtrForExternalKey; private bool _isSoftwareAesPrioritized; + private bool _isAvailableSwKey; private NcaHeader.EncryptionType _headerEncryptionType; private GetDecompressorFunction _getDecompressorFunc; - private IHash256GeneratorFactory _hashGeneratorFactory; + private IHash256GeneratorFactorySelector _hashGeneratorFactorySelector; public void Dispose() { @@ -44,23 +47,44 @@ public class NcaReader : IDisposable Assert.SdkRequiresNotNull(hashGeneratorFactorySelector); Assert.SdkRequiresNull(in _bodyStorage); - if (cryptoConfig.GenerateKey is null) + if (cryptoConfig.VerifySign1 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++) + if (cryptoConfig.IsAvailableSwKey) { - cryptoConfig.GenerateKey(commonDecryptionKeys[i].Items, cryptoConfig.HeaderEncryptedEncryptionKeys[i], 0x60, - in cryptoConfig); - } + if (cryptoConfig.GenerateKey is null) + return ResultFs.InvalidArgument.Log(); - // 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)); + ReadOnlySpan headerKeyTypes = stackalloc int[NcaCryptoConfiguration.HeaderEncryptionKeyCount] + { (int)KeyType.NcaHeaderKey1, (int)KeyType.NcaHeaderKey2 }; + + // 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], + headerKeyTypes[i]); + } + + // 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)); + } + else + { + // Software key isn't available, so we need to be able to decrypt externally. + if (cryptoConfig.DecryptAesXtsForExternalKey is null) + return ResultFs.InvalidArgument.Log(); + + // Create the header storage. + Array16 headerIv = default; + headerStorage.Reset(new AesXtsStorageExternal(baseStorage.Get, ReadOnlySpan.Empty, + ReadOnlySpan.Empty, headerIv, (uint)NcaHeader.XtsBlockSize, cryptoConfig.EncryptAesXtsForExternalKey, + cryptoConfig.DecryptAesXtsForExternalKey)); + } if (!headerStorage.HasValue) return ResultFs.AllocationMemoryFailedInNcaReaderA.Log(); @@ -108,11 +132,9 @@ public class NcaReader : IDisposable 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)) + if (!cryptoConfig.VerifySign1(signature, message, !cryptoConfig.IsDev, _header.Header1SignatureKeyGeneration)) return ResultFs.NcaHeaderSignature1VerificationFailed.Log(); // Validate the sdk version. @@ -120,32 +142,40 @@ public class NcaReader : IDisposable return ResultFs.UnsupportedSdkVersion.Log(); // Validate the key index. - if (_header.KeyAreaEncryptionKeyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount) + if (_header.KeyAreaEncryptionKeyIndex >= NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexCount && + _header.KeyAreaEncryptionKeyIndex != NcaCryptoConfiguration.KeyAreaEncryptionKeyIndexZeroKey) + { return ResultFs.InvalidNcaKeyIndex.Log(); + } + + _hashGeneratorFactorySelector = hashGeneratorFactorySelector; // 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); + // If we don't have a rights ID we need to generate decryption keys if software keys are available. + if (cryptoConfig.IsAvailableSwKey) + { + int keyTypeValue = NcaKeyFunctions.GetKeyTypeValue(_header.KeyAreaEncryptionKeyIndex, _header.GetProperKeyGeneration()); + ReadOnlySpan encryptedKeyCtr = _header.EncryptedKeys.ItemsRo.Slice((int)NcaHeader.DecryptionKey.AesCtr * Aes.KeySize128, Aes.KeySize128); - cryptoConfig.GenerateKey(_decryptionKeys[(int)NcaHeader.DecryptionKey.AesCtr].Items, encryptedKeyCtr, keyType, in cryptoConfig); + cryptoConfig.GenerateKey(_decryptionKeys[(int)NcaHeader.DecryptionKey.AesCtr].Items, encryptedKeyCtr, keyTypeValue); + } // Copy the plaintext hardware key. + ReadOnlySpan keyCtrHw = _header.EncryptedKeys.ItemsRo.Slice((int)NcaHeader.DecryptionKey.AesCtrHw * Aes.KeySize128, Aes.KeySize128); keyCtrHw.CopyTo(_decryptionKeys[(int)NcaHeader.DecryptionKey.AesCtrHw].Items); } + // Clear the external decryption key. _externalDataDecryptionKey.Items.Clear(); // Copy the configuration to the NcaReader. + _isAvailableSwKey = cryptoConfig.IsAvailableSwKey; _decryptAesCtr = cryptoConfig.DecryptAesCtr; _decryptAesCtrForExternalKey = cryptoConfig.DecryptAesCtrForExternalKey; _getDecompressorFunc = compressionConfig.GetDecompressorFunc; - _hashGeneratorFactory = hashGeneratorFactorySelector.GetFactory(); - Assert.SdkRequiresNotNull(_hashGeneratorFactory); _bodyStorage.SetByMove(ref baseStorage); _headerStorage.Set(ref headerStorage.Ref()); @@ -187,7 +217,7 @@ public class NcaReader : IDisposable public void GetHeaderSign2TargetHash(Span outBuffer) { - Assert.SdkRequiresNotNull(_hashGeneratorFactory); + Assert.SdkRequiresNotNull(_hashGeneratorFactorySelector); Assert.SdkRequiresEqual(IHash256Generator.HashSize, outBuffer.Length); int signTargetOffset = NcaHeader.HeaderSignSize * NcaHeader.HeaderSignCount; @@ -195,7 +225,8 @@ public class NcaReader : IDisposable ReadOnlySpan signTarget = SpanHelpers.AsReadOnlyByteSpan(in _header).Slice(signTargetOffset, signTargetSize); - _hashGeneratorFactory.GenerateHash(outBuffer, signTarget); + IHash256GeneratorFactory factory = _hashGeneratorFactorySelector.GetFactory(HashAlgorithmType.Sha2); + factory.GenerateHash(outBuffer, signTarget); } public SharedRef GetSharedBodyStorage() @@ -390,6 +421,11 @@ public class NcaReader : IDisposable _isSoftwareAesPrioritized = true; } + public bool IsAvailableSwKey() + { + return _isAvailableSwKey; + } + public void SetExternalDecryptionKey(ReadOnlySpan key) { Assert.SdkRequiresEqual(_externalDataDecryptionKey.ItemsRo.Length, key.Length); @@ -434,17 +470,17 @@ public class NcaReader : IDisposable return _getDecompressorFunc; } - public IHash256GeneratorFactory GetHashGeneratorFactory() + public IHash256GeneratorFactorySelector GetHashGeneratorFactorySelector() { - Assert.SdkRequiresNotNull(_hashGeneratorFactory); - return _hashGeneratorFactory; + Assert.SdkRequiresNotNull(_hashGeneratorFactorySelector); + return _hashGeneratorFactorySelector; } } /// /// Handles reading information from the of a file system inside an NCA file. /// -/// Based on FS 13.1.0 (nnSdk 13.4.0) +/// Based on FS 14.1.0 (nnSdk 14.3.0) public class NcaFsHeaderReader { private NcaFsHeader _header; @@ -468,7 +504,8 @@ public class NcaFsHeaderReader if (rc.IsFailure()) return rc.Miss(); Unsafe.SkipInit(out Hash hash); - reader.GetHashGeneratorFactory().GenerateHash(hash.Value.Items, SpanHelpers.AsReadOnlyByteSpan(in _header)); + IHash256GeneratorFactory generator = reader.GetHashGeneratorFactorySelector().GetFactory(HashAlgorithmType.Sha2); + generator.GenerateHash(hash.Value.Items, SpanHelpers.AsReadOnlyByteSpan(in _header)); if (!CryptoUtil.IsSameBytes(reader.GetFsHeaderHash(index).Value, hash.Value, Unsafe.SizeOf())) { @@ -515,6 +552,34 @@ public class NcaFsHeaderReader 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 rc = _header.GetHashTargetOffset(out outOffset); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + + public bool IsSkipLayerHashEncryption() + { + Assert.SdkRequires(IsInitialized()); + return _header.IsSkipLayerHashEncryption(); + } + public ref readonly NcaPatchInfo GetPatchInfo() { Assert.SdkRequires(IsInitialized()); @@ -551,6 +616,30 @@ public class NcaFsHeaderReader return ref _header.CompressionInfo; } + public bool ExistsPatchMetaHashLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashDataInfo.Size != 0 && GetPatchInfo().HasIndirectTable(); + } + + public bool ExistsSparseMetaHashLayer() + { + Assert.SdkRequires(IsInitialized()); + return _header.MetaDataHashDataInfo.Size != 0 && ExistsSparseLayer(); + } + + public ref readonly NcaMetaDataHashDataInfo GetPatchMetaDataHashDataInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.MetaDataHashDataInfo; + } + + public ref readonly NcaMetaDataHashDataInfo GetSparseMetaDataHashDataInfo() + { + Assert.SdkRequires(IsInitialized()); + return ref _header.MetaDataHashDataInfo; + } + public void GetRawData(Span outBuffer) { Assert.SdkRequires(IsInitialized()); diff --git a/tests/LibHac.Tests/Fs/TypeLayoutTests.cs b/tests/LibHac.Tests/Fs/TypeLayoutTests.cs index ad328e4e..8035c0c6 100644 --- a/tests/LibHac.Tests/Fs/TypeLayoutTests.cs +++ b/tests/LibHac.Tests/Fs/TypeLayoutTests.cs @@ -476,7 +476,7 @@ public class TypeLayoutTests [StructLayout(LayoutKind.Sequential)] private struct Int64AlignmentTest { - public int A; + public byte A; public Int64 B; } diff --git a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs index 092c1986..3e518524 100644 --- a/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs +++ b/tests/LibHac.Tests/FsSystem/TypeLayoutTests.cs @@ -28,13 +28,15 @@ public class TypeLayoutTests 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(0x005, GetOffset(in s, in s.MetaDataHashTypeValue)); + Assert.Equal(0x006, 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)); + Assert.Equal(0x1A0, GetOffset(in s, in s.MetaDataHashDataInfo)); + Assert.Equal(0x1D0, GetOffset(in s, in s.Padding)); } [Fact] @@ -99,7 +101,7 @@ public class TypeLayoutTests } [Fact] - public static void HierarchicalIntegrityVerificationLevelInformation_Layout() + public static void InfoLevelHash_HierarchicalIntegrityVerificationLevelInformation_Layout() { NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.HierarchicalIntegrityVerificationLevelInformation s = default; @@ -112,7 +114,7 @@ public class TypeLayoutTests } [Fact] - public static void SignatureSalt_Layout() + public static void InfoLevelHash_SignatureSalt_Layout() { NcaFsHeader.HashData.IntegrityMetaInfo.InfoLevelHash.SignatureSalt s = default; @@ -164,6 +166,29 @@ public class TypeLayoutTests Assert.Equal(0x20, GetOffset(in s, in s.Reserved)); } + [Fact] + public static void NcaMetaDataHashDataInfo_Layout() + { + NcaMetaDataHashDataInfo s = default; + + Assert.Equal(0x30, 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.Hash)); + } + + [Fact] + public static void NcaMetaDataHashData_Layout() + { + NcaMetaDataHashData s = default; + + Assert.Equal(0xE8, Unsafe.SizeOf()); + + Assert.Equal(0, GetOffset(in s, in s.LayerInfoOffset)); + Assert.Equal(8, GetOffset(in s, in s.IntegrityMetaInfo)); + } + [Fact] public static void NcaAesCtrUpperIv_Layout() { @@ -240,12 +265,6 @@ public class TypeLayoutTests 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); } [Fact] @@ -256,7 +275,8 @@ public class TypeLayoutTests Assert.Equal(0x10, Unsafe.SizeOf()); Assert.Equal(0x0, GetOffset(in s, in s.Offset)); - Assert.Equal(0x8, GetOffset(in s, in s.Reserved)); + Assert.Equal(0x8, GetOffset(in s, in s.EncryptionValue)); + Assert.Equal(0x9, GetOffset(in s, in s.Reserved)); Assert.Equal(0xC, GetOffset(in s, in s.Generation)); } } \ No newline at end of file