diff options
Diffstat (limited to 'dom/quota/EncryptingOutputStream_impl.h')
-rw-r--r-- | dom/quota/EncryptingOutputStream_impl.h | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/dom/quota/EncryptingOutputStream_impl.h b/dom/quota/EncryptingOutputStream_impl.h new file mode 100644 index 0000000000..bf48d49f69 --- /dev/null +++ b/dom/quota/EncryptingOutputStream_impl.h @@ -0,0 +1,281 @@ +/* -*- 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_EncryptingOutputStream_impl_h +#define mozilla_dom_quota_EncryptingOutputStream_impl_h + +#include "EncryptingOutputStream.h" + +#include <algorithm> +#include <utility> +#include "CipherStrategy.h" +#include "mozilla/Assertions.h" +#include "mozilla/Span.h" +#include "mozilla/fallible.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIAsyncOutputStream.h" +#include "nsIRandomGenerator.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla::dom::quota { +template <typename CipherStrategy> +EncryptingOutputStream<CipherStrategy>::EncryptingOutputStream( + nsCOMPtr<nsIOutputStream> aBaseStream, size_t aBlockSize, + typename CipherStrategy::KeyType aKey) + : EncryptingOutputStreamBase(std::move(aBaseStream), aBlockSize) { + // XXX Move this to a fallible init function. + MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Encrypt, + CipherStrategy::SerializeKey(aKey), + CipherStrategy::MakeBlockPrefix())); + + MOZ_ASSERT(mBlockSize > 0); + MOZ_ASSERT(mBlockSize % CipherStrategy::BasicBlockSize == 0); + static_assert( + CipherStrategy::BlockPrefixLength % CipherStrategy::BasicBlockSize == 0); + + // This implementation only supports sync base streams. Verify this in debug + // builds. Note, this is a bit complicated because the streams we support + // advertise different capabilities: + // - nsFileOutputStream - blocking and sync + // - FixedBufferOutputStream - non-blocking and sync + // - nsPipeOutputStream - can be blocking, but provides async interface +#ifdef DEBUG + bool baseNonBlocking; + nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (baseNonBlocking) { + nsCOMPtr<nsIAsyncOutputStream> async = + do_QueryInterface((*mBaseStream).get()); + MOZ_ASSERT(!async); + } +#endif +} + +template <typename CipherStrategy> +EncryptingOutputStream<CipherStrategy>::~EncryptingOutputStream() { + Close(); +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Close() { + if (!mBaseStream) { + return NS_OK; + } + + // When closing, flush to the base stream unconditionally, i.e. even if the + // buffer is not completely full. + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // XXX Maybe this Flush call can be removed, since the base stream is closed + // afterwards anyway. + rv = (*mBaseStream)->Flush(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // XXX What if closing the base stream failed? Fail this method, or at least + // log a warning? + (*mBaseStream)->Close(); + mBaseStream.destroy(); + + mBuffer.Clear(); + mEncryptedBlock.reset(); + + return NS_OK; +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Flush() { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // We cannot call FlushBaseStream() here if the buffer is not completely + // full, we would write an incomplete page, which might be read sequentially, + // but we want to support random accesses in DecryptingInputStream, which + // would no longer be feasible. + if (mNextByte && mNextByte == mEncryptedBlock->MaxPayloadLength()) { + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return (*mBaseStream)->Flush(); +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::StreamStatus() { + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + return (*mBaseStream)->StreamStatus(); +} + +template <typename CipherStrategy> +NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::WriteSegments( + nsReadSegmentFun aReader, void* aClosure, uint32_t aCount, + uint32_t* aBytesWrittenOut) { + *aBytesWrittenOut = 0; + + if (!mBaseStream) { + return NS_BASE_STREAM_CLOSED; + } + + if (!EnsureBuffers()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + const size_t plainBufferSize = mEncryptedBlock->MaxPayloadLength(); + + while (aCount > 0) { + // Determine how much space is left in our flat, plain buffer. + MOZ_ASSERT(mNextByte <= plainBufferSize); + uint32_t remaining = plainBufferSize - mNextByte; + + // If it is full, then encrypt and flush the data to the base stream. + if (remaining == 0) { + nsresult rv = FlushToBaseStream(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now the entire buffer should be available for copying. + MOZ_ASSERT(!mNextByte); + remaining = plainBufferSize; + } + + uint32_t numToRead = std::min(remaining, aCount); + uint32_t numRead = 0; + + nsresult rv = + aReader(this, aClosure, reinterpret_cast<char*>(&mBuffer[mNextByte]), + *aBytesWrittenOut, numToRead, &numRead); + + // As defined in nsIOutputStream.idl, do not pass reader func errors. + if (NS_FAILED(rv)) { + return NS_OK; + } + + // End-of-file + if (numRead == 0) { + return NS_OK; + } + + mNextByte += numRead; + *aBytesWrittenOut += numRead; + aCount -= numRead; + } + + return NS_OK; +} + +template <typename CipherStrategy> +bool EncryptingOutputStream<CipherStrategy>::EnsureBuffers() { + // Lazily create the encrypted buffer on our first flush. This + // allows us to report OOM during stream operation. This buffer + // will then get re-used 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(mBuffer.IsEmpty()); + + if (NS_WARN_IF(!mBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(), + fallible))) { + return false; + } + } + + return true; +} + +template <typename CipherStrategy> +nsresult EncryptingOutputStream<CipherStrategy>::FlushToBaseStream() { + MOZ_ASSERT(mBaseStream); + + if (!mNextByte) { + // Nothing to do. + return NS_OK; + } + + if (mNextByte < mEncryptedBlock->MaxPayloadLength()) { + if (!mRandomGenerator) { + mRandomGenerator = + do_GetService("@mozilla.org/security/random-generator;1"); + if (NS_WARN_IF(!mRandomGenerator)) { + return NS_ERROR_FAILURE; + } + } + + const auto payload = mEncryptedBlock->MutablePayload(); + + const auto unusedPayload = payload.From(mNextByte); + + uint8_t* buffer; + nsresult rv = + mRandomGenerator->GenerateRandomBytes(unusedPayload.Length(), &buffer); + if (NS_WARN_IF(NS_FAILED(rv)) || !buffer) { + return rv; + } + + memcpy(unusedPayload.Elements(), buffer, unusedPayload.Length()); + + free(buffer); + } + + // XXX The compressing stream implementation this was based on wrote a stream + // identifier, containing e.g. the block size. Should we do something like + // that as well? At the moment, we don't need it, but maybe this were + // convenient if we use this for persistent files in the future across version + // updates, which might change such parameters. + + const auto iv = mCipherStrategy.MakeBlockPrefix(); + static_assert(iv.size() * sizeof(decltype(*iv.begin())) == + CipherStrategy::BlockPrefixLength); + std::copy(iv.cbegin(), iv.cend(), + mEncryptedBlock->MutableCipherPrefix().begin()); + + // Encrypt the data to our internal encrypted buffer. + // XXX Do we need to know the actual encrypted size? + nsresult rv = mCipherStrategy.Cipher( + mEncryptedBlock->MutableCipherPrefix(), + mozilla::Span(reinterpret_cast<uint8_t*>(mBuffer.Elements()), + ((mNextByte + (CipherStrategy::BasicBlockSize - 1)) / + CipherStrategy::BasicBlockSize) * + CipherStrategy::BasicBlockSize), + mEncryptedBlock->MutablePayload()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mEncryptedBlock->SetActualPayloadLength(mNextByte); + + mNextByte = 0; + + // Write the encrypted buffer out to the base stream. + uint32_t numWritten = 0; + const auto& wholeBlock = mEncryptedBlock->WholeBlock(); + rv = WriteAll(AsChars(wholeBlock).Elements(), wholeBlock.Length(), + &numWritten); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(wholeBlock.Length() == numWritten); + + return NS_OK; +} + +} // namespace mozilla::dom::quota + +#endif |