From d1d2bfcb1e57ebd8c9226746cf7b151ed6954935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?i=E2=99=A5cabbages?= Date: Tue, 22 Dec 2009 10:54:19 +0000 Subject: [PATCH] =?UTF-8?q?B&N=20tools=20by=20i=E2=99=A5cabbages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw | 235 ++++++++++++++++++ Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw | 112 +++++++++ Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw | 147 +++++++++++ 3 files changed, 494 insertions(+) create mode 100644 Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw create mode 100644 Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw create mode 100644 Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw diff --git a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw new file mode 100644 index 0000000..c5de3ae --- /dev/null +++ b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw @@ -0,0 +1,235 @@ +#! /usr/bin/python + +# ignobleepub.pyw, version 1-rc2 + +# To run this program install Python 2.6 from +# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# (make sure to install the version for Python 2.6). Save this script file as +# ignobleepub.pyw and double-click on it to run it. + +# Revision history: +# 1 - Initial release + +""" +Decrypt Barnes & Noble ADEPT encrypted EPUB books. +""" + +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 + +try: + from Crypto.Cipher import AES +except ImportError: + AES = None + +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.new(bookkey, AES.MODE_CBC) + 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 + + +class ADEPTError(Exception): + pass + +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) + if AES is None: + print "%s: This script requires 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: + keyb64 = f.read() + key = keyb64.decode('base64')[:16] + aes = AES.new(key, AES.MODE_CBC) + 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 B&N 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 = aes.decrypt(bookkey.decode('base64')) + bookkey = bookkey[:-ord(bookkey[-1])] + 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='Select files for decryption') + 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=30) + self.keypath.grid(row=0, column=1, sticky=sticky) + if os.path.exists('bnepubkey.b64'): + self.keypath.insert(0, 'bnepubkey.b64') + button = Tkinter.Button(body, text="...", command=self.get_keypath) + button.grid(row=0, column=2) + Tkinter.Label(body, text='Input file').grid(row=1) + self.inpath = Tkinter.Entry(body, width=30) + 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 file').grid(row=2) + self.outpath = Tkinter.Entry(body, width=30) + 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", width=10, 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 B&N EPUB key file', + defaultextension='.b64', + filetypes=[('base64-encoded files', '.b64'), + ('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.askopenfilename( + parent=None, title='Select B&N-encrypted EPUB file to decrypt', + defaultextension='.epub', filetypes=[('EPUB files', '.epub'), + ('All files', '.*')]) + 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.asksaveasfilename( + parent=None, title='Select unencrypted EPUB file to produce', + defaultextension='.epub', filetypes=[('EPUB files', '.epub'), + ('All files', '.*')]) + 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 file does not exist' + return + if not outpath: + self.status['text'] = 'Output file not specified' + return + if inpath == outpath: + self.status['text'] = 'Must have different input and output files' + return + argv = [sys.argv[0], keypath, inpath, outpath] + self.status['text'] = 'Decrypting...' + try: + cli_main(argv) + except Exception, e: + self.status['text'] = 'Error: ' + str(e) + return + self.status['text'] = 'File successfully decrypted' + +def gui_main(): + root = Tkinter.Tk() + if AES is None: + root.withdraw() + tkMessageBox.showerror( + "Ignoble EPUB Decrypter", + "This script requires PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title('Ignoble EPUB Decrypter') + 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()) diff --git a/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw b/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw new file mode 100644 index 0000000..6f0798a --- /dev/null +++ b/Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw @@ -0,0 +1,112 @@ +#! /usr/bin/python + +# ignoblekey.pyw, version 2 + +# To run this program install Python 2.6 from +# Save this script file as ignoblekey.pyw and double-click on it to run it. + +# Revision history: +# 1 - Initial release +# 2 - Add some missing code + +""" +Retrieve B&N DesktopReader EPUB user AES key. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import binascii +import glob +import Tkinter +import Tkconstants +import tkMessageBox +import traceback + +BN_KEY_KEY = 'uhk00000000' +BN_APPDATA_DIR = r'Barnes & Noble\DesktopReader' + +class IgnobleError(Exception): + pass + +def retrieve_key(inpath, outpath): + # The B&N DesktopReader 'ClientAPI' file is just a sqlite3 DB. Requiring + # users to install sqlite3 and bindings seems like overkill for retrieving + # one value, so we go in hot and dirty. + with open(inpath, 'rb') as f: + data = f.read() + if BN_KEY_KEY not in data: + raise IgnobleError('B&N user key not found; unexpected DB format?') + index = data.rindex(BN_KEY_KEY) + len(BN_KEY_KEY) + 1 + data = data[index:index + 40] + for i in xrange(20, len(data)): + try: + keyb64 = data[:i] + if len(keyb64.decode('base64')) == 20: + break + except binascii.Error: + pass + else: + raise IgnobleError('Problem decoding key; unexpected DB format?') + with open(outpath, 'wb') as f: + f.write(keyb64 + '\n') + +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) + args = argv[1:] + if len(args) != 2: + sys.stderr.write("USAGE: %s CLIENTDB KEYFILE" % (progname,)) + return 1 + inpath, outpath = args + retrieve_key(inpath, outpath) + return 0 + +def find_bnclientdb_path(): + appdata = os.environ['APPDATA'] + bndir = os.path.join(appdata, BN_APPDATA_DIR) + if not os.path.isdir(bndir): + raise IgnobleError('Could not locate B&N Reader installation') + dbpath = glob.glob(os.path.join(bndir, 'ClientAPI_*.db')) + if len(dbpath) == 0: + raise IgnobleError('Problem locating B&N Reader DB') + return sorted(dbpath)[-1] + +class ExceptionDialog(Tkinter.Frame): + def __init__(self, root, text): + Tkinter.Frame.__init__(self, root, border=5) + label = Tkinter.Label(self, text="Unexpected error:", + anchor=Tkconstants.W, justify=Tkconstants.LEFT) + label.pack(fill=Tkconstants.X, expand=0) + self.text = Tkinter.Text(self) + self.text.pack(fill=Tkconstants.BOTH, expand=1) + self.text.insert(Tkconstants.END, text) + +def gui_main(argv=sys.argv): + root = Tkinter.Tk() + root.withdraw() + progname = os.path.basename(argv[0]) + keypath = 'bnepubkey.b64' + try: + dbpath = find_bnclientdb_path() + retrieve_key(dbpath, keypath) + except IgnobleError, e: + tkMessageBox.showerror("Ignoble Key", "Error: " + str(e)) + return 1 + except Exception: + root.wm_state('normal') + root.title('Ignoble Key') + text = traceback.format_exc() + ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1) + root.mainloop() + return 1 + tkMessageBox.showinfo( + "Ignoble Key", "Key successfully retrieved to %s" % (keypath)) + return 0 + +if __name__ == '__main__': + if len(sys.argv) > 1: + sys.exit(cli_main()) + sys.exit(gui_main()) diff --git a/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw new file mode 100644 index 0000000..9d5dc51 --- /dev/null +++ b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw @@ -0,0 +1,147 @@ +#! /usr/bin/python + +# ignoblekeygen.pyw, version 1 + +# To run this program install Python 2.6 from +# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# (make sure to install the version for Python 2.6). Save this script file as +# ignoblekeygen.pyw and double-click on it to run it. + +# Revision history: +# 1 - Initial release + +""" +Generate Barnes & Noble EPUB user key from name and credit card number. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import hashlib +import Tkinter +import Tkconstants +import tkFileDialog +import tkMessageBox + +try: + from Crypto.Cipher import AES +except ImportError: + AES = None + +def normalize_name(name): + return ''.join(x for x in name.lower() if x != ' ') + +def generate_keyfile(name, ccn, outpath): + name = normalize_name(name) + '\x00' + ccn = ccn + '\x00' + name_sha = hashlib.sha1(name).digest()[:16] + ccn_sha = hashlib.sha1(ccn).digest()[:16] + both_sha = hashlib.sha1(name + ccn).digest() + aes = AES.new(ccn_sha, AES.MODE_CBC, name_sha) + crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) + userkey = hashlib.sha1(crypt).digest() + with open(outpath, 'wb') as f: + f.write(userkey.encode('base64')) + return userkey + +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) + if AES is None: + print "%s: This script requires PyCrypto, which must be installed " \ + "separately. Read the top-of-script comment for details." % \ + (progname,) + return 1 + if len(argv) != 4: + print "usage: %s NAME CC# OUTFILE" % (progname,) + return 1 + name, ccn, outpath = argv[1:] + generate_keyfile(name, ccn, outpath) + return 0 + +class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + self.status = Tkinter.Label(self, text='Enter parameters') + 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='Name').grid(row=1) + self.name = Tkinter.Entry(body, width=30) + self.name.grid(row=1, column=1, sticky=sticky) + Tkinter.Label(body, text='CC#').grid(row=2) + self.ccn = Tkinter.Entry(body, width=30) + self.ccn.grid(row=2, column=1, sticky=sticky) + Tkinter.Label(body, text='Output file').grid(row=0) + self.keypath = Tkinter.Entry(body, width=30) + self.keypath.grid(row=0, column=1, sticky=sticky) + self.keypath.insert(0, 'bnepubkey.b64') + button = Tkinter.Button(body, text="...", command=self.get_keypath) + button.grid(row=0, column=2) + buttons = Tkinter.Frame(self) + buttons.pack() + botton = Tkinter.Button( + buttons, text="Generate", width=10, command=self.generate) + 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.asksaveasfilename( + parent=None, title='Select B&N EPUB key file to produce', + defaultextension='.b64', + filetypes=[('base64-encoded files', '.b64'), + ('All Files', '.*')]) + if keypath: + keypath = os.path.normpath(keypath) + self.keypath.delete(0, Tkconstants.END) + self.keypath.insert(0, keypath) + return + + def generate(self): + name = self.name.get() + ccn = self.ccn.get() + keypath = self.keypath.get() + if not name: + self.status['text'] = 'Name not specified' + return + if not ccn: + self.status['text'] = 'Credit card number not specified' + return + if not keypath: + self.status['text'] = 'Output keyfile path not specified' + return + self.status['text'] = 'Generating...' + try: + generate_keyfile(name, ccn, keypath) + except Exception, e: + self.status['text'] = 'Error: ' + str(e) + return + self.status['text'] = 'Keyfile successfully generated' + +def gui_main(): + root = Tkinter.Tk() + if AES is None: + root.withdraw() + tkMessageBox.showerror( + "Ignoble EPUB Keyfile Generator", + "This script requires PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title('Ignoble EPUB Keyfile Generator') + 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())