Guest User

ignobleepub_v2.pyw

a guest
Oct 12th, 2010
196
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.16 KB | None | 0 0
  1. #! /usr/bin/python
  2.  
  3. # ignobleepub.pyw, version 2
  4.  
  5. # To run this program install Python 2.6 from <http://www.python.org/download/>
  6. # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
  7. # (make sure to install the version for Python 2.6).  Save this script file as
  8. # ignobleepub.pyw and double-click on it to run it.
  9.  
  10. # Revision history:
  11. #   1 - Initial release
  12. #   2 - Added OS X support by using OpenSSL when available
  13.  
  14. from __future__ import with_statement
  15.  
  16. __license__ = 'GPL v3'
  17.  
  18. import sys
  19. import os
  20. import zlib
  21. import zipfile
  22. from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
  23. from contextlib import closing
  24. import xml.etree.ElementTree as etree
  25. import Tkinter
  26. import Tkconstants
  27. import tkFileDialog
  28. import tkMessageBox
  29.  
  30. def _load_crypto_libcrypto():
  31.     from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
  32.         Structure, c_ulong, create_string_buffer, cast
  33.     from ctypes.util import find_library
  34.  
  35.     libcrypto = find_library('crypto')
  36.     if libcrypto is None:
  37.         raise IGNOBLEError('libcrypto not found')
  38.     libcrypto = CDLL(libcrypto)
  39.  
  40.     AES_MAXNR = 14
  41.    
  42.     c_char_pp = POINTER(c_char_p)
  43.     c_int_p = POINTER(c_int)
  44.  
  45.     class AES_KEY(Structure):
  46.         _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
  47.                     ('rounds', c_int)]
  48.     AES_KEY_p = POINTER(AES_KEY)
  49.    
  50.     def F(restype, name, argtypes):
  51.         func = getattr(libcrypto, name)
  52.         func.restype = restype
  53.         func.argtypes = argtypes
  54.         return func
  55.    
  56.     AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
  57.                             [c_char_p, c_int, AES_KEY_p])
  58.     AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
  59.                         [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
  60.                          c_int])
  61.    
  62.     class AES(object):
  63.         def __init__(self, userkey):
  64.             self._blocksize = len(userkey)
  65.             key = self._key = AES_KEY()
  66.             rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
  67.             if rv < 0:
  68.                 raise IGNOBLEError('Failed to initialize AES key')
  69.    
  70.         def decrypt(self, data):
  71.             out = create_string_buffer(len(data))
  72.             iv = ("\x00" * self._blocksize)
  73.             rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
  74.             if rv == 0:
  75.                 raise IGNOBLEError('AES decryption failed')
  76.             return out.raw
  77.  
  78.     return AES
  79.  
  80. def _load_crypto_pycrypto():
  81.     from Crypto.Cipher import AES as _AES
  82.  
  83.     class AES(object):
  84.         def __init__(self, key):
  85.             self._aes = _AES.new(key, _AES.MODE_CBC)
  86.  
  87.         def decrypt(self, data):
  88.             return self._aes.decrypt(data)
  89.  
  90.     return AES
  91.  
  92. def _load_crypto():
  93.     AES = None
  94.     for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
  95.         try:
  96.             AES = loader()
  97.             break
  98.         except (ImportError, IGNOBLEError):
  99.             pass
  100.     return AES
  101. AES = _load_crypto()
  102.  
  103.  
  104.  
  105.  
  106. """
  107. Decrypt Barnes & Noble ADEPT encrypted EPUB books.
  108. """
  109.  
  110.  
  111. META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
  112. NSMAP = {'adept': 'http://ns.adobe.com/adept',
  113.          'enc': 'http://www.w3.org/2001/04/xmlenc#'}
  114.  
  115. class ZipInfo(zipfile.ZipInfo):
  116.     def __init__(self, *args, **kwargs):
  117.         if 'compress_type' in kwargs:
  118.             compress_type = kwargs.pop('compress_type')
  119.         super(ZipInfo, self).__init__(*args, **kwargs)
  120.         self.compress_type = compress_type
  121.  
  122. class Decryptor(object):
  123.     def __init__(self, bookkey, encryption):
  124.         enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
  125.         # self._aes = AES.new(bookkey, AES.MODE_CBC)
  126.         self._aes = AES(bookkey)
  127.         encryption = etree.fromstring(encryption)
  128.         self._encrypted = encrypted = set()
  129.         expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
  130.                                enc('CipherReference'))
  131.         for elem in encryption.findall(expr):
  132.             path = elem.get('URI', None)
  133.             if path is not None:
  134.                 encrypted.add(path)
  135.  
  136.     def decompress(self, bytes):
  137.         dc = zlib.decompressobj(-15)
  138.         bytes = dc.decompress(bytes)
  139.         ex = dc.decompress('Z') + dc.flush()
  140.         if ex:
  141.             bytes = bytes + ex
  142.         return bytes
  143.  
  144.     def decrypt(self, path, data):
  145.         if path in self._encrypted:
  146.             data = self._aes.decrypt(data)[16:]
  147.             data = data[:-ord(data[-1])]
  148.             data = self.decompress(data)
  149.         return data
  150.  
  151.  
  152. class IGNOBLEError(Exception):
  153.     pass
  154.  
  155. def cli_main(argv=sys.argv):
  156.     progname = os.path.basename(argv[0])
  157.     if AES is None:
  158.         print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
  159.               "separately.  Read the top-of-script comment for details." % \
  160.               (progname,)
  161.         return 1
  162.     if len(argv) != 4:
  163.         print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
  164.         return 1
  165.     keypath, inpath, outpath = argv[1:]
  166.     with open(keypath, 'rb') as f:
  167.         keyb64 = f.read()
  168.     key = keyb64.decode('base64')[:16]
  169.     # aes = AES.new(key, AES.MODE_CBC)
  170.     aes = AES(key)
  171.  
  172.     with closing(ZipFile(open(inpath, 'rb'))) as inf:
  173.         namelist = set(inf.namelist())
  174.         if 'META-INF/rights.xml' not in namelist or \
  175.            'META-INF/encryption.xml' not in namelist:
  176.             raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
  177.         for name in META_NAMES:
  178.             namelist.remove(name)
  179.         rights = etree.fromstring(inf.read('META-INF/rights.xml'))
  180.         adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
  181.         expr = './/%s' % (adept('encryptedKey'),)
  182.         bookkey = ''.join(rights.findtext(expr))
  183.         bookkey = aes.decrypt(bookkey.decode('base64'))
  184.         bookkey = bookkey[:-ord(bookkey[-1])]
  185.         encryption = inf.read('META-INF/encryption.xml')
  186.         decryptor = Decryptor(bookkey[-16:], encryption)
  187.         kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
  188.         with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
  189.             zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
  190.             outf.writestr(zi, inf.read('mimetype'))
  191.             for path in namelist:
  192.                 data = inf.read(path)
  193.                 outf.writestr(path, decryptor.decrypt(path, data))
  194.     return 0
  195.  
  196.  
  197. class DecryptionDialog(Tkinter.Frame):
  198.     def __init__(self, root):
  199.         Tkinter.Frame.__init__(self, root, border=5)
  200.         self.status = Tkinter.Label(self, text='Select files for decryption')
  201.         self.status.pack(fill=Tkconstants.X, expand=1)
  202.         body = Tkinter.Frame(self)
  203.         body.pack(fill=Tkconstants.X, expand=1)
  204.         sticky = Tkconstants.E + Tkconstants.W
  205.         body.grid_columnconfigure(1, weight=2)
  206.         Tkinter.Label(body, text='Key file').grid(row=0)
  207.         self.keypath = Tkinter.Entry(body, width=30)
  208.         self.keypath.grid(row=0, column=1, sticky=sticky)
  209.         if os.path.exists('bnepubkey.b64'):
  210.             self.keypath.insert(0, 'bnepubkey.b64')
  211.         button = Tkinter.Button(body, text="...", command=self.get_keypath)
  212.         button.grid(row=0, column=2)
  213.         Tkinter.Label(body, text='Input file').grid(row=1)
  214.         self.inpath = Tkinter.Entry(body, width=30)
  215.         self.inpath.grid(row=1, column=1, sticky=sticky)
  216.         button = Tkinter.Button(body, text="...", command=self.get_inpath)
  217.         button.grid(row=1, column=2)
  218.         Tkinter.Label(body, text='Output file').grid(row=2)
  219.         self.outpath = Tkinter.Entry(body, width=30)
  220.         self.outpath.grid(row=2, column=1, sticky=sticky)
  221.         button = Tkinter.Button(body, text="...", command=self.get_outpath)
  222.         button.grid(row=2, column=2)
  223.         buttons = Tkinter.Frame(self)
  224.         buttons.pack()
  225.         botton = Tkinter.Button(
  226.             buttons, text="Decrypt", width=10, command=self.decrypt)
  227.         botton.pack(side=Tkconstants.LEFT)
  228.         Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
  229.         button = Tkinter.Button(
  230.             buttons, text="Quit", width=10, command=self.quit)
  231.         button.pack(side=Tkconstants.RIGHT)
  232.  
  233.     def get_keypath(self):
  234.         keypath = tkFileDialog.askopenfilename(
  235.             parent=None, title='Select B&N EPUB key file',
  236.             defaultextension='.b64',
  237.             filetypes=[('base64-encoded files', '.b64'),
  238.                        ('All Files', '.*')])
  239.         if keypath:
  240.             keypath = os.path.normpath(keypath)
  241.             self.keypath.delete(0, Tkconstants.END)
  242.             self.keypath.insert(0, keypath)
  243.         return
  244.  
  245.     def get_inpath(self):
  246.         inpath = tkFileDialog.askopenfilename(
  247.             parent=None, title='Select B&N-encrypted EPUB file to decrypt',
  248.             defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
  249.                                                  ('All files', '.*')])
  250.         if inpath:
  251.             inpath = os.path.normpath(inpath)
  252.             self.inpath.delete(0, Tkconstants.END)
  253.             self.inpath.insert(0, inpath)
  254.         return
  255.  
  256.     def get_outpath(self):
  257.         outpath = tkFileDialog.asksaveasfilename(
  258.             parent=None, title='Select unencrypted EPUB file to produce',
  259.             defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
  260.                                                  ('All files', '.*')])
  261.         if outpath:
  262.             outpath = os.path.normpath(outpath)
  263.             self.outpath.delete(0, Tkconstants.END)
  264.             self.outpath.insert(0, outpath)
  265.         return
  266.  
  267.     def decrypt(self):
  268.         keypath = self.keypath.get()
  269.         inpath = self.inpath.get()
  270.         outpath = self.outpath.get()
  271.         if not keypath or not os.path.exists(keypath):
  272.             self.status['text'] = 'Specified key file does not exist'
  273.             return
  274.         if not inpath or not os.path.exists(inpath):
  275.             self.status['text'] = 'Specified input file does not exist'
  276.             return
  277.         if not outpath:
  278.             self.status['text'] = 'Output file not specified'
  279.             return
  280.         if inpath == outpath:
  281.             self.status['text'] = 'Must have different input and output files'
  282.             return
  283.         argv = [sys.argv[0], keypath, inpath, outpath]
  284.         self.status['text'] = 'Decrypting...'
  285.         try:
  286.             cli_main(argv)
  287.         except Exception, e:
  288.             self.status['text'] = 'Error: ' + str(e)
  289.             return
  290.         self.status['text'] = 'File successfully decrypted'
  291.  
  292. def gui_main():
  293.     root = Tkinter.Tk()
  294.     if AES is None:
  295.         root.withdraw()
  296.         tkMessageBox.showerror(
  297.             "Ignoble EPUB Decrypter",
  298.             "This script requires OpenSSL or PyCrypto, which must be installed "
  299.             "separately.  Read the top-of-script comment for details.")
  300.         return 1
  301.     root.title('Ignoble EPUB Decrypter')
  302.     root.resizable(True, False)
  303.     root.minsize(300, 0)
  304.     DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
  305.     root.mainloop()
  306.     return 0
  307.  
  308. if __name__ == '__main__':
  309.     if len(sys.argv) > 1:
  310.         sys.exit(cli_main())
  311.     sys.exit(gui_main())
Advertisement
Add Comment
Please, Sign In to add comment