Lots of B&N updates
This commit is contained in:
parent
db71d35b40
commit
3b9c201421
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.")
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue