Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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(&current);
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(&current);
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