Skip to content

Commit

Permalink
EPUB to EPUB conversions: Preserve font encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
kovidgoyal committed Mar 3, 2010
1 parent 5238aab commit 68f0f89
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 14 deletions.
34 changes: 21 additions & 13 deletions src/calibre/ebooks/epub/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os, re, uuid
import os, uuid
from itertools import cycle

from lxml import etree
Expand All @@ -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))
Expand All @@ -29,13 +28,18 @@ def decrypt_font(cls, key, path):
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")]'):
Expand All @@ -46,7 +50,8 @@ def process_encryption(cls, encfile, opf, log):
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
Expand Down Expand Up @@ -115,13 +120,16 @@ def convert(self, stream, options, file_ext, log, accelerators):
if opf is None:
raise ValueError('%s is not a valid EPUB file'%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

opf = os.path.relpath(opf, os.getcwdu())
parts = os.path.split(opf)
opf = OPF(opf, os.path.dirname(os.path.abspath(opf)))

if len(parts) > 1 and parts[0]:
delta = '/'.join(parts[:-1])+'/'
Expand Down
68 changes: 67 additions & 1 deletion src/calibre/ebooks/epub/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
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

Expand Down Expand Up @@ -170,17 +171,36 @@ def convert(self, oeb, output_path, input_plugin, opts, log):

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')
oeb_output.convert(oeb, tdir, input_plugin, opts, log)
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)
Expand All @@ -189,6 +209,52 @@ def convert(self, oeb, output_path, input_plugin, opts, log):
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'''
<enc:EncryptedData>
<enc:EncryptionMethod Algorithm="http://ns.adobe.com/pdf/enc#RC"/>
<enc:CipherData>
<enc:CipherReference URI="%s"/>
</enc:CipherData>
</enc:EncryptedData>
'''%(uri.replace('"', '\\"')))
if fonts:
ans = '''<encryption
xmlns="urn:oasis:names:tc:opendocument:xmlns:container"
xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
xmlns:deenc="http://ns.adobe.com/digitaleditions/enc">
'''
ans += (u'\n'.join(fonts)).encode('utf-8')
ans += '\n</encryption>'
return ans

def default_cover(self):
'''
Create a generic cover for books that dont have a cover
Expand Down
3 changes: 3 additions & 0 deletions src/calibre/ebooks/metadata/opf2.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ def fset(self, val):
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):
'''
Expand Down

0 comments on commit 68f0f89

Please sign in to comment.