blob: 215249db2aa60fe2cc360cea6a80dd4845d82a46 [file] [log] [blame]
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A file-like object that decrypts the data it reads.
It reads the ciphertext from a given other file-like object, and decrypts it.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import google_type_annotations
from __future__ import print_function
import errno
import io
from typing import BinaryIO
from tink.python.cc.clif import cc_streaming_aead_wrappers
from tink.python.core import tink_error
from tink.python.util import file_object_adapter
_OUT_OF_RANGE_ERROR_CODE = 11
class DecryptingStream(io.BufferedIOBase):
"""A file-like object which decrypts reads from an underlying object.
It reads the ciphertext from the wrapped file-like object, and decrypts it.
The additional method position() returns the number of read plaintext bytes.
Closing this wrapper also closes the underlying object.
"""
def __init__(self, stream_aead, ciphertext_source: BinaryIO,
associated_data: bytes):
"""Create a new DecryptingStream.
Args:
stream_aead: C++ StreamingAead primitive from which a C++ DecryptingStream
will be obtained.
ciphertext_source: A readable file-like object from which ciphertext bytes
will be read.
associated_data: The associated data to use for decryption.
"""
super(DecryptingStream, self).__init__()
self._closed = False
self._bytes_read = 0
self._ciphertext_source = ciphertext_source
# Create FileObjectAdapter
if not ciphertext_source.readable():
raise ValueError('ciphertext_source must be readable')
cc_ciphertext_source = file_object_adapter.FileObjectAdapter(
ciphertext_source)
# Get InputStreamAdapter of C++ DecryptingStream
self._input_stream_adapter = self._get_input_stream_adapter(
stream_aead, associated_data, cc_ciphertext_source)
@staticmethod
@tink_error.use_tink_errors
def _get_input_stream_adapter(cc_primitive, aad, source):
"""Implemented as a separate method to ensure correct error transform."""
return cc_streaming_aead_wrappers.new_cc_decrypting_stream(
cc_primitive, aad, source)
### Reading ###
def read(self, size: int = -1) -> bytes:
"""Read and return up to size bytes.
Multiple reads may be issued to the underlying object.
Args:
size: Maximum number of bytes to read. If the argument is omitted, None,
or negative, data is read and returned until EOF or if the read call
would block in non-blocking mode.
Returns:
Bytes read. An empty bytes object is returned if the stream is already at
EOF.
Raises:
BlockingIOError if no data is available at the moment.
TinkError if there was a permanent error.
"""
return self._read(size, read1=False)
def read1(self, size: int = -1) -> bytes:
"""Read and return up to size bytes.
At most one read will be issued to the underlying object.
Args:
size: Maximum number of bytes to read. If the argument is omitted, None,
or negative, an arbitrary number of bytes are returned.
Returns:
Bytes read. An empty bytes object is returned if the stream is already at
EOF.
Raises:
BlockingIOError if no data is available at the moment.
TinkError if there was a permanent error.
"""
return self._read(size, read1=True)
def readinto(self, b: bytearray) -> int:
"""Read bytes into a pre-allocated bytes-like object b.
Multiple reads may be issued to the underlying object.
Args:
b: Bytes-like object to which data will be read.
Returns:
Number of bytes read. If 0 is returned it means EOF is reached.
Raises:
BlockingIOError if no data is available at the moment.
TinkError if there was a permanent error.
"""
return self._readinto(b, read1=False)
def readinto1(self, b: bytearray) -> int:
"""Read bytes into a pre-allocated bytes-like object b.
At most one read will be issued to the underlying object.
Args:
b: Bytes-like object to which data will be read.
Returns:
Number of bytes read. If 0 is returned it means EOF is reached.
Raises:
BlockingIOError if no data is available at the moment.
TinkError if there was a permanent error.
"""
return self._readinto(b, read1=True)
def _read(self, size: int, read1: bool) -> bytes:
self._check_not_closed()
if size is None:
size = -1
try:
if read1:
data = self._read1_with_tink_error(size)
else:
data = self._read_with_tink_error(size)
if not data:
raise io.BlockingIOError(errno.EAGAIN,
'No data available at the moment.')
else:
self._bytes_read += len(data)
return data
except tink_error.TinkError as e:
# We are checking if the exception was raised because of C++
# OUT_OF_RANGE status, which signals EOF.
if e.args[0].code == _OUT_OF_RANGE_ERROR_CODE:
return b''
else:
raise e
# TODO(b/141344377) use the implementation in parent class
def _readinto(self, b: bytearray, read1: bool) -> int:
data = self._read(len(b), read1)
n = len(data)
b[:n] = data
return n
@tink_error.use_tink_errors
def _read_with_tink_error(self, size: int) -> bytes:
"""Implemented as a separate method to ensure correct error transform."""
return self._input_stream_adapter.read(size)
@tink_error.use_tink_errors
def _read1_with_tink_error(self, size: int) -> bytes:
"""Implemented as a separate method to ensure correct error transform."""
return self._input_stream_adapter.read1(size)
### Internal ###
# TODO(b/141344377) use parent class _checkClosed() instead
def _check_not_closed(self, msg=None):
"""Internal: raise a ValueError if file is closed."""
if self.closed:
raise ValueError('I/O operation on closed file.' if msg is None else msg)
### Positioning ###
def position(self) -> int:
"""Returns total number of read plaintext bytes."""
return self._bytes_read
### Flush and close ###
def flush(self) -> None:
"""This has no effect because the stream is read-only."""
self._check_not_closed()
def close(self) -> None:
"""Close the stream.
This has no effect on a closed stream.
"""
if self.closed:
return
self._ciphertext_source.close()
self._closed = True
### Inquiries ###
def readable(self) -> bool:
"""Indicates whether object was opened for reading.
Returns:
Whether object was opened for reading.
If False, read() will raise UnsupportedOperation.
"""
return True
@property
def closed(self) -> bool:
"""Indicates if the file has been closed.
Returns:
True if and only if the file has been closed.
For backwards compatibility, this is a property, not a predicate.
"""
return self._closed