diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3008a2f..462b1ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/DeDRM_plugin/__init__.py b/DeDRM_plugin/__init__.py
index 8f1c852..2db9acb 100644
--- a/DeDRM_plugin/__init__.py
+++ b/DeDRM_plugin/__init__.py
@@ -302,266 +302,288 @@ 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
-
-
- #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)))
-
- # Attempt to decrypt epub with each encryption key (generated or provided).
- for keyname, userkey in dedrmprefs['bandnkeys'].items():
- keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
- print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
- of = self.temporary_file(".epub")
-
- # Give the user key, ebook and TemporaryPersistent file to the decryption function.
- try:
- result = ignobleepub.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()
- result = 1
-
- of.close()
-
- if result == 0:
- # Decryption was successful.
- # Return the modified PersistentTemporary file to calibre.
- return self.postProcessEPUB(of.name)
-
- 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))
-
- # get the default NOOK Study keys
- defaultkeys = []
-
- try:
- if iswindows or isosx:
- from calibre_plugins.dedrm.ignoblekey import nookkeys
-
- defaultkeys = nookkeys()
- else: # linux
- from .wineutils import WineGetKeys
-
- scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
- defaultkeys = 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()
-
- newkeys = []
- for keyvalue in defaultkeys:
- if keyvalue not in dedrmprefs['bandnkeys'].values():
- newkeys.append(keyvalue)
-
- if len(newkeys) > 0:
- try:
- for i,userkey in enumerate(newkeys):
- print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
-
- of = self.temporary_file(".epub")
-
- # Give the user key, ebook and TemporaryPersistent file to the decryption function.
- try:
- result = ignobleepub.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()
- result = 1
-
- of.close()
-
- if result == 0:
- # Decryption was a success
- # 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.writeprefs()
- print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
- except:
- print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- traceback.print_exc()
- # Return the modified PersistentTemporary file to calibre.
- 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:
- 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))
-
- # import the Adobe Adept ePub handler
+ # import the Adobe 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.
- # If we know that we can directly use the correct key instead of having to
- # try them all.
- book_uuid = ineptepub.adeptGetUserUUID(inf.name)
- except:
- pass
- if book_uuid is None:
- print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
- else:
- print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
+ 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)))
-
- if book_uuid is not None:
- # Check if we have a key with that UUID in its name:
- for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
- if not book_uuid.lower() in keyname.lower():
- continue
-
- # Found matching key
- userkey = codecs.decode(userkeyhex, 'hex')
- print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ for keyname, userkey in dedrmprefs['bandnkeys'].items():
+ keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
+ print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
of = self.temporary_file(".epub")
- try:
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ 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()
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was successful.
+ # Return the modified PersistentTemporary file to calibre.
+ return self.postProcessEPUB(of.name)
+
+ 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 Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+
+ # get the default NOOK keys
+ defaultkeys = []
+
+ ###### Add keys from the NOOK Study application (ignoblekeyNookStudy.py)
+
+ try:
+ if iswindows or isosx:
+ from calibre_plugins.dedrm.ignoblekeyNookStudy import nookkeys
+
+ defaultkeys_study = nookkeys()
+ else: # linux
+ from .wineutils import WineGetKeys
+
+ 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_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:
+ try:
+ for i,userkey in enumerate(newkeys):
+ print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
+
+ of = self.temporary_file(".epub")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ 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()
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was a success
+ # 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)
+ 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:
+ print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+ # Return the modified PersistentTemporary file to calibre.
+ 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:
+ pass
+
+ else:
+ # This is a "normal" Adobe eBook.
+
+ book_uuid = None
+ try:
+ # This tries to figure out which Adobe account UUID the book is licensed for.
+ # If we know that we can directly use the correct key instead of having to
+ # try them all.
+ book_uuid = ineptepub.adeptGetUserUUID(inf.name)
+ except:
+ pass
+
+ if book_uuid is None:
+ print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
+ else:
+ print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
+
+
+ if book_uuid is not None:
+ # Check if we have a key with that UUID in its name:
+ for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
+ if not book_uuid.lower() in keyname.lower():
+ continue
+
+ # Found matching key
+ userkey = codecs.decode(userkeyhex, 'hex')
+ print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
+ of = self.temporary_file(".epub")
+ try:
+ result = ineptepub.decryptBook(userkey, inf.name, of.name)
+ of.close()
+ if result == 0:
+ print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
+ return self.postProcessEPUB(of.name)
+ except ineptepub.ADEPTNewVersionError:
+ print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ return self.postProcessEPUB(path_to_ebook)
+
+ except:
+ print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+
+
+ # Attempt to decrypt epub with each encryption key (generated or provided).
+ for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
+ userkey = codecs.decode(userkeyhex, 'hex')
+ print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
+ of = self.temporary_file(".epub")
+
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
- of.close()
- if result == 0:
- print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
- return self.postProcessEPUB(of.name)
except ineptepub.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return self.postProcessEPUB(path_to_ebook)
-
except:
- print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc()
+ result = 1
-
- # Attempt to decrypt epub with each encryption key (generated or provided).
- for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
- userkey = codecs.decode(userkeyhex, 'hex')
- print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
- of = self.temporary_file(".epub")
-
- # Give the user key, ebook and TemporaryPersistent file to the decryption function.
- try:
- result = ineptepub.decryptBook(userkey, inf.name, of.name)
- except ineptepub.ADEPTNewVersionError:
- print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- return self.postProcessEPUB(path_to_ebook)
- except:
- print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- traceback.print_exc()
- result = 1
-
- try:
- of.close()
- except:
- print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
-
- if result == 0:
- # Decryption was successful.
- # Return the modified PersistentTemporary file to calibre.
- print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
- return self.postProcessEPUB(of.name)
-
- print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
-
- # perhaps we need to get a new default ADE key
- print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
-
- # get the default Adobe keys
- defaultkeys = []
-
- try:
- if iswindows or isosx:
- from calibre_plugins.dedrm.adobekey import adeptkeys
-
- defaultkeys, defaultnames = adeptkeys()
- else: # linux
- from .wineutils import WineGetKeys
-
- scriptpath = os.path.join(self.alfdir,"adobekey.py")
- defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
-
- except:
- print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- traceback.print_exc()
-
- newkeys = []
- newnames = []
- idx = 0
- for keyvalue in defaultkeys:
- if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
- newkeys.append(keyvalue)
- newnames.append("default_ade_key_uuid_" + defaultnames[idx])
- idx += 1
-
- # Check for DeACSM keys:
- try:
- from calibre_plugins.dedrm.config import checkForDeACSMkeys
-
- newkey, newname = checkForDeACSMkeys()
-
- if newkey is not None:
- if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
- print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
- newkeys.append(newkey)
- newnames.append(newname)
- except:
- traceback.print_exc()
- pass
-
- if len(newkeys) > 0:
- try:
- for i,userkey in enumerate(newkeys):
- print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
- of = self.temporary_file(".epub")
-
- # Give the user key, ebook and TemporaryPersistent file to the decryption function.
- try:
- result = ineptepub.decryptBook(userkey, inf.name, of.name)
- except:
- print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- traceback.print_exc()
- result = 1
-
+ try:
of.close()
+ except:
+ print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- if result == 0:
- # Decryption was a success
- # 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('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
- 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:
- print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
- traceback.print_exc()
- print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
- # Return the modified PersistentTemporary file to calibre.
- return self.postProcessEPUB(of.name)
+ if result == 0:
+ # Decryption was successful.
+ # Return the modified PersistentTemporary file to calibre.
+ print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
+ 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:
- print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, 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,time.time()-self.starttime))
+
+ # perhaps we need to get a new default ADE key
+ print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+
+ # get the default Adobe keys
+ defaultkeys = []
+
+ try:
+ if iswindows or isosx:
+ from calibre_plugins.dedrm.adobekey import adeptkeys
+
+ defaultkeys, defaultnames = adeptkeys()
+ else: # linux
+ from .wineutils import WineGetKeys
+
+ scriptpath = os.path.join(self.alfdir,"adobekey.py")
+ defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
+
+ except:
+ print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+
+ newkeys = []
+ newnames = []
+ idx = 0
+ for keyvalue in defaultkeys:
+ if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
+ newkeys.append(keyvalue)
+ newnames.append("default_ade_key_uuid_" + defaultnames[idx])
+ idx += 1
+
+ # Check for DeACSM keys:
+ try:
+ from calibre_plugins.dedrm.config import checkForDeACSMkeys
+
+ newkey, newname = checkForDeACSMkeys()
+
+ if newkey is not None:
+ if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
+ print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
+ newkeys.append(newkey)
+ newnames.append(newname)
+ except:
traceback.print_exc()
pass
- # Something went wrong with decryption.
- 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))
+ if len(newkeys) > 0:
+ try:
+ for i,userkey in enumerate(newkeys):
+ print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ of = self.temporary_file(".epub")
- # Not a Barnes & Noble nor an Adobe Adept
- # Probably a DRM-free EPUB, but we should still check for fonts.
- print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
- return self.postProcessEPUB(inf.name)
- #raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
+ # Give the user key, ebook and TemporaryPersistent file to the decryption function.
+ try:
+ result = ineptepub.decryptBook(userkey, inf.name, of.name)
+ except:
+ print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+ result = 1
+
+ of.close()
+
+ if result == 0:
+ # Decryption was a success
+ # 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('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
+ 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:
+ print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+ print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
+ # Return the modified PersistentTemporary file to calibre.
+ 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:
+ print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+ traceback.print_exc()
+ pass
+
+ # Something went wrong with decryption.
+ 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))
+
+ # Not a Barnes & Noble nor an Adobe Adept
+ # Probably a DRM-free EPUB, but we should still check for fonts.
+ print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
+ return self.postProcessEPUB(inf.name)
+ #raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs
diff --git a/DeDRM_plugin/config.py b/DeDRM_plugin/config.py
index 6a9b920..c1850e0 100755
--- a/DeDRM_plugin/config.py
+++ b/DeDRM_plugin/config.py
@@ -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.
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(_("
Enter an identifying name for this new key.
")) + 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(_("Enter path to .adobe-digital-editions folder.
")) + 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(_("Enter an identifying name for this new key.
")) + 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(_("Enter an identifying name for this new key.
" + "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(_("
Enter your email address as it appears in your B&N " + - "account.
" + - "It will only be used to generate this " + - "key and won\'t be stored anywhere " + - "in calibre or on your computer.
" + - "eg: apprenticeharper@gmail.com
")) - 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(_("Enter the password " + - "for your B&N account.
" + - "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.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.cc_ledit.setToolTip(_("
Enter the Base64 key string
")) + ph_name_group.addWidget(self.cc_ledit) + 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(_("Enter an identifying name for this new key.
" + + "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(_("
Enter the PassHash username
")) + 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(_("Enter the PassHash password
")) + 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 len(self.key_name) < 4: + errmsg = "Key name must be at least 4 characters long!" + return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False) - 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")) + 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: - # 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) + 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) - def accept(self): + 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 least 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 least 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 least 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) diff --git a/DeDRM_plugin/ignoblekeyAndroid.py b/DeDRM_plugin/ignoblekeyAndroid.py new file mode 100644 index 0000000..2b3f0ec --- /dev/null +++ b/DeDRM_plugin/ignoblekeyAndroid.py @@ -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.") diff --git a/DeDRM_plugin/ignoblekeygen.py b/DeDRM_plugin/ignoblekeyGenPassHash.py similarity index 99% rename from DeDRM_plugin/ignoblekeygen.py rename to DeDRM_plugin/ignoblekeyGenPassHash.py index 5893553..cb6d208 100644 --- a/DeDRM_plugin/ignoblekeygen.py +++ b/DeDRM_plugin/ignoblekeyGenPassHash.py @@ -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 diff --git a/DeDRM_plugin/ignoblekey.py b/DeDRM_plugin/ignoblekeyNookStudy.py similarity index 100% rename from DeDRM_plugin/ignoblekey.py rename to DeDRM_plugin/ignoblekeyNookStudy.py diff --git a/DeDRM_plugin/ignoblekeyWindowsStore.py b/DeDRM_plugin/ignoblekeyWindowsStore.py new file mode 100644 index 0000000..919d2e6 --- /dev/null +++ b/DeDRM_plugin/ignoblekeyWindowsStore.py @@ -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) diff --git a/DeDRM_plugin/ignoblekeyfetch.py b/DeDRM_plugin/ignoblekeyfetch.py index 25c18f6..278879b 100644 --- a/DeDRM_plugin/ignoblekeyfetch.py +++ b/DeDRM_plugin/ignoblekeyfetch.py @@ -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' diff --git a/DeDRM_plugin/ignoblepdf.py b/DeDRM_plugin/ignoblepdf.py deleted file mode 100644 index 1e6d66a..0000000 --- a/DeDRM_plugin/ignoblepdf.py +++ /dev/null @@ -1,2199 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -# ignoblepdf.py -# Copyright © 2009-2020 by Apprentice Harper et al. - -# Released under the terms of the GNU General Public Licence, version 3 -#