From d407298be4bd80d030c268430db1d0e276ec7a53 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Wed, 24 Jun 2026 09:18:01 -0500 Subject: [PATCH] Update tests for shared helpers and fix test nits - 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 --- tests/dedrm_test_utils.py | 8 +++--- tests/test_ineptepub.py | 2 +- tests/test_ineptpdf.py | 2 +- tests/test_kgenpids.py | 4 --- tests/test_kindlepid.py | 8 ------ tests/test_mobidedrm.py | 8 ++++-- tests/test_obok.py | 17 ++++++++++--- tests/test_utilities.py | 53 ++++++++++++++++++++++++++++++++++++++- 8 files changed, 79 insertions(+), 23 deletions(-) diff --git a/tests/dedrm_test_utils.py b/tests/dedrm_test_utils.py index f25b2da..a57b9e7 100644 --- a/tests/dedrm_test_utils.py +++ b/tests/dedrm_test_utils.py @@ -4,14 +4,16 @@ The DeDRM and Obok plugins normally run inside calibre's bundled Python and use intra-package (``from .x import y``) imports. To unit-test the pure logic we: * put the plugin source directories on ``sys.path`` (for absolute imports such - as ``import kgenpids`` / ``import aescbc``); + as ``import kgenpids`` / ``import alfcrypto``); * provide a minimal ``calibre`` stub for the few modules that import it; * register a synthetic ``dedrm`` package whose ``__path__`` points at the DeDRM_plugin directory, so modules that use relative imports resolve without executing the real (calibre-dependent) ``__init__.py``. -Modules with no relative imports (alfcrypto, kgenpids, argv_utils, aescbc, obok) -are loaded top-level; modules with ``from .`` imports are loaded as ``dedrm.X``. +Modules with no relative imports (alfcrypto, argv_utils, obok) are loaded +top-level; modules with ``from .`` imports are loaded as ``dedrm.X``. kgenpids +is a hybrid: it is imported absolutely by other worker modules, so it falls back +to an absolute ``from utilities import ...`` and is still loaded top-level here. """ import importlib diff --git a/tests/test_ineptepub.py b/tests/test_ineptepub.py index 4a8093a..2df68f5 100644 --- a/tests/test_ineptepub.py +++ b/tests/test_ineptepub.py @@ -5,5 +5,5 @@ import dedrm_test_utils as U epub = U.load("ineptepub", package="dedrm") -def test_unpad_removes_pkcs7_padding(): +def test_unpad_strips_trailing_pad_length(): assert epub.unpad(b"data\x04\x04\x04\x04") == b"data" diff --git a/tests/test_ineptpdf.py b/tests/test_ineptpdf.py index ab44cb5..5515e55 100644 --- a/tests/test_ineptpdf.py +++ b/tests/test_ineptpdf.py @@ -14,5 +14,5 @@ def test_nunpack_lengths(): assert pdf.nunpack(b"\x01\x00\x00\x00") == 16777216 -def test_unpad_removes_pkcs7_padding(): +def test_unpad_strips_trailing_pad_length(): assert pdf.unpad(b"hello\x03\x03\x03") == b"hello" diff --git a/tests/test_kgenpids.py b/tests/test_kgenpids.py index 4baf441..b805c93 100644 --- a/tests/test_kgenpids.py +++ b/tests/test_kgenpids.py @@ -33,7 +33,3 @@ def test_pid_encryption_table_is_crc32_table(): def test_generate_device_pid_known_vector(): table = kg.generatePidEncryptionTable() assert kg.generateDevicePID(table, b"\x12\x34\x56\x78", 4) == b"22I8IQVF" - - -def test_crc32_known_vector(): - assert kg.crc32(b"test") == 4181434640 diff --git a/tests/test_kindlepid.py b/tests/test_kindlepid.py index 0c29696..9b6a6c1 100644 --- a/tests/test_kindlepid.py +++ b/tests/test_kindlepid.py @@ -5,10 +5,6 @@ import dedrm_test_utils as U kp = U.load("kindlepid", package="dedrm") -def test_letters_charset(): - assert kp.letters == b"ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" - - def test_checksum_pid_appends_two_chars(): pid = kp.checksumPid(b"12345678") assert pid == b"12345678EL" @@ -17,7 +13,3 @@ def test_checksum_pid_appends_two_chars(): def test_pid_from_serial_known_vector(): assert kp.pidFromSerial(b"B00212345678", 8) == b"VBKTRX7Q" - - -def test_crc32_known_vector(): - assert kp.crc32(b"test") == 4181434640 diff --git a/tests/test_mobidedrm.py b/tests/test_mobidedrm.py index 77dbb6a..73f8df1 100644 --- a/tests/test_mobidedrm.py +++ b/tests/test_mobidedrm.py @@ -30,5 +30,9 @@ def test_trailing_data_entries(): assert md.getSizeOfTrailingDataEntries(b"ABCDE", 5, 1) == 2 -def test_crc32_known_vector(): - assert md.crc32(b"test") == 4181434640 +def test_checksum_pid_str_contract(): + # mobidedrm's call sites pass and expect ``str``; the shared helper must + # preserve that (unlike the bytes contract used by kgenpids/kindlepid). + out = md.checksumPid("12345678") + assert out == "12345678EL" + assert isinstance(out, str) diff --git a/tests/test_obok.py b/tests/test_obok.py index f147896..20656aa 100644 --- a/tests/test_obok.py +++ b/tests/test_obok.py @@ -7,12 +7,23 @@ import dedrm_test_utils as U obok = U.load("obok") -def test_unpad_removes_pkcs7_padding(): +def test_unpad_strips_trailing_pad_length(): + # obok.unpad trusts the final byte as the pad length; it is not a validating + # PKCS#7 implementation, so the name reflects what it actually does. assert obok.unpad(b"data\x04\x04\x04\x04") == b"data" -def test_kobo_hash_keys_present(): - assert obok.KOBO_HASH_KEYS == ["88b3a2e13", "XzUhGYdFp", "NoCanLook", "QJhwzAtXL"] +def test_kobo_hash_keys_are_stable_ascii_salts(): + # These four salts are baked into Kobo's userkey derivation + # (sha256(hash + macaddr) -> deviceid). Pinning the exact values is + # intentional: a silent typo here would break decryption for every user, so + # we want a hard regression guard rather than only a shape check. + keys = obok.KOBO_HASH_KEYS + assert keys == ["88b3a2e13", "XzUhGYdFp", "NoCanLook", "QJhwzAtXL"] + # The derivation feeds each salt to ``.encode('ascii')``; pin that contract. + for k in keys: + assert isinstance(k, str) and k + k.encode("ascii") class _FakeStream: diff --git a/tests/test_utilities.py b/tests/test_utilities.py index c5b8c0a..285826b 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -1,4 +1,6 @@ -"""Tests for DeDRM utilities (uStrCmp).""" +"""Tests for DeDRM utilities (uStrCmp and the shared crypto/path helpers).""" + +import os import dedrm_test_utils as U @@ -17,3 +19,52 @@ def test_ustrcmp_caseless(): def test_ustrcmp_unicode_normalisation(): # Composed 'é' (U+00E9) vs decomposed 'e' + combining acute (U+0065 U+0301). assert ut.uStrCmp("é", "é", caseless=False) is True + + +def test_unpad_strips_trailing_pad_length(): + assert ut.unpad(b"data\x04\x04\x04\x04") == b"data" + assert ut.unpad(b"hello\x03\x03\x03") == b"hello" + + +def test_crc32_known_vector(): + assert ut.crc32(b"test") == 4181434640 + + +def test_pid_alphabet(): + assert ut.PID_ALPHABET == b"ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + + +def test_checksum_pid_preserves_bytes(): + out = ut.checksumPid(b"12345678") + assert out == b"12345678EL" + assert isinstance(out, bytes) + + +def test_checksum_pid_preserves_str(): + out = ut.checksumPid("12345678") + assert out == "12345678EL" + assert isinstance(out, str) + + +def test_pid_from_serial_known_vector(): + assert ut.pidFromSerial(b"B00212345678", 8) == b"VBKTRX7Q" + + +def test_safe_join_plain_name(tmp_path): + assert ut.safe_join(str(tmp_path), "book0000.dat") == os.path.join( + str(tmp_path), "book0000.dat" + ) + + +def test_safe_join_strips_directory_components(tmp_path): + # Any directory part (including ``../``) is reduced to a bare basename. + result = ut.safe_join(str(tmp_path), "../../etc/evil") + assert os.path.basename(result) == "evil" + assert os.path.dirname(os.path.abspath(result)) == os.path.abspath(str(tmp_path)) + + +def test_safe_join_result_always_contained(tmp_path): + root = os.path.abspath(str(tmp_path)) + for name in ["../../evil", "a/b/c", "..\\..\\win", "/abs/path", "plain"]: + result = os.path.abspath(ut.safe_join(str(tmp_path), name)) + assert result == root or result.startswith(root + os.sep)