diff --git a/src/LibHac/FsSystem/HierarchicalSha256Storage.cs b/src/LibHac/FsSystem/HierarchicalSha256Storage.cs new file mode 100644 index 00000000..8e892538 --- /dev/null +++ b/src/LibHac/FsSystem/HierarchicalSha256Storage.cs @@ -0,0 +1,255 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Os; +using LibHac.Util; + +namespace LibHac.FsSystem; + +file static class Anonymous +{ + public static int Log2(int value) + { + Assert.SdkRequiresGreater(value, 0); + Assert.SdkRequires(BitUtil.IsPowerOfTwo(value)); + + int log = 0; + while ((value >>= 1) > 0) + { + log++; + } + + return log; + } +} + +/// +/// Read and writes the storages generally used by code filesystems in NCAs. These use a Merkle tree to verify the +/// integrity of the data. +/// +/// Based on nnSdk 17.5.0 (FS 17.0.0) +public class HierarchicalSha256Storage : IStorage +{ + private const int LayerCount = 3; + private const int HashSize = Sha256.DigestSize; + + private ValueSubStorage _baseStorage; + private long _baseStorageSize; + private Memory _hashBuffer; + private int _hashTargetBlockSize; + private int _log2SizeRatio; + private IHash256GeneratorFactory _hashGeneratorFactory; + private SdkMutexType _mutex; + + [InlineArray(LayerCount), NonCopyableDisposable] + public struct BaseStorages : IDisposable + { + private ValueSubStorage _0; + + public void Dispose() + { + for (int i = 0; i < LayerCount; i++) + { + this[i].Dispose(); + } + } + } + + public HierarchicalSha256Storage() + { + _baseStorage = new ValueSubStorage(); + _mutex = new SdkMutexType(); + } + + public override void Dispose() + { + _baseStorage.Dispose(); + base.Dispose(); + } + + public Result Initialize(ref readonly BaseStorages baseStorages, int layerCount, uint hashTargetBlockSize, + Memory hashBuffer, IHash256GeneratorFactory hashGeneratorFactory) + { + Assert.SdkRequiresEqual(layerCount, LayerCount); + Assert.SdkRequires(BitUtil.IsPowerOfTwo(hashTargetBlockSize)); + Assert.SdkRequiresNotNull(hashGeneratorFactory); + + _hashTargetBlockSize = (int)hashTargetBlockSize; + _log2SizeRatio = Anonymous.Log2((int)(hashTargetBlockSize / HashSize)); + _hashGeneratorFactory = hashGeneratorFactory; + + Result res = baseStorages[2].GetSize(out _baseStorageSize); + if (res.IsFailure()) return res.Miss(); + + if (_baseStorageSize > HashSize << _log2SizeRatio << _log2SizeRatio) + { + _baseStorageSize = 0; + return ResultFs.HierarchicalSha256BaseStorageTooLarge.Log(); + } + + _hashBuffer = hashBuffer; + _baseStorage.Set(in baseStorages[2]); + + Span masterHash = stackalloc byte[HashSize]; + res = baseStorages[0].Read(0, masterHash); + if (res.IsFailure()) return res.Miss(); + + res = baseStorages[1].GetSize(out long hashStorageSize); + if (res.IsFailure()) return res.Miss(); + + Assert.SdkRequiresAligned(hashStorageSize, HashSize); + Assert.SdkRequiresLessEqual(hashStorageSize, _hashTargetBlockSize); + Assert.SdkRequiresLessEqual(hashStorageSize, hashBuffer.Length); + + Span buffer = _hashBuffer.Span.Slice(0, (int)hashStorageSize); + res = baseStorages[1].Read(0, buffer); + if (res.IsFailure()) return res.Miss(); + + Span hash = stackalloc byte[HashSize]; + res = _hashGeneratorFactory.GenerateHash(hash, buffer); + if (res.IsFailure()) return res.Miss(); + + if (!CryptoUtil.IsSameBytes(hash, masterHash, HashSize)) + return ResultFs.HierarchicalSha256HashVerificationFailed.Log(); + + return Result.Success; + } + + public override Result Read(long offset, Span destination) + { + if (destination.Length == 0) + return Result.Success; + + if (!Alignment.IsAligned(offset, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAligned(destination.Length, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + // The last block in a HierarchicalSha256Storage is allowed to be a partial block, but reads are required to be + // in complete blocks. Calculate the actual amount of data we need to read from the base storage. + long reducedSize = Math.Min(_baseStorageSize, Alignment.AlignUp(offset + destination.Length, (ulong)_hashTargetBlockSize)) - offset; + + Result res = _baseStorage.Read(offset, destination.Slice(0, (int)reducedSize)); + if (res.IsFailure()) return res.Miss(); + + using var changeThreadPriority = new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative); + + Span hash = stackalloc byte[HashSize]; + ReadOnlySpan hashBuffer = _hashBuffer.Span; + + long currentOffset = offset; + long remainingSize = reducedSize; + while (remainingSize > 0) + { + long currentSize = Math.Min(_hashTargetBlockSize, remainingSize); + res = _hashGeneratorFactory.GenerateHash(hash, destination.Slice((int)(currentOffset - offset), (int)currentSize)); + if (res.IsFailure()) return res.Miss(); + + Assert.SdkAssert((currentOffset >> _log2SizeRatio) < _hashBuffer.Length); + + using (new ScopedLock(ref _mutex)) + { + if (!CryptoUtil.IsSameBytes(hash, hashBuffer.Slice((int)(currentOffset >> _log2SizeRatio)), HashSize)) + { + destination.Clear(); + return ResultFs.HierarchicalSha256HashVerificationFailed.Log(); + } + } + + currentOffset += currentSize; + remainingSize -= currentOffset; + } + + return Result.Success; + } + + public override Result Write(long offset, ReadOnlySpan source) + { + // Succeed if zero-size. + if (source.Length == 0) + return Result.Success; + + // Validate preconditions. + if (!Alignment.IsAligned(offset, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAligned(source.Length, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + Span newHash = stackalloc byte[HashSize]; + Span hashBuffer = _hashBuffer.Span; + + // Setup tracking variables. + long reducedSize = Math.Min(_baseStorageSize, Alignment.AlignUp(offset + source.Length, (ulong)_hashTargetBlockSize)) - offset; + long currentOffset = offset; + long remainingSize = reducedSize; + + while (remainingSize > 0) + { + Result res; + + // Generate the hash of the region we're validating. + long currentSize = Math.Min(_hashTargetBlockSize, remainingSize); + + // Temporarily increase our thread priority. + using (new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative)) + { + res = _hashGeneratorFactory.GenerateHash(newHash, source.Slice((int)(currentOffset - offset), (int)currentSize)); + if (res.IsFailure()) return res.Miss(); + } + + // Write the data. + res = _baseStorage.Write(currentOffset, source.Slice((int)(currentOffset - offset), (int)currentSize)); + if (res.IsFailure()) return res.Miss(); + + // Write the hash. + using (new ScopedLock(ref _mutex)) + { + newHash.CopyTo(hashBuffer.Slice((int)(currentOffset >> _log2SizeRatio), HashSize)); + } + + // Advance. + currentOffset += currentSize; + remainingSize -= currentSize; + } + + return Result.Success; + } + + public override Result Flush() + { + return _baseStorage.Flush().Ret(); + } + + public override Result GetSize(out long size) + { + return _baseStorage.GetSize(out size).Ret(); + } + + public override Result SetSize(long size) + { + return ResultFs.UnsupportedSetSizeForHierarchicalSha256Storage.Log(); + } + + public override Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, ReadOnlySpan inBuffer) + { + if (operationId == OperationId.InvalidateCache) + { + return _baseStorage.OperateRange(OperationId.InvalidateCache, offset, size).Ret(); + } + + if (!Alignment.IsAligned(offset, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + if (!Alignment.IsAligned(size, (ulong)_hashTargetBlockSize)) + return ResultFs.InvalidArgument.Log(); + + long reducedSize = Math.Min(_baseStorageSize, Alignment.AlignUp(offset + size, (ulong)_hashTargetBlockSize)) - offset; + + return _baseStorage.OperateRange(outBuffer, operationId, offset, reducedSize, inBuffer).Ret(); + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/IHash256Generator.cs b/src/LibHac/FsSystem/IHash256Generator.cs index 46a1475c..ebf8026b 100644 --- a/src/LibHac/FsSystem/IHash256Generator.cs +++ b/src/LibHac/FsSystem/IHash256Generator.cs @@ -57,15 +57,15 @@ public abstract class IHash256GeneratorFactory : IDisposable return DoCreate(ref outGenerator); } - public void GenerateHash(Span hashBuffer, ReadOnlySpan data) + public Result GenerateHash(Span hashBuffer, ReadOnlySpan data) { Assert.SdkRequiresEqual(IHash256Generator.HashSize, hashBuffer.Length); - DoGenerateHash(hashBuffer, data); + return DoGenerateHash(hashBuffer, data).Ret(); } protected abstract Result DoCreate(ref UniqueRef outGenerator); - protected abstract void DoGenerateHash(Span hashBuffer, ReadOnlySpan data); + protected abstract Result DoGenerateHash(Span hashBuffer, ReadOnlySpan data); } ///