Skip to content

Commit 1874fab

Browse files
authored
Merge pull request #14 from mattsb42-aws/non_seekable_sources
Fixes issue with non-seekable sources breaking StreamEncryptor and StreamDecryptor handling
2 parents 33badf5 + 7d42cf0 commit 1874fab

20 files changed

+278
-130
lines changed

CHANGELOG.rst

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Changelog
33
*********
44

5+
1.3.2
6+
=====
7+
* Addressed `issue #13 <https://github.com/awslabs/aws-encryption-sdk-python/issues/13>`_
8+
to properly handle non-seekable source streams.
9+
510
1.3.1
611
=====
712

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
boto3>=1.4.4
22
cryptography>=1.8.1
33
attrs>=16.3.0
4+
wrapt>=1.10.11

src/aws_encryption_sdk/identifiers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from aws_encryption_sdk.exceptions import InvalidAlgorithmError
2323

24-
__version__ = '1.3.1'
24+
__version__ = '1.3.2'
2525
USER_AGENT_SUFFIX = 'AwsEncryptionSdkPython-KMSMasterKey/{}'.format(__version__)
2626

2727

src/aws_encryption_sdk/internal/formatting/deserialize.py

+23-24
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# language governing permissions and limitations under the License.
1313
"""Components for handling AWS Encryption SDK message deserialization."""
1414
from __future__ import division
15+
import io
1516
import logging
1617
import struct
1718

@@ -29,38 +30,34 @@
2930
EncryptedData, MessageFooter,
3031
MessageFrameBody, MessageHeaderAuthentication
3132
)
33+
from aws_encryption_sdk.internal.utils.streams import TeeStream
3234
from aws_encryption_sdk.structures import EncryptedDataKey, MasterKeyInfo, MessageHeader
3335

3436
_LOGGER = logging.getLogger(__name__)
3537

3638

37-
def validate_header(header, header_auth, stream, header_start, header_end, data_key):
39+
def validate_header(header, header_auth, raw_header, data_key):
3840
"""Validates the header using the header authentication data.
3941
4042
:param header: Deserialized header
4143
:type header: aws_encryption_sdk.structures.MessageHeader
4244
:param header_auth: Deserialized header auth
4345
:type header_auth: aws_encryption_sdk.internal.structures.MessageHeaderAuthentication
44-
:param stream: Stream containing serialized message
4546
:type stream: io.BytesIO
46-
:param int header_start: Position in stream of start of serialized header
47-
:param int header_end: Position in stream of end of serialized header
47+
:param bytes raw_header: Raw header bytes
4848
:param bytes data_key: Data key with which to perform validation
4949
:raises SerializationError: if header authorization fails
5050
"""
5151
_LOGGER.debug('Starting header validation')
52-
current_position = stream.tell()
53-
stream.seek(header_start)
5452
try:
5553
decrypt(
5654
algorithm=header.algorithm,
5755
key=data_key,
5856
encrypted_data=EncryptedData(header_auth.iv, b'', header_auth.tag),
59-
associated_data=stream.read(header_end - header_start)
57+
associated_data=raw_header
6058
)
6159
except InvalidTag:
6260
raise SerializationError('Header authorization failed')
63-
stream.seek(current_position)
6461

6562

6663
def deserialize_header(stream):
@@ -69,13 +66,15 @@ def deserialize_header(stream):
6966
:param stream: Source data stream
7067
:type stream: io.BytesIO
7168
:returns: Deserialized MessageHeader object
72-
:rtype: aws_encryption_sdk.structures.MessageHeader
69+
:rtype: :class:`aws_encryption_sdk.structures.MessageHeader` and bytes
7370
:raises NotSupportedError: if unsupported data types are found
7471
:raises UnknownIdentityError: if unknown data types are found
7572
:raises SerializationError: if IV length does not match algorithm
7673
"""
7774
_LOGGER.debug('Starting header deserialization')
78-
version_id, message_type_id = unpack_values('>BB', stream)
75+
tee = io.BytesIO()
76+
tee_stream = TeeStream(stream, tee)
77+
version_id, message_type_id = unpack_values('>BB', tee_stream)
7978
try:
8079
message_type = ObjectType(message_type_id)
8180
except ValueError as error:
@@ -89,7 +88,7 @@ def deserialize_header(stream):
8988
raise NotSupportedError('Unsupported version {}'.format(version_id), error)
9089
header = {'version': version, 'type': message_type}
9190

92-
algorithm_id, message_id, ser_encryption_context_length = unpack_values('>H16sH', stream)
91+
algorithm_id, message_id, ser_encryption_context_length = unpack_values('>H16sH', tee_stream)
9392

9493
try:
9594
alg = Algorithm.get_by_id(algorithm_id)
@@ -101,24 +100,24 @@ def deserialize_header(stream):
101100
header['message_id'] = message_id
102101

103102
header['encryption_context'] = deserialize_encryption_context(
104-
stream.read(ser_encryption_context_length)
103+
tee_stream.read(ser_encryption_context_length)
105104
)
106-
(encrypted_data_key_count,) = unpack_values('>H', stream)
105+
(encrypted_data_key_count,) = unpack_values('>H', tee_stream)
107106

108107
encrypted_data_keys = set([])
109108
for _ in range(encrypted_data_key_count):
110-
(key_provider_length,) = unpack_values('>H', stream)
109+
(key_provider_length,) = unpack_values('>H', tee_stream)
111110
(key_provider_identifier,) = unpack_values(
112111
'>{}s'.format(key_provider_length),
113-
stream
112+
tee_stream
114113
)
115-
(key_provider_information_length,) = unpack_values('>H', stream)
114+
(key_provider_information_length,) = unpack_values('>H', tee_stream)
116115
(key_provider_information,) = unpack_values(
117116
'>{}s'.format(key_provider_information_length),
118-
stream
117+
tee_stream
119118
)
120-
(encrypted_data_key_length,) = unpack_values('>H', stream)
121-
encrypted_data_key = stream.read(encrypted_data_key_length)
119+
(encrypted_data_key_length,) = unpack_values('>H', tee_stream)
120+
encrypted_data_key = tee_stream.read(encrypted_data_key_length)
122121
encrypted_data_keys.add(EncryptedDataKey(
123122
key_provider=MasterKeyInfo(
124123
provider_id=to_str(key_provider_identifier),
@@ -128,7 +127,7 @@ def deserialize_header(stream):
128127
))
129128
header['encrypted_data_keys'] = encrypted_data_keys
130129

131-
(content_type_id,) = unpack_values('>B', stream)
130+
(content_type_id,) = unpack_values('>B', tee_stream)
132131
try:
133132
content_type = ContentType(content_type_id)
134133
except ValueError as error:
@@ -138,14 +137,14 @@ def deserialize_header(stream):
138137
)
139138
header['content_type'] = content_type
140139

141-
(content_aad_length,) = unpack_values('>I', stream)
140+
(content_aad_length,) = unpack_values('>I', tee_stream)
142141
if content_aad_length != 0:
143142
raise SerializationError(
144143
'Content AAD length field is currently unused, its value must be always 0'
145144
)
146145
header['content_aad_length'] = 0
147146

148-
(iv_length,) = unpack_values('>B', stream)
147+
(iv_length,) = unpack_values('>B', tee_stream)
149148
if iv_length != alg.iv_len:
150149
raise SerializationError(
151150
'Specified IV length ({length}) does not match algorithm IV length ({alg})'.format(
@@ -155,7 +154,7 @@ def deserialize_header(stream):
155154
)
156155
header['header_iv_length'] = iv_length
157156

158-
(frame_length,) = unpack_values('>I', stream)
157+
(frame_length,) = unpack_values('>I', tee_stream)
159158
if content_type == ContentType.FRAMED_DATA and frame_length > MAX_FRAME_SIZE:
160159
raise SerializationError('Specified frame length larger than allowed maximum: {found} > {max}'.format(
161160
found=frame_length,
@@ -165,7 +164,7 @@ def deserialize_header(stream):
165164
raise SerializationError('Non-zero frame length found for non-framed message')
166165
header['frame_length'] = frame_length
167166

168-
return MessageHeader(**header)
167+
return MessageHeader(**header), tee.getvalue()
169168

170169

171170
def deserialize_header_auth(stream, algorithm, verifier=None):

src/aws_encryption_sdk/internal/utils.py renamed to src/aws_encryption_sdk/internal/utils/__init__.py

+1-35
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717

1818
import six
1919

20-
from aws_encryption_sdk.exceptions import (
21-
ActionNotAllowedError, InvalidDataKeyError,
22-
SerializationError, UnknownIdentityError
23-
)
20+
from aws_encryption_sdk.exceptions import InvalidDataKeyError, SerializationError, UnknownIdentityError
2421
from aws_encryption_sdk.identifiers import ContentAADString, ContentType
2522
import aws_encryption_sdk.internal.defaults
2623
from aws_encryption_sdk.internal.str_ops import to_bytes
@@ -95,37 +92,6 @@ def get_aad_content_string(content_type, is_final_frame):
9592
return aad_content_string
9693

9794

98-
class ROStream(object):
99-
"""Provides a read-only interface on top of a stream object.
100-
101-
Used to provide MasterKeyProviders with read-only access to plaintext.
102-
103-
:param source_stream: File-like object
104-
"""
105-
106-
def __init__(self, source_stream):
107-
"""Prepares the passthroughs."""
108-
self._source_stream = source_stream
109-
self._duplicate_api()
110-
111-
def _duplicate_api(self):
112-
"""Maps the source stream API onto this object."""
113-
source_attributes = set([
114-
method for method in dir(self._source_stream)
115-
if not method.startswith('_')
116-
])
117-
self_attributes = set(dir(self))
118-
for attribute in source_attributes.difference(self_attributes):
119-
setattr(self, attribute, getattr(self._source_stream, attribute))
120-
121-
def write(self, b): # pylint: disable=unused-argument
122-
"""Blocks calls to write.
123-
124-
:raises ActionNotAllowedError: when called
125-
"""
126-
raise ActionNotAllowedError('Write not allowed on ROStream objects')
127-
128-
12995
def prepare_data_keys(primary_master_key, master_keys, algorithm, encryption_context):
13096
"""Prepares a DataKey to be used for encrypting message and list
13197
of EncryptedDataKey objects to be serialized into header.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Helper stream utility objects for AWS Encryption SDK."""
14+
from wrapt import ObjectProxy
15+
16+
from aws_encryption_sdk.exceptions import ActionNotAllowedError
17+
18+
19+
class ROStream(ObjectProxy):
20+
"""Provides a read-only interface on top of a file-like object.
21+
22+
Used to provide MasterKeyProviders with read-only access to plaintext.
23+
24+
:param wrapped: File-like object
25+
"""
26+
27+
def write(self, b): # pylint: disable=unused-argument
28+
"""Blocks calls to write.
29+
30+
:raises ActionNotAllowedError: when called
31+
"""
32+
raise ActionNotAllowedError('Write not allowed on ROStream objects')
33+
34+
35+
class TeeStream(ObjectProxy):
36+
"""Provides a ``tee``-like interface on top of a file-like object, which collects read bytes
37+
into a local :class:`io.BytesIO`.
38+
39+
:param wrapped: File-like object
40+
:param tee: Stream to copy read bytes into.
41+
:type tee: io.BaseIO
42+
"""
43+
44+
__tee = None # Prime ObjectProxy's attributes to allow setting in init.
45+
46+
def __init__(self, wrapped, tee):
47+
"""Creates the local tee stream."""
48+
super(TeeStream, self).__init__(wrapped)
49+
self.__tee = tee
50+
51+
def read(self, b=None):
52+
"""Reads data from source, copying it into ``tee`` before returning.
53+
54+
:param int b: number of bytes to read
55+
"""
56+
data = self.__wrapped__.read(b)
57+
self.__tee.write(data)
58+
return data

src/aws_encryption_sdk/key_providers/base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def master_keys_for_encryption(self, encryption_context, plaintext_rostream, pla
110110
111111
:param dict encryption_context: Encryption context passed to client
112112
:param plaintext_rostream: Source plaintext read-only stream
113-
:type plaintext_rostream: aws_encryption_sdk.internal.utils.ROStream
113+
:type plaintext_rostream: aws_encryption_sdk.internal.utils.streams.ROStream
114114
:param int plaintext_length: Length of source plaintext (optional)
115115
:returns: Tuple containing Primary Master Key and List of all Master Keys added to
116116
this Provider and any member Providers
@@ -387,7 +387,7 @@ def master_keys_for_encryption(self, encryption_context, plaintext_rostream, pla
387387
388388
:param dict encryption_context: Encryption context passed to client
389389
:param plaintext_rostream: Source plaintext read-only stream
390-
:type plaintext_rostream: aws_encryption_sdk.internal.utils.ROStream
390+
:type plaintext_rostream: aws_encryption_sdk.internal.utils.streams.ROStream
391391
:param int plaintext_length: Length of source plaintext (optional)
392392
:returns: Tuple containing self and a list of self
393393
:rtype: tuple containing :class:`aws_encryption_sdk.key_providers.base.MasterKey`

src/aws_encryption_sdk/materials_managers/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import six
1919

2020
from ..identifiers import Algorithm
21-
from ..internal.utils import ROStream
21+
from ..internal.utils.streams import ROStream
2222
from ..structures import DataKey
2323

2424

@@ -34,7 +34,7 @@ class EncryptionMaterialsRequest(object):
3434
:param dict encryption_context: Encryption context passed to underlying master key provider and master keys
3535
:param int frame_length: Frame length to be used while encrypting stream
3636
:param plaintext_rostream: Source plaintext read-only stream (optional)
37-
:type plaintext_rostream: aws_encryption_sdk.internal.utils.ROStream
37+
:type plaintext_rostream: aws_encryption_sdk.internal.utils.streams.ROStream
3838
:param algorithm: Algorithm passed to underlying master key provider and master keys (optional)
3939
:type algorithm: aws_encryption_sdk.identifiers.Algorithm
4040
:param int plaintext_length: Length of source plaintext (optional)

0 commit comments

Comments
 (0)