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())