Lots of B&N updates

This commit is contained in:
NoDRM 2021-12-23 11:29:58 +01:00
parent db71d35b40
commit 3b9c201421
11 changed files with 726 additions and 2544 deletions

View File

@ -41,3 +41,7 @@ List of changes since the fork of Apprentice Harper's repository:
- Add code to support importing multiple decryption keys from ADE (click the 'plus' button multiple times). - 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.

View File

@ -302,266 +302,288 @@ 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
#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 calibre_plugins.dedrm.ineptepub as ineptepub import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name): 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: if ineptepub.isPassHashBook(inf.name):
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) # This is an Adobe PassHash / B&N encrypted eBook
else: print("{0} v{1}: “{2}” is a secure PassHash-protected (B&N) ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
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))
# Attempt to decrypt epub with each encryption key (generated or provided).
if book_uuid is not None: for keyname, userkey in dedrmprefs['bandnkeys'].items():
# Check if we have a key with that UUID in its name: keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
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") 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) 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: except ineptepub.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) 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) return self.postProcessEPUB(path_to_ebook)
except: 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() traceback.print_exc()
result = 1
try:
# 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
of.close() 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: if result == 0:
# Decryption was a success # Decryption was successful.
# Store the new successful key in the defaults # Return the modified PersistentTemporary file to calibre.
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION)) print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
try: return self.postProcessEPUB(of.name)
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)) 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))
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)) # 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() traceback.print_exc()
pass pass
# Something went wrong with decryption. if len(newkeys) > 0:
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)) try:
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)) 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 # Give the user key, ebook and TemporaryPersistent file to the decryption function.
# Probably a DRM-free EPUB, but we should still check for fonts. try:
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))) result = ineptepub.decryptBook(userkey, inf.name, of.name)
return self.postProcessEPUB(inf.name) except:
#raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".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
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): def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs

View File

@ -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.chkOldAlgo.setToolTip(_("Leave this off if you're unsure."))
data_group_box_layout.addWidget(self.chkOldAlgo)
layout.addSpacing(10)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Retrieved key:", self))
self.key_display = QLabel("", self)
self.key_display.setToolTip(_("Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
key_group.addWidget(self.key_display)
self.retrieve_button = QtGui.QPushButton(self)
self.retrieve_button.setToolTip(_("Click to retrieve your B&N encryption key from the B&N servers"))
self.retrieve_button.setText("Retrieve Key")
self.retrieve_button.clicked.connect(self.retrieve_key)
key_group.addWidget(self.retrieve_button)
layout.addSpacing(10)
self.button_box.hide()
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box = 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 len(self.key_name) < 4:
errmsg = "Key name must be at <i>least</i> 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if self.chkOldAlgo.isChecked(): path_to_ade_data = self.cc_number
# old method, try to generate
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key if (os.path.isfile(os.path.join(path_to_ade_data, ".adobe-digital-editions", "activation.xml"))):
generated_key = generate_bandn_key(self.user_name, self.cc_number) path_to_ade_data = os.path.join(path_to_ade_data, ".adobe-digital-editions")
if generated_key == "": elif (os.path.isfile(os.path.join(path_to_ade_data, "activation.xml"))):
errmsg = "Could not generate key." pass
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: else:
# New method, try to connect to server errmsg = "This isn't the correct path, or the data is invalid."
from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
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): 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)

View File

@ -0,0 +1,65 @@
'''
Extracts the user's ccHash from an .adobe-digital-editions folder
typically included in the Nook Android app's data folder.
Based on ignoblekeyWindowsStore.py, updated for Android by noDRM.
'''
import sys
import os
import base64
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
def unpad(data):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
def dump_keys(path_to_adobe_folder):
activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
device_path = os.path.join(path_to_adobe_folder, "device.xml")
if not os.path.isfile(activation_path):
print("Nook activation file is missing: %s\n" % activation_path)
return []
if not os.path.isfile(device_path):
print("Nook device file is missing: %s\n" % device_path)
return []
# Load files:
activation_xml = etree.parse(activation_path)
device_xml = etree.parse(device_path)
# Get fingerprint:
device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text
device_fingerprint = base64.b64decode(device_fingerprint).hex()
hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16]
hashes = []
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
encrypted_cc_hash = base64.b64decode(pass_hash.text)
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
hashes.append(base64.b64encode(cc_hash).decode("ascii"))
#print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
return hashes
if __name__ == "__main__":
print("No standalone version available.")

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/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

View File

@ -0,0 +1,75 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
'''
Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app.
https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h
(Requires a recent Windows version in a supported region (US).)
This procedure has been tested with Nook app version 1.11.0.4 under Windows 11.
Based on experimental standalone python script created by fesiwi at
https://github.com/noDRM/DeDRM_tools/discussions/9
'''
import sys, os
import apsw
import base64
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
def unpad(data):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
def dump_keys(print_result=False):
db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
if not os.path.isfile(db_filename):
print("Database file not found. Is the Nook Windows Store app installed?")
return []
# Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result.
# There should only be one result anyways.
serial_number = apsw.Connection(db_filename).cursor().execute(
"SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0]
hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml")
if not os.path.isfile(activation_file_name):
print("Activation file not found. Are you logged in to your Nook account?")
return []
activation_xml = etree.parse(activation_file_name)
decrypted_hashes = []
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
encrypted_cc_hash = base64.b64decode(pass_hash.text)
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
if print_result:
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
return decrypted_hashes
if __name__ == "__main__":
dump_keys(True)

View File

@ -25,7 +25,12 @@
# 2.0 - Python 3 for calibre 5.0 # 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

View File

@ -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,9 +211,14 @@ def _load_crypto_libcrypto():
return (AES, RSA) return (AES, RSA)
def _load_crypto_pycrypto(): def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES try:
from Crypto.PublicKey import RSA as _RSA from Cryptodome.Cipher import AES as _AES
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 from Cryptodome.PublicKey import RSA as _RSA
from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
except:
from Crypto.Cipher import AES as _AES
from Crypto.PublicKey import RSA as _RSA
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
# ASN.1 parsing code from tlslite # ASN.1 parsing code from tlslite
class ASN1Error(Exception): class ASN1Error(Exception):
@ -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)

View File

@ -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'