#! /usr/bin/python # -*- coding: utf-8 -*- # # Automated Inept Epub (bulk processing) # # aineptepub.pyw, version 5.2 # Copyright © 2009-2010 i♥cabbages, Tetrachroma (bulk automation) # # Released under the terms of the GNU General Public Licence, version 3 or # later. # Windows users: Before running this program, you must first install Python 2.6 # from and PyCrypto from # (make sure to # install the version for Python 2.6). Save this script file as # ineptepub.pyw and double-click on it to run it. # # Mac OS X users: Save this script file as ineptepub.pyw. You can run this # program from the command line (pythonw ineptepub.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release # 2 - Rename to INEPT, fix exit code # 5 - Version bump to avoid (?) confusion; # Improve OS X support by using OpenSSL when available # 5.1 - Improve OpenSSL error checking # 5.2 - Fix ctypes error causing segfaults on some systems # 5.2 Rev. 2 - added automated bulk processing (Tetrachroma) """ Automatically decrypt Adobe ADEPT-encrypted EPUB books in a directory. """ from __future__ import with_statement __license__ = 'GPL v3' import sys import os import zlib import zipfile from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from contextlib import closing import xml.etree.ElementTree as etree import Tkinter import Tkconstants import tkFileDialog import tkMessageBox import re class ADEPTError(Exception): pass def _load_crypto_libcrypto(): from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ Structure, c_ulong, create_string_buffer, cast from ctypes.util import find_library libcrypto = find_library('crypto') if libcrypto is None: raise ADEPTError('libcrypto not found') libcrypto = CDLL(libcrypto) RSA_NO_PADDING = 3 AES_MAXNR = 14 c_char_pp = POINTER(c_char_p) c_int_p = POINTER(c_int) class RSA(Structure): pass RSA_p = POINTER(RSA) class AES_KEY(Structure): _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)] AES_KEY_p = POINTER(AES_KEY) def F(restype, name, argtypes): func = getattr(libcrypto, name) func.restype = restype func.argtypes = argtypes return func d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey', [RSA_p, c_char_pp, c_long]) RSA_size = F(c_int, 'RSA_size', [RSA_p]) RSA_private_decrypt = F(c_int, 'RSA_private_decrypt', [c_int, c_char_p, c_char_p, RSA_p, c_int]) RSA_free = F(None, 'RSA_free', [RSA_p]) AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class RSA(object): def __init__(self, der): buf = create_string_buffer(der) pp = c_char_pp(cast(buf, c_char_p)) rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der)) if rsa is None: raise ADEPTError('Error parsing ADEPT user key DER') def decrypt(self, from_): rsa = self._rsa to = create_string_buffer(RSA_size(rsa)) dlen = RSA_private_decrypt(len(from_), from_, to, rsa, RSA_NO_PADDING) if dlen < 0: raise ADEPTError('RSA decryption failed') return to[:dlen] def __del__(self): if self._rsa is not None: RSA_free(self._rsa) self._rsa = None class AES(object): def __init__(self, userkey): self._blocksize = len(userkey) key = self._key = AES_KEY() rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: raise ADEPTError('Failed to initialize AES key') def decrypt(self, data): out = create_string_buffer(len(data)) iv = ("\x00" * self._blocksize) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: raise ADEPTError('AES decryption failed') return out.raw return (AES, RSA) def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES from Crypto.PublicKey import RSA as _RSA # ASN.1 parsing code from tlslite class ASN1Error(Exception): pass class ASN1Parser(object): class Parser(object): def __init__(self, bytes): self.bytes = bytes self.index = 0 def get(self, length): if self.index + length > len(self.bytes): raise ASN1Error("Error decoding ASN.1") x = 0 for count in range(length): x <<= 8 x |= self.bytes[self.index] self.index += 1 return x def getFixBytes(self, lengthBytes): bytes = self.bytes[self.index : self.index+lengthBytes] self.index += lengthBytes return bytes def getVarBytes(self, lengthLength): lengthBytes = self.get(lengthLength) return self.getFixBytes(lengthBytes) def getFixList(self, length, lengthList): l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def getVarList(self, length, lengthLength): lengthList = self.get(lengthLength) if lengthList % length != 0: raise ASN1Error("Error decoding ASN.1") lengthList = int(lengthList/length) l = [0] * lengthList for x in range(lengthList): l[x] = self.get(length) return l def startLengthCheck(self, lengthLength): self.lengthCheck = self.get(lengthLength) self.indexCheck = self.index def setLengthCheck(self, length): self.lengthCheck = length self.indexCheck = self.index def stopLengthCheck(self): if (self.index - self.indexCheck) != self.lengthCheck: raise ASN1Error("Error decoding ASN.1") def atLengthCheck(self): if (self.index - self.indexCheck) < self.lengthCheck: return False elif (self.index - self.indexCheck) == self.lengthCheck: return True else: raise ASN1Error("Error decoding ASN.1") def __init__(self, bytes): p = self.Parser(bytes) p.get(1) self.length = self._getASN1Length(p) self.value = p.getFixBytes(self.length) def getChild(self, which): p = self.Parser(self.value) for x in range(which+1): markIndex = p.index p.get(1) length = self._getASN1Length(p) p.getFixBytes(length) return ASN1Parser(p.bytes[markIndex:p.index]) def _getASN1Length(self, p): firstLength = p.get(1) if firstLength<=127: return firstLength else: lengthLength = firstLength & 0x7F return p.get(lengthLength) class AES(object): def __init__(self, key): self._aes = _AES.new(key, _AES.MODE_CBC) def decrypt(self, data): return self._aes.decrypt(data) class RSA(object): def __init__(self, der): key = ASN1Parser([ord(x) for x in der]) key = [key.getChild(x).value for x in xrange(1, 4)] key = [self.bytesToNumber(v) for v in key] self._rsa = _RSA.construct(key) def bytesToNumber(self, bytes): total = 0L for byte in bytes: total = (total << 8) + byte return total def decrypt(self, data): return self._rsa.decrypt(data) return (AES, RSA) def _load_crypto(): AES = RSA = None for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto): try: AES, RSA = loader() break except (ImportError, ADEPTError): pass return (AES, RSA) AES, RSA = _load_crypto() META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') NSMAP = {'adept': 'http://ns.adobe.com/adept', 'enc': 'http://www.w3.org/2001/04/xmlenc#'} class ZipInfo(zipfile.ZipInfo): def __init__(self, *args, **kwargs): if 'compress_type' in kwargs: compress_type = kwargs.pop('compress_type') super(ZipInfo, self).__init__(*args, **kwargs) self.compress_type = compress_type class Decryptor(object): def __init__(self, bookkey, encryption): enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) self._aes = AES(bookkey) encryption = etree.fromstring(encryption) self._encrypted = encrypted = set() expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), enc('CipherReference')) for elem in encryption.findall(expr): path = elem.get('URI', None) if path is not None: encrypted.add(path) def decompress(self, bytes): dc = zlib.decompressobj(-15) bytes = dc.decompress(bytes) ex = dc.decompress('Z') + dc.flush() if ex: bytes = bytes + ex return bytes def decrypt(self, path, data): if path in self._encrypted: data = self._aes.decrypt(data)[16:] data = data[:-ord(data[-1])] data = self.decompress(data) return data def cli_main(argv=sys.argv): progname = os.path.basename(argv[0]) if AES is None: print "%s: This script requires OpenSSL or PyCrypto, which must be" \ " installed separately. Read the top-of-script comment for" \ " details." % (progname,) return 1 if len(argv) != 4: print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) return 1 keypath, inpath, outpath = argv[1:] with open(keypath, 'rb') as f: keyder = f.read() rsa = RSA(keyder) with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = set(inf.namelist()) if 'META-INF/rights.xml' not in namelist or \ 'META-INF/encryption.xml' not in namelist: raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,)) for name in META_NAMES: namelist.remove(name) rights = etree.fromstring(inf.read('META-INF/rights.xml')) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) bookkey = rsa.decrypt(bookkey.decode('base64')) # Padded as per RSAES-PKCS1-v1_5 if bookkey[-17] != '\x00': raise ADEPTError('problem decrypting session key') encryption = inf.read('META-INF/encryption.xml') decryptor = Decryptor(bookkey[-16:], encryption) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: zi = ZipInfo('mimetype', compress_type=ZIP_STORED) outf.writestr(zi, inf.read('mimetype')) for path in namelist: data = inf.read(path) outf.writestr(path, decryptor.decrypt(path, data)) return 0 class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) self.status = Tkinter.Label(self, text='Automated Inept epub V5.2 (Tetrachroma)') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) Tkinter.Label(body, text='Key file').grid(row=0) self.keypath = Tkinter.Entry(body, width=35) self.keypath.grid(row=0, column=1, sticky=sticky) if os.path.exists('adeptkey.der'): self.keypath.insert(0, 'adeptkey.der') button = Tkinter.Button(body, text="...", command=self.get_keypath) button.grid(row=0, column=2) Tkinter.Label(body, text='Input Path').grid(row=1) self.inpath = Tkinter.Entry(body, width=35) self.inpath.grid(row=1, column=1, sticky=sticky) button = Tkinter.Button(body, text="...", command=self.get_inpath) button.grid(row=1, column=2) Tkinter.Label(body, text='Output Path').grid(row=2) self.outpath = Tkinter.Entry(body, width=35) self.outpath.grid(row=2, column=1, sticky=sticky) button = Tkinter.Button(body, text="...", command=self.get_outpath) button.grid(row=2, column=2) buttons = Tkinter.Frame(self) buttons.pack() botton = Tkinter.Button( buttons, text="Decrypt whole directory", width=35, command=self.decrypt) botton.pack(side=Tkconstants.LEFT) Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) button = Tkinter.Button( buttons, text="Quit", width=10, command=self.quit) button.pack(side=Tkconstants.RIGHT) def get_keypath(self): keypath = tkFileDialog.askopenfilename( parent=None, title='Select ADEPT key file', defaultextension='.der', filetypes=[('DER-encoded files', '.der'), ('All Files', '.*')]) if keypath: keypath = os.path.normpath(keypath) self.keypath.delete(0, Tkconstants.END) self.keypath.insert(0, keypath) return def get_inpath(self): inpath = tkFileDialog.askdirectory(parent=None, title='Select encrypted ePub source directory') if inpath: inpath = os.path.normpath(inpath) self.inpath.delete(0, Tkconstants.END) self.inpath.insert(0, inpath) return def get_outpath(self): outpath = tkFileDialog.askdirectory(parent=None, title='Select directory to decrypt to') if outpath: outpath = os.path.normpath(outpath) self.outpath.delete(0, Tkconstants.END) self.outpath.insert(0, outpath) return def decrypt(self): keypath = self.keypath.get() inpath = self.inpath.get() outpath = self.outpath.get() if not keypath or not os.path.exists(keypath): self.status['text'] = 'Specified key file does not exist' return if not inpath or not os.path.exists(inpath): self.status['text'] = 'Specified input directory does not exist' return if not outpath: self.status['text'] = 'Output directory not specified' return if inpath == outpath: self.status['text'] = 'Must have different input and output directory' return dirlist = os.listdir(inpath) #get regular expression match for epubs match = r'.epub' inpath=inpath+'//' outpath=outpath+'//' for fname in dirlist: root, ext = os.path.splitext(fname) if (os.path.isfile(os.path.normpath(inpath+fname)) == True) and re.match(match,ext,re.IGNORECASE): argv = [sys.argv[0], os.path.normpath(keypath), os.path.normpath(inpath+fname), os.path.normpath(outpath+root+".epub")] self.status['text'] = 'Decrypting:'+fname try: cli_main(argv) except Exception, e: self.status['text'] = 'Error in: '+ fname + str(e) return self.status['text'] = 'Decrypted directory '+inpath def gui_main(): root = Tkinter.Tk() if AES is None: root.withdraw() tkMessageBox.showerror( "INEPT EPUB Decrypter", "This script requires OpenSSL or PyCrypto, which must be" " installed separately. Read the top-of-script comment for" " details.") return 1 root.title('Automated INEPT EPUB Decrypter (Tetrachroma)') root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) root.mainloop() return 0 if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) sys.exit(gui_main())