Support for extracting PassHashes from ADE

This commit is contained in:
NoDRM 2021-12-24 14:35:53 +01:00
parent 620c90b695
commit 8986855a47
5 changed files with 321 additions and 21 deletions

View File

@ -44,4 +44,5 @@ List of changes since the fork of Apprentice Harper's repository:
- 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.
- Support adding an existing PassHash / B&N key base64 string without having to write it to a file first.
- Support extracting PassHash keys from Adobe Digital Editions.

View File

@ -38,6 +38,7 @@ Instead, they started generating a random key on their server and send that to t
<li>B&N: The NOOK Android application supports / accepts user-added CA certificates, so you can set up something like mitmproxy on your computer, tunnel your phone's traffic through that, and extract the ccHash key data from the server response. You can then add that hash through the "Base64-encoded PassHash key string" option.</li>
<li>If you already have a copy of the Nook ccHash key string (or, more general, the PassHash key string) in base64 encoding, you can either click on "Import existing keyfiles" if it's a file in b64 format, or you click on the "Base64-encoded PassHash key string" option while adding a new PassHash key.</li>
<li>For retailers other than B&N that are using the PassHash algorihm as intended, you can click on "Adobe PassHash username & password" to enter your credentials while adding a key. This is the same algorihm as the original credit card number based key generation for B&N.</li>
<li>Windows only: If you've successfully opened a PassHash-encrypted book in Adobe Digital Editions by entering username and password, you can dump the stored credentials from ADE.</li>
</ul>

View File

@ -371,6 +371,19 @@ class DeDRM(FileTypePlugin):
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()
###### Add keys from Adobe PassHash ADE activation data (adobekey_get_passhash.py)
try:
if iswindows:
# Right now this is only implemented for Windows. MacOS support still needs to be added.
from calibre_plugins.dedrm.adobekey_get_passhash import passhash_keys
defaultkeys_ade, names = passhash_keys()
if isosx:
print("{0} v{1}: Dumping ADE PassHash data is not yet supported on MacOS.".format(PLUGIN_NAME, PLUGIN_VERSION))
except:
print("{0} v{1}: Exception when getting PassHashes from ADE 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:
@ -384,6 +397,10 @@ class DeDRM(FileTypePlugin):
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
newkeys.append(keyvalue)
for keyvalue in defaultkeys_ade:
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
newkeys.append(keyvalue)
if len(newkeys) > 0:
try:
for i,userkey in enumerate(newkeys):
@ -406,7 +423,10 @@ 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_key_'+time.strftime("%Y-%m-%d"),keyvalue)
if userkey in defaultkeys_ade:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','ade_passhash_'+str(int(time.time())),keyvalue)
else:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_key_'+str(int(time.time())),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:

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# adobekey_get_passhash.py, version 1
# based on adobekey.pyw, version 7.2
# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al.
# Copyright © 2021 noDRM
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1 - Initial release
"""
Retrieve Adobe ADEPT user passhash keys
"""
__license__ = 'GPL v3'
__version__ = '1'
import sys, os, time
import base64, hashlib
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
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]
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
class ADEPTError(Exception):
pass
def decrypt_passhash(passhash, fp):
serial_number = base64.b64decode(fp).hex()
hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
encrypted_cc_hash = base64.b64decode(passhash)
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
return base64.b64encode(cc_hash).decode("ascii")
if iswindows:
try:
import winreg
except ImportError:
import _winreg as winreg
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
def passhash_keys():
cuser = winreg.HKEY_CURRENT_USER
keys = []
names = []
try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError:
raise ADEPTError("Could not locate ADE activation")
idx = 1
fp = None
i = -1
while True:
i = i + 1 # start with 0
try:
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
except:
# No more keys
break
ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype == "activationToken":
# find fingerprint for hash decryption
j = -1
while True:
j = j + 1 # start with 0
try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype == 'fingerprint':
fp = winreg.QueryValueEx(plkkey, 'value')[0]
#print("Found fingerprint: " + fp)
if ktype == 'passHashList':
j = -1
lastOperator = "Unknown"
while True:
j = j + 1 # start with 0
try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype == 'operatorURL':
operatorURL = winreg.QueryValueEx(plkkey, 'value')[0]
#print("Found operator URL: " + operatorURL)
try:
lastOperator = operatorURL.split('//')[1].split('/')[0]
except:
lastOperator = "Unknown"
elif ktype == "passHash":
passhash_encrypted = winreg.QueryValueEx(plkkey, 'value')[0]
names.append("ADE_key_" + lastOperator + "_" + str(int(time.time())) + "_" + str(idx))
idx = idx + 1
keys.append(passhash_encrypted)
if fp is None:
#print("Didn't find fingerprint for decryption ...")
return [], []
print("Found {0:d} passhashes".format(len(keys)))
keys_decrypted = []
for key in keys:
decrypted = decrypt_passhash(key, fp)
#print("Input key: " + key)
#print("Output key: " + decrypted)
keys_decrypted.append(decrypted)
return keys_decrypted, names
else:
def passhash_keys():
raise ADEPTError("This script only supports Windows.")
#TODO: Add MacOS support by parsing the activation.xml file.
return [], []
if __name__ == '__main__':
print("This is a python calibre plugin. It can't be directly executed.")

View File

@ -361,21 +361,57 @@ class ManageKeysDialog(QDialog):
if d.result() != d.Accepted:
# New key generation cancelled.
return
new_key_value = d.key_value
if type(self.plugin_keys) == dict:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
return
self.plugin_keys[d.key_name] = new_key_value
else:
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
if d.k_key_list is not None:
# importing multiple keys
idx = -1
dup_key_count = 0
added_key_count = 0
while True:
idx = idx + 1
try:
new_key_value = d.k_key_list[idx]
except:
break
if type(self.plugin_keys) == dict:
if new_key_value in self.plugin_keys.values():
dup_key_count = dup_key_count + 1
continue
print("Setting idx " + str(idx) + ", name " + d.k_name_list[idx] + " to " + new_key_value)
self.plugin_keys[d.k_name_list[idx]] = new_key_value
added_key_count = added_key_count + 1
else:
if new_key_value in self.plugin_keys:
dup_key_count = dup_key_count + 1
continue
self.plugin_keys.append(new_key_value)
added_key_count = added_key_count + 1
if (added_key_count > 0):
info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
"Successfully added {0} key(s).".format(added_key_count), show=True)
else:
# Import single key
new_key_value = d.key_value
if type(self.plugin_keys) == dict:
if new_key_value in self.plugin_keys.values():
old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
return
self.plugin_keys[d.key_name] = new_key_value
else:
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
@ -574,12 +610,35 @@ class AddBandNKeyDialog(QDialog):
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 == 3:
self.add_fields_for_ade_passhash()
elif idx == 4:
self.add_fields_for_windows_nook()
elif idx == 5:
self.add_fields_for_android_nook()
def add_fields_for_ade_passhash(self):
self.ade_extr_group_box = QGroupBox("", self)
ade_extr_group_box_layout = QVBoxLayout()
self.ade_extr_group_box.setLayout(ade_extr_group_box_layout)
self.layout.addWidget(self.ade_extr_group_box)
ade_extr_group_box_layout.addWidget(QLabel("Click \"OK\" to try and dump PassHash data \nfrom Adobe Digital Editions. This works if\nyou've opened your PassHash books in ADE before.", self))
self.button_box.hide()
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept_ade_dump_passhash)
self.button_box.rejected.connect(self.reject)
self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def add_fields_for_android_nook(self):
self.andr_nook_group_box = QGroupBox("", self)
@ -725,6 +784,7 @@ class AddBandNKeyDialog(QDialog):
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 passhashes from Adobe Digital Editions")
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())
@ -738,7 +798,10 @@ class AddBandNKeyDialog(QDialog):
@property
def key_name(self):
return str(self.key_ledit.text()).strip()
try:
return str(self.key_ledit.text()).strip()
except:
return self.result_data_name
@property
def key_value(self):
@ -752,6 +815,23 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return str(self.cc_ledit.text()).strip()
@property
def k_name_list(self):
# If the plugin supports returning multiple keys, return a list of names.
if self.k_full_name_list is not None and self.k_full_key_list is not None:
return self.k_full_name_list
return None
@property
def k_key_list(self):
# If the plugin supports returning multiple keys, return a list of keys.
if self.k_full_name_list is not None and self.k_full_key_list is not None:
return self.k_full_key_list
return None
def accept_android_nook(self):
if len(self.key_name) < 4:
@ -775,10 +855,47 @@ class AddBandNKeyDialog(QDialog):
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)
# Take the first key we found. In the future it might be a good idea to import them all.
# See accept_ade_dump_passhash for an example on how to do that.
self.result_data = store_result[0]
QDialog.accept(self)
def accept_ade_dump_passhash(self):
try:
from calibre_plugins.dedrm.adobekey_get_passhash import passhash_keys
keys, names = passhash_keys()
except:
errmsg = "Failed to grab PassHash keys from ADE."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
# Take the first new key we found.
idx = -1
new_keys = []
new_names = []
for key in keys:
idx = idx + 1
if key in self.parent.plugin_keys.values():
continue
new_keys.append(key)
new_names.append(names[idx])
if len(new_keys) == 0:
# Okay, we didn't find anything. How do we get rid of the window?
errmsg = "Didn't find any PassHash keys in ADE."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.reject(self)
return
# Add new keys to list.
self.k_full_name_list = new_names
self.k_full_key_list = new_keys
QDialog.accept(self)
return
def accept_win_nook(self):
@ -799,8 +916,8 @@ class AddBandNKeyDialog(QDialog):
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.
# Take the first key we found. In the future it might be a good idea to import them all.
# See accept_ade_dump_passhash for an example on how to do that.
if len(store_result) > 0:
self.result_data = store_result[0]
QDialog.accept(self)
@ -1012,6 +1129,9 @@ class AddAdeptDialog(QDialog):
# Right now this code only supports adding one key per each invocation,
# so if the user has multiple keys, he's going to need to add the "plus" button multiple times.
# In the future it might be a good idea to import them all.
# See accept_ade_dump_passhash for an example on how to do that.
if len(self.new_keys)>0:
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)