From 45ad3cedece5e18b552edbe9aae8af006197de7e Mon Sep 17 00:00:00 2001 From: Apprentice Harper Date: Fri, 10 Apr 2015 18:12:39 +0100 Subject: [PATCH] Added in fetching B&N key via URL instead of generating from name & CC# --- .../DeDRM_Barnes and Noble Key_Help.htm | 30 +-- DeDRM_calibre_plugin/DeDRM_plugin/config.py | 30 +-- .../DeDRM_plugin/ignoblekeyfetch.py | 238 ++++++++++++++++++ 3 files changed, 263 insertions(+), 35 deletions(-) create mode 100644 DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm b/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm index 47da891..aba641d 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm +++ b/DeDRM_calibre_plugin/DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm @@ -1,4 +1,4 @@ - @@ -24,28 +24,17 @@ li {margin-top: 0.5em}

Changes at Barnes & Noble

-

In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that some users will find that no combination of their name and CC# will work in decrypting their ebooks.

+

In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.

-

There is a work-around. Barnes & Noble’s desktop app NOOK Study generates a log file that contains the encryption key. You can download NOOK Study from https://yuzu.com/nsdownload.

-

Once downloaded, install the application, register with your Barnes & Noble or nook account, and download at least one DRMed ebook through NOOK Study. It will be saved somewhere in a folder called "My Barnes & Noble eBooks" in your Documents folder.

-

Now import that book into calibre. The log file and the key in the log should be automatically found by the plugin and used to decrypt the book.

-

If the automatic process doesn't work for you, you can still find extract it manually and save it as a .b64 file for import into the plugin's preferences as follows:

-
  1. In NOOK Study, select Settings/About (Windows) or NOOK Study/About NOOK Study (Mac) and in the dialog that appears click the link at the bottom to copy the log into the clipboard.
  2. -
  3. Paste the copied log into a text editor
  4. -
  5. Search for the text CCHashResponseV1
  6. -
  7. On the line below which starts with ccHash, copy the text between the " marks after ccHash, but don't include the " marks.
  8. -
  9. Save that text in a new plain text file, with file name extension .b64 (for example, key.b64)
  10. -
  11. Import that file into the preferences through this dialog, using the "Import Existing Key Files" button.
  12. -
+

Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin. - -

Old instructions: Creating New Keys:

+

Creating New Keys:

On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.

Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.

@@ -69,6 +58,11 @@ li {margin-top: 0.5em}

Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.

+

NOOK Study

+

Books downloaded through NOOK Study may or may not use the key fetched using the above method. If a books is not decrypted successfully with any of the keys, the plugin will attempt to recover a key from the NOOK Study log file and use that.

+ + + diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/config.py b/DeDRM_calibre_plugin/DeDRM_plugin/config.py index d474257..432a0ee 100644 --- a/DeDRM_calibre_plugin/DeDRM_plugin/config.py +++ b/DeDRM_calibre_plugin/DeDRM_plugin/config.py @@ -546,14 +546,14 @@ class AddBandNKeyDialog(QDialog): name_group = QHBoxLayout() data_group_box_layout.addLayout(name_group) - name_group.addWidget(QLabel(u"Your Name:", self)) + name_group.addWidget(QLabel(u"B&N/nook account email address:", self)) self.name_ledit = QLineEdit(u"", self) - self.name_ledit.setToolTip(_(u"

Enter your name as it appears in your B&N " + - u"account or on your credit card.

" + + self.name_ledit.setToolTip(_(u"

Enter your email address as it appears in your B&N " + + u"account.

" + u"

It will only be used to generate this " + - u"one-time key and won\'t be stored anywhere " + + u"key and won\'t be stored anywhere " + u"in calibre or on your computer.

" + - u"

(ex: Jonathan Smith)")) + u"

eg: apprenticeharper@gmail.com

")) name_group.addWidget(self.name_ledit) name_disclaimer_label = QLabel(_(u"(Will not be saved in configuration data)"), self) name_disclaimer_label.setAlignment(Qt.AlignHCenter) @@ -561,13 +561,12 @@ class AddBandNKeyDialog(QDialog): ccn_group = QHBoxLayout() data_group_box_layout.addLayout(ccn_group) - ccn_group.addWidget(QLabel(u"Credit Card#:", self)) + ccn_group.addWidget(QLabel(u"B&N/nook account password:", self)) self.cc_ledit = QLineEdit(u"", self) - self.cc_ledit.setToolTip(_(u"

Enter the full credit card number on record " + - u"in your B&N account.

" + - u"

No spaces or dashes... just the numbers. " + - u"This number will only be used to generate this " + - u"one-time key and won\'t be stored anywhere in " + + self.cc_ledit.setToolTip(_(u"

Enter the password " + + u"for your B&N account.

" + + u"

The password will only be used to generate this " + + u"key and won\'t be stored anywhere in " + u"calibre or on your computer.")) ccn_group.addWidget(self.cc_ledit) ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self) @@ -588,8 +587,8 @@ class AddBandNKeyDialog(QDialog): @property def key_value(self): - from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key - return generate_bandn_key(self.user_name,self.cc_number) + from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key + return fetch_bandn_key(self.user_name,self.cc_number) @property def user_name(self): @@ -597,16 +596,13 @@ class AddBandNKeyDialog(QDialog): @property def cc_number(self): - return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','') + return unicode(self.cc_ledit.text()).strip() def accept(self): if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace(): errmsg = u"All fields are required!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - if not self.cc_number.isdigit(): - errmsg = u"Numbers only in the credit card number field!" - return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) if len(self.key_name) < 4: errmsg = u"Key name must be at least 4 characters long!" return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) diff --git a/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py b/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py new file mode 100644 index 0000000..5e62b5c --- /dev/null +++ b/DeDRM_calibre_plugin/DeDRM_plugin/ignoblekeyfetch.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +# ignoblekeyfetch.pyw, version 1.0 +# Copyright © 2015 Apprentice Harper + +# Released under the terms of the GNU General Public Licence, version 3 +# + +# Based on discoveries by "Nobody You Know" + +# Windows users: Before running this program, you must first install Python. +# We recommend ActiveState Python 2.7.X for Windows (x86) from +# http://www.activestate.com/activepython/downloads. +# You must also install PyCrypto from +# http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# (make certain to install the version for Python 2.7). +# Then save this script file as ignoblekeygen.pyw and double-click on it to run it. +# +# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this +# program from the command line (python ignoblekeygen.pyw) or by double-clicking +# it when it has been associated with PythonLauncher. + +# Revision history: +# 1.0 - Initial release + +""" +Fetch Barnes & Noble EPUB user key from B&N servers using email and password +""" + +__license__ = 'GPL v3' +__version__ = "1.0" + +import sys +import os + +# Wrap a stream so that output gets flushed immediately +# and also make sure that any unicode strings get +# encoded using "replace" before writing them. +class SafeUnbuffered: + def __init__(self, stream): + self.stream = stream + self.encoding = stream.encoding + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +try: + from calibre.constants import iswindows, isosx +except: + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') + +def unicode_argv(): + if iswindows: + # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode + # strings. + + # Versions 2.x of Python don't support Unicode in sys.argv on + # Windows, with the underlying Windows API instead replacing multi-byte + # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv + # as a list of Unicode strings and encode them as utf-8 + + from ctypes import POINTER, byref, cdll, c_int, windll + from ctypes.wintypes import LPCWSTR, LPWSTR + + GetCommandLineW = cdll.kernel32.GetCommandLineW + GetCommandLineW.argtypes = [] + GetCommandLineW.restype = LPCWSTR + + CommandLineToArgvW = windll.shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] + CommandLineToArgvW.restype = POINTER(LPWSTR) + + cmd = GetCommandLineW() + argc = c_int(0) + argv = CommandLineToArgvW(cmd, byref(argc)) + if argc.value > 0: + # Remove Python executable and commands if present + start = argc.value - len(sys.argv) + return [argv[i] for i in + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"ignoblekeygen.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = "utf-8" + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + + +class IGNOBLEError(Exception): + pass + +def fetch_key(email, password): + # remove spaces and case from name and CC numbers. + if type(email)==unicode: + email = email.encode('utf-8') + if type(password)==unicode: + password = password.encode('utf-8') + + import random + random = "%030x" % random.randrange(16**30) + + import urllib, urllib2 + fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" + fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress=" + fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" + #print fetch_url + + found = '' + try: + req = urllib2.Request(fetch_url) + response = urllib2.urlopen(req) + the_page = response.read() + + import re + + found = re.search('ccHash>(.+?) ".format(progname) + return 1 + email, password, keypath = argv[1:] + userkey = fetch_key(email, password) + if len(userkey) == 28: + open(keypath,'wb').write(userkey) + return 0 + print u"Failed to fetch key." + return 1 + + +def gui_main(): + try: + import Tkinter + import Tkconstants + import tkMessageBox + import traceback + except: + return cli_main() + + class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + self.status = Tkinter.Label(self, text=u"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=u"Account email address").grid(row=0) + self.name = Tkinter.Entry(body, width=40) + self.name.grid(row=0, column=1, sticky=sticky) + Tkinter.Label(body, text=u"Account password").grid(row=1) + self.ccn = Tkinter.Entry(body, width=40) + self.ccn.grid(row=1, column=1, sticky=sticky) + Tkinter.Label(body, text=u"Output file").grid(row=2) + self.keypath = Tkinter.Entry(body, width=40) + self.keypath.grid(row=2, column=1, sticky=sticky) + self.keypath.insert(2, u"bnepubkey.b64") + button = Tkinter.Button(body, text=u"...", command=self.get_keypath) + button.grid(row=2, column=2) + buttons = Tkinter.Frame(self) + buttons.pack() + botton = Tkinter.Button( + buttons, text=u"Fetch", width=10, command=self.generate) + botton.pack(side=Tkconstants.LEFT) + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + button = Tkinter.Button( + buttons, text=u"Quit", width=10, command=self.quit) + button.pack(side=Tkconstants.RIGHT) + + def get_keypath(self): + keypath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".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): + email = self.name.get() + password = self.ccn.get() + keypath = self.keypath.get() + if not email: + self.status['text'] = u"Email address not given" + return + if not password: + self.status['text'] = u"Account password not given" + return + if not keypath: + self.status['text'] = u"Output keyfile path not set" + return + self.status['text'] = u"Fetching..." + try: + userkey = fetch_key(email, password) + except Exception, e: + self.status['text'] = u"Error: {0}".format(e.args[0]) + return + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" + + root = Tkinter.Tk() + root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__)) + 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())