diff options
Diffstat (limited to 'dom/quota/DecryptingInputStream_impl.h')
-rw-r--r-- | dom/quota/DecryptingInputStream_impl.h | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/dom/quota/DecryptingInputStream_impl.h b/dom/quota/DecryptingInputStream_impl.h new file mode 100644 index 0000000000..48100f114e --- /dev/null +++ b/dom/quota/DecryptingInputStream_impl.h @@ -0,0 +1,531 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_quota_DecryptingInputStream_impl_h +#define mozilla_dom_quota_DecryptingInputStream_impl_h + +#include "DecryptingInputStream.h" + +#include <algorithm> +#include <cstdio> +#include <type_traits> +#include <utility> +#include "CipherStrategy.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Span.h" +#include "mozilla/fallible.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsFileStreams.h" +#include "nsID.h" +#include "nsIFileStreams.h" + +namespace mozilla::dom::quota { + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::DecryptingInputStream( + MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize, + typename CipherStrategy::KeyType aKey) + : DecryptingInputStreamBase(std::move(aBaseStream), aBlockSize), + mKey(aKey) { + // XXX Move this to a fallible init function. + MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Decrypt, + CipherStrategy::SerializeKey(aKey))); + + // This implementation only supports sync base streams. Verify this in debug + // builds. +#ifdef DEBUG + bool baseNonBlocking; + nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(!baseNonBlocking); +#endif +} + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::~DecryptingInputStream() { + Close(); +} + +template <typename CipherStrategy> +DecryptingInputStream<CipherStrategy>::DecryptingInputStream() + : DecryptingInputStreamBase{} {} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Close() { + if (!mBaseStream) { + return NS_OK; + } + + (*mBaseStream)->Close(); + mBaseStream.destroy(); + + mPlainBuffer.Clear(); + mEncryptedBlock.reset(); + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Available( + uint64_t* aLengthOut) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + int64_t oldPos, endPos; + nsresult rv = Tell(&oldPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Seek(SEEK_END, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Tell(&endPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = Seek(SEEK_SET, oldPos); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aLengthOut = endPos - oldPos; + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::StreamStatus() { + return mBaseStream ? NS_OK : NS_BASE_STREAM_CLOSED; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::ReadSegments( + nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, + uint32_t* aBytesReadOut) { + *aBytesReadOut = 0; + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + nsresult rv; + + // Do not try to use the base stream's ReadSegments here. Its very + // unlikely we will get a single buffer that contains all of the encrypted + // data and therefore would have to copy into our own buffer anyways. + // Instead, focus on making efficient use of the Read() interface. + + while (aCount > 0) { + // We have some decrypted data in our buffer. Provide it to the callers + // writer function. + if (mPlainBytes > 0) { + MOZ_ASSERT(!mPlainBuffer.IsEmpty()); + uint32_t remaining = PlainLength(); + uint32_t numToWrite = std::min(aCount, remaining); + uint32_t numWritten; + rv = aWriter(this, aClosure, + reinterpret_cast<const char*>(&mPlainBuffer[mNextByte]), + *aBytesReadOut, numToWrite, &numWritten); + + // As defined in nsIInputputStream.idl, do not pass writer func errors. + if (NS_FAILED(rv)) { + return NS_OK; + } + + // End-of-file + if (numWritten == 0) { + return NS_OK; + } + + *aBytesReadOut += numWritten; + mNextByte += numWritten; + MOZ_ASSERT(mNextByte <= mPlainBytes); + + if (mNextByte == mPlainBytes) { + mNextByte = 0; + mLastBlockLength = mPlainBytes; + mPlainBytes = 0; + } + + aCount -= numWritten; + + continue; + } + + // Otherwise decrypt the next chunk and loop. Any resulting data + // will set mPlainBytes which we check at the top of the loop. + uint32_t bytesRead; + rv = ParseNextChunk(&bytesRead); + if (NS_FAILED(rv)) { + return rv; + } + + // If we couldn't read anything and there is no more data to provide + // to the caller, then this is eof. + if (bytesRead == 0 && mPlainBytes == 0) { + return NS_OK; + } + + mPlainBytes += bytesRead; + } + + return NS_OK; +} + +template <typename CipherStrategy> +nsresult DecryptingInputStream<CipherStrategy>::ParseNextChunk( + uint32_t* const aBytesReadOut) { + // There must not be any plain data already in mPlainBuffer. + MOZ_ASSERT(mPlainBytes == 0); + MOZ_ASSERT(mNextByte == 0); + + *aBytesReadOut = 0; + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Read the data to our internal encrypted buffer. + auto wholeBlock = mEncryptedBlock->MutableWholeBlock(); + nsresult rv = + ReadAll(AsWritableChars(wholeBlock).Elements(), wholeBlock.Length(), + wholeBlock.Length(), aBytesReadOut); + if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) { + return rv; + } + + // XXX Do we need to know the actual decrypted size? + rv = mCipherStrategy.Cipher(mEncryptedBlock->MutableCipherPrefix(), + mEncryptedBlock->Payload(), + AsWritableBytes(Span{mPlainBuffer})); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aBytesReadOut = mEncryptedBlock->ActualPayloadLength(); + + return NS_OK; +} + +template <typename CipherStrategy> +nsresult DecryptingInputStream<CipherStrategy>::ReadAll( + char* aBuf, uint32_t aCount, uint32_t aMinValidCount, + uint32_t* aBytesReadOut) { + MOZ_ASSERT(aCount >= aMinValidCount); + MOZ_ASSERT(mBaseStream); + + *aBytesReadOut = 0; + + uint32_t offset = 0; + while (aCount > 0) { + uint32_t bytesRead = 0; + nsresult rv = (*mBaseStream)->Read(aBuf + offset, aCount, &bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // EOF, but don't immediately return. We need to validate min read bytes + // below. + if (bytesRead == 0) { + break; + } + + *aBytesReadOut += bytesRead; + offset += bytesRead; + aCount -= bytesRead; + } + + // Reading zero bytes is not an error. Its the expected EOF condition. + // Only compare to the minimum valid count if we read at least one byte. + if (*aBytesReadOut != 0 && *aBytesReadOut < aMinValidCount) { + return NS_ERROR_CORRUPTED_CONTENT; + } + + return NS_OK; +} + +template <typename CipherStrategy> +bool DecryptingInputStream<CipherStrategy>::EnsureBuffers() { + // Lazily create our two buffers so we can report OOM during stream + // operation. These allocations only happens once. The buffers are reused + // until the stream is closed. + if (!mEncryptedBlock) { + // XXX Do we need to do this fallible (as the comment above suggests)? + mEncryptedBlock.emplace(*mBlockSize); + + MOZ_ASSERT(mPlainBuffer.IsEmpty()); + if (NS_WARN_IF(!mPlainBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(), + fallible))) { + return false; + } + } + + return true; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Tell( + int64_t* const aRetval) { + MOZ_ASSERT(aRetval); + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int64_t basePosition; + nsresult rv = (*mBaseSeekableStream)->Tell(&basePosition); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + const auto fullBlocks = basePosition / *mBlockSize; + MOZ_ASSERT(0 == basePosition % *mBlockSize); + + *aRetval = (fullBlocks - ((mPlainBytes || mLastBlockLength) ? 1 : 0)) * + mEncryptedBlock->MaxPayloadLength() + + mNextByte + (mNextByte ? 0 : mLastBlockLength); + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence, + int64_t aOffset) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int64_t baseBlocksOffset; + int64_t nextByteOffset; + switch (aWhence) { + case NS_SEEK_CUR: + // XXX Simplify this without using Tell. + { + int64_t current; + nsresult rv = Tell(¤t); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aOffset += current; + } + break; + case NS_SEEK_SET: + break; + + case NS_SEEK_END: + // XXX Simplify this without using Seek/Tell. + { + // XXX The size of the stream could also be queried and stored once + // only. + nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_SET, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint64_t baseStreamSize; + rv = (*mBaseStream)->Available(&baseStreamSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + auto decryptedStreamSizeOrErr = [baseStreamSize, + this]() -> Result<int64_t, nsresult> { + if (!baseStreamSize) { + return 0; + } + + nsresult rv = + (*mBaseSeekableStream) + ->Seek(NS_SEEK_END, -static_cast<int64_t>(*mBlockSize)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + mNextByte = 0; + mPlainBytes = 0; + + uint32_t bytesRead; + rv = ParseNextChunk(&bytesRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + MOZ_ASSERT(bytesRead); + + // XXX Shouldn't ParseNextChunk better update mPlainBytes? + mPlainBytes = bytesRead; + + mNextByte = bytesRead; + + int64_t current; + rv = Tell(¤t); + if (NS_WARN_IF(NS_FAILED(rv))) { + return Err(rv); + } + + return current; + }(); + + if (decryptedStreamSizeOrErr.isErr()) { + return decryptedStreamSizeOrErr.unwrapErr(); + } + + aOffset += decryptedStreamSizeOrErr.unwrap(); + } + break; + + default: + return NS_ERROR_ILLEGAL_VALUE; + } + + baseBlocksOffset = aOffset / mEncryptedBlock->MaxPayloadLength(); + nextByteOffset = aOffset % mEncryptedBlock->MaxPayloadLength(); + + // XXX If we remain in the same block as before, we can skip this. + nsresult rv = + (*mBaseSeekableStream)->Seek(NS_SEEK_SET, baseBlocksOffset * *mBlockSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mNextByte = 0; + mPlainBytes = 0; + + uint32_t readBytes; + rv = ParseNextChunk(&readBytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + // XXX Do we need to do more here? Restore any previous state? + return rv; + } + + // We positioned after the last block, we must read that to know its size. + // XXX We could know earlier if we positioned us after the last block. + if (!readBytes) { + if (baseBlocksOffset == 0) { + // The stream is empty. + return aOffset == 0 ? NS_OK : NS_ERROR_ILLEGAL_VALUE; + } + + nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_CUR, -*mBlockSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = ParseNextChunk(&readBytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + // XXX Do we need to do more here? Restore any previous state? + return rv; + } + } + + mPlainBytes = readBytes; + mNextByte = nextByteOffset; + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Clone( + nsIInputStream** _retval) { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!(*mBaseCloneableInputStream)->GetCloneable()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIInputStream> clonedStream; + nsresult rv = + (*mBaseCloneableInputStream)->Clone(getter_AddRefs(clonedStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *_retval = MakeAndAddRef<DecryptingInputStream>( + WrapNotNull(std::move(clonedStream)), *mBlockSize, *mKey) + .take(); + + return NS_OK; +} + +template <typename CipherStrategy> +void DecryptingInputStream<CipherStrategy>::Serialize( + mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize, + uint32_t* aSizeUsed) { + MOZ_ASSERT(mBaseStream); + MOZ_ASSERT(mBaseIPCSerializableInputStream); + + mozilla::ipc::InputStreamParams baseStreamParams; + (*mBaseIPCSerializableInputStream) + ->Serialize(baseStreamParams, aMaxSize, aSizeUsed); + + MOZ_ASSERT(baseStreamParams.type() == + mozilla::ipc::InputStreamParams::TFileInputStreamParams); + + mozilla::ipc::EncryptedFileInputStreamParams encryptedFileInputStreamParams; + encryptedFileInputStreamParams.fileInputStreamParams() = + std::move(baseStreamParams); + encryptedFileInputStreamParams.key().AppendElements( + mCipherStrategy.SerializeKey(*mKey)); + encryptedFileInputStreamParams.blockSize() = *mBlockSize; + + aParams = std::move(encryptedFileInputStreamParams); +} + +template <typename CipherStrategy> +bool DecryptingInputStream<CipherStrategy>::Deserialize( + const mozilla::ipc::InputStreamParams& aParams) { + MOZ_ASSERT(aParams.type() == + mozilla::ipc::InputStreamParams::TEncryptedFileInputStreamParams); + const auto& params = aParams.get_EncryptedFileInputStreamParams(); + + nsCOMPtr<nsIFileInputStream> stream; + nsFileInputStream::Create(NS_GET_IID(nsIFileInputStream), + getter_AddRefs(stream)); + nsCOMPtr<nsIIPCSerializableInputStream> baseSerializable = + do_QueryInterface(stream); + + if (NS_WARN_IF( + !baseSerializable->Deserialize(params.fileInputStreamParams()))) { + return false; + } + + Init(WrapNotNull<nsCOMPtr<nsIInputStream>>(std::move(stream)), + params.blockSize()); + + auto key = mCipherStrategy.DeserializeKey(params.key()); + if (NS_WARN_IF(!key)) { + return false; + } + + mKey.init(*key); + if (NS_WARN_IF( + NS_FAILED(mCipherStrategy.Init(CipherMode::Decrypt, params.key())))) { + return false; + } + + return true; +} + +} // namespace mozilla::dom::quota + +#endif |