-
Notifications
You must be signed in to change notification settings - Fork 0
feat(decryption): streaming decryption with cipher-agnostic BufferedDecryptingStream and DelayedAuthDecryptingStream #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
aa38719
feat(decryption): add buffered streaming decryption for AES-GCM
texastony 24eae31
feat(config): add enable_delayed_authentication option with duvet cit…
texastony 44aac6c
feat(stream): add DelayedAuthDecryptingStream for unauthenticated str…
texastony f5eae60
feat(decrypt): wire delayed authentication through pipeline and event…
texastony f348251
test(integration): add parametrized roundtrip tests for buffered and …
texastony 622b5a1
test(duvet): add streaming decryption tests for delayed authenticatio…
texastony 5869c25
test(integration): parametrize instruction file tests with buffered a…
texastony 377004b
test(integration): add large file and 61 GiB placeholder tests for de…
texastony 182d228
fix: update decrypt return type docstring and verify full delayed-aut…
texastony 4aaddc5
chore: register large pytest mark in pyproject.toml
texastony ef17832
test(integration): remove xlarge placeholder tests and large pytest mark
texastony edfcfb8
fix(test): use tuple for parametrize args and remove duplicate test_n…
texastony cf00af7
refactor: streaming decryptors accept cipher object and tag_length
texastony a2dd100
merge staging: integrate streaming decryptors with key commitment and…
texastony 8c3fbd6
refactor: use AlgorithmSuite properties for tag length and block size
texastony a6fb136
fix: address PR #150 review comments from kessplas
texastony 8d4dbdc
refactor: split DelayedAuthDecryptingStream into CBC and GCM classes
texastony 4991e1b
refactor: rename BufferedDecryptingStream to BufferedDecryptingGCMStr…
texastony 07dbee4
test: add unit and integration tests for CBC and GCM decrypting streams
texastony dad00c2
fix(stream): enforce minimum read size on DelayedAuthGCMDecryptingStream
texastony 4a35e88
test(stream): strengthen CBC test assertions and include empty plaint…
texastony c8bd8a2
fix(pipelines): fix docstring param order and remove misplaced encryp…
texastony 6fc4910
docs: add Google-style docstring to S3EncryptionClientConfig
texastony 29bfb1c
docs: detail that CBC is always streamed
texastony 0d58080
chore: address linting concern
texastony ad22880
refactor(stream): use ContentLength to eliminate rolling GCM tag buffer
texastony 5d0ad56
fix(stream): return self from __enter__ and validate content_length
texastony ce010b3
Merge remote-tracking branch 'origin/staging' into tonyknap/feat-buff…
texastony 7f8252d
refactor: extract Decryptor ABC, make streams cipher-agnostic
texastony 395505d
refactor: replace BufferedDecryptingStream with one_shot_decrypt, add…
texastony e19b1bf
docs: add inline comments to DecryptingStream.read explaining control…
texastony 3e40173
chore: document how stream and decryptor are a little leaky
texastony e7ecd7b
fix: address self-code review findings for streaming decryption
texastony File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """One Shot decryption into a buffer.""" | ||
|
|
||
| from io import BytesIO | ||
|
|
||
| from botocore.response import StreamingBody | ||
|
|
||
| from s3_encryption.decryptor import Decryptor | ||
|
|
||
|
|
||
| def one_shot_decrypt(streaming_body: StreamingBody, decryptor: Decryptor): | ||
| """Decrypt a streaming object. | ||
|
|
||
| Args: | ||
| streaming_body (object): A streaming object. | ||
| decryptor (Decryptor): Decryptor object. | ||
| """ | ||
| plaintext = decryptor.finalize(streaming_body.read()) | ||
| return StreamingBody(BytesIO(plaintext), len(plaintext)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """Decryptor abstractions for S3 Encryption Client.""" | ||
|
|
||
| from abc import ABC, abstractmethod | ||
|
|
||
| from attrs import define, field | ||
| from cryptography.exceptions import InvalidTag | ||
|
|
||
| from .exceptions import S3EncryptionClientError, S3EncryptionClientSecurityError | ||
|
|
||
|
|
||
| class Decryptor(ABC): | ||
| """Abstract base class for content decryption. | ||
|
|
||
| Implementations own all cipher and padding state, presenting a uniform | ||
| streaming interface to the decrypting stream classes. | ||
| """ | ||
|
|
||
| @property | ||
| @abstractmethod | ||
| def content_length(self) -> int: | ||
| """Total byte length of the encrypted content (ciphertext + any trailing tag).""" | ||
|
|
||
| @property | ||
| @abstractmethod | ||
| def amount_read(self) -> int: | ||
| """Number of ciphertext bytes consumed so far.""" | ||
|
|
||
| @abstractmethod | ||
| def update(self, data: bytes) -> bytes: | ||
| """Process a chunk of ciphertext, returning any available plaintext.""" | ||
|
|
||
| @abstractmethod | ||
| def finalize(self, data: bytes) -> bytes: | ||
| """Process the final chunk of ciphertext and finalize decryption.""" | ||
|
|
||
|
|
||
| @define | ||
| class AesCbcDecryptor(Decryptor): | ||
| """AES-CBC decryptor that owns both the cipher and PKCS7 unpadder. | ||
|
|
||
| Args: | ||
| decryptor: A cryptography CBC cipher decryptor context. | ||
| unpadder: A cryptography PKCS7 unpadding context. | ||
| content_length: Total byte length of the CBC ciphertext. | ||
| """ | ||
|
|
||
| _decryptor: object = field() | ||
| _unpadder: object = field() | ||
| _content_length: int = field() | ||
| _amount_read: int = field(init=False, default=0) | ||
|
|
||
| @property | ||
| def content_length(self) -> int: # noqa: D102 | ||
| return self._content_length | ||
|
|
||
| @property | ||
| def amount_read(self) -> int: # noqa: D102 | ||
| return self._amount_read | ||
|
|
||
| def update(self, data: bytes) -> bytes: | ||
| """Decrypt a chunk and unpad incrementally.""" | ||
| self._amount_read += len(data) | ||
| plaintext = self._decryptor.update(data) | ||
| return self._unpadder.update(plaintext) | ||
|
|
||
| def finalize(self, data: bytes) -> bytes: | ||
| """Finalize CBC decryption and flush the unpadder.""" | ||
| try: | ||
| self._amount_read += len(data) | ||
| plaintext = self._decryptor.update(data) if data else b"" | ||
| plaintext += self._decryptor.finalize() | ||
| return self._unpadder.update(plaintext) + self._unpadder.finalize() | ||
| except Exception as e: | ||
| raise S3EncryptionClientSecurityError(f"Failed to decrypt CBC content: {e}") from e | ||
|
|
||
|
|
||
| @define | ||
| class AesGcmDecryptor(Decryptor): | ||
| """AES-GCM decryptor that handles trailing auth tag verification. | ||
|
|
||
| Args: | ||
| decryptor: A cryptography GCM cipher decryptor context. | ||
| tag_length: Length of the GCM authentication tag in bytes. | ||
| content_length: Total byte length of the encrypted content (ciphertext + tag). | ||
| """ | ||
|
|
||
| _decryptor: object = field() | ||
| _tag_length: int = field() | ||
| _content_length: int = field() | ||
| _amount_read: int = field(init=False, default=0) | ||
| _tail: bytes = field(init=False, default=b"") | ||
|
|
||
| @property | ||
| def content_length(self) -> int: # noqa: D102 | ||
| return self._content_length | ||
|
|
||
| @property | ||
| def amount_read(self) -> int: # noqa: D102 | ||
| return self._amount_read | ||
|
|
||
| @property | ||
| def tag_length(self) -> int: | ||
| """Length of the GCM authentication tag in bytes.""" | ||
| return self._tag_length | ||
|
|
||
| def update(self, data: bytes) -> bytes: | ||
| """Decrypt a chunk, holding back the last tag_length bytes. | ||
|
|
||
| A rolling _tail buffer always retains the last tag_length bytes | ||
| so the GCM tag is never passed to the cipher's update(). | ||
| """ | ||
| self._amount_read += len(data) | ||
| buf = self._tail + data | ||
| if len(buf) <= self._tag_length: | ||
| self._tail = buf | ||
| return b"" | ||
| self._tail = buf[-self._tag_length :] | ||
| return self._decryptor.update(buf[: -self._tag_length]) | ||
|
|
||
| def finalize(self, data: bytes) -> bytes: | ||
| """Finalize decryption using the buffered tag.""" | ||
| try: | ||
| self._amount_read += len(data) | ||
| buf = self._tail + data | ||
| if len(buf) < self._tag_length: | ||
| raise S3EncryptionClientError( | ||
| f"Incomplete GCM data: expected at least {self._tag_length} " | ||
| f"tag bytes, got {len(buf)} total remaining bytes." | ||
| ) | ||
| tag = buf[-self._tag_length :] | ||
| ciphertext = buf[: -self._tag_length] | ||
| plaintext = self._decryptor.update(ciphertext) if ciphertext else b"" | ||
| return plaintext + self._decryptor.finalize_with_tag(tag) | ||
| except S3EncryptionClientError: | ||
| raise | ||
| except InvalidTag as e: | ||
| raise S3EncryptionClientSecurityError(f"Failed to decrypt Object: {e}") from e | ||
| except Exception as e: | ||
| raise S3EncryptionClientError(f"Failed to decrypt Object: {e}") from e |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.