The packaging workflow pushed alpha releases to noDRM/DeDRM_tools_autorelease
using secrets.AUTORELEASE_KEY, which only exists on the upstream repo. On a
fork the secret is empty, so the "Delete older auto-releases" step failed with
"no GITHUB_TOKEN found", and even with a token a fork cannot publish into
noDRM's release repo.
Point the release at ${{ github.repository }} and use the built-in
secrets.GITHUB_TOKEN (scoped to the current repo) with contents: write
permission, so each fork publishes its own alpha release with no PAT required.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Rename main.yml to release.yml to reflect that it packages the plugin and
publishes the auto-release. Name the job and the previously unnamed step,
strip trailing whitespace, and pin every action to a commit SHA (with a
version comment) at its latest release:
- actions/checkout v2 -> v7.0.0
- actions/upload-artifact v4 -> v7.0.1
- dev-drprasad/delete-older-releases v0.2.1 -> v0.3.4
- softprops/action-gh-release v1 -> v3.0.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run the pytest suite on pull requests to master across Python 3.9, 3.11
and 3.13 (the last exercises the legacy-cgi backport), installing
dependencies with Poetry. Actions are pinned to commit SHAs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Cover the new utilities helpers (unpad, crc32, checksumPid type
preservation, pidFromSerial, safe_join) and drop the now-redundant
per-module crc32 tests.
- Rename test_unpad_removes_pkcs7_padding to reflect that unpad only
trusts the trailing pad-length byte rather than validating PKCS#7.
- Replace the KOBO_HASH_KEYS change-detector with a test that documents
the intentional value pin and checks the ASCII-salt contract.
- Note kgenpids' hybrid import in the test-loader docstring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the copies of unpad, crc32, checksumPid and pidFromSerial that were
scattered across the plugin into utilities.py, and add a shared safe_join
that generalizes the Topaz extraction path-traversal fix.
- unpad: adobekey, ineptepub and ineptpdf import the shared helper. The
four bare-script key tools keep their local copies since they run
without a package context.
- checksumPid is type-preserving (bytes for kgenpids/kindlepid, str for
mobidedrm) so every call site keeps its exact behavior.
- kgenpids falls back to an absolute import because it is imported
top-level by the worker modules.
- topazextract.extractFiles uses safe_join; genbook is unchanged as it
only handles already-sanitized names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aescbc.py provided a pure-Python Rijndael/AES-CBC fallback consumed only
by alfcrypto.AES_CBC, whose sole caller (kindlekey's macOS
CryptUnprotectData) never assigns self.crp and so raised AttributeError
before reaching it. The live crypto path uses Cryptodome, so delete the
dead module and the unused wrapper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strengthen weak-assertion / false-confidence tests:
- test_alfcrypto: replace the tautological ctx_init determinism check
with an independent golden vector, and add a golden Topaz decrypt
vector that does not rely on the test's own inverse helper (so a
systematic cipher bug is caught, not just round-trip symmetry). Pin
the PC1 bad-key assertion to match="Bad key length", and load
alfcrypto via the dedrm package so the test exercises the same module
object that topazextract/mobidedrm import.
- test_mobidedrm: pin the PC1 bad-key assertion to the guard message.
- test_topazextract: make the path-traversal regression rely on the
depth-independent positive oracle (the sanitised file must land inside
outdir, which fails against the pre-fix code) plus an exact-contents
check, instead of brittle parent-path negatives.
- test_erdr2pml: narrow the import skip to only the missing-cgi case so
a genuinely broken module fails loudly instead of silently skipping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>