Fix str/bytes and leftover-py2 defects across the plugin, several on live
decryption paths:
- erdr2pml.py: getText() footnote/sidebar handling mixed a bytes
accumulator with str literals and called ord() on a bytes element,
crashing on eReader/.pdb books that contain footnotes or sidebars.
- ion.py: readdecimal() did `[ord(x) for x in self.read(...)]` over
bytes (ord(int)), crashing KFX decryption on Ion DECIMAL values;
printlob() had the same ord()-over-bytes in its debug path.
- zipfilerugged.py: `isinstance(file, unicode)` raised NameError when a
file-like object (not a path string) was passed to the ZipFile.
- utilities.py: SafeUnbuffered.write referenced the undefined `unicode`
(same fix already applied to the obok copy).
- ineptpdf.py: ord(bookkey[0]) over an int in an error-diagnostic print.
- epubfontdecrypt.py: removed a dead py2 itertools.izip fallback.
- convert2xml.py: escapestr did bytes.replace(str, ...).
- kgenpids.py: decode() built a str result then += bytes.
Cleanups from the earlier shim removal: drop now-unused `import sys`
(alfcrypto, utilities, kgenpids); unicode_argv returns list(sys.argv) so
callers can't mutate the process-global sys.argv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
erdr2pml (eReader/.pdb support) imports the stdlib `cgi` module at
runtime, which was removed in Python 3.13. Having legacy-cgi only in the
dev group meant eReader decryption would break on a 3.13+ runtime that
installed just the main dependencies. Move it to the main dependency
group, scoped with a `python >= 3.13` marker so it is installed only
where the stdlib module is gone (3.8-3.12 keep using the stdlib `cgi`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
erdr2pml imports the stdlib `cgi` module, removed in Python 3.13, so its
tests were skipped on modern interpreters. Add the `legacy-cgi` backport
as a dev dependency so the module imports, and flesh out its tests:
deXOR involution, fixKey known vector / length, cleanPML high-ASCII
escaping, and sanitizeFileName separator/colon/control-char/angle-bracket
handling. The skip guard is kept as a safety net for environments without
the backport.
Suite is now 44 passing, 0 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Python 3.12+ emits SyntaxWarning for unrecognised backslash escapes (and
these will eventually become SyntaxErrors). Convert the affected string
literals to raw strings so the escapes are explicit:
- scriptinterface.py: filename filter regexes (\.der, \.b64, ...).
- obok.py: MAC-address detection regexes (\s, \-) and the title
sanitiser ([^\s\w]).
- flatxml2svg.py: the emitted JavaScript snippet contains \? and \d but
also a trailing newline, so the regex part is made a raw string and
the "\n" is concatenated separately.
All changes are value-preserving (an unrecognised escape already evaluated
to the backslash plus the character); verified the emitted JS string and
the compiled regexes are unchanged, and that both plugins now compile with
zero SyntaxWarnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The plugins target calibre 5/6 (Python 3) and the Poetry environment
pins Python >=3.8, so the Python 2 fallbacks are dead code. This removes
them throughout both plugins (behaviour on Python 3 is unchanged):
- Drop all `from __future__` imports.
- Collapse `if sys.version_info[0] == 2: ... else: ...` blocks to their
Python 3 branch (ineptpdf, mobidedrm, kindlekey, kgenpids, alfcrypto,
erdr2pml, ineptepub, obok, and the various unpad() helpers, etc.).
- Replace `_winreg` import fallbacks with plain `import winreg`, and
delete the py2-only adobekey_winreg_unicode module (now unreferenced).
- Drop py2 name shims: `unicode`/`unichr`, `.iteritems()`,
`from StringIO import StringIO`, `htmlentitydefs` fallback, and the
Windows CommandLineToArgvW dance in unicode_argv (py3 sys.argv is
already Unicode on every platform).
- Remove the "Calibre < 5" (py2) bugfix block from the compat header.
Verified: every .py file in both plugins still byte-compiles.
Scope: the maintained DeDRM_plugin and Obok_plugin only. The archival
standalone scripts under Other_Tools/ are left as historical snapshots.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Poetry manifest (pyproject.toml) and lock file to manage the
development / standalone-CLI environment for the plugins. The plugins
themselves run inside calibre's bundled Python, so the project is set to
package-mode = false and the manifest documents the real third-party
dependency set rather than building a distributable package.
Declared dependencies:
- pycryptodomex (>=3.20): maintained crypto library exposing the
`Cryptodome` namespace that every crypto import already prefers. This
replaces the abandoned pycrypto (unmaintained since 2014,
CVE-2013-7459), which is no longer needed and is not declared.
- lxml (>=5.0): EPUB/PDF/ADEPT XML handling.
- apsw (>=3.46): optional `nook` group, only used by
ignoblekeyWindowsStore.py for Nook Windows Store key extraction.
calibre/calibre_lzma/PyQt are supplied by the calibre runtime and the
Python <3.3 lzma fallbacks (backports.lzma, pylzma) are unnecessary on
the supported Python 3.8+ range, so none are declared.
Also update the stale PyCrypto install instructions in
ignoblekeyGenPassHash.py to point at the maintained pycryptodomex.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Topaz header record "tag" is read verbatim from the untrusted book
file by bookReadString() and then used unsanitized to build the output
filename in extractFiles(). A crafted tag such as "../../foo" let a
malicious .azw/Topaz file write attacker-controlled bytes outside the
extraction directory. The payload content requires no book key, since an
unencrypted record with compressedLength == 0 is returned raw.
Strip the record name to its basename before joining it to destdir so
traversal sequences (../, /, \) can no longer escape, and add an
abspath-based containment check as defense in depth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>