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).
- Improve epubtest.py to also detect Kobo & Apple DRM.
- 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.
# import the Barnes & Noble ePub handler
import calibre_plugins.dedrm.ignobleepub as ignobleepub
# import the Adobe ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name):
#check the book
if ignobleepub.ignobleBook(inf.name):
print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
if ineptepub.isPassHashBook(inf.name):
# This is an Adobe PassHash / B&N encrypted 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).
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.
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
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()
@ -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))
# 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 = []
###### Add keys from the NOOK Study application (ignoblekeyNookStudy.py)
try:
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
from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
scriptpath = os.path.join(self.alfdir,"ignoblekeyNookStudy.py")
defaultkeys_study = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
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))
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 = []
for keyvalue in defaultkeys:
if keyvalue not in dedrmprefs['bandnkeys'].values():
for keyvalue in defaultkeys_study:
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)
if len(newkeys) > 0:
@ -368,7 +393,7 @@ class DeDRM(FileTypePlugin):
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
result = ineptepub.decryptBook(userkey, inf.name, of.name)
except:
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()
@ -381,7 +406,7 @@ class DeDRM(FileTypePlugin):
# Store the new successful key in the defaults
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_key_'+time.strftime("%Y-%m-%d"),keyvalue)
dedrmprefs.writeprefs()
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except:
@ -391,16 +416,13 @@ class DeDRM(FileTypePlugin):
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))
except Exception as e:
except:
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))
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))
else:
# 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
try:
# 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
# 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,
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
QCheckBox)
QCheckBox, QComboBox)
from PyQt5 import Qt as QtGui
from zipfile import ZipFile
@ -113,8 +113,8 @@ class ConfigWidget(QWidget):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self.bandn_button = QtGui.QPushButton(self)
self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks"))
self.bandn_button.setText("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("ADE PassHash (B&&N) ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks"))
@ -196,7 +196,7 @@ class ConfigWidget(QWidget):
d.exec_()
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_()
def ereader_keys(self):
@ -566,79 +566,173 @@ class RenameKeyDialog(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)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
def update_form(self, idx):
self.cbType.hide()
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Unique Key Name:", self))
if idx == 1:
self.add_fields_for_passhash()
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.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."))
key_group.addWidget(self.key_ledit)
ph_key_name_group.addWidget(self.key_ledit)
name_group = QHBoxLayout()
data_group_box_layout.addLayout(name_group)
name_group.addWidget(QLabel("B&N/nook account email address:", 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))
ph_name_group = QHBoxLayout()
passhash_group_box_layout.addLayout(ph_name_group)
ph_name_group.addWidget(QLabel("Base64 key string:", self))
self.cc_ledit = QLineEdit("", self)
self.cc_ledit.setToolTip(_("<p>Enter the password " +
"for your B&N account.</p>" +
"<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.cc_ledit.setToolTip(_("<p>Enter the Base64 key string</p>"))
ph_name_group.addWidget(self.cc_ledit)
self.chkOldAlgo = QCheckBox(_("Try to use the old algorithm"))
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.hide()
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)
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())
@ -648,7 +742,7 @@ class AddBandNKeyDialog(QDialog):
@property
def key_value(self):
return str(self.key_display.text()).strip()
return self.result_data
@property
def user_name(self):
@ -658,40 +752,108 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return str(self.cc_ledit.text()).strip()
def retrieve_key(self):
def accept_android_nook(self):
if self.chkOldAlgo.isChecked():
# old method, try to generate
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
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)
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)
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():
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)
if len(self.key_value) == 0:
self.retrieve_key()
if len(self.key_value) == 0:
return
try:
from calibre_plugins.dedrm.ignoblekeyGenPassHash import generate_key
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)
class AddEReaderDialog(QDialog):
def __init__(self, parent=None,):
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
# -*- coding: utf-8 -*-
# ignoblekeygen.py
# ignoblekeyGenPassHash.py
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# 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
"""
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'

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# <http://www.gnu.org/licenses/>
@ -30,18 +30,19 @@
# 6.5 - Completely remove erroneous check on DER file sanity
# 6.6 - Import tkFileDialog, don't assume something else will import it.
# 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.
"""
__license__ = 'GPL v3'
__version__ = "7.0"
__version__ = "7.1"
import codecs
import sys
import os
import traceback
import base64
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
@ -210,6 +211,11 @@ def _load_crypto_libcrypto():
return (AES, RSA)
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.PublicKey import RSA as _RSA
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)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172:
if len(bookkey) in [192, 172, 64]:
return True
except:
# if we couldn't check, assume it is
return True
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.
# This is used so that the Calibre plugin can pick the correct decryption key
# first try without having to loop through all possible keys.
@ -463,7 +488,7 @@ def verify_book_key(bookkey):
def decryptBook(userkey, inpath, outpath):
if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist()
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("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")
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
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
if len(bookkey) > 16:
if verify_book_key(bookkey):
@ -494,6 +541,7 @@ def decryptBook(userkey, inpath, outpath):
else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)

View File

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