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 -# - -# Based on version 8.0.6 of ineptpdf.py - - -# Revision history: -# 0.1 - Initial alpha testing release 2020 by Pu D. Pud -# 0.2 - Python 3 for calibre 5.0 (in testing) -# 0.3 - More Python3 fixes - - -""" -Decrypts Barnes & Noble encrypted PDF files. -""" - -__license__ = 'GPL v3' -__version__ = "0.3" - -import codecs -import sys -import os -import re -import zlib -import struct -import hashlib -from io import BytesIO -from decimal import Decimal -import itertools -import xml.etree.ElementTree as etree - -# Wrap a stream so that output gets flushed immediately -# and also make sure that any unicode strings get -# encoded using "replace" before writing them. -class SafeUnbuffered: - def __init__(self, stream): - self.stream = stream - self.encoding = stream.encoding - if self.encoding == None: - self.encoding = "utf-8" - def write(self, data): - if isinstance(data,str) or isinstance(data,unicode): - # str for Python3, unicode for Python2 - data = data.encode(self.encoding,"replace") - try: - buffer = getattr(self.stream, 'buffer', self.stream) - # self.stream.buffer for Python3, self.stream for Python2 - buffer.write(data) - buffer.flush() - except: - # We can do nothing if a write fails - raise - def __getattr__(self, attr): - return getattr(self.stream, attr) - -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') - -def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. - - # Versions 2.x of Python don't support Unicode in sys.argv on - # Windows, with the underlying Windows API instead replacing multi-byte - # characters with '?'. - - - from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR - - GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR - - CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) - - cmd = GetCommandLineW() - argc = c_int(0) - argv = CommandLineToArgvW(cmd, byref(argc)) - if argc.value > 0: - # Remove Python executable and commands if present - start = argc.value - len(sys.argv) - return [argv[i] for i in - range(start, argc.value)] - return ["ignoblepdf.py"] - else: - argvencoding = sys.stdin.encoding or "utf-8" - return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv] - - -class IGNOBLEError(Exception): - pass - - -import hashlib - -def SHA256(message): - ctx = hashlib.sha256() - ctx.update(message) - return ctx.digest() - - -def _load_crypto_libcrypto(): - from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ - Structure, c_ulong, create_string_buffer, cast - from ctypes.util import find_library - - if sys.platform.startswith('win'): - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') - - if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) - - AES_MAXNR = 14 - - c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) - - class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) - - class RC4_KEY(Structure): - _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] - RC4_KEY_p = POINTER(RC4_KEY) - - def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func - - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) - AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - - RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) - RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._blocksize = len(userkey) - key = self._key = RC4_KEY() - RC4_set_key(key, self._blocksize, userkey) - return self - def __init__(self): - self._blocksize = 0 - self._key = None - def decrypt(self, data): - out = create_string_buffer(len(data)) - RC4_crypt(self._key, len(data), data, out) - return out.raw - - class AES(object): - MODE_CBC = 0 - @classmethod - def new(cls, userkey, mode, iv): - self = AES() - self._blocksize = len(userkey) - # mode is ignored since CBCMODE is only thing supported/used so far - self._mode = mode - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise IGNOBLEError('AES improper key used') - return - keyctx = self._keyctx = AES_KEY() - self._iv = iv - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES key') - return self - def __init__(self): - self._blocksize = 0 - self._keyctx = None - self._iv = 0 - self._mode = 0 - def decrypt(self, data): - out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0) - if rv == 0: - raise IGNOBLEError('AES decryption failed') - return out.raw - - return (ARC4, AES) - - -def _load_crypto_pycrypto(): - from Crypto.Cipher import ARC4 as _ARC4 - from Crypto.Cipher import AES as _AES - - class ARC4(object): - @classmethod - def new(cls, userkey): - self = ARC4() - self._arc4 = _ARC4.new(userkey) - return self - def __init__(self): - self._arc4 = None - def decrypt(self, data): - return self._arc4.decrypt(data) - - class AES(object): - MODE_CBC = _AES.MODE_CBC - @classmethod - def new(cls, userkey, mode, iv): - self = AES() - self._aes = _AES.new(userkey, mode, iv) - return self - def __init__(self): - self._aes = None - def decrypt(self, data): - return self._aes.decrypt(data) - - return (ARC4, AES) - -def _load_crypto(): - ARC4 = AES = None - cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto) - if sys.platform.startswith('win'): - cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto) - for loader in cryptolist: - try: - ARC4, AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return (ARC4, AES) -ARC4, AES = _load_crypto() - - - - -# Do we generate cross reference streams on output? -# 0 = never -# 1 = only if present in input -# 2 = always - -GEN_XREF_STM = 1 - -# This is the value for the current document -gen_xref_stm = False # will be set in PDFSerializer - -# PDF parsing routines from pdfminer, with changes for EBX_HANDLER - -# Utilities - -def choplist(n, seq): - '''Groups every n elements of the list.''' - r = [] - for x in seq: - r.append(x) - if len(r) == n: - yield tuple(r) - r = [] - return - -def nunpack(s, default=0): - '''Unpacks up to 4 bytes big endian.''' - l = len(s) - if not l: - return default - elif l == 1: - return ord(s) - elif l == 2: - return struct.unpack('>H', s)[0] - elif l == 3: - return struct.unpack('>L', bytes([0]) + s)[0] - elif l == 4: - return struct.unpack('>L', s)[0] - else: - return TypeError('invalid length: %d' % l) - - -STRICT = 0 - - -# PS Exceptions - -class PSException(Exception): pass -class PSEOF(PSException): pass -class PSSyntaxError(PSException): pass -class PSTypeError(PSException): pass -class PSValueError(PSException): pass - - -# Basic PostScript Types - - -# PSLiteral -class PSObject(object): pass - -class PSLiteral(PSObject): - ''' - PS literals (e.g. "/Name"). - Caution: Never create these objects directly. - Use PSLiteralTable.intern() instead. - ''' - def __init__(self, name): - self.name = name.decode('utf-8') - return - - def __repr__(self): - name = [] - for char in self.name: - if not char.isalnum(): - char = '#%02x' % ord(char) - name.append(char) - return '/%s' % ''.join(name) - -# PSKeyword -class PSKeyword(PSObject): - ''' - PS keywords (e.g. "showpage"). - Caution: Never create these objects directly. - Use PSKeywordTable.intern() instead. - ''' - def __init__(self, name): - self.name = name.decode('utf-8') - return - - def __repr__(self): - return self.name - -# PSSymbolTable -class PSSymbolTable(object): - - ''' - Symbol table that stores PSLiteral or PSKeyword. - ''' - - def __init__(self, classe): - self.dic = {} - self.classe = classe - return - - def intern(self, name): - if name in self.dic: - lit = self.dic[name] - else: - lit = self.classe(name) - self.dic[name] = lit - return lit - -PSLiteralTable = PSSymbolTable(PSLiteral) -PSKeywordTable = PSSymbolTable(PSKeyword) -LIT = PSLiteralTable.intern -KWD = PSKeywordTable.intern -KEYWORD_BRACE_BEGIN = KWD(b'{') -KEYWORD_BRACE_END = KWD(b'}') -KEYWORD_ARRAY_BEGIN = KWD(b'[') -KEYWORD_ARRAY_END = KWD(b']') -KEYWORD_DICT_BEGIN = KWD(b'<<') -KEYWORD_DICT_END = KWD(b'>>') - - -def literal_name(x): - if not isinstance(x, PSLiteral): - if STRICT: - raise PSTypeError('Literal required: %r' % x) - else: - return str(x) - return x.name - -def keyword_name(x): - if not isinstance(x, PSKeyword): - if STRICT: - raise PSTypeError('Keyword required: %r' % x) - else: - return str(x) - return x.name - - -## PSBaseParser -## -EOL = re.compile(br'[\r\n]') -SPC = re.compile(br'\s') -NONSPC = re.compile(br'\S') -HEX = re.compile(br'[0-9a-fA-F]') -END_LITERAL = re.compile(br'[#/%\[\]()<>{}\s]') -END_HEX_STRING = re.compile(br'[^\s0-9a-fA-F]') -HEX_PAIR = re.compile(br'[0-9a-fA-F]{2}|.') -END_NUMBER = re.compile(br'[^0-9]') -END_KEYWORD = re.compile(br'[#/%\[\]()<>{}\s]') -END_STRING = re.compile(br'[()\\]') -OCT_STRING = re.compile(br'[0-7]') -ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 } - -class PSBaseParser(object): - - ''' - Most basic PostScript parser that performs only basic tokenization. - ''' - BUFSIZ = 4096 - - def __init__(self, fp): - self.fp = fp - self.seek(0) - return - - def __repr__(self): - return '' % (self.fp, self.bufpos) - - def flush(self): - return - - def close(self): - self.flush() - return - - def tell(self): - return self.bufpos+self.charpos - - def poll(self, pos=None, n=80): - pos0 = self.fp.tell() - if not pos: - pos = self.bufpos+self.charpos - self.fp.seek(pos) - # print('poll(%d): %r' % (pos, self.fp.read(n)), file=sys.stderr) - self.fp.seek(pos0) - return - - def seek(self, pos): - ''' - Seeks the parser to the given position. - ''' - self.fp.seek(pos) - # reset the status for nextline() - self.bufpos = pos - self.buf = b'' - self.charpos = 0 - # reset the status for nexttoken() - self.parse1 = self.parse_main - self.tokens = [] - return - - def fillbuf(self): - if self.charpos < len(self.buf): return - # fetch next chunk. - self.bufpos = self.fp.tell() - self.buf = self.fp.read(self.BUFSIZ) - if not self.buf: - raise PSEOF('Unexpected EOF') - self.charpos = 0 - return - - def parse_main(self, s, i): - m = NONSPC.search(s, i) - if not m: - return (self.parse_main, len(s)) - j = m.start(0) - if isinstance(s[j], str): - # Python 2 - c = s[j] - else: - # Python 3 - c = bytes([s[j]]) - self.tokenstart = self.bufpos+j - if c == b'%': - self.token = c - return (self.parse_comment, j+1) - if c == b'/': - self.token = b'' - return (self.parse_literal, j+1) - if c in b'-+' or c.isdigit(): - self.token = c - return (self.parse_number, j+1) - if c == b'.': - self.token = c - return (self.parse_decimal, j+1) - if c.isalpha(): - self.token = c - return (self.parse_keyword, j+1) - if c == b'(': - self.token = b'' - self.paren = 1 - return (self.parse_string, j+1) - if c == b'<': - self.token = b'' - return (self.parse_wopen, j+1) - if c == b'>': - self.token = b'' - return (self.parse_wclose, j+1) - self.add_token(KWD(c)) - return (self.parse_main, j+1) - - def add_token(self, obj): - self.tokens.append((self.tokenstart, obj)) - return - - def parse_comment(self, s, i): - m = EOL.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_comment, len(s)) - j = m.start(0) - self.token += s[i:j] - # We ignore comments. - #self.tokens.append(self.token) - return (self.parse_main, j) - - def parse_literal(self, s, i): - m = END_LITERAL.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_literal, len(s)) - j = m.start(0) - self.token += s[i:j] - if isinstance(s[j], str): - c = s[j] - else: - c = bytes([s[j]]) - if c == b'#': - self.hex = b'' - return (self.parse_literal_hex, j+1) - self.add_token(PSLiteralTable.intern(self.token)) - return (self.parse_main, j) - - def parse_literal_hex(self, s, i): - if isinstance(s[i], str): - c = s[i] - else: - c = bytes([s[i]]) - if HEX.match(c) and len(self.hex) < 2: - self.hex += c - return (self.parse_literal_hex, i+1) - if self.hex: - self.token += bytes([int(self.hex, 16)]) - return (self.parse_literal, i) - - def parse_number(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_number, len(s)) - j = m.start(0) - self.token += s[i:j] - if isinstance(s[j], str): - c = s[j] - else: - c = bytes([s[j]]) - if c == b'.': - self.token += c - return (self.parse_decimal, j+1) - try: - self.add_token(int(self.token)) - except ValueError: - pass - return (self.parse_main, j) - - def parse_decimal(self, s, i): - m = END_NUMBER.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_decimal, len(s)) - j = m.start(0) - self.token += s[i:j] - self.add_token(Decimal(self.token.decode('utf-8'))) - return (self.parse_main, j) - - def parse_keyword(self, s, i): - m = END_KEYWORD.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_keyword, len(s)) - j = m.start(0) - self.token += s[i:j] - if self.token == 'true': - token = True - elif self.token == 'false': - token = False - else: - token = KWD(self.token) - self.add_token(token) - return (self.parse_main, j) - - def parse_string(self, s, i): - m = END_STRING.search(s, i) - if not m: - self.token += s[i:] - return (self.parse_string, len(s)) - j = m.start(0) - self.token += s[i:j] - if isinstance(s[j], str): - c = s[j] - else: - c = bytes([s[j]]) - if c == b'\\': - self.oct = '' - return (self.parse_string_1, j+1) - if c == b'(': - self.paren += 1 - self.token += c - return (self.parse_string, j+1) - if c == b')': - self.paren -= 1 - if self.paren: - self.token += c - return (self.parse_string, j+1) - self.add_token(self.token) - return (self.parse_main, j+1) - def parse_string_1(self, s, i): - if isinstance(s[i], str): - c = s[i] - else: - c = bytes([s[i]]) - if OCT_STRING.match(c) and len(self.oct) < 3: - self.oct += c - return (self.parse_string_1, i+1) - if self.oct: - self.token += bytes([int(self.oct, 8)]) - return (self.parse_string, i) - if c in ESC_STRING: - self.token += bytes([ESC_STRING[c]]) - return (self.parse_string, i+1) - - def parse_wopen(self, s, i): - if isinstance(s[i], str): - c = s[i] - else: - c = bytes([s[i]]) - if c.isspace() or HEX.match(c): - return (self.parse_hexstring, i) - if c == b'<': - self.add_token(KEYWORD_DICT_BEGIN) - i += 1 - return (self.parse_main, i) - - def parse_wclose(self, s, i): - if isinstance(s[i], str): - c = s[i] - else: - c = bytes([s[i]]) - if c == b'>': - self.add_token(KEYWORD_DICT_END) - i += 1 - return (self.parse_main, i) - - def parse_hexstring(self, s, i): - m1 = END_HEX_STRING.search(s, i) - if not m1: - self.token += s[i:] - return (self.parse_hexstring, len(s)) - j = m1.start(0) - self.token += s[i:j] - token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]), - SPC.sub(b'', self.token)) - self.add_token(token) - return (self.parse_main, j) - - def nexttoken(self): - while not self.tokens: - self.fillbuf() - (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) - token = self.tokens.pop(0) - return token - - def nextline(self): - ''' - Fetches a next line that ends either with \\r or \\n. - ''' - linebuf = b'' - linepos = self.bufpos + self.charpos - eol = False - while 1: - self.fillbuf() - if eol: - c = bytes([self.buf[self.charpos]]) - # handle '\r\n' - if c == b'\n': - linebuf += c - self.charpos += 1 - break - m = EOL.search(self.buf, self.charpos) - if m: - linebuf += self.buf[self.charpos:m.end(0)] - self.charpos = m.end(0) - if bytes([linebuf[-1]]) == b'\r': - eol = True - else: - break - else: - linebuf += self.buf[self.charpos:] - self.charpos = len(self.buf) - return (linepos, linebuf) - - def revreadlines(self): - ''' - Fetches a next line backword. This is used to locate - the trailers at the end of a file. - ''' - self.fp.seek(0, 2) - pos = self.fp.tell() - buf = b'' - while 0 < pos: - prevpos = pos - pos = max(0, pos-self.BUFSIZ) - self.fp.seek(pos) - s = self.fp.read(prevpos-pos) - if not s: break - while 1: - n = max(s.rfind(b'\r'), s.rfind(b'\n')) - if n == -1: - buf = s + buf - break - yield s[n:]+buf - s = s[:n] - buf = b'' - return - - -## PSStackParser -## -class PSStackParser(PSBaseParser): - - def __init__(self, fp): - PSBaseParser.__init__(self, fp) - self.reset() - return - - def reset(self): - self.context = [] - self.curtype = None - self.curstack = [] - self.results = [] - return - - def seek(self, pos): - PSBaseParser.seek(self, pos) - self.reset() - return - - def push(self, *objs): - self.curstack.extend(objs) - return - def pop(self, n): - objs = self.curstack[-n:] - self.curstack[-n:] = [] - return objs - def popall(self): - objs = self.curstack - self.curstack = [] - return objs - def add_results(self, *objs): - self.results.extend(objs) - return - - def start_type(self, pos, type): - self.context.append((pos, self.curtype, self.curstack)) - (self.curtype, self.curstack) = (type, []) - return - def end_type(self, type): - if self.curtype != type: - raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) - objs = [ obj for (_,obj) in self.curstack ] - (pos, self.curtype, self.curstack) = self.context.pop() - return (pos, objs) - - def do_keyword(self, pos, token): - return - - def nextobject(self, direct=False): - ''' - Yields a list of objects: keywords, literals, strings (byte arrays), - numbers, arrays and dictionaries. Arrays and dictionaries - are represented as Python sequence and dictionaries. - ''' - while not self.results: - (pos, token) = self.nexttoken() - # print((pos, token), (self.curtype, self.curstack)) - if (isinstance(token, int) or - isinstance(token, Decimal) or - isinstance(token, bool) or - isinstance(token, bytearray) or - isinstance(token, bytes) or - isinstance(token, str) or - isinstance(token, PSLiteral)): - # normal token - self.push((pos, token)) - elif token == KEYWORD_ARRAY_BEGIN: - # begin array - self.start_type(pos, 'a') - elif token == KEYWORD_ARRAY_END: - # end array - try: - self.push(self.end_type('a')) - except PSTypeError: - if STRICT: raise - elif token == KEYWORD_DICT_BEGIN: - # begin dictionary - self.start_type(pos, 'd') - elif token == KEYWORD_DICT_END: - # end dictionary - try: - (pos, objs) = self.end_type('d') - if len(objs) % 2 != 0: - print("Incomplete dictionary construct") - objs.append("") # this isn't necessary. - # temporary fix. is this due to rental books? - # raise PSSyntaxError( - # 'Invalid dictionary construct: %r' % objs) - d = dict((literal_name(k), v) \ - for (k,v) in choplist(2, objs)) - self.push((pos, d)) - except PSTypeError: - if STRICT: raise - else: - self.do_keyword(pos, token) - if self.context: - continue - else: - if direct: - return self.pop(1)[0] - self.flush() - obj = self.results.pop(0) - return obj - - -LITERAL_CRYPT = PSLiteralTable.intern(b'Crypt') -LITERALS_FLATE_DECODE = (PSLiteralTable.intern(b'FlateDecode'), PSLiteralTable.intern(b'Fl')) -LITERALS_LZW_DECODE = (PSLiteralTable.intern(b'LZWDecode'), PSLiteralTable.intern(b'LZW')) -LITERALS_ASCII85_DECODE = (PSLiteralTable.intern(b'ASCII85Decode'), PSLiteralTable.intern(b'A85')) - - -## PDF Objects -## -class PDFObject(PSObject): pass - -class PDFException(PSException): pass -class PDFTypeError(PDFException): pass -class PDFValueError(PDFException): pass -class PDFNotImplementedError(PSException): pass - - -## PDFObjRef -## -class PDFObjRef(PDFObject): - - def __init__(self, doc, objid, genno): - if objid == 0: - if STRICT: - raise PDFValueError('PDF object id cannot be 0.') - self.doc = doc - self.objid = objid - self.genno = genno - return - - def __repr__(self): - return '' % (self.objid, self.genno) - - def resolve(self): - return self.doc.getobj(self.objid) - - -# resolve -def resolve1(x): - ''' - Resolve an object. If this is an array or dictionary, - it may still contains some indirect objects inside. - ''' - while isinstance(x, PDFObjRef): - x = x.resolve() - return x - -def resolve_all(x): - ''' - Recursively resolve X and all the internals. - Make sure there is no indirect reference within the nested object. - This procedure might be slow. - ''' - while isinstance(x, PDFObjRef): - x = x.resolve() - if isinstance(x, list): - x = [ resolve_all(v) for v in x ] - elif isinstance(x, dict): - for (k,v) in iter(x.items()): - x[k] = resolve_all(v) - return x - -def decipher_all(decipher, objid, genno, x): - ''' - Recursively decipher X. - ''' - if isinstance(x, bytearray) or isinstance(x,bytes) or isinstance(x,str): - return decipher(objid, genno, x) - decf = lambda v: decipher_all(decipher, objid, genno, v) - if isinstance(x, list): - x = [decf(v) for v in x] - elif isinstance(x, dict): - x = dict((k, decf(v)) for (k, v) in iter(x.items())) - return x - - -# Type cheking -def int_value(x): - x = resolve1(x) - if not isinstance(x, int): - if STRICT: - raise PDFTypeError('Integer required: %r' % x) - return 0 - return x - -def decimal_value(x): - x = resolve1(x) - if not isinstance(x, Decimal): - if STRICT: - raise PDFTypeError('Decimal required: %r' % x) - return 0.0 - return x - -def num_value(x): - x = resolve1(x) - if not (isinstance(x, int) or isinstance(x, Decimal)): - if STRICT: - raise PDFTypeError('Int or Float required: %r' % x) - return 0 - return x - -def str_value(x): - x = resolve1(x) - if not (isinstance(x, bytearray) or isinstance(x, bytes) or isinstance(x, str)): - if STRICT: - raise PDFTypeError('String required: %r' % x) - return '' - return x - -def list_value(x): - x = resolve1(x) - if not (isinstance(x, list) or isinstance(x, tuple)): - if STRICT: - raise PDFTypeError('List required: %r' % x) - return [] - return x - -def dict_value(x): - x = resolve1(x) - if not isinstance(x, dict): - if STRICT: - raise PDFTypeError('Dict required: %r' % x) - return {} - return x - -def stream_value(x): - x = resolve1(x) - if not isinstance(x, PDFStream): - if STRICT: - raise PDFTypeError('PDFStream required: %r' % x) - return PDFStream({}, '') - return x - -# ascii85decode(data) -def ascii85decode(data): - n = b = 0 - out = b'' - for c in data: - if b'!' <= c and c <= b'u': - n += 1 - b = b*85+(c-33) - if n == 5: - out += struct.pack('>L',b) - n = b = 0 - elif c == b'z': - assert n == 0 - out += b'\0\0\0\0' - elif c == b'~': - if n: - for _ in range(5-n): - b = b*85+84 - out += struct.pack('>L',b)[:n-1] - break - return out - - -## PDFStream type -class PDFStream(PDFObject): - def __init__(self, dic, rawdata, decipher=None): - length = int_value(dic.get('Length', 0)) - eol = rawdata[length:] - # quick and dirty fix for false length attribute, - # might not work if the pdf stream parser has a problem - if decipher != None and decipher.__name__ == 'decrypt_aes': - if (len(rawdata) % 16) != 0: - cutdiv = len(rawdata) // 16 - rawdata = rawdata[:16*cutdiv] - else: - if eol in (b'\r', b'\n', b'\r\n'): - rawdata = rawdata[:length] - - self.dic = dic - self.rawdata = rawdata - self.decipher = decipher - self.data = None - self.decdata = None - self.objid = None - self.genno = None - return - - def set_objid(self, objid, genno): - self.objid = objid - self.genno = genno - return - - def __repr__(self): - if self.rawdata: - return '' % \ - (self.objid, len(self.rawdata), self.dic) - else: - return '' % \ - (self.objid, len(self.data), self.dic) - - def decode(self): - assert self.data is None and self.rawdata is not None - data = self.rawdata - if self.decipher: - # Handle encryption - data = self.decipher(self.objid, self.genno, data) - if gen_xref_stm: - self.decdata = data # keep decrypted data - if 'Filter' not in self.dic: - self.data = data - self.rawdata = None - ##print(self.dict) - return - filters = self.dic['Filter'] - if not isinstance(filters, list): - filters = [ filters ] - for f in filters: - if f in LITERALS_FLATE_DECODE: - # will get errors if the document is encrypted. - data = zlib.decompress(data) - elif f in LITERALS_LZW_DECODE: - data = b''.join(LZWDecoder(BytesIO(data)).run()) - elif f in LITERALS_ASCII85_DECODE: - data = ascii85decode(data) - elif f == LITERAL_CRYPT: - raise PDFNotImplementedError('/Crypt filter is unsupported') - else: - raise PDFNotImplementedError('Unsupported filter: %r' % f) - # apply predictors - if 'DP' in self.dic: - params = self.dic['DP'] - else: - params = self.dic.get('DecodeParms', {}) - if 'Predictor' in params: - pred = int_value(params['Predictor']) - if pred: - if pred != 12: - raise PDFNotImplementedError( - 'Unsupported predictor: %r' % pred) - if 'Columns' not in params: - raise PDFValueError( - 'Columns undefined for predictor=12') - columns = int_value(params['Columns']) - buf = b'' - ent0 = b'\x00' * columns - for i in range(0, len(data), columns+1): - pred = data[i] - ent1 = data[i+1:i+1+columns] - if pred == 2: - ent1 = b''.join(bytes([(a+b) & 255]) \ - for (a,b) in zip(ent0,ent1)) - buf += ent1 - ent0 = ent1 - data = buf - self.data = data - self.rawdata = None - return - - def get_data(self): - if self.data is None: - self.decode() - return self.data - - def get_rawdata(self): - return self.rawdata - - def get_decdata(self): - if self.decdata is not None: - return self.decdata - data = self.rawdata - if self.decipher and data: - # Handle encryption - data = self.decipher(self.objid, self.genno, data) - return data - - -## PDF Exceptions -## -class PDFSyntaxError(PDFException): pass -class PDFNoValidXRef(PDFSyntaxError): pass -class PDFEncryptionError(PDFException): pass -class PDFPasswordIncorrect(PDFEncryptionError): pass - -# some predefined literals and keywords. -LITERAL_OBJSTM = PSLiteralTable.intern(b'ObjStm') -LITERAL_XREF = PSLiteralTable.intern(b'XRef') -LITERAL_PAGE = PSLiteralTable.intern(b'Page') -LITERAL_PAGES = PSLiteralTable.intern(b'Pages') -LITERAL_CATALOG = PSLiteralTable.intern(b'Catalog') - - -## XRefs -## - -## PDFXRef -## -class PDFXRef(object): - - def __init__(self): - self.offsets = None - return - - def __repr__(self): - return '' % len(self.offsets) - - def objids(self): - return iter(self.offsets.keys()) - - def load(self, parser): - self.offsets = {} - while 1: - try: - (pos, line) = parser.nextline() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF - file corrupted?') - if not line: - raise PDFNoValidXRef('Premature eof: %r' % parser) - if line.startswith(b'trailer'): - parser.seek(pos) - break - f = line.strip().split(b' ') - if len(f) != 2: - raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) - try: - (start, nobjs) = map(int, f) - except ValueError: - raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) - for objid in range(start, start+nobjs): - try: - (_, line) = parser.nextline() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF - file corrupted?') - f = line.strip().split(b' ') - if len(f) != 3: - raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) - (pos, genno, use) = f - if use != b'n': - continue - self.offsets[objid] = (int(genno.decode('utf-8')), int(pos.decode('utf-8'))) - self.load_trailer(parser) - return - - KEYWORD_TRAILER = PSKeywordTable.intern(b'trailer') - def load_trailer(self, parser): - try: - (_,kwd) = parser.nexttoken() - assert kwd is self.KEYWORD_TRAILER - (_,dic) = parser.nextobject(direct=True) - except PSEOF: - x = parser.pop(1) - if not x: - raise PDFNoValidXRef('Unexpected EOF - file corrupted') - (_,dic) = x[0] - self.trailer = dict_value(dic) - return - - def getpos(self, objid): - try: - (genno, pos) = self.offsets[objid] - except KeyError: - raise - return (None, pos) - - -## PDFXRefStream -## -class PDFXRefStream(object): - - def __init__(self): - self.index = None - self.data = None - self.entlen = None - self.fl1 = self.fl2 = self.fl3 = None - return - - def __repr__(self): - return '' % self.index - - def objids(self): - for first, size in self.index: - for objid in range(first, first + size): - yield objid - - def load(self, parser, debug=0): - (_,objid) = parser.nexttoken() # ignored - (_,genno) = parser.nexttoken() # ignored - (_,kwd) = parser.nexttoken() - (_,stream) = parser.nextobject() - if not isinstance(stream, PDFStream) or \ - stream.dic['Type'] is not LITERAL_XREF: - raise PDFNoValidXRef('Invalid PDF stream spec.') - size = stream.dic['Size'] - index = stream.dic.get('Index', (0,size)) - self.index = list(zip(itertools.islice(index, 0, None, 2), - itertools.islice(index, 1, None, 2))) - (self.fl1, self.fl2, self.fl3) = stream.dic['W'] - self.data = stream.get_data() - self.entlen = self.fl1+self.fl2+self.fl3 - self.trailer = stream.dic - return - - def getpos(self, objid): - offset = 0 - for first, size in self.index: - if first <= objid and objid < (first + size): - break - offset += size - else: - raise KeyError(objid) - i = self.entlen * ((objid - first) + offset) - ent = self.data[i:i+self.entlen] - f1 = nunpack(ent[:self.fl1], 1) - if f1 == 1: - pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) - genno = nunpack(ent[self.fl1+self.fl2:]) - return (None, pos) - elif f1 == 2: - objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) - index = nunpack(ent[self.fl1+self.fl2:]) - return (objid, index) - # this is a free object - raise KeyError(objid) - - -## PDFDocument -## -## A PDFDocument object represents a PDF document. -## Since a PDF file is usually pretty big, normally it is not loaded -## at once. Rather it is parsed dynamically as processing goes. -## A PDF parser is associated with the document. -## -class PDFDocument(object): - - def __init__(self): - self.xrefs = [] - self.objs = {} - self.parsed_objs = {} - self.root = None - self.catalog = None - self.parser = None - self.encryption = None - self.decipher = None - return - - # set_parser(parser) - # Associates the document with an (already initialized) parser object. - def set_parser(self, parser): - if self.parser: - return - self.parser = parser - # The document is set to be temporarily ready during collecting - # all the basic information about the document, e.g. - # the header, the encryption information, and the access rights - # for the document. - self.ready = True - # Retrieve the information of each header that was appended - # (maybe multiple times) at the end of the document. - self.xrefs = parser.read_xref() - for xref in self.xrefs: - trailer = xref.trailer - if not trailer: continue - - # If there's an encryption info, remember it. - if 'Encrypt' in trailer: - #assert not self.encryption - try: - self.encryption = (list_value(trailer['ID']), - dict_value(trailer['Encrypt'])) - # fix for bad files - except: - self.encryption = (b'ffffffffffffffffffffffffffffffffffff', - dict_value(trailer['Encrypt'])) - if 'Root' in trailer: - self.set_root(dict_value(trailer['Root'])) - break - else: - raise PDFSyntaxError('No /Root object! - Is this really a PDF?') - # The document is set to be non-ready again, until all the - # proper initialization (asking the password key and - # verifying the access permission, so on) is finished. - self.ready = False - return - - # set_root(root) - # Set the Root dictionary of the document. - # Each PDF file must have exactly one /Root dictionary. - def set_root(self, root): - self.root = root - self.catalog = dict_value(self.root) - if self.catalog.get('Type') is not LITERAL_CATALOG: - if STRICT: - raise PDFSyntaxError('Catalog not found!') - return - # initialize(password='') - # Perform the initialization with a given password. - # This step is mandatory even if there's no password associated - # with the document. - def initialize(self, password=b''): - if not self.encryption: - self.is_printable = self.is_modifiable = self.is_extractable = True - self.ready = True - raise PDFEncryptionError('Document is not encrypted.') - return - (docid, param) = self.encryption - type = literal_name(param['Filter']) - if type == 'Adobe.APS': - return self.initialize_adobe_ps(password, docid, param) - if type == 'Standard': - return self.initialize_standard(password, docid, param) - if type == 'EBX_HANDLER': - return self.initialize_ebx(password, docid, param) - raise PDFEncryptionError('Unknown filter: param=%r' % param) - - def initialize_adobe_ps(self, password, docid, param): - global KEYFILEPATH - self.decrypt_key = self.genkey_adobe_ps(param) - self.genkey = self.genkey_v4 - self.decipher = self.decrypt_aes - self.ready = True - return - - def genkey_adobe_ps(self, param): - # nice little offline principal keys dictionary - # global static principal key for German Onleihe / Bibliothek Digital - principalkeys = { b'bibliothek-digital.de': codecs.decode(b'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw=','base64')} - self.is_printable = self.is_modifiable = self.is_extractable = True - length = int_value(param.get('Length', 0)) // 8 - edcdata = str_value(param.get('EDCData')).decode('base64') - pdrllic = str_value(param.get('PDRLLic')).decode('base64') - pdrlpol = str_value(param.get('PDRLPol')).decode('base64') - edclist = [] - for pair in edcdata.split(b'\n'): - edclist.append(pair) - # principal key request - for key in principalkeys: - if key in pdrllic: - principalkey = principalkeys[key] - else: - raise IGNOBLEError('Cannot find principal key for this pdf') - shakey = SHA256(principalkey) - ivector = bytes(16) - plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) - if plaintext[-16:] != bytearray(b'\0x10')*16: - raise IGNOBLEError('Offlinekey cannot be decrypted, aborting ...') - pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) - if pdrlpol[-1] < 1 or pdrlpol[-1] > 16: - raise IGNOBLEError('Could not decrypt PDRLPol, aborting ...') - else: - cutter = -1 * pdrlpol[-1] - pdrlpol = pdrlpol[:cutter] - return plaintext[:16] - - PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ - b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' - # experimental aes pw support - def initialize_standard(self, password, docid, param): - # copy from a global variable - V = int_value(param.get('V', 0)) - if (V <=0 or V > 4): - raise PDFEncryptionError('Unknown algorithm: param=%r' % param) - length = int_value(param.get('Length', 40)) # Key length (bits) - O = str_value(param['O']) - R = int_value(param['R']) # Revision - if 5 <= R: - raise PDFEncryptionError('Unknown revision: %r' % R) - U = str_value(param['U']) - P = int_value(param['P']) - try: - EncMetadata = str_value(param['EncryptMetadata']) - except: - EncMetadata = b'True' - self.is_printable = bool(P & 4) - self.is_modifiable = bool(P & 8) - self.is_extractable = bool(P & 16) - self.is_annotationable = bool(P & 32) - self.is_formsenabled = bool(P & 256) - self.is_textextractable = bool(P & 512) - self.is_assemblable = bool(P & 1024) - self.is_formprintable = bool(P & 2048) - # Algorithm 3.2 - password = (password+self.PASSWORD_PADDING)[:32] # 1 - hash = hashlib.md5(password) # 2 - hash.update(O) # 3 - hash.update(struct.pack('= 3: - # Algorithm 3.5 - hash = hashlib.md5(self.PASSWORD_PADDING) # 2 - hash.update(docid[0]) # 3 - x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 - for i in range(1,19+1): - k = b''.join(bytes([c ^ i]) for c in key ) - x = ARC4.new(k).decrypt(x) - u1 = x+x # 32bytes total - if R == 2: - is_authenticated = (u1 == U) - else: - is_authenticated = (u1[:16] == U[:16]) - if not is_authenticated: - raise IGNOBLEError('Password is not correct.') - self.decrypt_key = key - # genkey method - if V == 1 or V == 2: - self.genkey = self.genkey_v2 - elif V == 3: - self.genkey = self.genkey_v3 - elif V == 4: - self.genkey = self.genkey_v2 - #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 - # rc4 - if V != 4: - self.decipher = self.decipher_rc4 # XXX may be AES - # aes - elif V == 4 and length == 128: - self.decipher = self.decipher_aes - elif V == 4 and length == 256: - raise PDFNotImplementedError('AES256 encryption is currently unsupported') - self.ready = True - return - - def initialize_ebx(self, keyb64, docid, param): - self.is_printable = self.is_modifiable = self.is_extractable = True - key = keyb64.decode('base64')[:16] - aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key)) - length = int_value(param.get('Length', 0)) / 8 - rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') - rights = zlib.decompress(rights, -15) - rights = etree.fromstring(rights) - expr = './/{http://ns.adobe.com/adept}encryptedKey' - bookkey = ''.join(rights.findtext(expr)).decode('base64') - bookkey = aes.decrypt(bookkey) - bookkey = bookkey[:-ord(bookkey[-1])] - # todo: Take a look at this. - # This seems to be the only function that's different between ignoblepdf and ineptpdf. - # A ton of useless duplicated code ..... - bookkey = bookkey[-16:] - ebx_V = int_value(param.get('V', 4)) - ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) - # added because of improper booktype / decryption book session key errors - if length > 0: - if len(bookkey) == length: - if ebx_V == 3: - V = 3 - else: - V = 2 - elif len(bookkey) == length + 1: - V = bookkey[0] - bookkey = bookkey[1:] - else: - print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) - print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) - print("bookkey[0] is %d" % bookkey[0]) - raise IGNOBLEError('error decrypting book session key - mismatched length') - else: - # proper length unknown try with whatever you have - print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)) - print("length is %d and len(bookkey) is %d" % (length, len(bookkey))) - print("bookkey[0] is %d" % bookkey[0]) - if ebx_V == 3: - V = 3 - else: - V = 2 - self.decrypt_key = bookkey - self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 - self.decipher = self.decrypt_rc4 - self.ready = True - return - - # genkey functions - def genkey_v2(self, objid, genno): - objid = struct.pack(' PDFObjStmRef.maxindex: - PDFObjStmRef.maxindex = index - - -## PDFParser -## -class PDFParser(PSStackParser): - - def __init__(self, doc, fp): - PSStackParser.__init__(self, fp) - self.doc = doc - self.doc.set_parser(self) - return - - def __repr__(self): - return '' - - KEYWORD_R = PSKeywordTable.intern(b'R') - KEYWORD_ENDOBJ = PSKeywordTable.intern(b'endobj') - KEYWORD_STREAM = PSKeywordTable.intern(b'stream') - KEYWORD_XREF = PSKeywordTable.intern(b'xref') - KEYWORD_STARTXREF = PSKeywordTable.intern(b'startxref') - def do_keyword(self, pos, token): - if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): - self.add_results(*self.pop(1)) - return - if token is self.KEYWORD_ENDOBJ: - self.add_results(*self.pop(4)) - return - - if token is self.KEYWORD_R: - # reference to indirect object - try: - ((_,objid), (_,genno)) = self.pop(2) - (objid, genno) = (int(objid), int(genno)) - obj = PDFObjRef(self.doc, objid, genno) - self.push((pos, obj)) - except PSSyntaxError: - pass - return - - if token is self.KEYWORD_STREAM: - # stream object - ((_,dic),) = self.pop(1) - dic = dict_value(dic) - try: - objlen = int_value(dic['Length']) - except KeyError: - if STRICT: - raise PDFSyntaxError('/Length is undefined: %r' % dic) - objlen = 0 - self.seek(pos) - try: - (_, line) = self.nextline() # 'stream' - except PSEOF: - if STRICT: - raise PDFSyntaxError('Unexpected EOF') - return - pos += len(line) - self.fp.seek(pos) - data = self.fp.read(objlen) - self.seek(pos+objlen) - while 1: - try: - (linepos, line) = self.nextline() - except PSEOF: - if STRICT: - raise PDFSyntaxError('Unexpected EOF') - break - if b'endstream' in line: - i = line.index(b'endstream') - objlen += i - data += line[:i] - break - objlen += len(line) - data += line - self.seek(pos+objlen) - obj = PDFStream(dic, data, self.doc.decipher) - self.push((pos, obj)) - return - - # others - self.push((pos, token)) - return - - def find_xref(self): - # search the last xref table by scanning the file backwards. - prev = None - for line in self.revreadlines(): - line = line.strip() - if line == b'startxref': break - if line: - prev = line - else: - raise PDFNoValidXRef('Unexpected EOF') - return int(prev) - - # read xref table - def read_xref_from(self, start, xrefs): - self.seek(start) - self.reset() - try: - (pos, token) = self.nexttoken() - except PSEOF: - raise PDFNoValidXRef('Unexpected EOF') - if isinstance(token, int): - # XRefStream: PDF-1.5 - if GEN_XREF_STM == 1: - global gen_xref_stm - gen_xref_stm = True - self.seek(pos) - self.reset() - xref = PDFXRefStream() - xref.load(self) - else: - if token is not self.KEYWORD_XREF: - raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % - (pos, token)) - self.nextline() - xref = PDFXRef() - xref.load(self) - xrefs.append(xref) - trailer = xref.trailer - if 'XRefStm' in trailer: - pos = int_value(trailer['XRefStm']) - self.read_xref_from(pos, xrefs) - if 'Prev' in trailer: - # find previous xref - pos = int_value(trailer['Prev']) - self.read_xref_from(pos, xrefs) - return - - # read xref tables and trailers - def read_xref(self): - xrefs = [] - trailerpos = None - try: - pos = self.find_xref() - self.read_xref_from(pos, xrefs) - except PDFNoValidXRef: - # fallback - self.seek(0) - pat = re.compile(b'^(\\d+)\\s+(\\d+)\\s+obj\\b') - offsets = {} - xref = PDFXRef() - while 1: - try: - (pos, line) = self.nextline() - except PSEOF: - break - if line.startswith(b'trailer'): - trailerpos = pos # remember last trailer - m = pat.match(line) - if not m: continue - (objid, genno) = m.groups() - offsets[int(objid)] = (0, pos) - if not offsets: raise - xref.offsets = offsets - if trailerpos: - self.seek(trailerpos) - xref.load_trailer(self) - xrefs.append(xref) - return xrefs - -## PDFObjStrmParser -## -class PDFObjStrmParser(PDFParser): - - def __init__(self, data, doc): - PSStackParser.__init__(self, BytesIO(data)) - self.doc = doc - return - - def flush(self): - self.add_results(*self.popall()) - return - - KEYWORD_R = KWD(b'R') - def do_keyword(self, pos, token): - if token is self.KEYWORD_R: - # reference to indirect object - try: - ((_,objid), (_,genno)) = self.pop(2) - (objid, genno) = (int(objid), int(genno)) - obj = PDFObjRef(self.doc, objid, genno) - self.push((pos, obj)) - except PSSyntaxError: - pass - return - # others - self.push((pos, token)) - return - -### -### My own code, for which there is none else to blame - -class PDFSerializer(object): - def __init__(self, inf, userkey): - global GEN_XREF_STM, gen_xref_stm - gen_xref_stm = GEN_XREF_STM > 1 - self.version = inf.read(8) - inf.seek(0) - self.doc = doc = PDFDocument() - parser = PDFParser(doc, inf) - doc.initialize(userkey) - self.objids = objids = set() - for xref in reversed(doc.xrefs): - trailer = xref.trailer - for objid in xref.objids(): - objids.add(objid) - trailer = dict(trailer) - trailer.pop('Prev', None) - trailer.pop('XRefStm', None) - if 'Encrypt' in trailer: - objids.remove(trailer.pop('Encrypt').objid) - self.trailer = trailer - - def dump(self, outf): - self.outf = outf - self.write(self.version) - self.write(b'\n%\xe2\xe3\xcf\xd3\n') - doc = self.doc - objids = self.objids - xrefs = {} - maxobj = max(objids) - trailer = dict(self.trailer) - trailer['Size'] = maxobj + 1 - for objid in objids: - obj = doc.getobj(objid) - if isinstance(obj, PDFObjStmRef): - xrefs[objid] = obj - continue - if obj is not None: - try: - genno = obj.genno - except AttributeError: - genno = 0 - xrefs[objid] = (self.tell(), genno) - self.serialize_indirect(objid, obj) - startxref = self.tell() - - if not gen_xref_stm: - self.write(b'xref\n') - self.write(b'0 %d\n' % (maxobj + 1,)) - for objid in range(0, maxobj + 1): - if objid in xrefs: - # force the genno to be 0 - self.write(b"%010d 00000 n \n" % xrefs[objid][0]) - else: - self.write(b"%010d %05d f \n" % (0, 65535)) - - self.write(b'trailer\n') - self.serialize_object(trailer) - self.write(b'\nstartxref\n%d\n%%%%EOF' % startxref) - - else: # Generate crossref stream. - - # Calculate size of entries - maxoffset = max(startxref, maxobj) - maxindex = PDFObjStmRef.maxindex - fl2 = 2 - power = 65536 - while maxoffset >= power: - fl2 += 1 - power *= 256 - fl3 = 1 - power = 256 - while maxindex >= power: - fl3 += 1 - power *= 256 - - index = [] - first = None - prev = None - data = [] - # Put the xrefstream's reference in itself - startxref = self.tell() - maxobj += 1 - xrefs[maxobj] = (startxref, 0) - for objid in sorted(xrefs): - if first is None: - first = objid - elif objid != prev + 1: - index.extend((first, prev - first + 1)) - first = objid - prev = objid - objref = xrefs[objid] - if isinstance(objref, PDFObjStmRef): - f1 = 2 - f2 = objref.stmid - f3 = objref.index - else: - f1 = 1 - f2 = objref[0] - # we force all generation numbers to be 0 - # f3 = objref[1] - f3 = 0 - - data.append(struct.pack('>B', f1)) - data.append(struct.pack('>L', f2)[-fl2:]) - data.append(struct.pack('>L', f3)[-fl3:]) - index.extend((first, prev - first + 1)) - data = zlib.compress(b''.join(data)) - dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, - 'W': [1, fl2, fl3], 'Length': len(data), - 'Filter': LITERALS_FLATE_DECODE[0], - 'Root': trailer['Root'],} - if 'Info' in trailer: - dic['Info'] = trailer['Info'] - xrefstm = PDFStream(dic, data) - self.serialize_indirect(maxobj, xrefstm) - self.write(b'startxref\n%d\n%%%%EOF' % startxref) - def write(self, data): - self.outf.write(data) - self.last = data[-1:] - - def tell(self): - return self.outf.tell() - - def escape_string(self, string): - string = string.replace(b'\\', b'\\\\') - string = string.replace(b'\n', b'\\n') - string = string.replace(b'(', b'\\(') - string = string.replace(b')', b'\\)') - return string - - def serialize_object(self, obj): - if isinstance(obj, dict): - # Correct malformed Mac OS resource forks for Stanza - if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ - and isinstance(obj['Type'], int): - obj['Subtype'] = obj['Type'] - del obj['Type'] - # end - hope this doesn't have bad effects - self.write(b'<<') - for key, val in obj.items(): - self.write(str(PSLiteralTable.intern(key.encode('utf-8'))).encode('utf-8')) - self.serialize_object(val) - self.write(b'>>') - elif isinstance(obj, list): - self.write(b'[') - for val in obj: - self.serialize_object(val) - self.write(b']') - elif isinstance(obj, bytearray): - self.write(b'(%s)' % self.escape_string(obj)) - elif isinstance(obj, bytes): - self.write(b'(%s)' % self.escape_string(obj)) - elif isinstance(obj, str): - self.write(b'(%s)' % self.escape_string(obj.encode('utf-8'))) - elif isinstance(obj, bool): - if self.last.isalnum(): - self.write(b' ') - self.write(str(obj).lower().encode('utf-8')) - elif isinstance(obj, (int, long)): - if self.last.isalnum(): - self.write(b' ') - self.write(str(obj).encode('utf-8')) - elif isinstance(obj, Decimal): - if self.last.isalnum(): - self.write(b' ') - self.write(str(obj).encode('utf-8')) - elif isinstance(obj, PDFObjRef): - if self.last.isalnum(): - self.write(b' ') - self.write(b'%d %d R' % (obj.objid, 0)) - elif isinstance(obj, PDFStream): - ### If we don't generate cross ref streams the object streams - ### are no longer useful, as we have extracted all objects from - ### them. Therefore leave them out from the output. - if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: - self.write('(deleted)') - else: - data = obj.get_decdata() - self.serialize_object(obj.dic) - self.write(b'stream\n') - self.write(data) - self.write(b'\nendstream') - else: - data = str(obj).encode('utf-8') - if bytes([data[0]]).isalnum() and self.last.isalnum(): - self.write(b' ') - self.write(data) - - def serialize_indirect(self, objid, obj): - self.write(b'%d 0 obj' % (objid,)) - self.serialize_object(obj) - if self.last.isalnum(): - self.write(b'\n') - self.write(b'endobj\n') - - - - -def decryptBook(userkey, inpath, outpath): - if AES is None: - raise IGNOBLEError("PyCrypto or OpenSSL must be installed.") - with open(inpath, 'rb') as inf: - serializer = PDFSerializer(inf, userkey) - with open(outpath, 'wb') as outf: - # help construct to make sure the method runs to the end - try: - serializer.dump(outf) - except Exception as e: - print("error writing pdf: {0}".format(e.args[0])) - return 2 - return 0 - - -def cli_main(): - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - argv=unicode_argv() - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print("usage: {0} ".format(progname)) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - if result == 0: - print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) - return result - - -def gui_main(): - try: - import tkinter - import tkinter.constants - import tkinter.filedialog - import tkinter.messagebox - import traceback - except: - return cli_main() - - class DecryptionDialog(tkinter.Frame): - def __init__(self, root): - tkinter.Frame.__init__(self, root, border=5) - self.status = tkinter.Label(self, text="Select files for decryption") - self.status.pack(fill=tkinter.constants.X, expand=1) - body = tkinter.Frame(self) - body.pack(fill=tkinter.constants.X, expand=1) - sticky = tkinter.constants.E + tkinter.constants.W - body.grid_columnconfigure(1, weight=2) - tkinter.Label(body, text="Key file").grid(row=0) - self.keypath = tkinter.Entry(body, width=30) - self.keypath.grid(row=0, column=1, sticky=sticky) - if os.path.exists("bnpdfkey.b64"): - self.keypath.insert(0, "bnpdfkey.b64") - button = tkinter.Button(body, text="...", command=self.get_keypath) - button.grid(row=0, column=2) - tkinter.Label(body, text="Input file").grid(row=1) - self.inpath = tkinter.Entry(body, width=30) - self.inpath.grid(row=1, column=1, sticky=sticky) - button = tkinter.Button(body, text="...", command=self.get_inpath) - button.grid(row=1, column=2) - tkinter.Label(body, text="Output file").grid(row=2) - self.outpath = tkinter.Entry(body, width=30) - self.outpath.grid(row=2, column=1, sticky=sticky) - button = tkinter.Button(body, text="...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = tkinter.Frame(self) - buttons.pack() - botton = tkinter.Button( - buttons, text="Decrypt", width=10, command=self.decrypt) - botton.pack(side=tkinter.constants.LEFT) - tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT) - button = tkinter.Button( - buttons, text="Quit", width=10, command=self.quit) - button.pack(side=tkinter.constants.RIGHT) - - def get_keypath(self): - keypath = tkinter.filedialog.askopenfilename( - parent=None, title="Select Barnes & Noble \'.b64\' key file", - defaultextension=".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, tkinter.constants.END) - self.keypath.insert(0, keypath) - return - - def get_inpath(self): - inpath = tkinter.filedialog.askopenfilename( - parent=None, title="Select B&N-encrypted PDF file to decrypt", - defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, tkinter.constants.END) - self.inpath.insert(0, inpath) - return - - def get_outpath(self): - outpath = tkinter.filedialog.asksaveasfilename( - parent=None, title="Select unencrypted PDF file to produce", - defaultextension=".pdf", filetypes=[('PDF files', '.pdf')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, tkinter.constants.END) - self.outpath.insert(0, outpath) - return - - def decrypt(self): - keypath = self.keypath.get() - inpath = self.inpath.get() - outpath = self.outpath.get() - if not keypath or not os.path.exists(keypath): - self.status['text'] = "Specified key file does not exist" - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = "Specified input file does not exist" - return - if not outpath: - self.status['text'] = "Output file not specified" - return - if inpath == outpath: - self.status['text'] = "Must have different input and output files" - return - userkey = open(keypath,'rb').read() - self.status['text'] = "Decrypting..." - try: - decrypt_status = decryptBook(userkey, inpath, outpath) - except Exception as e: - self.status['text'] = "Error; {0}".format(e.args[0]) - return - if decrypt_status == 0: - self.status['text'] = "File successfully decrypted" - else: - self.status['text'] = "The was an error decrypting the file." - - - root = tkinter.Tk() - if AES is None: - root.withdraw() - tkinter.messagebox.showerror( - "IGNOBLE PDF", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title("Barnes & Noble PDF Decrypter v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(370, 0) - DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1) - root.mainloop() - return 0 - - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.exit(cli_main()) - sys.exit(gui_main()) diff --git a/DeDRM_plugin/ineptepub.py b/DeDRM_plugin/ineptepub.py index 759a606..2c6ceca 100644 --- a/DeDRM_plugin/ineptepub.py +++ b/DeDRM_plugin/ineptepub.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # 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 # @@ -30,18 +30,19 @@ # 6.5 - Completely remove erroneous check on DER file sanity # 6.6 - Import tkFileDialog, don't assume something else will import it. # 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. """ __license__ = 'GPL v3' -__version__ = "7.0" +__version__ = "7.1" -import codecs import sys import os import traceback +import base64 import zlib import zipfile from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED @@ -210,9 +211,14 @@ def _load_crypto_libcrypto(): return (AES, RSA) def _load_crypto_pycrypto(): - 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 + try: + from Cryptodome.Cipher import AES as _AES + from Cryptodome.PublicKey import RSA as _RSA + from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 + except: + from Crypto.Cipher import AES as _AES + from Crypto.PublicKey import RSA as _RSA + from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5 # ASN.1 parsing code from tlslite class ASN1Error(Exception): @@ -417,13 +423,32 @@ def adeptBook(inpath): adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) - if len(bookkey) == 172: + if len(bookkey) in [192, 172, 64]: return True except: # if we couldn't check, assume it is return True 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. # This is used so that the Calibre plugin can pick the correct decryption key # first try without having to loop through all possible keys. @@ -463,7 +488,7 @@ def verify_book_key(bookkey): def decryptBook(userkey, inpath, outpath): if AES is None: raise ADEPTError("PyCrypto or OpenSSL must be installed.") - rsa = RSA(userkey) + with closing(ZipFile(open(inpath, 'rb'))) as inf: namelist = inf.namelist() 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("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") - 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 - 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 if len(bookkey) > 16: if verify_book_key(bookkey): @@ -494,6 +541,7 @@ def decryptBook(userkey, inpath, outpath): else: print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) return 2 + encryption = inf.read('META-INF/encryption.xml') decryptor = Decryptor(bookkey, encryption) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) diff --git a/DeDRM_plugin/utilities.py b/DeDRM_plugin/utilities.py index c6670cf..47d6106 100644 --- a/DeDRM_plugin/utilities.py +++ b/DeDRM_plugin/utilities.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from calibre_plugins.dedrm.ignoblekeygen import generate_key +from calibre_plugins.dedrm.ignoblekeyGenPassHash import generate_key __license__ = 'GPL v3'