Skip to content

Commit be80aa5

Browse files
feat: Improvements to the message decryption process (#211)
See GHSA-89v2-g37m-g3ff
1 parent ce1ecb0 commit be80aa5

15 files changed

+754
-58
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
Changelog
33
*********
44

5+
1.9.0 -- 2021-05-27
6+
===================
7+
8+
Features
9+
--------
10+
* Improvements to the message decryption process
11+
12+
See https://github.com/aws/aws-encryption-sdk-cli/security/advisories/GHSA-89v2-g37m-g3ff.
13+
514
1.8.0 -- 2020-10-27
615
===================
716

README.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Required Prerequisites
4343
======================
4444

4545
* Python 2.7+ or 3.4+
46-
* aws-encryption-sdk >= 1.7.0
46+
* aws-encryption-sdk >= 1.9.0
4747

4848
Installation
4949
============
@@ -168,7 +168,7 @@ Metadata Contents
168168
`````````````````
169169
The metadata JSON contains the following fields:
170170

171-
* ``"mode"`` : ``"encrypt"``/``"decrypt"``
171+
* ``"mode"`` : ``"encrypt"``/``"decrypt"``/``"decrypt-unsigned"``
172172
* ``"input"`` : Full path to input file (or ``"<stdin>"`` if stdin)
173173
* ``"output"`` : Full path to output file (or ``"<stdout>"`` if stdout)
174174
* ``"header"`` : JSON representation of `message header data`_
@@ -342,7 +342,6 @@ Allowed parameters:
342342
* **max_messages_encrypted** : Determines how long each entry can remain in the cache, beginning when it was added.
343343
* **max_bytes_encrypted** : Specifies the maximum number of bytes that a cached data key can encrypt.
344344

345-
346345
Logging and Verbosity
347346
---------------------
348347
The ``-v`` argument allows you to tune the verbosity of the built-in logging to your desired level.

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
base64io>=1.0.1
2-
aws-encryption-sdk~=1.7
2+
aws-encryption-sdk~=1.9
33
setuptools
44
attrs>=17.1.0

src/aws_encryption_sdk_cli/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ def process_cli_request(stream_args, parsed_args):
173173
_LOGGER.warning("Invalid commitment policy: %s", parsed_args.commitment_policy)
174174
commitment_policy = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT
175175

176+
# We don't want commitment policy to percolate through to be passed directly to the ESDK, since it is set
177+
# via the ESDK Client object instead
178+
stream_args.pop("commitment_policy", None)
179+
176180
handler = IOHandler(
177181
metadata_writer=parsed_args.metadata_output,
178182
interactive=parsed_args.interactive,
@@ -182,6 +186,8 @@ def process_cli_request(stream_args, parsed_args):
182186
required_encryption_context=parsed_args.encryption_context,
183187
required_encryption_context_keys=parsed_args.required_encryption_context_keys,
184188
commitment_policy=commitment_policy,
189+
buffer_output=parsed_args.buffer,
190+
max_encrypted_data_keys=parsed_args.max_encrypted_data_keys,
185191
)
186192

187193
if parsed_args.input == "-":
@@ -277,7 +283,6 @@ def cli(raw_args=None):
277283
)
278284

279285
stream_args = stream_kwargs_from_args(args, crypto_materials_manager)
280-
281286
process_cli_request(stream_args, args)
282287

283288
return None

src/aws_encryption_sdk_cli/internal/arg_parsing.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import aws_encryption_sdk
2424
import six
2525

26-
from aws_encryption_sdk_cli.exceptions import ParameterParseError
26+
from aws_encryption_sdk_cli.exceptions import BadUserArgumentError, ParameterParseError
2727
from aws_encryption_sdk_cli.internal.identifiers import (
2828
ALGORITHM_NAMES,
2929
DEFAULT_MASTER_KEY_PROVIDER,
@@ -57,6 +57,21 @@
5757
_LOGGER = logging.getLogger(LOGGER_NAME)
5858

5959

60+
def _is_decrypt_mode(mode):
61+
# type: (str) -> bool
62+
"""
63+
Determines whether the provided mode does decryption
64+
65+
:param str filepath: Full file path to file in question
66+
:rtype: bool
67+
"""
68+
if mode in ("decrypt", "decrypt-unsigned"):
69+
return True
70+
if mode == "encrypt":
71+
return False
72+
raise BadUserArgumentError("Mode {mode} has not been implemented".format(mode=mode))
73+
74+
6075
class CommentIgnoringArgumentParser(argparse.ArgumentParser):
6176
"""``ArgumentParser`` that ignores lines in ``fromfile_prefix_chars`` files which start with ``#``."""
6277

@@ -202,6 +217,14 @@ def _build_parser():
202217
"-d", "--decrypt", dest="action", action="store_const", const="decrypt", help="Decrypt data"
203218
)
204219
parser.add_dummy_redirect_argument("--decrypt")
220+
operating_action.add_argument(
221+
"--decrypt-unsigned",
222+
dest="action",
223+
action="store_const",
224+
const="decrypt-unsigned",
225+
help="Decrypt data and enforce messages are unsigned during decryption.",
226+
)
227+
parser.add_dummy_redirect_argument("--decrypt-unsigned")
205228

206229
# For each argument added to this group, a dummy redirect argument must
207230
# be added to the parent parser for each long form option string.
@@ -284,6 +307,10 @@ def _build_parser():
284307
),
285308
)
286309

310+
parser.add_argument(
311+
"-b", "--buffer", action="store_true", help="Buffer result in memory before releasing to output"
312+
)
313+
287314
parser.add_argument(
288315
"-i",
289316
"--input",
@@ -341,6 +368,13 @@ def _build_parser():
341368
),
342369
)
343370

371+
parser.add_argument(
372+
"--max-encrypted-data-keys",
373+
type=int,
374+
action=UniqueStoreAction,
375+
help="Maximum number of encrypted data keys to wrap (during encryption) or to unwrap (during decryption)",
376+
)
377+
344378
parser.add_argument(
345379
"--suffix",
346380
nargs="?",
@@ -496,7 +530,7 @@ def _process_master_key_provider_configs(
496530
:raises ParameterParseError: if no key values are provided
497531
"""
498532
if raw_keys is None:
499-
if action == "decrypt":
533+
if _is_decrypt_mode(action):
500534
# We allow not defining any master key provider configuration if decrypting with aws-kms.
501535
_LOGGER.debug(
502536
"No master key provider config provided on decrypt request. Using aws-kms with no master keys."
@@ -529,7 +563,9 @@ def _process_master_key_provider_configs(
529563
)
530564
parsed_args["provider"] = provider[0] # type: ignore
531565

532-
aws_kms_on_decrypt = parsed_args["provider"] in ("aws-kms", DEFAULT_MASTER_KEY_PROVIDER) and action == "decrypt"
566+
aws_kms_on_decrypt = parsed_args["provider"] in ("aws-kms", DEFAULT_MASTER_KEY_PROVIDER) and _is_decrypt_mode(
567+
action
568+
)
533569

534570
if aws_kms_on_decrypt:
535571
if "key" in parsed_args:
@@ -559,7 +595,7 @@ def _process_wrapping_key_provider_configs( # noqa: C901
559595
:raises ParameterParseError: if no key values are provided
560596
"""
561597
if raw_keys is None:
562-
if action == "decrypt":
598+
if _is_decrypt_mode(action):
563599
# We allow not defining any wrapping key provider configuration if decrypting with aws-kms.
564600
_LOGGER.debug(
565601
"No wrapping key provider config provided on decrypt request. Using aws-kms with no wrapping keys."
@@ -592,7 +628,7 @@ def _process_wrapping_key_provider_configs( # noqa: C901
592628
_process_discovery_args(parsed_args)
593629

594630
discovery = parsed_args["discovery"]
595-
if provider_is_kms and action == "decrypt":
631+
if provider_is_kms and _is_decrypt_mode(action):
596632
if "key" in parsed_args and discovery:
597633
# Decrypt MUST fail without attempting any decryption if discovery mode is enabled
598634
# and at least one key=<Key ARN> parameter value is provided
@@ -713,15 +749,15 @@ def parse_args(raw_args=None): # noqa
713749
raise ParameterParseError("You cannot specify both the --master-keys and --wrapping-keys parameters")
714750
if parsed_args.wrapping_keys:
715751
if not parsed_args.commitment_policy:
716-
raise ParameterParseError('Commitment policy is required when specifying the --wrapping-keys parameter')
752+
raise ParameterParseError("Commitment policy is required when specifying the --wrapping-keys parameter")
717753

718754
parsed_args.wrapping_keys = _process_wrapping_key_provider_configs(
719755
parsed_args.wrapping_keys, parsed_args.action
720756
)
721757
else:
722758
if parsed_args.commitment_policy:
723759
raise ParameterParseError(
724-
'Commitment policy is only supported when using the --wrapping-keys parameter'
760+
"Commitment policy is only supported when using the --wrapping-keys parameter"
725761
)
726762

727763
_LOGGER.warning(

src/aws_encryption_sdk_cli/internal/identifiers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@
3131
"DEFAULT_MASTER_KEY_PROVIDER",
3232
"OperationResult",
3333
)
34-
__version__ = "1.8.0" # type: str
34+
__version__ = "1.9.0" # type: str
3535

3636
#: Suffix added to output files if specific output filename is not specified.
37-
OUTPUT_SUFFIX = {"encrypt": ".encrypted", "decrypt": ".decrypted"} # type: Dict[str, str]
37+
OUTPUT_SUFFIX = {
38+
"encrypt": ".encrypted",
39+
"decrypt": ".decrypted",
40+
"decrypt-unsigned": ".decrypted",
41+
} # type: Dict[str, str]
3842

3943
ALGORITHM_NAMES = {
4044
alg for alg in dir(aws_encryption_sdk.Algorithm) if not alg.startswith("_")

src/aws_encryption_sdk_cli/internal/io_handling.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from aws_encryption_sdk.materials_managers import CommitmentPolicy
2525
from base64io import Base64IO
2626

27+
from aws_encryption_sdk_cli.internal.arg_parsing import _is_decrypt_mode
2728
from aws_encryption_sdk_cli.internal.identifiers import OUTPUT_SUFFIX, OperationResult
2829
from aws_encryption_sdk_cli.internal.logging_utils import LOGGER_NAME
2930
from aws_encryption_sdk_cli.internal.metadata import MetadataWriter, json_ready_header, json_ready_header_auth
@@ -153,6 +154,7 @@ class IOHandler(object):
153154
:param bool no_overwrite: Should never overwrite existing files
154155
:param bool decode_input: Should input be base64 decoded before operation
155156
:param bool encode_output: Should output be base64 encoded after operation
157+
:param bool buffer_output: Should buffer entire output before releasing to destination
156158
:param dict required_encryption_context: Encryption context key-value pairs to require
157159
:param list required_encryption_context_keys: Encryption context keys to require
158160
"""
@@ -162,13 +164,14 @@ class IOHandler(object):
162164
no_overwrite = attr.ib(validator=attr.validators.instance_of(bool))
163165
decode_input = attr.ib(validator=attr.validators.instance_of(bool))
164166
encode_output = attr.ib(validator=attr.validators.instance_of(bool))
167+
buffer_output = attr.ib(validator=attr.validators.instance_of(bool))
165168
required_encryption_context = attr.ib(validator=attr.validators.instance_of(dict))
166169
required_encryption_context_keys = attr.ib(
167170
validator=attr.validators.instance_of(list)
168171
) # noqa pylint: disable=invalid-name
169172
client = attr.ib(validator=attr.validators.instance_of(aws_encryption_sdk.EncryptionSDKClient))
170173

171-
def __init__(
174+
def __init__( # noqa pylint: disable=too-many-arguments
172175
self,
173176
metadata_writer, # type: MetadataWriter
174177
interactive, # type: bool
@@ -178,6 +181,8 @@ def __init__(
178181
required_encryption_context, # type: Dict[str, str]
179182
required_encryption_context_keys, # type: List[str]
180183
commitment_policy, # type: Union[CommitmentPolicy, None]
184+
buffer_output,
185+
max_encrypted_data_keys, # type: Union[None, int]
181186
):
182187
# type: (...) -> None
183188
"""Workaround pending resolution of attrs/mypy interaction.
@@ -193,7 +198,11 @@ def __init__(
193198
self.required_encryption_context_keys = required_encryption_context_keys # pylint: disable=invalid-name
194199
if not commitment_policy:
195200
commitment_policy = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT
196-
self.client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=commitment_policy)
201+
self.buffer_output = buffer_output
202+
self.client = aws_encryption_sdk.EncryptionSDKClient(
203+
commitment_policy=commitment_policy,
204+
max_encrypted_data_keys=max_encrypted_data_keys,
205+
)
197206
attr.validate(self)
198207

199208
def _single_io_write(self, stream_args, source, destination_writer):
@@ -226,7 +235,7 @@ def _single_io_write(self, stream_args, source, destination_writer):
226235
else:
227236
metadata_kwargs["header_auth"] = json_ready_header_auth(header_auth)
228237

229-
if stream_args["mode"] == "decrypt":
238+
if _is_decrypt_mode(str(stream_args["mode"])):
230239
discovered_ec = handler.header.encryption_context
231240
missing_keys = set(self.required_encryption_context_keys).difference(set(discovered_ec.keys()))
232241
missing_pairs = set(self.required_encryption_context.items()).difference(set(discovered_ec.items()))
@@ -246,9 +255,12 @@ def _single_io_write(self, stream_args, source, destination_writer):
246255
return OperationResult.FAILED_VALIDATION
247256

248257
metadata.write_metadata(**metadata_kwargs)
249-
for chunk in handler:
250-
_destination.write(chunk)
251-
_destination.flush()
258+
if self.buffer_output:
259+
_destination.write(handler.read())
260+
else:
261+
for chunk in handler:
262+
_destination.write(chunk)
263+
_destination.flush()
252264
return OperationResult.SUCCESS
253265

254266
def process_single_operation(self, stream_args, source, destination):

test/integration/integration_test_utils.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ def cmk_arn():
5959

6060
def encrypt_args_template(metadata=False, caching=False, encode=False, decode=False):
6161
template = "-e -i {source} -o {target} --encryption-context a=b c=d -m key=" + cmk_arn_value()
62+
return _encrypt_args_modify_template(template, metadata, caching, encode, decode)
63+
64+
65+
def encrypt_args_template_wrapping(metadata=False, caching=False, encode=False, decode=False, append_commitment=False):
66+
template_wrapping = "-e -i {source} -o {target} --encryption-context a=b c=d -w key=" + cmk_arn_value()
67+
template_wrapping = _encrypt_args_modify_template(template_wrapping, metadata, caching, encode, decode)
68+
if append_commitment:
69+
template_wrapping += (
70+
" --commitment-policy forbid-encrypt-allow-decrypt" # required in 1.8 when using --wrapping-keys
71+
)
72+
return template_wrapping
73+
74+
75+
def _encrypt_args_modify_template(template, metadata=False, caching=False, encode=False, decode=False):
6276
if metadata:
6377
template += " {metadata}"
6478
else:
@@ -72,7 +86,7 @@ def encrypt_args_template(metadata=False, caching=False, encode=False, decode=Fa
7286
return template
7387

7488

75-
def decrypt_args_template(metadata=False, encode=False, decode=False):
89+
def decrypt_args_template(metadata=False, encode=False, decode=False, buffer=False):
7690
template = "-d -i {source} -o {target}"
7791
if metadata:
7892
template += " {metadata}"
@@ -82,6 +96,17 @@ def decrypt_args_template(metadata=False, encode=False, decode=False):
8296
template += " --encode"
8397
if decode:
8498
template += " --decode"
99+
if buffer:
100+
template += " --buffer"
101+
return template
102+
103+
104+
def decrypt_unsigned_args_template(metadata=False):
105+
template = "--decrypt-unsigned -i {source} -o {target}"
106+
if metadata:
107+
template += " {metadata}"
108+
else:
109+
template += " -S"
85110
return template
86111

87112

0 commit comments

Comments
 (0)