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>
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""Tests for topazextract, including the path-traversal regression test."""
|
|
|
|
from io import BytesIO
|
|
|
|
import dedrm_test_utils as U
|
|
|
|
tz = U.load("topazextract", package="dedrm")
|
|
|
|
|
|
def test_book_read_encoded_number():
|
|
assert tz.bookReadEncodedNumber(BytesIO(bytes([0x05]))) == 5
|
|
assert tz.bookReadEncodedNumber(BytesIO(bytes([0x81, 0x00]))) == 128
|
|
|
|
|
|
def test_book_read_string():
|
|
assert tz.bookReadString(BytesIO(bytes([0x03]) + b"abc")) == b"abc"
|
|
|
|
|
|
def _bare_book(outdir):
|
|
"""A TopazBook instance with just the attributes extractFiles() needs."""
|
|
book = tz.TopazBook.__new__(tz.TopazBook)
|
|
book.outdir = str(outdir)
|
|
return book
|
|
|
|
|
|
def test_extractfiles_strips_path_traversal(tmp_path):
|
|
"""A malicious Topaz header tag must not write outside the output dir.
|
|
|
|
Regression test for the arbitrary-file-write fix: the record name is read
|
|
verbatim from the (untrusted) book file, so ``../../evil`` must be reduced
|
|
to a basename inside ``outdir`` rather than escaping it.
|
|
"""
|
|
outdir = tmp_path / "out"
|
|
outdir.mkdir()
|
|
book = _bare_book(outdir)
|
|
|
|
evil_tag = b"../../evil"
|
|
book.bookHeaderRecords = {evil_tag: [[0, 5, 0]]}
|
|
# Attacker-controlled payload bytes, no key required.
|
|
book.getBookPayloadRecord = lambda name, index: b"PWNED"
|
|
|
|
book.extractFiles()
|
|
|
|
# The traversal sequence was stripped: nothing was written above outdir.
|
|
assert not (tmp_path / "evil0000.dat").exists()
|
|
assert not (tmp_path.parent / "evil0000.dat").exists()
|
|
# The (sanitised) write landed safely inside outdir.
|
|
assert (outdir / "evil0000.dat").read_bytes() == b"PWNED"
|
|
|
|
|
|
def test_extractfiles_writes_normal_record(tmp_path):
|
|
outdir = tmp_path / "out"
|
|
outdir.mkdir()
|
|
book = _bare_book(outdir)
|
|
book.bookHeaderRecords = {b"spam": [[0, 4, 0]]}
|
|
book.getBookPayloadRecord = lambda name, index: b"eggs"
|
|
|
|
book.extractFiles()
|
|
|
|
assert (outdir / "spam0000.dat").read_bytes() == b"eggs"
|