Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ jobs:
- name: Install dependencies and run linting
run: |
make install
make format-check
make lint
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ install:

# Run linting checks
lint:
uv run black --check src/ test/
# Enforce ruff checks on src/ but allow test/ to fail
uv run ruff check src/
uv run ruff check test/ || true

# Format code with Black and Ruff
# Check formatting (no changes, just verify)
format-check:
uv run ruff format --check src/ test/

# Format code
format:
uv run black src/ test/
uv run ruff format src/ test/
uv run ruff check --fix src/ test/

# Run all tests with combined coverage
Expand Down
6 changes: 0 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ test = [
"pytest-cov>=7.1.0",
]
dev = [
"black>=26.3.1",
"ruff>=0.15.12",
"boto3-stubs~=1.43.6",
]
Expand All @@ -32,11 +31,6 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/s3_encryption"]

[tool.black]
line-length = 100
target-version = ["py311"]
include = '\.pyi?$'

[tool.ruff]
line-length = 100
target-version = "py311"
Expand Down
2 changes: 1 addition & 1 deletion src/s3_encryption/instruction_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def parse_instruction_file(instruction_data: bytes, key: str) -> dict[str, Any]:
# Validate that it's a dictionary
if not isinstance(metadata, dict):
raise S3EncryptionClientError(
f"Instruction file must contain a JSON object, " f"got {type(metadata).__name__}: {key}"
f"Instruction file must contain a JSON object, got {type(metadata).__name__}: {key}"
)

# Validate that all keys are S3EC metadata keys
Expand Down
6 changes: 2 additions & 4 deletions src/s3_encryption/materials/kms_keyring.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None):
##% context.
if KMS_CONTEXT_DEFAULT_KEY in encryption_context_from_request:
raise S3EncryptionClientError(
f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the "
f"S3 encryption client"
f"{KMS_CONTEXT_DEFAULT_KEY} is a reserved key for the S3 encryption client"
)

##= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context
Expand All @@ -183,8 +182,7 @@ def on_decrypt(self, dec_materials, encrypted_data_keys=None):
if encryption_context_stored_copy != encryption_context_from_request:
# TODO: modeled error
raise S3EncryptionClientError(
"Provided encryption context does not match information "
"retrieved from S3"
"Provided encryption context does not match information retrieved from S3"
)

##= specification/s3-encryption/materials/s3-kms-keyring.md#decryptdatakey
Expand Down
5 changes: 1 addition & 4 deletions src/s3_encryption/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ def decrypt(

# Check if we need to fetch instruction file
if metadata.should_use_instruction_file():

if self.instruction_file_config.disable_get_object:
raise S3EncryptionClientError(
"Exception encountered while fetching Instruction File. "
Expand Down Expand Up @@ -371,9 +370,7 @@ def decrypt(
##% [legacy unauthenticated algorithm suites](#legacy-decryption) is NOT enabled,
##% the S3EC MUST throw an error which details that client was
##% not configured to decrypt objects with ALG_AES_256_CBC_IV16_NO_KDF.
if (
algorithm_suite.is_legacy and not self.enable_legacy_unauthenticated_modes
): # noqa: SIM102
if algorithm_suite.is_legacy and not self.enable_legacy_unauthenticated_modes: # noqa: SIM102
##= specification/s3-encryption/decryption.md#legacy-decryption
##= type=implementation
##% The S3EC MUST NOT decrypt objects encrypted using legacy unauthenticated algorithm suites
Expand Down
21 changes: 10 additions & 11 deletions test/integration/test_i_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ def test_v3_downgrade_wrap_alg_to_kms_rejected_without_legacy(self):
plain_s3 = boto3.client("s3")
head = plain_s3.head_object(Bucket=bucket, Key=key)
original_metadata = head["Metadata"]
assert (
original_metadata.get("x-amz-w") == "12"
), f"Expected x-amz-w='12', got {original_metadata.get('x-amz-w')}"
assert original_metadata.get("x-amz-w") == "12", (
f"Expected x-amz-w='12', got {original_metadata.get('x-amz-w')}"
)

tampered_metadata = original_metadata.copy()
tampered_metadata["x-amz-w"] = "kms"
Expand Down Expand Up @@ -348,9 +348,9 @@ def test_v2_downgrade_wrap_alg_to_kms_mismatched_context(self):
plain_s3 = boto3.client("s3")
head = plain_s3.head_object(Bucket=bucket, Key=key)
original_metadata = head["Metadata"]
assert (
original_metadata.get("x-amz-wrap-alg") == "kms+context"
), f"Expected x-amz-wrap-alg='kms+context', got {original_metadata.get('x-amz-wrap-alg')}"
assert original_metadata.get("x-amz-wrap-alg") == "kms+context", (
f"Expected x-amz-wrap-alg='kms+context', got {original_metadata.get('x-amz-wrap-alg')}"
)

tampered_metadata = original_metadata.copy()
tampered_metadata["x-amz-wrap-alg"] = "kms"
Expand Down Expand Up @@ -476,14 +476,13 @@ def test_wrong_key_and_tampered_ciphertext_produce_same_error(self):

# Both MUST produce the same error message
assert str(exc1.value) == str(exc2.value), (
f"Error messages differ: wrong_key={str(exc1.value)!r}, "
f"tampered={str(exc2.value)!r}"
f"Error messages differ: wrong_key={str(exc1.value)!r}, tampered={str(exc2.value)!r}"
)

# Neither message should contain details about the underlying failure
assert (
"padding" not in str(exc1.value).lower()
), f"Error message leaks padding information: {str(exc1.value)!r}"
assert "padding" not in str(exc1.value).lower(), (
f"Error message leaks padding information: {str(exc1.value)!r}"
)

def test_truncated_ciphertext_produces_same_error(self):
"""Truncated ciphertext MUST produce the same error as padding failure.
Expand Down
22 changes: 11 additions & 11 deletions test/performance/generate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _bar_chart_svg(chart_id, title, groups, sizes, width=700, bar_h=28, gap=6):
lx = label_col_w
for g in groups:
svg.append(f'<rect x="{lx}" y="30" width="12" height="12" fill="{g["color"]}" rx="2"/>')
svg.append(f'<text x="{lx+16}" y="41" font-size="11" fill="#333">{g["label"]}</text>')
svg.append(f'<text x="{lx + 16}" y="41" font-size="11" fill="#333">{g["label"]}</text>')
lx += len(g["label"]) * 7 + 30

y = 58
Expand Down Expand Up @@ -150,7 +150,7 @@ def _histogram_svg(chart_id, title, series_list, width=700, height=220, n_bins=1
lx = margin_l
for s in series_list:
svg.append(f'<rect x="{lx}" y="26" width="10" height="10" fill="{s["color"]}" rx="2"/>')
svg.append(f'<text x="{lx+14}" y="35" font-size="10" fill="#333">{s["label"]}</text>')
svg.append(f'<text x="{lx + 14}" y="35" font-size="10" fill="#333">{s["label"]}</text>')
lx += len(s["label"]) * 6 + 24

# Axes
Expand Down Expand Up @@ -302,14 +302,14 @@ def _build_table(results):
durations_str = ", ".join(_fmt(v) for v in d)
rows += f"""
<tr>
<td>{r['test']}</td>
<td>{r['size_mb']} MB</td>
<td>{r['rounds']}</td>
<td>{r["test"]}</td>
<td>{r["size_mb"]} MB</td>
<td>{r["rounds"]}</td>
<td>{_fmt(med)}</td>
<td>{_fmt(r['mean_s'])}</td>
<td>{_fmt(r["mean_s"])}</td>
<td>{_fmt(p95)}</td>
<td>{_fmt(r['min_s'])}</td>
<td>{_fmt(r['max_s'])}</td>
<td>{_fmt(r["min_s"])}</td>
<td>{_fmt(r["max_s"])}</td>
<td class="durations">{durations_str}</td>
</tr>"""

Expand Down Expand Up @@ -366,9 +366,9 @@ def generate_html(data: dict) -> str:
<h1>S3 Encryption Client &mdash; Performance Report</h1>
<div class="meta">
Generated: {timestamp}<br>
Rounds per test: {config['num_rounds']} &middot;
Object sizes: {', '.join(str(s) + ' MB' for s in sizes)} &middot;
Bucket: {config['bucket']} &middot; Region: {config['region']}
Rounds per test: {config["num_rounds"]} &middot;
Object sizes: {", ".join(str(s) + " MB" for s in sizes)} &middot;
Bucket: {config["bucket"]} &middot; Region: {config["region"]}
</div>

<div class="visuals">
Expand Down
4 changes: 0 additions & 4 deletions test/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ def spy_finalize(data):


class TestDelayedAuthCBCDecryption:

def test_roundtrip(self):
plaintext = b"hello world, this is a CBC test!!"
ciphertext, key, iv = _encrypt_cbc(plaintext)
Expand Down Expand Up @@ -242,7 +241,6 @@ def test_empty_ciphertext(self):


class TestBufferedDecryptingStream:

def test_full_read(self):
plaintext = os.urandom(1024)
ct, key, nonce = _encrypt_gcm(plaintext)
Expand Down Expand Up @@ -379,7 +377,6 @@ def test_idempotent_decrypt(self):


class TestDelayedAuthGCMDecryption:

def test_full_read(self):
plaintext = os.urandom(1024)
ct, key, nonce = _encrypt_gcm(plaintext)
Expand Down Expand Up @@ -511,7 +508,6 @@ def test_large_data(self):


class TestEdgeCasePlaintextLengths:

@pytest.mark.parametrize("length", EDGE_CASE_LENGTHS)
def test_buffered_gcm(self, length):
plaintext = os.urandom(length)
Expand Down
Loading