diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 49f9eaf2..e390c181 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -996,10 +996,10 @@ Module,DescriptionStart,DescriptionEnd,Flags,Namespace,Name,Summary 2,6316,,,,UnsupportedSetSizeForHierarchicalIntegrityVerificationStorage, 2,6317,,,,UnsupportedOperateRangeForHierarchicalIntegrityVerificationStorage, 2,6318,,,,UnsupportedSetSizeForIntegrityVerificationStorage, -2,6319,,,,UnsupportedOperateRangeForNonSaveDataIntegrityVerificationStorage, +2,6319,,,,UnsupportedOperateRangeForWritableIntegrityVerificationStorage, 2,6320,,,,UnsupportedOperateRangeForIntegrityVerificationStorage, 2,6321,,,,UnsupportedSetSizeForBlockCacheBufferedStorage, -2,6322,,,,UnsupportedOperateRangeForNonSaveDataBlockCacheBufferedStorage, +2,6322,,,,UnsupportedOperateRangeForWritableBlockCacheBufferedStorage, 2,6323,,,,UnsupportedOperateRangeForBlockCacheBufferedStorage, 2,6324,,,,UnsupportedWriteForIndirectStorage, 2,6325,,,,UnsupportedSetSizeForIndirectStorage, diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index b81011f9..50943f44 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -1817,13 +1817,13 @@ public static class ResultFs /// Error code: 2002-6318; Inner value: 0x315c02 public static Result.Base UnsupportedSetSizeForIntegrityVerificationStorage => new Result.Base(ModuleFs, 6318); /// Error code: 2002-6319; Inner value: 0x315e02 - public static Result.Base UnsupportedOperateRangeForNonSaveDataIntegrityVerificationStorage => new Result.Base(ModuleFs, 6319); + public static Result.Base UnsupportedOperateRangeForWritableIntegrityVerificationStorage => new Result.Base(ModuleFs, 6319); /// Error code: 2002-6320; Inner value: 0x316002 public static Result.Base UnsupportedOperateRangeForIntegrityVerificationStorage => new Result.Base(ModuleFs, 6320); /// Error code: 2002-6321; Inner value: 0x316202 public static Result.Base UnsupportedSetSizeForBlockCacheBufferedStorage => new Result.Base(ModuleFs, 6321); /// Error code: 2002-6322; Inner value: 0x316402 - public static Result.Base UnsupportedOperateRangeForNonSaveDataBlockCacheBufferedStorage => new Result.Base(ModuleFs, 6322); + public static Result.Base UnsupportedOperateRangeForWritableBlockCacheBufferedStorage => new Result.Base(ModuleFs, 6322); /// Error code: 2002-6323; Inner value: 0x316602 public static Result.Base UnsupportedOperateRangeForBlockCacheBufferedStorage => new Result.Base(ModuleFs, 6323); /// Error code: 2002-6324; Inner value: 0x316802 diff --git a/src/LibHac/FsSrv/Delegates.cs b/src/LibHac/FsSrv/Delegates.cs index 4ee0163c..725f9ec0 100644 --- a/src/LibHac/FsSrv/Delegates.cs +++ b/src/LibHac/FsSrv/Delegates.cs @@ -2,12 +2,10 @@ namespace LibHac.FsSrv; -public delegate Result RandomDataGenerator(Span buffer); - public delegate Result SaveTransferAesKeyGenerator(Span key, SaveDataTransferCryptoConfiguration.KeyIndex index, ReadOnlySpan keySource, int keyGeneration); public delegate Result SaveTransferCmacGenerator(Span mac, ReadOnlySpan data, SaveDataTransferCryptoConfiguration.KeyIndex index, int keyGeneration); -public delegate Result PatrolAllocateCountGetter(out long successCount, out long failureCount); +public delegate Result PatrolAllocateCountGetter(out long successCount, out long failureCount); \ No newline at end of file diff --git a/src/LibHac/FsSrv/SaveDataTransferCryptoConfiguration.cs b/src/LibHac/FsSrv/SaveDataTransferCryptoConfiguration.cs index d6b6b15d..21fdc3ef 100644 --- a/src/LibHac/FsSrv/SaveDataTransferCryptoConfiguration.cs +++ b/src/LibHac/FsSrv/SaveDataTransferCryptoConfiguration.cs @@ -1,5 +1,6 @@ using System; using LibHac.Common.FixedArrays; +using LibHac.FsSystem; namespace LibHac.FsSrv; diff --git a/src/LibHac/FsSystem/Delegates.cs b/src/LibHac/FsSystem/Delegates.cs new file mode 100644 index 00000000..89a10d26 --- /dev/null +++ b/src/LibHac/FsSystem/Delegates.cs @@ -0,0 +1,5 @@ +using System; + +namespace LibHac.FsSystem; + +public delegate Result RandomDataGenerator(Span buffer); \ No newline at end of file diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs index ac1aca8d..7e610c01 100644 --- a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -4,7 +4,6 @@ using LibHac.Common; using LibHac.Diag; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.FsSrv; using LibHac.Os; namespace LibHac.FsSystem; diff --git a/src/LibHac/FsSystem/IntegrityVerificationStorage.cs b/src/LibHac/FsSystem/IntegrityVerificationStorage.cs new file mode 100644 index 00000000..6f69e41d --- /dev/null +++ b/src/LibHac/FsSystem/IntegrityVerificationStorage.cs @@ -0,0 +1,623 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Common.FixedArrays; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.Util; + +namespace LibHac.FsSystem; + +/// +/// An that can verify the integrity of its data by using a second +/// that contains hash digests of the main data. +/// +/// An consists of a data +/// and a hash . The main storage is split into blocks of a provided size and the hashes +/// of all these blocks are stored sequentially in the hash storage. Each time a data block is read its hash is +/// also read and used to verify the integrity of the main data. +/// An may be writable, updating the hash storage as required +/// when written to. Writable storages have some additional features compared to read-only storages:
+/// If the hash for a data block is all zeros then that block is treated as if the actual data is all zeros.
+/// To avoid collisions in the case where a block's actual hash is all zeros, a certain bit in all writable storage +/// hashes is always set to 1.
+/// An optional may be provided. This salt will be added to the beginning of each block of +/// data before it is hashed.
+/// Based on FS 14.1.0 (nnSdk 14.3.0)
+public class IntegrityVerificationStorage : IStorage +{ + public struct BlockHash + { + public Array32 Hash; + } + + public const int HashSize = Sha256.DigestSize; + + private ValueSubStorage _hashStorage; + private ValueSubStorage _dataStorage; + private int _verificationBlockSize; + private int _verificationBlockOrder; + private int _upperLayerVerificationBlockSize; + private int _upperLayerVerificationBlockOrder; + private IBufferManager _bufferManager; + private Optional _hashSalt; + private bool _isRealData; + private IHash256GeneratorFactory _hashGeneratorFactory; + private bool _isWritable; + private bool _allowClearedBlocks; + + public IntegrityVerificationStorage() + { + _hashStorage = new ValueSubStorage(); + _dataStorage = new ValueSubStorage(); + } + + public override void Dispose() + { + FinalizeObject(); + _dataStorage.Dispose(); + _hashStorage.Dispose(); + + base.Dispose(); + } + + /// + /// Sets the validation bit on the provided . The hashes of all writable blocks + /// have a certain bit set so we can tell the difference between a cleared block and a hash of all zeros. + /// + /// The to have its validation bit set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetValidationBit(ref BlockHash hash) + { + hash.Hash.Items[HashSize - 1] |= 0x80; + } + + /// + /// Checks if the provided has its validation bit set. The hashes of all writable blocks + /// have a certain bit set so we can tell the difference between a cleared block and a hash of all zeros. + /// + /// The to check. + /// if the validation bit is set; otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidationBit(in BlockHash hash) + { + return (hash.Hash.ItemsRo[HashSize - 1] & 0x80) != 0; + } + + public int GetBlockSize() + { + return _verificationBlockSize; + } + + public void Initialize(in ValueSubStorage hashStorage, in ValueSubStorage dataStorage, int sizeVerificationBlock, + int sizeUpperLayerVerificationBlock, IBufferManager bufferManager, + IHash256GeneratorFactory hashGeneratorFactory, in Optional hashSalt, bool isRealData, bool isWritable, + bool allowClearedBlocks) + { + Assert.SdkRequiresGreaterEqual(sizeVerificationBlock, HashSize); + Assert.SdkRequiresNotNull(bufferManager); + Assert.SdkRequiresNotNull(hashGeneratorFactory); + + _hashStorage.Set(in hashStorage); + _dataStorage.Set(in dataStorage); + + _hashGeneratorFactory = hashGeneratorFactory; + + _verificationBlockSize = sizeVerificationBlock; + _verificationBlockOrder = BitmapUtils.ILog2((uint)sizeVerificationBlock); + Assert.SdkRequiresEqual(1 << _verificationBlockOrder, _verificationBlockSize); + + _bufferManager = bufferManager; + + sizeUpperLayerVerificationBlock = Math.Max(sizeUpperLayerVerificationBlock, HashSize); + _upperLayerVerificationBlockSize = sizeUpperLayerVerificationBlock; + _upperLayerVerificationBlockOrder = BitmapUtils.ILog2((uint)sizeUpperLayerVerificationBlock); + Assert.SdkRequiresEqual(1 << _upperLayerVerificationBlockOrder, _upperLayerVerificationBlockSize); + + Assert.SdkAssert(_dataStorage.GetSize(out long dataSize).IsSuccess()); + Assert.SdkAssert(_hashStorage.GetSize(out long hashSize).IsSuccess()); + Assert.SdkAssert(hashSize / HashSize * _verificationBlockSize >= dataSize); + + _hashSalt = hashSalt; + + _isRealData = isRealData; + _isWritable = isWritable; + _allowClearedBlocks = allowClearedBlocks; + } + + public void FinalizeObject() + { + if (_bufferManager is not null) + { + using (var emptySubStorage = new ValueSubStorage()) + { + _hashStorage.Set(in emptySubStorage); + } + + using (var emptySubStorage = new ValueSubStorage()) + { + _dataStorage.Set(in emptySubStorage); + } + + _bufferManager = null; + } + } + + public override Result GetSize(out long size) + { + return _dataStorage.GetSize(out size); + } + + public override Result SetSize(long size) + { + return ResultFs.UnsupportedSetSizeForIntegrityVerificationStorage.Log(); + } + + public override Result Read(long offset, Span destination) + { + Assert.SdkRequiresNotEqual(0, destination.Length); + + Assert.SdkRequiresAligned(offset, _verificationBlockSize); + Assert.SdkRequiresAligned(destination.Length, _verificationBlockSize); + + if (destination.Length == 0) + return Result.Success; + + Result rc = _dataStorage.GetSize(out long dataSize); + if (rc.IsFailure()) return rc.Miss(); + + if (dataSize < offset) + return ResultFs.InvalidOffset.Log(); + + long alignedDataSize = Alignment.AlignUpPow2(dataSize, (uint)_verificationBlockSize); + rc = CheckAccessRange(offset, destination.Length, alignedDataSize); + if (rc.IsFailure()) return rc.Miss(); + + int readSize = destination.Length; + if (offset + readSize > dataSize) + { + // All reads to this storage must be aligned to the block size, but if the last data block is a partial block + // it will not be written to the base data storage. If that's the case, fill the unused portion of the block + // with zeros. The hash for the partial block is calculated using the padded, complete block. + int paddingOffset = (int)(dataSize - offset); + int paddingSize = _verificationBlockSize - (paddingOffset & (_verificationBlockSize - 1)); + Assert.SdkLess(paddingSize, _verificationBlockSize); + + // Clear the padding. + destination.Slice(paddingOffset, paddingSize).Clear(); + + // Set the new in-bounds size. + readSize = (int)(dataSize - offset); + } + + // Read all of the data to be validated. + rc = _dataStorage.Read(offset, destination.Slice(0, readSize)); + if (rc.IsFailure()) + { + destination.Clear(); + return rc.Log(); + } + + // Validate the hashes of the read data blocks. + Result verifyHashResult = Result.Success; + + using var hashGenerator = new UniqueRef(); + rc = _hashGeneratorFactory.Create(ref hashGenerator.Ref()); + if (rc.IsFailure()) return rc.Miss(); + + int signatureCount = destination.Length >> _verificationBlockOrder; + using var signatureBuffer = + new PooledBuffer(signatureCount * Unsafe.SizeOf(), Unsafe.SizeOf()); + int bufferCount = (int)Math.Min(signatureCount, signatureBuffer.GetSize() / (uint)Unsafe.SizeOf()); + + // Loop over each block while validating their signatures + int verifiedCount = 0; + while (verifiedCount < signatureCount) + { + int currentCount = Math.Min(bufferCount, signatureCount - verifiedCount); + + Result currentResult = ReadBlockSignature(signatureBuffer.GetBuffer(), + offset + (verifiedCount << _verificationBlockOrder), currentCount << _verificationBlockOrder); + + using var changePriority = new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative); + + for (int i = 1; i < currentCount && currentResult.IsSuccess(); i++) + { + int verifiedSize = (verifiedCount + i) << _verificationBlockOrder; + ref BlockHash blockHash = ref signatureBuffer.GetBuffer()[i]; + currentResult = VerifyHash(destination.Slice(verifiedCount), ref blockHash, in hashGenerator); + + if (ResultFs.IntegrityVerificationStorageCorrupted.Includes(currentResult)) + { + // Don't output the corrupted block to the destination buffer + destination.Slice(verifiedSize, _verificationBlockSize).Clear(); + + if (!ResultFs.ClearedRealDataVerificationFailed.Includes(currentResult) && !_allowClearedBlocks) + { + verifyHashResult = currentResult; + } + + currentResult = Result.Success; + } + } + + if (currentResult.IsFailure()) + { + destination.Clear(); + return currentResult; + } + + verifiedCount += currentCount; + } + + return verifyHashResult; + } + + public override Result Write(long offset, ReadOnlySpan source) + { + if (source.Length == 0) + return Result.Success; + + Result rc = CheckOffsetAndSize(offset, source.Length); + if (rc.IsFailure()) return rc.Miss(); + + rc = _dataStorage.GetSize(out long dataSize); + if (rc.IsFailure()) return rc.Miss(); + + if (offset >= dataSize) + return ResultFs.InvalidOffset.Log(); + + rc = CheckAccessRange(offset, source.Length, Alignment.AlignUpPow2(dataSize, (uint)_verificationBlockSize)); + if (rc.IsFailure()) return rc.Miss(); + + Assert.SdkRequiresAligned(offset, _verificationBlockSize); + Assert.SdkRequiresAligned(source.Length, _verificationBlockSize); + Assert.SdkLessEqual(offset, dataSize); + Assert.SdkLess(offset + source.Length, dataSize + _verificationBlockSize); + + // When writing to a partial final block, the data past the end of the partial block should be all zeros. + if (offset + source.Length > dataSize) + { + Assert.SdkAssert(source.Slice((int)(dataSize - offset)).IsZeros()); + } + + // Determine the size of the unpadded data we're writing to the base data storage + int writeSize = source.Length; + if (offset + writeSize > dataSize) + { + writeSize = (int)(dataSize - offset); + + if (writeSize == 0) + return Result.Success; + } + + int alignedWriteSize = Alignment.AlignUpPow2(writeSize, (uint)_verificationBlockSize); + + Result updateResult = Result.Success; + int updatedSignatureCount = 0; + { + int signatureCount = alignedWriteSize >> _verificationBlockOrder; + + using var signatureBuffer = + new PooledBuffer(signatureCount * Unsafe.SizeOf(), Unsafe.SizeOf()); + int bufferCount = Math.Min(signatureCount, signatureBuffer.GetSize() / Unsafe.SizeOf()); + + using var hashGenerator = new UniqueRef(); + rc = _hashGeneratorFactory.Create(ref hashGenerator.Ref()); + if (rc.IsFailure()) return rc.Miss(); + + while (updatedSignatureCount < signatureCount) + { + int remainingCount = signatureCount - updatedSignatureCount; + int currentCount = Math.Min(bufferCount, remainingCount); + + using (new ScopedThreadPriorityChanger(1, ScopedThreadPriorityChanger.Mode.Relative)) + { + // Calculate the new hashes for the current set of blocks + for (int i = 0; i < currentCount; i++) + { + int updatedSize = (updatedSignatureCount + i) << _verificationBlockOrder; + CalcBlockHash(out signatureBuffer.GetBuffer()[i], source.Slice(updatedSize), + in hashGenerator); + } + } + + // Write the new block signatures. + updateResult = WriteBlockSignature(signatureBuffer.GetBuffer(), + offset: offset + (updatedSignatureCount << _verificationBlockOrder), + size: currentCount << _verificationBlockOrder); + + if (updateResult.IsFailure()) + break; + + updatedSignatureCount += currentCount; + } + } + + // The updated hash values have all been written. Now write the actual data. + // If there was an error writing the updated hashes, only the data for the blocks that were + // successfully updated will be written. + int dataWriteSize = Math.Min(writeSize, updatedSignatureCount << _verificationBlockOrder); + rc = _dataStorage.Write(offset, source.Slice(0, dataWriteSize)); + if (rc.IsFailure()) return rc.Miss(); + + return updateResult; + } + + public override Result Flush() + { + Result rc = _hashStorage.Flush(); + if (rc.IsFailure()) return rc.Miss(); + + rc = _dataStorage.Flush(); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + + public override Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + if (operationId != OperationId.InvalidateCache) + { + Assert.SdkRequiresAligned(offset, _verificationBlockSize); + Assert.SdkRequiresAligned(size, _verificationBlockSize); + } + + switch (operationId) + { + case OperationId.FillZero: + { + Assert.SdkRequires(_isWritable); + + Result rc = _dataStorage.GetSize(out long dataStorageSize); + if (rc.IsFailure()) return rc.Miss(); + + if (offset < 0 || dataStorageSize < offset) + return ResultFs.InvalidOffset.Log(); + + // Get the range of the signatures for the blocks that will be cleared + long signOffset = (offset >> _verificationBlockOrder) * Unsafe.SizeOf(); + long signSize = Math.Min(size, dataStorageSize - offset) * Unsafe.SizeOf(); + + // Allocate a work buffer up to 4 times the size of the hash storage's verification block size. + int bufferSize = (int)Math.Min(signSize, 1 << (_upperLayerVerificationBlockOrder + 2)); + using var workBuffer = new RentedArray(bufferSize); + if (workBuffer.Array is null) + return ResultFs.AllocationMemoryFailedInIntegrityVerificationStorageA.Log(); + + workBuffer.Span.Clear(); + + long remainingSize = signSize; + + // Clear the hash storage in chunks. + while (remainingSize > 0) + { + int currentSize = (int)Math.Min(remainingSize, bufferSize); + + rc = _hashStorage.Write(signOffset + signSize - remainingSize, + workBuffer.Span.Slice(0, currentSize)); + if (rc.IsFailure()) return rc.Miss(); + + remainingSize -= currentSize; + } + + return Result.Success; + } + case OperationId.DestroySignature: + { + Assert.SdkRequires(_isWritable); + + Result rc = _dataStorage.GetSize(out long dataStorageSize); + if (rc.IsFailure()) return rc.Miss(); + + if (offset < 0 || dataStorageSize < offset) + return ResultFs.InvalidOffset.Log(); + + // Get the range of the signatures for the blocks that will be cleared + long signOffset = (offset >> _verificationBlockOrder) * Unsafe.SizeOf(); + long signSize = Math.Min(size, dataStorageSize - offset) * Unsafe.SizeOf(); + + using var workBuffer = new RentedArray((int)signSize); + if (workBuffer.Array is null) + return ResultFs.AllocationMemoryFailedInIntegrityVerificationStorageB.Log(); + + // Read the existing signature. + rc = _hashStorage.Read(signOffset, workBuffer.Span); + if (rc.IsFailure()) return rc.Miss(); + + // Clear the signature. + // This flips all bits, leaving the verification bit cleared. + for (int i = 0; i < workBuffer.Span.Length; i++) + { + workBuffer.Span[i] ^= (byte)((i + 1) % (uint)HashSize == 0 ? 0x7F : 0xFF); + } + + // Write the cleared signature. + return _hashStorage.Write(signOffset, workBuffer.Span); + } + case OperationId.InvalidateCache: + { + // Only allow cache invalidation for read-only storages. + if (_isWritable) + return ResultFs.UnsupportedOperateRangeForWritableIntegrityVerificationStorage.Log(); + + Result rc = _hashStorage.OperateRange(operationId, 0, long.MaxValue); + if (rc.IsFailure()) return rc.Miss(); + + rc = _dataStorage.OperateRange(operationId, offset, size); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + case OperationId.QueryRange: + { + Result rc = _dataStorage.GetSize(out long dataStorageSize); + if (rc.IsFailure()) return rc.Miss(); + + if (offset < 0 || dataStorageSize < offset) + return ResultFs.InvalidOffset.Log(); + + long actualSize = Math.Min(size, dataStorageSize - offset); + rc = _dataStorage.OperateRange(outBuffer, operationId, offset, actualSize, inBuffer); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + default: + return ResultFs.UnsupportedOperateRangeForIntegrityVerificationStorage.Log(); + } + } + + private Result ReadBlockSignature(Span destination, long offset, int size) + { + Assert.SdkRequiresAligned(offset, _verificationBlockSize); + Assert.SdkRequiresAligned(size, _verificationBlockSize); + + // Calculate the range that contains the signatures. + long offsetSignData = (offset >> _verificationBlockOrder) * HashSize; + long sizeSignData = (size >> _verificationBlockOrder) * HashSize; + Assert.SdkGreaterEqual(destination.Length, sizeSignData); + + // Validate the hash storage contains the calculated range. + Result rc = _hashStorage.GetSize(out long sizeHash); + if (rc.IsFailure()) return rc.Miss(); + + Assert.SdkLessEqual(offsetSignData + sizeSignData, sizeHash); + + if (offsetSignData + sizeSignData > sizeHash) + return ResultFs.OutOfRange.Log(); + + // Read the signature. + rc = _hashStorage.Read(offsetSignData, destination.Slice(0, (int)sizeSignData)); + if (rc.IsFailure()) + { + // Clear any read signature data if something goes wrong. + destination.Slice(0, (int)sizeSignData); + return rc.Miss(); + } + + return Result.Success; + } + + private Result WriteBlockSignature(ReadOnlySpan source, long offset, int size) + { + Assert.SdkRequiresAligned(offset, _verificationBlockSize); + + long offsetSignData = (offset >> _verificationBlockOrder) * HashSize; + long sizeSignData = (size >> _verificationBlockOrder) * HashSize; + Assert.SdkGreaterEqual(source.Length, sizeSignData); + + Result rc = _hashStorage.Write(offsetSignData, source.Slice(0, (int)sizeSignData)); + if (rc.IsFailure()) return rc.Miss(); + + return Result.Success; + } + + public Result CalcBlockHash(out BlockHash outHash, ReadOnlySpan buffer, int verificationBlockSize) + { + UnsafeHelpers.SkipParamInit(out outHash); + + using var hashGenerator = new UniqueRef(); + Result rc = _hashGeneratorFactory.Create(ref hashGenerator.Ref()); + if (rc.IsFailure()) return rc.Miss(); + + CalcBlockHash(out outHash, buffer, verificationBlockSize, in hashGenerator); + return Result.Success; + } + + private void CalcBlockHash(out BlockHash outHash, ReadOnlySpan buffer, + in UniqueRef hashGenerator) + { + CalcBlockHash(out outHash, buffer, _verificationBlockSize, in hashGenerator); + } + + private void CalcBlockHash(out BlockHash outHash, ReadOnlySpan buffer, int verificationBlockSize, + in UniqueRef hashGenerator) + { + UnsafeHelpers.SkipParamInit(out outHash); + + if (_isWritable) + { + if (_hashSalt.HasValue) + { + // Calculate the hash using the salt if enabled. + hashGenerator.Get.Initialize(); + hashGenerator.Get.Update(_hashSalt.ValueRo.HashRo); + + hashGenerator.Get.Update(buffer.Slice(0, verificationBlockSize)); + hashGenerator.Get.GetHash(SpanHelpers.AsByteSpan(ref outHash)); + } + else + { + // Otherwise calculate the hash of just the data. + _hashGeneratorFactory.GenerateHash(SpanHelpers.AsByteSpan(ref outHash), + buffer.Slice(0, verificationBlockSize)); + } + + // The hashes of all writable blocks have the validation bit set. + SetValidationBit(ref outHash); + } + else + { + // Nothing special needed for read-only blocks. Just calculate the hash. + _hashGeneratorFactory.GenerateHash(SpanHelpers.AsByteSpan(ref outHash), + buffer.Slice(0, verificationBlockSize)); + } + } + + private Result VerifyHash(ReadOnlySpan buffer, ref BlockHash hash, + in UniqueRef hashGenerator) + { + Assert.SdkRequiresGreaterEqual(buffer.Length, HashSize); + + // Writable storages allow using an all-zeros hash to indicate an empty block. + if (_isWritable) + { + Result rc = IsCleared(out bool isCleared, in hash); + if (rc.IsFailure()) return rc.Miss(); + + if (isCleared) + return ResultFs.ClearedRealDataVerificationFailed.Log(); + } + + CalcBlockHash(out BlockHash actualHash, buffer, hashGenerator); + + if (!CryptoUtil.IsSameBytes(SpanHelpers.AsReadOnlyByteSpan(in hash), + SpanHelpers.AsReadOnlyByteSpan(in actualHash), Unsafe.SizeOf())) + { + hash = default; + + if (_isRealData) + { + return ResultFs.UnclearedRealDataVerificationFailed.Log(); + } + else + { + return ResultFs.NonRealDataVerificationFailed.Log(); + } + } + + return Result.Success; + } + + private Result IsCleared(out bool isCleared, in BlockHash hash) + { + Assert.SdkRequires(_isWritable); + + isCleared = false; + + if (IsValidationBit(in hash)) + return Result.Success; + + for (int i = 0; i < hash.Hash.ItemsRo.Length; i++) + { + if (hash.Hash.ItemsRo[i] != 0) + return ResultFs.InvalidZeroHash.Log(); + } + + isCleared = true; + return Result.Success; + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PooledBuffer.cs b/src/LibHac/FsSystem/PooledBuffer.cs index 154db9c9..7f6733ed 100644 --- a/src/LibHac/FsSystem/PooledBuffer.cs +++ b/src/LibHac/FsSystem/PooledBuffer.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.Runtime.InteropServices; using LibHac.Diag; using LibHac.FsSrv; using LibHac.Os; @@ -116,6 +117,12 @@ public struct PooledBuffer : IDisposable return _array.AsSpan(0, _length); } + public Span GetBuffer() where T : unmanaged + { + Assert.SdkRequiresNotNull(_array); + return MemoryMarshal.Cast(_array.AsSpan(0, _length)); + } + public int GetSize() { Assert.SdkRequiresNotNull(_array); diff --git a/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs b/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs index 9f7f41fa..8707dfdb 100644 --- a/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs +++ b/tests/LibHac.Tests/FsSystem/DirectorySaveDataFileSystemTests.cs @@ -4,7 +4,6 @@ using System.Runtime.InteropServices; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; -using LibHac.FsSrv; using LibHac.FsSystem; using LibHac.Tests.Fs; using LibHac.Tests.Fs.IFileSystemTestBase;