Skip to content

Commit 952a22a

Browse files
CyberAlp0claude
andcommitted
Add pytest suite and GitHub Actions CI
- tests/test_roundtrip.py: DES/RC2 CBC+ECB round trips and helper checks - conftest.py so tests import the module from the repo root - requirements-dev.txt (pytest) - .github/workflows/ci.yml: run tests on push/PR across Python 3.9-3.13 - README: document RC2 128-bit key limit and add a Development section Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0fc227b commit 952a22a

5 files changed

Lines changed: 153 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install -r requirements-dev.txt
29+
30+
- name: Smoke test (CLI runs)
31+
run: python hardcoded_key_decryptor.py --list
32+
33+
- name: Run tests
34+
run: pytest -v

‎README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ No code changes needed.
193193
- **DES via TripleDES:** modern `cryptography` doesn't expose single DES
194194
directly, so the engine uses `TripleDES` with an 8-byte key. 3DES with one
195195
repeated key is mathematically identical to DES.
196+
- **RC2 key size:** recent `cryptography` releases accept only **128-bit
197+
(16-byte)** RC2 keys, even though RC2 historically allowed shorter keys. If a
198+
target uses a shorter RC2 key, use an older `cryptography` release or another
199+
RC2 implementation.
196200
- **Key-derivation branches:** some applications derive a key from a passphrase
197201
(e.g. .NET's `PasswordDeriveBytes` / PBKDF1) instead of using a fixed byte
198202
array. This tool handles fixed keys and raw key/IV input, not on-the-fly
@@ -203,6 +207,20 @@ No code changes needed.
203207

204208
---
205209

210+
## Development
211+
212+
Run the test suite (round-trip encrypt/decrypt plus helper checks):
213+
214+
```bash
215+
pip install -r requirements-dev.txt
216+
pytest -v
217+
```
218+
219+
CI runs the same suite on every push and pull request across Python 3.9–3.13
220+
(see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)).
221+
222+
---
223+
206224
## License
207225

208226
Released under the [MIT License](LICENSE). Copyright (c) 2026 CyberAlp0.

‎conftest.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Present so pytest adds the repo root to sys.path, making
2+
# `import hardcoded_key_decryptor` work from tests/.

‎requirements-dev.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-r requirements.txt
2+
pytest>=8.0

‎tests/test_roundtrip.py‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Round-trip and helper tests for hardcoded_key_decryptor.
2+
3+
Each cipher/mode should satisfy: strip_pkcs7(decrypt(encrypt(x))) == x.
4+
"""
5+
import base64
6+
7+
import pytest
8+
9+
import hardcoded_key_decryptor as hkd
10+
11+
# Example key material (not real recovered keys — just fixed test vectors).
12+
DES_KEY = bytes.fromhex("B43F84D110B4E991")
13+
DES_IV = bytes.fromhex("01D8AEE649AD9227")
14+
# RC2 block size is 8 bytes (so the CBC IV is 8 bytes). Modern `cryptography`
15+
# only accepts 128-bit (16-byte) RC2 keys, so use one here.
16+
RC2_KEY = bytes.fromhex("0102030405060708090A0B0C0D0E0F10")
17+
RC2_IV = bytes.fromhex("1112131415161718")
18+
19+
TEXTS = [
20+
"", # empty -> a full block of padding
21+
"a",
22+
"8bytes!!", # exactly one block
23+
"RiverDragon#Storm25",
24+
"unicode: café ☕ 日本語",
25+
"x" * 100, # spans many blocks
26+
]
27+
28+
29+
@pytest.mark.parametrize("text", TEXTS)
30+
def test_des_cbc_roundtrip(text):
31+
ct = hkd.encrypt(text.encode("utf-8"), DES_KEY, DES_IV, "des", "cbc")
32+
pt = hkd.strip_pkcs7(hkd.decrypt(ct, DES_KEY, DES_IV, "des", "cbc"))
33+
assert pt.decode("utf-8") == text
34+
35+
36+
@pytest.mark.parametrize("text", TEXTS)
37+
def test_rc2_cbc_roundtrip(text):
38+
ct = hkd.encrypt(text.encode("utf-8"), RC2_KEY, RC2_IV, "rc2", "cbc")
39+
pt = hkd.strip_pkcs7(hkd.decrypt(ct, RC2_KEY, RC2_IV, "rc2", "cbc"))
40+
assert pt.decode("utf-8") == text
41+
42+
43+
@pytest.mark.parametrize("text", TEXTS)
44+
def test_des_ecb_roundtrip(text):
45+
# ECB ignores the IV, but the engine still accepts one.
46+
ct = hkd.encrypt(text.encode("utf-8"), DES_KEY, DES_IV, "des", "ecb")
47+
pt = hkd.strip_pkcs7(hkd.decrypt(ct, DES_KEY, DES_IV, "des", "ecb"))
48+
assert pt.decode("utf-8") == text
49+
50+
51+
def test_ciphertext_is_base64_recoverable():
52+
"""The base64 the tool prints must decode back through fix_b64 + decrypt."""
53+
text = "RiverDragon#Storm25"
54+
ct = hkd.encrypt(text.encode("utf-8"), DES_KEY, DES_IV, "des", "cbc")
55+
b64 = base64.b64encode(ct).decode()
56+
pt = hkd.strip_pkcs7(
57+
hkd.decrypt(hkd.fix_b64(b64), DES_KEY, DES_IV, "des", "cbc")
58+
)
59+
assert pt.decode("utf-8") == text
60+
61+
62+
def test_pkcs7_add_then_strip_is_identity():
63+
for n in range(0, 33):
64+
data = b"A" * n
65+
padded = hkd.add_pkcs7(data)
66+
assert len(padded) % hkd.BLOCK_SIZE == 0
67+
assert 1 <= (padded[-1]) <= hkd.BLOCK_SIZE
68+
assert hkd.strip_pkcs7(padded) == data
69+
70+
71+
def test_parse_hex_accepts_common_formats():
72+
expected = bytes([0xB4, 0x3F, 0x84, 0xD1])
73+
assert hkd.parse_hex("B4 3F 84 D1") == expected
74+
assert hkd.parse_hex("B4,3F,84,D1") == expected
75+
assert hkd.parse_hex("0xB4 0x3F 0x84 0xD1") == expected
76+
assert hkd.parse_hex("B43F84D1") == expected
77+
78+
79+
def test_fix_b64_restores_missing_padding():
80+
raw = b"\x00\x01\x02\x03\x04"
81+
stripped = base64.b64encode(raw).decode().rstrip("=")
82+
assert hkd.fix_b64(stripped) == raw
83+
84+
85+
def test_wrong_key_does_not_recover_plaintext():
86+
text = "RiverDragon#Storm25"
87+
ct = hkd.encrypt(text.encode("utf-8"), DES_KEY, DES_IV, "des", "cbc")
88+
wrong_key = bytes.fromhex("0000000000000000")
89+
pt = hkd.strip_pkcs7(hkd.decrypt(ct, wrong_key, DES_IV, "des", "cbc"))
90+
assert pt != text.encode("utf-8")
91+
92+
93+
def test_unknown_method_and_mode_raise():
94+
with pytest.raises(ValueError):
95+
hkd.build_algorithm("aes", DES_KEY)
96+
with pytest.raises(ValueError):
97+
hkd.build_mode("ctr", DES_IV)

0 commit comments

Comments
 (0)