]> xmof Git - DeDRM.git/commitdiff
B&N tools by i♥cabbages
authori♥cabbages <i♥cabbages@blogspot.co.uk>
Tue, 22 Dec 2009 10:54:19 +0000 (10:54 +0000)
committerApprentice Alf <apprenticealf@gmail.com>
Sat, 28 Feb 2015 10:54:59 +0000 (10:54 +0000)
Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw [new file with mode: 0644]
Barnes_and_Noble_EPUB_Tools/ignoblekey.pyw [new file with mode: 0644]
Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw [new file with mode: 0644]

diff --git a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw
new file mode 100644 (file)
index 0000000..c5de3ae
--- /dev/null
@@ -0,0 +1,235 @@
+#! /usr/bin/python
+
+# ignobleepub.pyw, version 1-rc2
+
+# To run this program install Python 2.6 from <http://www.python.org/download/>
+# 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 (file)
index 0000000..6f0798a
--- /dev/null
@@ -0,0 +1,112 @@
+#! /usr/bin/python
+
+# ignoblekey.pyw, version 2
+
+# To run this program install Python 2.6 from <http://www.python.org/download/>
+# 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 (file)
index 0000000..9d5dc51
--- /dev/null
@@ -0,0 +1,147 @@
+#! /usr/bin/python
+
+# ignoblekeygen.pyw, version 1
+
+# To run this program install Python 2.6 from <http://www.python.org/download/>
+# 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())