Skip to content

Commit 7944482

Browse files
committed
Harden crypto fallbacks and expand local/CI test coverage
1 parent f0ec3a2 commit 7944482

4 files changed

Lines changed: 31 additions & 10 deletions

File tree

.github/workflows/tests.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ jobs:
88
strategy:
99
matrix:
1010
os: [ubuntu-latest, macos-latest, windows-latest]
11+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1112
steps:
1213
- uses: actions/checkout@v4
1314
- uses: actions/setup-python@v5
1415
with:
15-
python-version: "3.11"
16+
python-version: ${{ matrix.python-version }}
1617
- run: python -m pip install --upgrade pip
1718
- run: pip install pytest
1819
- run: pip install -e .

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
## [1.0.2] - 2026-04-01
1111

1212
### Changed
13-
- add a `make pytest` target that installs the package in editable mode before running the test suite
13+
- add a self-contained `make test` target that installs `pytest` and the package in editable mode before running the test suite
14+
- mark `all` as a phony Make target to avoid filename collisions
1415

1516
### Fixed
1617
- remove `return` statements from `finally` blocks in encryption helpers to avoid newer Python `SyntaxWarning`s
17-
- harden optional `cryptography` imports so missing crypto dependencies do not break exception handling paths
18+
- harden optional `cryptography` imports with a decorator-based availability guard so missing crypto dependencies raise a clear runtime error instead of `NoneType` failures
1819

1920
---
2021

Makefile

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,9 @@ build: clean
3636
# Combined target to build for both platforms
3737
all: build
3838

39-
# Test target to verify the build
40-
test:
41-
$(ENVSTACK_CMD) -- ls -al
42-
${ENVSTACK_CMD} -- which python
43-
4439
# Run the pytest suite from an editable install, matching CI behavior
45-
pytest:
40+
test:
41+
python -m pip install pytest
4642
python -m pip install -e .
4743
pytest tests -q
4844

@@ -56,4 +52,4 @@ install: build
5652
dist --force --yes
5753

5854
# Phony targets
59-
.PHONY: build dryrun install clean test pytest
55+
.PHONY: all build dryrun install clean test pytest

lib/envstack/encrypt.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
import os
3939
import secrets
4040
from base64 import b64decode, b64encode
41+
from functools import wraps
4142

4243
from envstack.logger import log
4344

4445
# cryptography and _rust dependency may not be available everywhere
4546
# ImportError: DLL load failed while importing _rust: Module not found.
47+
CRYPTOGRAPHY_AVAILABLE = False
4648
Fernet = None
4749
InvalidToken = type("InvalidToken", (Exception,), {})
4850
InvalidTag = type("InvalidTag", (Exception,), {})
@@ -55,10 +57,23 @@
5557
from cryptography.exceptions import InvalidTag
5658
from cryptography.hazmat.primitives import padding
5759
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
60+
CRYPTOGRAPHY_AVAILABLE = True
5861
except ImportError as err:
5962
log.debug("cryptography module not available: %s", err)
6063

6164

65+
def require_cryptography(func):
66+
"""Guard crypto-backed functions when cryptography is unavailable."""
67+
68+
@wraps(func)
69+
def wrapper(*args, **kwargs):
70+
if not CRYPTOGRAPHY_AVAILABLE:
71+
raise RuntimeError("cryptography support is not available")
72+
return func(*args, **kwargs)
73+
74+
return wrapper
75+
76+
6277
class Base64Encryptor(object):
6378
"""Encrypt and decrypt secrets using base64 encoding."""
6479

@@ -90,6 +105,7 @@ def __init__(self, key: str = None, env: dict = os.environ):
90105
self.key = self.get_key(env)
91106

92107
@classmethod
108+
@require_cryptography
93109
def generate_key(csl):
94110
"""Generate a new 256-bit encryption key."""
95111
if Fernet:
@@ -110,6 +126,7 @@ def get_key(self, env: dict = os.environ):
110126
return Fernet(key)
111127
return key
112128

129+
@require_cryptography
113130
def encrypt(self, data: str):
114131
"""Encrypt a secret using Fernet.
115132
@@ -129,6 +146,7 @@ def encrypt(self, data: str):
129146
log.error("unhandled error: %s", e)
130147
return results
131148

149+
@require_cryptography
132150
def decrypt(self, data: str):
133151
"""Decrypt a secret using Fernet.
134152
@@ -158,6 +176,7 @@ def __init__(self, key: str = None, env: dict = os.environ):
158176
self.key = self.get_key(env)
159177

160178
@classmethod
179+
@require_cryptography
161180
def generate_key(csl):
162181
"""Generate a new 256-bit encryption key."""
163182
key = secrets.token_bytes(32)
@@ -176,6 +195,7 @@ def get_key(self, env: dict = os.environ):
176195
raise ValueError("invalid base64 encoding: %s" % e)
177196
return key
178197

198+
@require_cryptography
179199
def encrypt_data(self, secret: str):
180200
"""Encrypt a secret using AES-GCM.
181201
@@ -194,6 +214,7 @@ def encrypt_data(self, secret: str):
194214
"tag": b64encode(encryptor.tag).decode(),
195215
}
196216

217+
@require_cryptography
197218
def decrypt_data(self, encrypted_data: dict):
198219
"""Decrypt a secret using AES-GCM.
199220
@@ -252,6 +273,7 @@ def decrypt(self, data: str):
252273
return data
253274

254275

276+
@require_cryptography
255277
def pad_data(data: str):
256278
"""Pad data to be block-aligned for AES encryption.
257279
@@ -262,6 +284,7 @@ def pad_data(data: str):
262284
return padder.update(str(data).encode()) + padder.finalize()
263285

264286

287+
@require_cryptography
265288
def unpad_data(data: dict):
266289
"""Unpad data after decryption.
267290

0 commit comments

Comments
 (0)