From 8986855a47cae416ed70843076af206f148afad5 Mon Sep 17 00:00:00 2001 From: NoDRM Date: Fri, 24 Dec 2021 14:35:53 +0100 Subject: [PATCH] Support for extracting PassHashes from ADE --- CHANGELOG.md | 3 +- DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm | 1 + DeDRM_plugin/__init__.py | 22 ++- DeDRM_plugin/adobekey_get_passhash.py | 158 +++++++++++++++++++ DeDRM_plugin/config.py | 158 ++++++++++++++++--- 5 files changed, 321 insertions(+), 21 deletions(-) create mode 100644 DeDRM_plugin/adobekey_get_passhash.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 462b1ce..4df06e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm b/DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm index 6f61260..cc6d878 100644 --- a/DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm +++ b/DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm @@ -38,6 +38,7 @@ Instead, they started generating a random key on their server and send that to t
  • 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.
  • 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.
  • 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.
  • +
  • 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.
  • diff --git a/DeDRM_plugin/__init__.py b/DeDRM_plugin/__init__.py index 2db9acb..2351220 100644 --- a/DeDRM_plugin/__init__.py +++ b/DeDRM_plugin/__init__.py @@ -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: diff --git a/DeDRM_plugin/adobekey_get_passhash.py b/DeDRM_plugin/adobekey_get_passhash.py new file mode 100644 index 0000000..d575d13 --- /dev/null +++ b/DeDRM_plugin/adobekey_get_passhash.py @@ -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 +# + +# 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.") diff --git a/DeDRM_plugin/config.py b/DeDRM_plugin/config.py index c1850e0..62f34db 100755 --- a/DeDRM_plugin/config.py +++ b/DeDRM_plugin/config.py @@ -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 {0} 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 {0} 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)