Lots of B&N updates

This commit is contained in:
NoDRM 2021-12-23 11:29:58 +01:00
parent db71d35b40
commit 3b9c201421
11 changed files with 726 additions and 2544 deletions

View File

@ -41,3 +41,7 @@ List of changes since the fork of Apprentice Harper's repository:
- Add code to support importing multiple decryption keys from ADE (click the 'plus' button multiple times). - Add code to support importing multiple decryption keys from ADE (click the 'plus' button multiple times).
- Improve epubtest.py to also detect Kobo & Apple DRM. - Improve epubtest.py to also detect Kobo & Apple DRM.
- Small updates to the LCP DRM error messages. - Small updates to the LCP DRM error messages.
- Merge ignobleepub into ineptepub so there's no duplicate code.
- Support extracting the B&N / Nook key from the NOOK Microsoft Store application (based on [this script](https://github.com/noDRM/DeDRM_tools/discussions/9) by fesiwi).
- Support extracting the B&N / Nook key from a data dump of the NOOK Android application.
- Support adding an existing B&N key base64 string without having to write it to a file first.

View File

@ -302,13 +302,14 @@ class DeDRM(FileTypePlugin):
# Not an LCP book, do the normal EPUB (Adobe) handling. # Not an LCP book, do the normal EPUB (Adobe) handling.
# import the Barnes & Noble ePub handler # import the Adobe ePub handler
import calibre_plugins.dedrm.ignobleepub as ignobleepub import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name):
#check the book if ineptepub.isPassHashBook(inf.name):
if ignobleepub.ignobleBook(inf.name): # This is an Adobe PassHash / B&N encrypted eBook
print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) print("{0} v{1}: “{2}” is a secure PassHash-protected (B&N) ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
# Attempt to decrypt epub with each encryption key (generated or provided). # Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['bandnkeys'].items(): for keyname, userkey in dedrmprefs['bandnkeys'].items():
@ -318,7 +319,7 @@ class DeDRM(FileTypePlugin):
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name) result = ineptepub.decryptBook(userkey, inf.name, of.name)
except: except:
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
@ -334,29 +335,53 @@ class DeDRM(FileTypePlugin):
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime)) print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
# perhaps we should see if we can get a key from a log file # perhaps we should see if we can get a key from a log file
print("{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Looking for new NOOK Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# get the default NOOK Study keys # get the default NOOK keys
defaultkeys = [] defaultkeys = []
###### Add keys from the NOOK Study application (ignoblekeyNookStudy.py)
try: try:
if iswindows or isosx: if iswindows or isosx:
from calibre_plugins.dedrm.ignoblekey import nookkeys from calibre_plugins.dedrm.ignoblekeyNookStudy import nookkeys
defaultkeys = nookkeys() defaultkeys_study = nookkeys()
else: # linux else: # linux
from .wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,"ignoblekey.py") scriptpath = os.path.join(self.alfdir,"ignoblekeyNookStudy.py")
defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix']) defaultkeys_study = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
except: except:
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
###### Add keys from the NOOK Microsoft Store application (ignoblekeyNookStudy.py)
try:
if iswindows:
# That's a Windows store app, it won't run on Linux or MacOS anyways.
# No need to waste time running Wine.
from calibre_plugins.dedrm.ignoblekeyWindowsStore import dump_keys as dump_nook_keys
defaultkeys_store = dump_nook_keys(False)
except:
print("{0} v{1}: Exception when getting default NOOK Microsoft App keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc()
###### Check if one of the new keys decrypts the book:
newkeys = [] newkeys = []
for keyvalue in defaultkeys: for keyvalue in defaultkeys_study:
if keyvalue not in dedrmprefs['bandnkeys'].values(): if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
newkeys.append(keyvalue)
if iswindows:
for keyvalue in defaultkeys_store:
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
newkeys.append(keyvalue) newkeys.append(keyvalue)
if len(newkeys) > 0: if len(newkeys) > 0:
@ -368,7 +393,7 @@ class DeDRM(FileTypePlugin):
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name) result = ineptepub.decryptBook(userkey, inf.name, of.name)
except: except:
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
@ -381,7 +406,7 @@ class DeDRM(FileTypePlugin):
# Store the new successful key in the defaults # Store the new successful key in the defaults
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue) dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_key_'+time.strftime("%Y-%m-%d"),keyvalue)
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
@ -391,16 +416,13 @@ class DeDRM(FileTypePlugin):
return self.postProcessEPUB(of.name) return self.postProcessEPUB(of.name)
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception as e:
except:
pass pass
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) else:
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) # This is a "normal" Adobe eBook.
# import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name):
book_uuid = None book_uuid = None
try: try:
# This tries to figure out which Adobe account UUID the book is licensed for. # This tries to figure out which Adobe account UUID the book is licensed for.

View File

@ -6,12 +6,12 @@ __license__ = 'GPL v3'
# Python 3, September 2020 # Python 3, September 2020
# Standard Python modules. # Standard Python modules.
import sys, os, traceback, json, codecs import sys, os, traceback, json, codecs, base64
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit, from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox, QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl, QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
QCheckBox) QCheckBox, QComboBox)
from PyQt5 import Qt as QtGui from PyQt5 import Qt as QtGui
from zipfile import ZipFile from zipfile import ZipFile
@ -113,8 +113,8 @@ class ConfigWidget(QWidget):
button_layout = QVBoxLayout() button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout) keys_group_box_layout.addLayout(button_layout)
self.bandn_button = QtGui.QPushButton(self) self.bandn_button = QtGui.QPushButton(self)
self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks")) self.bandn_button.setToolTip(_("Click to manage keys for ADE books with PassHash algorithm. <br/>Commonly used by Barnes and Noble"))
self.bandn_button.setText("Barnes and Noble ebooks") self.bandn_button.setText("ADE PassHash (B&&N) ebooks")
self.bandn_button.clicked.connect(self.bandn_keys) self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self) self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks")) self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks"))
@ -196,7 +196,7 @@ class ConfigWidget(QWidget):
d.exec_() d.exec_()
def bandn_keys(self): def bandn_keys(self):
d = ManageKeysDialog(self,"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64') d = ManageKeysDialog(self,"ADE PassHash Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
d.exec_() d.exec_()
def ereader_keys(self): def ereader_keys(self):
@ -566,79 +566,173 @@ class RenameKeyDialog(QDialog):
class AddBandNKeyDialog(QDialog): class AddBandNKeyDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle("{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox("", self) def update_form(self, idx):
layout.addWidget(data_group_box) self.cbType.hide()
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout() if idx == 1:
data_group_box_layout.addLayout(key_group) self.add_fields_for_passhash()
key_group.addWidget(QLabel("Unique Key Name:", self)) elif idx == 2:
self.add_fields_for_b64_passhash()
elif idx == 3:
self.add_fields_for_windows_nook()
elif idx == 4:
self.add_fields_for_android_nook()
def add_fields_for_android_nook(self):
self.andr_nook_group_box = QGroupBox("", self)
andr_nook_group_box_layout = QVBoxLayout()
self.andr_nook_group_box.setLayout(andr_nook_group_box_layout)
self.layout.addWidget(self.andr_nook_group_box)
ph_key_name_group = QHBoxLayout()
andr_nook_group_box_layout.addLayout(ph_key_name_group)
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>"))
ph_key_name_group.addWidget(self.key_ledit)
andr_nook_group_box_layout.addWidget(QLabel("Hidden in the Android application data is a " +
"folder\nnamed '.adobe-digital-editions'. Please enter\nthe full path to that folder.", self))
ph_path_group = QHBoxLayout()
andr_nook_group_box_layout.addLayout(ph_path_group)
ph_path_group.addWidget(QLabel("Path:", self))
self.cc_ledit = QLineEdit("", self)
self.cc_ledit.setToolTip(_("<p>Enter path to .adobe-digital-editions folder.</p>"))
ph_path_group.addWidget(self.cc_ledit)
self.button_box.hide()
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept_android_nook)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def add_fields_for_windows_nook(self):
self.win_nook_group_box = QGroupBox("", self)
win_nook_group_box_layout = QVBoxLayout()
self.win_nook_group_box.setLayout(win_nook_group_box_layout)
self.layout.addWidget(self.win_nook_group_box)
ph_key_name_group = QHBoxLayout()
win_nook_group_box_layout.addLayout(ph_key_name_group)
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>"))
ph_key_name_group.addWidget(self.key_ledit)
self.button_box.hide()
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept_win_nook)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def add_fields_for_b64_passhash(self):
self.passhash_group_box = QGroupBox("", self)
passhash_group_box_layout = QVBoxLayout()
self.passhash_group_box.setLayout(passhash_group_box_layout)
self.layout.addWidget(self.passhash_group_box)
ph_key_name_group = QHBoxLayout()
passhash_group_box_layout.addLayout(ph_key_name_group)
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("", self) self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" + self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
"<p>It should be something that will help you remember " + "<p>It should be something that will help you remember " +
"what personal information was used to create it.")) "what personal information was used to create it."))
key_group.addWidget(self.key_ledit) ph_key_name_group.addWidget(self.key_ledit)
name_group = QHBoxLayout() ph_name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group) passhash_group_box_layout.addLayout(ph_name_group)
name_group.addWidget(QLabel("B&N/nook account email address:", self)) ph_name_group.addWidget(QLabel("Base64 key string:", self))
self.name_ledit = QLineEdit("", self)
self.name_ledit.setToolTip(_("<p>Enter your email address as it appears in your B&N " +
"account.</p>" +
"<p>It will only be used to generate this " +
"key and won\'t be stored anywhere " +
"in calibre or on your computer.</p>" +
"<p>eg: apprenticeharper@gmail.com</p>"))
name_group.addWidget(self.name_ledit)
name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self)
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(name_disclaimer_label)
ccn_group = QHBoxLayout()
data_group_box_layout.addLayout(ccn_group)
ccn_group.addWidget(QLabel("B&N/nook account password:", self))
self.cc_ledit = QLineEdit("", self) self.cc_ledit = QLineEdit("", self)
self.cc_ledit.setToolTip(_("<p>Enter the password " + self.cc_ledit.setToolTip(_("<p>Enter the Base64 key string</p>"))
"for your B&N account.</p>" + ph_name_group.addWidget(self.cc_ledit)
"<p>The password will only be used to generate this " +
"key and won\'t be stored anywhere in " +
"calibre or on your computer."))
ccn_group.addWidget(self.cc_ledit)
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
data_group_box_layout.addWidget(ccn_disclaimer_label)
layout.addSpacing(10)
self.chkOldAlgo = QCheckBox(_("Try to use the old algorithm")) self.button_box.hide()
self.chkOldAlgo.setToolTip(_("Leave this off if you're unsure."))
data_group_box_layout.addWidget(self.chkOldAlgo)
layout.addSpacing(10)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Retrieved key:", self))
self.key_display = QLabel("", self)
self.key_display.setToolTip(_("Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
key_group.addWidget(self.key_display)
self.retrieve_button = QtGui.QPushButton(self)
self.retrieve_button.setToolTip(_("Click to retrieve your B&N encryption key from the B&N servers"))
self.retrieve_button.setText("Retrieve Key")
self.retrieve_button.clicked.connect(self.retrieve_key)
key_group.addWidget(self.retrieve_button)
layout.addSpacing(10)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept) self.button_box.accepted.connect(self.accept_b64_passhash)
self.button_box.rejected.connect(self.reject) self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box) self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def add_fields_for_passhash(self):
self.passhash_group_box = QGroupBox("", self)
passhash_group_box_layout = QVBoxLayout()
self.passhash_group_box.setLayout(passhash_group_box_layout)
self.layout.addWidget(self.passhash_group_box)
ph_key_name_group = QHBoxLayout()
passhash_group_box_layout.addLayout(ph_key_name_group)
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
"<p>It should be something that will help you remember " +
"what personal information was used to create it."))
ph_key_name_group.addWidget(self.key_ledit)
ph_name_group = QHBoxLayout()
passhash_group_box_layout.addLayout(ph_name_group)
ph_name_group.addWidget(QLabel("Username:", self))
self.name_ledit = QLineEdit("", self)
self.name_ledit.setToolTip(_("<p>Enter the PassHash username</p>"))
ph_name_group.addWidget(self.name_ledit)
ph_pass_group = QHBoxLayout()
passhash_group_box_layout.addLayout(ph_pass_group)
ph_pass_group.addWidget(QLabel("Password:", self))
self.cc_ledit = QLineEdit("", self)
self.cc_ledit.setToolTip(_("<p>Enter the PassHash password</p>"))
ph_pass_group.addWidget(self.cc_ledit)
self.button_box.hide()
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept_passhash)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle("{0} {1}: Create New PassHash (B&N) Key".format(PLUGIN_NAME, PLUGIN_VERSION))
self.layout = QVBoxLayout(self)
self.setLayout(self.layout)
self.cbType = QComboBox()
self.cbType.addItem("--- Select key type ---")
self.cbType.addItem("Adobe PassHash username & password")
self.cbType.addItem("Base64-encoded PassHash key string")
self.cbType.addItem("Extract key from Nook Windows application")
self.cbType.addItem("Extract key from Nook Android application")
self.cbType.currentIndexChanged.connect(self.update_form, self.cbType.currentIndex())
self.layout.addWidget(self.cbType)
self.button_box = QDialogButtonBox(QDialogButtonBox.Cancel)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.resize(self.sizeHint()) self.resize(self.sizeHint())
@ -648,7 +742,7 @@ class AddBandNKeyDialog(QDialog):
@property @property
def key_value(self): def key_value(self):
return str(self.key_display.text()).strip() return self.result_data
@property @property
def user_name(self): def user_name(self):
@ -658,40 +752,108 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self): def cc_number(self):
return str(self.cc_ledit.text()).strip() return str(self.cc_ledit.text()).strip()
def retrieve_key(self): def accept_android_nook(self):
if self.chkOldAlgo.isChecked(): if len(self.key_name) < 4:
# old method, try to generate errmsg = "Key name must be at <i>least</i> 4 characters long!"
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
generated_key = generate_bandn_key(self.user_name, self.cc_number)
if generated_key == "":
errmsg = "Could not generate key."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
else:
self.key_display.setText(generated_key.decode("latin-1"))
else:
# New method, try to connect to server
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
fetched_key = fetch_bandn_key(self.user_name,self. cc_number)
if fetched_key == "":
errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
else:
self.key_display.setText(fetched_key)
def accept(self): path_to_ade_data = self.cc_number
if (os.path.isfile(os.path.join(path_to_ade_data, ".adobe-digital-editions", "activation.xml"))):
path_to_ade_data = os.path.join(path_to_ade_data, ".adobe-digital-editions")
elif (os.path.isfile(os.path.join(path_to_ade_data, "activation.xml"))):
pass
else:
errmsg = "This isn't the correct path, or the data is invalid."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
from calibre_plugins.dedrm.ignoblekeyAndroid import dump_keys
store_result = dump_keys(path_to_ade_data)
if len(store_result) == 0:
errmsg = "Failed to extract keys. Is this the correct folder?"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
self.result_data = store_result[0]
QDialog.accept(self)
def accept_win_nook(self):
if len(self.key_name) < 4:
errmsg = "Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
try:
from calibre_plugins.dedrm.ignoblekeyWindowsStore import dump_keys
store_result = dump_keys(False)
except:
errmsg = "Failed to import from Nook Microsoft Store app."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(store_result) == 0:
# Nothing found, try the Nook Study app
from calibre_plugins.dedrm.ignoblekeyNookStudy import nookkeys
store_result = nookkeys()
# Take the first key we found. In the future it might be a good idea to import them all,
# but with how the import dialog is currently structured that's not easily possible.
if len(store_result) > 0:
self.result_data = store_result[0]
QDialog.accept(self)
return
# Okay, we didn't find anything. How do we get rid of the window?
errmsg = "Didn't find any Nook keys in the Windows app."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.reject(self)
def accept_b64_passhash(self):
if len(self.key_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.cc_number.isspace():
errmsg = "All fields are required!"
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 = "Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
try:
x = base64.b64decode(self.cc_number)
except:
errmsg = "Key data is no valid base64 string!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
self.result_data = self.cc_number
QDialog.accept(self)
def accept_passhash(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(): 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 = "All fields are required!" errmsg = "All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4: if len(self.key_name) < 4:
errmsg = "Key name must be at <i>least</i> 4 characters long!" errmsg = "Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_value) == 0:
self.retrieve_key() try:
if len(self.key_value) == 0: from calibre_plugins.dedrm.ignoblekeyGenPassHash import generate_key
return self.result_data = generate_key(self.user_name, self.cc_number)
except:
errmsg = "Key generation failed."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.result_data) == 0:
errmsg = "Key generation failed."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self) QDialog.accept(self)
class AddEReaderDialog(QDialog): class AddEReaderDialog(QDialog):
def __init__(self, parent=None,): def __init__(self, parent=None,):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)

View File

@ -0,0 +1,65 @@
'''
Extracts the user's ccHash from an .adobe-digital-editions folder
typically included in the Nook Android app's data folder.
Based on ignoblekeyWindowsStore.py, updated for Android by noDRM.
'''
import sys
import os
import base64
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
def unpad(data):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
def dump_keys(path_to_adobe_folder):
activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
device_path = os.path.join(path_to_adobe_folder, "device.xml")
if not os.path.isfile(activation_path):
print("Nook activation file is missing: %s\n" % activation_path)
return []
if not os.path.isfile(device_path):
print("Nook device file is missing: %s\n" % device_path)
return []
# Load files:
activation_xml = etree.parse(activation_path)
device_xml = etree.parse(device_path)
# Get fingerprint:
device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text
device_fingerprint = base64.b64decode(device_fingerprint).hex()
hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16]
hashes = []
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
encrypted_cc_hash = base64.b64decode(pass_hash.text)
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
hashes.append(base64.b64encode(cc_hash).decode("ascii"))
#print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
return hashes
if __name__ == "__main__":
print("No standalone version available.")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ignoblekeygen.py # ignoblekeyGenPassHash.py
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al. # Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3

View File

@ -0,0 +1,75 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
'''
Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app.
https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h
(Requires a recent Windows version in a supported region (US).)
This procedure has been tested with Nook app version 1.11.0.4 under Windows 11.
Based on experimental standalone python script created by fesiwi at
https://github.com/noDRM/DeDRM_tools/discussions/9
'''
import sys, os
import apsw
import base64
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
def unpad(data):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
def dump_keys(print_result=False):
db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
if not os.path.isfile(db_filename):
print("Database file not found. Is the Nook Windows Store app installed?")
return []
# Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result.
# There should only be one result anyways.
serial_number = apsw.Connection(db_filename).cursor().execute(
"SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0]
hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml")
if not os.path.isfile(activation_file_name):
print("Activation file not found. Are you logged in to your Nook account?")
return []
activation_xml = etree.parse(activation_file_name)
decrypted_hashes = []
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
encrypted_cc_hash = base64.b64decode(pass_hash.text)
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
if print_result:
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
return decrypted_hashes
if __name__ == "__main__":
dump_keys(True)

View File

@ -25,7 +25,12 @@
# 2.0 - Python 3 for calibre 5.0 # 2.0 - Python 3 for calibre 5.0
""" """
Fetch Barnes & Noble EPUB user key from B&N servers using email and password Fetch Barnes & Noble EPUB user key from B&N servers using email and password.
NOTE: This script used to work in the past, but the server it uses is long gone.
It can no longer be used to download keys from B&N servers, it is no longer
supported by the Calibre plugin, and it will be removed in the future.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ineptepub.py # ineptepub.py
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al. # Copyright © 2009-2021 by i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
@ -30,18 +30,19 @@
# 6.5 - Completely remove erroneous check on DER file sanity # 6.5 - Completely remove erroneous check on DER file sanity
# 6.6 - Import tkFileDialog, don't assume something else will import it. # 6.6 - Import tkFileDialog, don't assume something else will import it.
# 7.0 - Add Python 3 compatibility for calibre 5.0 # 7.0 - Add Python 3 compatibility for calibre 5.0
# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
""" """
Decrypt Adobe Digital Editions encrypted ePub books. Decrypt Adobe Digital Editions encrypted ePub books.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "7.0" __version__ = "7.1"
import codecs
import sys import sys
import os import os
import traceback import traceback
import base64
import zlib import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
@ -210,6 +211,11 @@ def _load_crypto_libcrypto():
return (AES, RSA) return (AES, RSA)
def _load_crypto_pycrypto(): def _load_crypto_pycrypto():
try:
from Cryptodome.Cipher import AES as _AES
from Cryptodome.PublicKey import RSA as _RSA
from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
except:
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES
from Crypto.PublicKey import RSA as _RSA from Crypto.PublicKey import RSA as _RSA
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
@ -417,13 +423,32 @@ def adeptBook(inpath):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172: if len(bookkey) in [192, 172, 64]:
return True return True
except: except:
# if we couldn't check, assume it is # if we couldn't check, assume it is
return True return True
return False return False
def isPassHashBook(inpath):
# If this is an Adobe book, check if it's a PassHash-encrypted book (B&N)
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:
return False
try:
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))
if len(bookkey) == 64:
return True
except:
pass
return False
# Checks the license file and returns the UUID the book is licensed for. # Checks the license file and returns the UUID the book is licensed for.
# This is used so that the Calibre plugin can pick the correct decryption key # This is used so that the Calibre plugin can pick the correct decryption key
# first try without having to loop through all possible keys. # first try without having to loop through all possible keys.
@ -463,7 +488,7 @@ def verify_book_key(bookkey):
def decryptBook(userkey, inpath, outpath): def decryptBook(userkey, inpath, outpath):
if AES is None: if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed.") raise ADEPTError("PyCrypto or OpenSSL must be installed.")
rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf: with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist() namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \ if 'META-INF/rights.xml' not in namelist or \
@ -483,10 +508,32 @@ def decryptBook(userkey, inpath, outpath):
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).") print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.") print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
raise ADEPTNewVersionError("Book uses new ADEPT encryption") raise ADEPTNewVersionError("Book uses new ADEPT encryption")
if len(bookkey) != 172:
print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))) if len(bookkey) == 172:
print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
elif len(bookkey) == 64:
print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
else:
print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath)))
return 1 return 1
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
if len(bookkey) != 64:
# Normal Adobe ADEPT
rsa = RSA(userkey)
bookkey = rsa.decrypt(base64.b64decode(bookkey.encode('ascii')))
else:
# Adobe PassHash / B&N
key = base64.b64decode(userkey)[:16]
aes = AES(key)
bookkey = aes.decrypt(base64.b64decode(bookkey))
if type(bookkey[-1]) != int:
pad = ord(bookkey[-1])
else:
pad = bookkey[-1]
bookkey = bookkey[:-pad]
# Padded as per RSAES-PKCS1-v1_5 # Padded as per RSAES-PKCS1-v1_5
if len(bookkey) > 16: if len(bookkey) > 16:
if verify_book_key(bookkey): if verify_book_key(bookkey):
@ -494,6 +541,7 @@ def decryptBook(userkey, inpath, outpath):
else: else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2 return 2
encryption = inf.read('META-INF/encryption.xml') encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption) decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from calibre_plugins.dedrm.ignoblekeygen import generate_key from calibre_plugins.dedrm.ignoblekeyGenPassHash import generate_key
__license__ = 'GPL v3' __license__ = 'GPL v3'