diff --git a/src/calibre/ebooks/epub/input.py b/src/calibre/ebooks/epub/input.py index cf903c0a5d..48699521c7 100644 --- a/src/calibre/ebooks/epub/input.py +++ b/src/calibre/ebooks/epub/input.py @@ -3,7 +3,7 @@ __license__ = 'GPL 3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, re, uuid +import os, uuid from itertools import cycle from lxml import etree @@ -19,8 +19,7 @@ class EPUBInput(InputFormatPlugin): recommendations = set([('page_breaks_before', '/', OptionRecommendation.MED)]) - @classmethod - def decrypt_font(cls, key, path): + def decrypt_font(self, key, path): raw = open(path, 'rb').read() crypt = raw[:1024] key = cycle(iter(key)) @@ -29,13 +28,18 @@ class EPUBInput(InputFormatPlugin): f.write(decrypt) f.write(raw[1024:]) - @classmethod - def process_encryption(cls, encfile, opf, log): + def process_encryption(self, encfile, opf, log): key = None - m = re.search(r'(?i)(urn:uuid:[0-9a-f-]+)', open(opf, 'rb').read()) - if m: - key = m.group(1) - key = list(map(ord, uuid.UUID(key).bytes)) + for item in opf.identifier_iter(): + scheme = None + for key in item.attrib.keys(): + if key.endswith('scheme'): + scheme = item.get(key) + if (scheme and scheme.lower() == 'uuid') or \ + (item.text and item.text.startswith('urn:uuid:')): + key = str(item.text).rpartition(':')[-1] + key = list(map(ord, uuid.UUID(key).bytes)) + try: root = etree.parse(encfile) for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'): @@ -46,7 +50,8 @@ class EPUBInput(InputFormatPlugin): uri = cr.get('URI') path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) if os.path.exists(path): - cls.decrypt_font(key, path) + self._encrypted_font_uris.append(uri) + self.decrypt_font(key, path) return True except: import traceback @@ -115,14 +120,17 @@ class EPUBInput(InputFormatPlugin): if opf is None: raise ValueError('%s is not a valid EPUB file'%path) - if os.path.exists(encfile): - if not self.process_encryption(encfile, opf, log): - raise DRMError(os.path.basename(path)) - opf = os.path.relpath(opf, os.getcwdu()) parts = os.path.split(opf) opf = OPF(opf, os.path.dirname(os.path.abspath(opf))) + self._encrypted_font_uris = [] + if os.path.exists(encfile): + if not self.process_encryption(encfile, opf, log): + raise DRMError(os.path.basename(path)) + self.encrypted_fonts = self._encrypted_font_uris + + if len(parts) > 1 and parts[0]: delta = '/'.join(parts[:-1])+'/' for elem in opf.itermanifest(): diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index 6e74a748b1..2b27f09664 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -12,8 +12,9 @@ from urllib import unquote from calibre.customize.conversion import OutputFormatPlugin from calibre.ptempfile import TemporaryDirectory from calibre.constants import __appname__, __version__ -from calibre import strftime, guess_type, prepare_string_for_xml +from calibre import strftime, guess_type, prepare_string_for_xml, CurrentDir from calibre.customize.conversion import OptionRecommendation +from calibre.constants import filesystem_encoding from lxml import etree @@ -170,6 +171,19 @@ class EPUBOutput(OutputFormatPlugin): self.workaround_sony_quirks() + from calibre.ebooks.oeb.base import OPF + identifiers = oeb.metadata['identifier'] + uuid = None + for x in identifiers: + if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode(x).startswith('urn:uuid:'): + uuid = unicode(x).split(':')[-1] + break + if uuid is None: + self.log.warn('No UUID identifier found') + from uuid import uuid4 + uuid = str(uuid4()) + oeb.metadata.add('identifier', uuid, scheme='uuid', id=uuid) + with TemporaryDirectory('_epub_output') as tdir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') @@ -177,10 +191,16 @@ class EPUBOutput(OutputFormatPlugin): opf = [x for x in os.listdir(tdir) if x.endswith('.opf')][0] self.condense_ncx([os.path.join(tdir, x) for x in os.listdir(tdir)\ if x.endswith('.ncx')][0]) + encrypted_fonts = getattr(input_plugin, 'encrypted_fonts', []) + encryption = None + if encrypted_fonts: + encryption = self.encrypt_fonts(encrypted_fonts, tdir, uuid) from calibre.ebooks.epub import initialize_container epub = initialize_container(output_path, os.path.basename(opf)) epub.add_dir(tdir) + if encryption is not None: + epub.writestr('META-INF/encryption.xml', encryption) if opts.extract_to is not None: if os.path.exists(opts.extract_to): shutil.rmtree(opts.extract_to) @@ -189,6 +209,52 @@ class EPUBOutput(OutputFormatPlugin): self.log.info('EPUB extracted to', opts.extract_to) epub.close() + def encrypt_fonts(self, uris, tdir, uuid): + from binascii import unhexlify + + key = re.sub(r'[^a-fA-F0-9]', '', uuid) + if len(key) < 16: + raise ValueError('UUID identifier %r is invalid'%uuid) + key = unhexlify((key + key)[:32]) + key = tuple(map(ord, key)) + paths = [] + with CurrentDir(tdir): + paths = [os.path.join(*x.split('/')) for x in uris] + uris = dict(zip(uris, paths)) + fonts = [] + for uri in list(uris.keys()): + path = uris[uri] + if isinstance(path, unicode): + path = path.encode(filesystem_encoding) + if not os.path.exists(path): + uris.pop(uri) + continue + self.log.debug('Encrypting font:', uri) + with open(path, 'r+b') as f: + data = f.read(1024) + f.seek(0) + for i in range(1024): + f.write(chr(ord(data[i]) ^ key[i%16])) + if not isinstance(uri, unicode): + uri = uri.decode('utf-8') + fonts.append(u''' + + + + + + + '''%(uri.replace('"', '\\"'))) + if fonts: + ans = ''' + ''' + ans += (u'\n'.join(fonts)).encode('utf-8') + ans += '\n' + return ans + def default_cover(self): ''' Create a generic cover for books that dont have a cover diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 5e57b0b515..5cbaf604c4 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -779,6 +779,9 @@ class OPF(object): self.set_text(matches[0], unicode(val)) return property(fget=fget, fset=fset) + def identifier_iter(self): + for item in self.identifier_path(self.metadata): + yield item def guess_cover(self): '''