Add a pytest suite covering the pure logic of the DeDRM and Obok plugins.
Because the plugins normally run inside calibre and use intra-package
imports, tests/dedrm_test_utils.py sets up an import shim (plugin dirs on
sys.path, a minimal calibre stub, and a synthetic `dedrm` package) so the
modules can be imported and exercised standalone.
Coverage:
- alfcrypto: PC1 and Topaz cipher round-trips, PBKDF2 vs hashlib.
- kgenpids / kindlepid: PID encoding, bit-field extraction, device-PID
and serial-PID known vectors, CRC32.
- mobidedrm: PC1 round-trip, trailing-data sizing, bad-key handling.
- ineptpdf / ineptepub: nunpack and PKCS7 unpad.
- topazextract: encoded number/string parsing, plus a regression test
that a malicious header tag ("../../evil") cannot escape the output
directory (covers the path-traversal fix).
- obok: unpad, hash-key table, SafeUnbuffered str/bytes handling.
- utilities / argv_utils: uStrCmp normalisation, unicode_argv.
- erdr2pml: deXOR/sanitiseFileName, skipped on Python 3.13+ where the
module's `cgi` import is unavailable.
Run with `poetry install` then `poetry run pytest`. 36 pass, 2 skip on
Python 3.13+.
Note: this surfaced two dead-code modules that are broken on Python 3
(aescbc's pure-Python AES and kgenpids.decode, neither on a live path,
since the real crypto goes through pycryptodome); left as-is here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
"""Tests for the DeDRM crypto primitives (alfcrypto)."""
|
|
|
|
import hashlib
|
|
|
|
import pytest
|
|
|
|
import dedrm_test_utils as U
|
|
|
|
alf = U.load("alfcrypto")
|
|
|
|
|
|
def test_pc1_roundtrip():
|
|
key = bytes(range(16))
|
|
plaintext = b"Hello, Topaz DRM world! 12345678"
|
|
encrypted = alf.Pukall_Cipher().PC1(key, plaintext, decryption=False)
|
|
assert encrypted != plaintext
|
|
assert alf.Pukall_Cipher().PC1(key, encrypted, decryption=True) == plaintext
|
|
|
|
|
|
def test_pc1_rejects_bad_key_length():
|
|
with pytest.raises(Exception):
|
|
alf.Pukall_Cipher().PC1(b"\x00" * 8, b"data............", decryption=False)
|
|
|
|
|
|
def _topaz_encrypt(data, ctx):
|
|
"""Inverse of Topaz_Cipher.decrypt, used to drive a round-trip test."""
|
|
ctx1, ctx2 = ctx
|
|
out = bytearray()
|
|
for plain in data:
|
|
out.append((plain ^ ((ctx1 >> 3) & 0xFF) ^ ((ctx2 << 3) & 0xFF)) & 0xFF)
|
|
ctx2 = ctx1
|
|
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) & 0xFFFFFFFF) ^ ((plain * plain * 0x0F902007) & 0xFFFFFFFF)
|
|
return bytes(out)
|
|
|
|
|
|
def test_topaz_cipher_roundtrip():
|
|
plaintext = b"topaz plaintext bytes"
|
|
cipher = alf.Topaz_Cipher()
|
|
ctx = cipher.ctx_init(b"mykey123")
|
|
ciphertext = _topaz_encrypt(plaintext, list(ctx))
|
|
recovered = cipher.decrypt(ciphertext, list(ctx)).encode("latin-1")
|
|
assert recovered == plaintext
|
|
|
|
|
|
def test_topaz_ctx_init_is_deterministic():
|
|
assert alf.Topaz_Cipher().ctx_init(b"abc") == alf.Topaz_Cipher().ctx_init(b"abc")
|
|
|
|
|
|
def test_pbkdf2_matches_hashlib():
|
|
out = alf.KeyIVGen().pbkdf2(b"password", b"salt", 100, 32)
|
|
assert out == hashlib.pbkdf2_hmac("sha1", b"password", b"salt", 100, 32)
|