Update some Nca classes for 14.0.0

This commit is contained in:
Alex Barney 2022-04-16 20:55:46 -07:00
parent fc3fb188c7
commit 398a142b27
8 changed files with 634 additions and 82 deletions

View file

@ -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<byte> signature, ReadOnlySpan<byte> modulus,
ReadOnlySpan<byte> exponent, ReadOnlySpan<byte> message) =>

View file

@ -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;
/// <summary>
/// Reads and writes to an <see cref="IStorage"/> 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.
/// </summary>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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<Array16<byte>> _key;
private Array16<byte> _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<IStorage> _baseStorageShared;
public AesXtsStorageExternal(IStorage baseStorage, ReadOnlySpan<byte> key1, ReadOnlySpan<byte> key2,
ReadOnlySpan<byte> 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<IStorage> baseStorage, ReadOnlySpan<byte> key1, ReadOnlySpan<byte> key2,
ReadOnlySpan<byte> iv, uint blockSize, CryptAesXtsFunction encryptFunction, CryptAesXtsFunction decryptFunction)
: this(baseStorage.Get, key1, key2, iv, blockSize, encryptFunction, decryptFunction)
{
_baseStorageShared = SharedRef<IStorage>.CreateCopy(in baseStorage);
}
public override void Dispose()
{
_baseStorageShared.Destroy();
base.Dispose();
}
public override Result Read(long offset, Span<byte> 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<byte> 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<byte> 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<byte> currentOutput = destination.Slice(processedSize);
int remainingSize = destination.Length - processedSize;
while (remainingSize > 0)
{
Span<byte> 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<byte> 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<byte> 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<byte> 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<byte> encryptSource = source.Slice(processedSize + encryptOffset, currentSize);
// const_cast the input buffer and encrypt in-place if it's a "device buffer".
Span<byte> 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<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> 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;
}
}

View file

@ -5,11 +5,17 @@ using LibHac.Diag;
namespace LibHac.FsSystem;
public enum HashAlgorithmType : byte
{
Sha2 = 0,
Sha3 = 1
}
/// <summary>
/// Generates a hash for a stream of data. The data can be given to the <see cref="IHash256Generator"/>
/// as multiple, smaller sequential blocks of data.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
public abstract class IHash256Generator : IDisposable
{
public static readonly long HashSize = 256 / 8;
@ -41,14 +47,14 @@ public abstract class IHash256Generator : IDisposable
/// <summary>
/// Creates <see cref="IHash256Generator"/> objects and can generate a hash for a single, in-memory block of data.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
public abstract class IHash256GeneratorFactory : IDisposable
{
public virtual void Dispose() { }
public UniqueRef<IHash256Generator> Create()
public Result Create(ref UniqueRef<IHash256Generator> outGenerator)
{
return DoCreate();
return DoCreate(ref outGenerator);
}
public void GenerateHash(Span<byte> hashBuffer, ReadOnlySpan<byte> data)
@ -58,22 +64,22 @@ public abstract class IHash256GeneratorFactory : IDisposable
DoGenerateHash(hashBuffer, data);
}
protected abstract UniqueRef<IHash256Generator> DoCreate();
protected abstract Result DoCreate(ref UniqueRef<IHash256Generator> outGenerator);
protected abstract void DoGenerateHash(Span<byte> hashBuffer, ReadOnlySpan<byte> data);
}
/// <summary>
/// Creates <see cref="IHash256GeneratorFactory"/> objects.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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);
}

View file

@ -8,21 +8,27 @@ using LibHac.FsSrv;
namespace LibHac.FsSystem;
/// <summary>
/// Contains the configuration used for decrypting NCAs.
/// </summary>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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<Array256<byte>> Header1SignKeyModuli;
public Array3<byte> Header1SignKeyPublicExponent;
@ -30,9 +36,13 @@ public struct NcaCryptoConfiguration
public Array16<byte> HeaderEncryptionKeySource;
public Array2<Array16<byte>> 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<IStorage> FsDataStorage;
public SharedRef<IStorage> CompressedStorageMetaStorage;
public SharedRef<CompressedStorage> CompressedStorage;
public SharedRef<IStorage> PatchLayerInfoStorage;
public SharedRef<IStorage> 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<IStorage> outStorage,
ref SharedRef<IStorage> outLayerInfoStorage, ref SharedRef<IStorage> baseStorage, long offset,
in NcaAesCtrUpperIv upperIv, in NcaSparseInfo sparseInfo, in NcaMetaDataHashDataInfo metaDataHashDataInfo,
IHash256GeneratorFactory hashGeneratorFactory)
{
throw new NotImplementedException();
}
private Result CreateSparseStorageCore(ref SharedRef<SparseStorage> outStorage, ref SharedRef<IStorage> baseStorage,
long baseStorageSize, ref SharedRef<IStorage> sparseStorageMetaStorage, in NcaSparseInfo sparseInfo,
bool hasExternalInfo)
@ -198,8 +228,26 @@ public class NcaFileSystemDriver : IDisposable
throw new NotImplementedException();
}
private Result CreateSparseStorageWithVerification(ref SharedRef<IStorage> outStorage, out long outFsDataOffset,
out SharedRef<SparseStorage> outSparseStorage, ref SharedRef<IStorage> outSparseStorageMetaStorage,
ref SharedRef<IStorage> outLayerInfoStorage, int index, in NcaAesCtrUpperIv upperIv,
in NcaSparseInfo sparseInfo, in NcaMetaDataHashDataInfo metaDataHashDataInfo,
NcaFsHeader.MetaDataHashType metaDataHashType)
{
throw new NotImplementedException();
}
private Result CreatePatchMetaStorage(ref SharedRef<IStorage> outAesCtrExMetaStorage,
ref SharedRef<IStorage> outIndirectMetaStorage, ref SharedRef<IStorage> outLayerInfoStorage,
ref SharedRef<IStorage> baseStorage, long offset, in NcaAesCtrUpperIv upperIv, in NcaPatchInfo patchInfo,
in NcaMetaDataHashDataInfo metaDataHashDataInfo, IHash256GeneratorFactory hashGeneratorFactory)
{
throw new NotImplementedException();
}
private Result CreateAesCtrExStorageMetaStorage(ref SharedRef<IStorage> outStorage,
ref SharedRef<IStorage> baseStorage, long offset, in NcaAesCtrUpperIv upperIv, in NcaPatchInfo patchInfo)
ref SharedRef<IStorage> 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<IStorage> outStorage, ref SharedRef<IStorage> baseStorage,
in NcaFsHeader.HashData.HierarchicalSha256Data sha256Data)
in NcaFsHeader.HashData.HierarchicalSha256Data sha256Data, IHash256GeneratorFactory hashGeneratorFactory)
{
throw new NotImplementedException();
}
private Result HierarchicalSha256Data(ref SharedRef<IStorage> outStorage, ref SharedRef<IStorage> baseStorage,
in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo)
private Result CreateIntegrityVerificationStorage(ref SharedRef<IStorage> outStorage,
ref SharedRef<IStorage> baseStorage, in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo,
IHash256GeneratorFactory hashGeneratorFactory)
{
throw new NotImplementedException();
}
private Result CreateIntegrityVerificationStorageImpl(ref SharedRef<IStorage> outStorage,
ref SharedRef<IStorage> baseStorage, in NcaFsHeader.HashData.IntegrityMetaInfo metaInfo, long layerInfoOffset,
int maxDataCacheEntries, int maxHashCacheEntries, sbyte bufferLevel,
IHash256GeneratorFactory hashGeneratorFactory)
{
throw new NotImplementedException();
}
private Result CreateIntegrityVerificationStorageForMeta(ref SharedRef<IStorage> outStorage,
ref SharedRef<IStorage> outLayerInfoStorage, ref SharedRef<IStorage> 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<IStorage> outStorage, NcaFsHeaderReader headerReader,
ref SharedRef<IStorage> insideRegionStorage, ref SharedRef<IStorage> outsideRegionStorage)
{
throw new NotImplementedException();
}
}

View file

@ -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;
/// <summary>
/// The structure used as the header for an NCA file.
/// </summary>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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
}
}
/// <summary>
/// The structure used as the header for an NCA file system.
/// </summary>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
public struct NcaFsHeader
{
public ushort Version;
public FsType FsTypeValue;
public HashType HashTypeValue;
public EncryptionType EncryptionTypeValue;
public Array3<byte> Reserved;
public MetaDataHashType MetaDataHashTypeValue;
public Array2<byte> Reserved;
public HashData HashDataValue;
public NcaPatchInfo PatchInfo;
public NcaAesCtrUpperIv AesCtrUpperIv;
public NcaSparseInfo SparseInfo;
public NcaCompressionInfo CompressionInfo;
public Array96<byte> Padding;
public NcaMetaDataHashDataInfo MetaDataHashDataInfo;
public Array48<byte> 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;
}
}

View file

@ -8,13 +8,15 @@ 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);
public delegate void GenerateKeyFunction(Span<byte> destKey, ReadOnlySpan<byte> sourceKey, int keyType);
public delegate Result DecryptAesCtrFunction(Span<byte> dest, int keyIndex, int keyGeneration, ReadOnlySpan<byte> encryptedKey, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> source);
public delegate Result CryptAesXtsFunction(Span<byte> dest, ReadOnlySpan<byte> key1, ReadOnlySpan<byte> key2, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> source);
public delegate bool VerifySign1Function(ReadOnlySpan<byte> signature, ReadOnlySpan<byte> data, bool isProd, byte generation);
/// <summary>
/// Handles reading information from an NCA file's header.
/// </summary>
/// <remarks>Based on FS 13.1.0 (nnSdk 13.4.0)</remarks>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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<IStorage>();
// Generate the keys for decrypting the NCA header.
Unsafe.SkipInit(out Array2<Array16<byte>> 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<byte> headerIv = default;
headerStorage.Reset(new AesXtsStorage(baseStorage.Get, commonDecryptionKeys[0], commonDecryptionKeys[1],
headerIv, NcaHeader.XtsBlockSize));
ReadOnlySpan<int> headerKeyTypes = stackalloc int[NcaCryptoConfiguration.HeaderEncryptionKeyCount]
{ (int)KeyType.NcaHeaderKey1, (int)KeyType.NcaHeaderKey2 };
// 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],
headerKeyTypes[i]);
}
// 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));
}
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<byte> headerIv = default;
headerStorage.Reset(new AesXtsStorageExternal(baseStorage.Get, ReadOnlySpan<byte>.Empty,
ReadOnlySpan<byte>.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<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))
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<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);
// 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<byte> 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<byte> 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<byte> 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<byte> signTarget =
SpanHelpers.AsReadOnlyByteSpan(in _header).Slice(signTargetOffset, signTargetSize);
_hashGeneratorFactory.GenerateHash(outBuffer, signTarget);
IHash256GeneratorFactory factory = _hashGeneratorFactorySelector.GetFactory(HashAlgorithmType.Sha2);
factory.GenerateHash(outBuffer, signTarget);
}
public SharedRef<IStorage> GetSharedBodyStorage()
@ -390,6 +421,11 @@ public class NcaReader : IDisposable
_isSoftwareAesPrioritized = true;
}
public bool IsAvailableSwKey()
{
return _isAvailableSwKey;
}
public void SetExternalDecryptionKey(ReadOnlySpan<byte> 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;
}
}
/// <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>
/// <remarks>Based on FS 14.1.0 (nnSdk 14.3.0)</remarks>
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<Hash>()))
{
@ -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<byte> outBuffer)
{
Assert.SdkRequires(IsInitialized());

View file

@ -476,7 +476,7 @@ public class TypeLayoutTests
[StructLayout(LayoutKind.Sequential)]
private struct Int64AlignmentTest
{
public int A;
public byte A;
public Int64 B;
}

View file

@ -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<NcaMetaDataHashDataInfo>());
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<NcaMetaDataHashData>());
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<AesCtrCounterExtendedStorage.Entry>());
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));
}
}