import hashlib
import uuid
import zipfile
import pytest
from lxml import etree
from src.edition.fonts import ADOBE, IDPF, _key, decoded_fonts, primary_identifier, rebind_fonts
OPF = 'http://www.idpf.org/2007/opf'
def key(identifier, algorithm):
if algorithm == IDPF:
return hashlib.sha1(''.join(identifier.split()).encode()).digest()
return uuid.UUID(identifier.removeprefix('urn:uuid:')).bytes
def xor(data, identifier, algorithm):
k = key(identifier, algorithm); n = 1040 if algorithm == IDPF else 1024
return bytes(b ^ k[i % len(k)] for i, b in enumerate(data[:n])) + data[n:]
def fixture(tmp_path, algorithm):
old = 'urn:uuid:' + str(uuid.UUID('12345678-1234-5678-1234-567812345678'))
new = 'urn:uuid:' + str(uuid.UUID('87654321-4321-8765-4321-876543218765'))
plain = b'\x00\x01\x00\x00' + bytes(range(256)) * 20
encrypted = xor(plain, old, algorithm)
opf = f'''
x
') root = etree.fromstring(opf.encode()) return path, root, old, new, plain, encrypted @pytest.mark.parametrize('algorithm', [IDPF, ADOBE]) def test_decode_and_rebind_preserves_original_and_rekeys_css_urls(tmp_path, algorithm): path, root, old, new, plain, encrypted = fixture(tmp_path, algorithm) with zipfile.ZipFile(path) as z: assert primary_identifier(root) == old assert decoded_fonts(z, 'EPUB/package.opf', root) == {'EPUB/fonts/a.ttf': plain} additions, replacements = {}, {} records = rebind_fonts(z, 'EPUB/package.opf', root, old, new, additions, replacements) assert len(records) == 1 and records[0]['algorithm'] == algorithm assert records[0]['original_sha256'] == hashlib.sha256(encrypted).hexdigest() assert records[0]['decoded_sha256'] == hashlib.sha256(plain).hexdigest() assert additions['EPUB/fonts/edition-a.ttf'] == xor(plain, new, algorithm) assert z.read('EPUB/fonts/a.ttf') == encrypted assert root.xpath('//*[local-name()="item" and @id="font"]/@href') == ['fonts/edition-a.ttf'] assert root.xpath('//*[local-name()="item" and @id="font-source"]/@media-type') == ['application/octet-stream'] assert b'../fonts/edition-a.ttf' in replacements['EPUB/styles/main.css'] assert b'../fonts/edition-a.ttf' in replacements['EPUB/text/ch.xhtml'] assert b'https://example.org/fonts/a.ttf' in replacements['EPUB/styles/main.css'] assert b'edition-a.ttf' in replacements['EPUB/text/ch.xhtml'] enc = etree.fromstring(replacements['META-INF/encryption.xml']) assert enc.xpath('//*[local-name()="CipherReference"]/@URI') == ['EPUB/fonts/edition-a.ttf'] # The replacement payload decodes under the new primary identifier. root.xpath('//*[local-name()="identifier" and @id="uid"]')[0].text = new replacements['EPUB/package.opf'] = etree.tostring(root) class ZipView: def read(self, name): if name in replacements: return replacements[name] return additions[name] if name in additions else z.read(name) def namelist(self): return z.namelist() + list(additions) view = ZipView() decoded = decoded_fonts(view, 'EPUB/package.opf', root) assert decoded['EPUB/fonts/edition-a.ttf'] == plain def test_rejects_unknown_algorithm_and_non_font_payload(tmp_path): path, root, old, new, plain, encrypted = fixture(tmp_path, 'urn:unknown') with zipfile.ZipFile(path) as z, pytest.raises(ValueError, match='Unsupported EPUB encryption algorithm'): decoded_fonts(z, 'EPUB/package.opf', root) path, root, old, new, plain, encrypted = fixture(tmp_path, IDPF) with zipfile.ZipFile(path, 'w') as z: z.writestr('EPUB/package.opf', f'''