]> xmof Git - DeDRM.git/commitdiff
Lots of B&N updates
authorNoDRM <no_drm123@protonmail.com>
Thu, 23 Dec 2021 10:29:58 +0000 (11:29 +0100)
committerNoDRM <no_drm123@protonmail.com>
Thu, 23 Dec 2021 10:58:40 +0000 (11:58 +0100)
CHANGELOG.md
DeDRM_plugin/__init__.py
DeDRM_plugin/config.py
DeDRM_plugin/ignoblekeyAndroid.py [new file with mode: 0644]
DeDRM_plugin/ignoblekeyGenPassHash.py [moved from DeDRM_plugin/ignoblekeygen.py with 99% similarity]
DeDRM_plugin/ignoblekeyNookStudy.py [moved from DeDRM_plugin/ignoblekey.py with 100% similarity]
DeDRM_plugin/ignoblekeyWindowsStore.py [new file with mode: 0644]
DeDRM_plugin/ignoblekeyfetch.py
DeDRM_plugin/ignoblepdf.py [deleted file]
DeDRM_plugin/ineptepub.py
DeDRM_plugin/utilities.py

index 3008a2f0282b4373598812b4add42829df003fc4..462b1cea7841da0b10655346c35368ea914950d9 100644 (file)
@@ -41,3 +41,7 @@ List of changes since the fork of Apprentice Harper's repository:
 - Add code to support importing multiple decryption keys from ADE (click the 'plus' button multiple times).
 - 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.
index 8f1c8521c2dc02903faf7330217ddaa6ae595a37..2db9acbe7801d17f73288dd40164ab3a34e979e8 100644 (file)
@@ -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
+        # import the Adobe ePub handler
+        import calibre_plugins.dedrm.ineptepub as ineptepub
 
+        if ineptepub.adeptBook(inf.name):
 
-        #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)))
+            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)))
 
-            # 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")
+                # 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
+                    # 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()
+                    of.close()
+
+                    if  result == 0:
+                        # Decryption was successful.
+                        # 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.
-                    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))
 
-                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))
 
-            # 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 keys
+                defaultkeys = []
 
-            # get the default NOOK Study keys
-            defaultkeys = []
+                ###### Add keys from the NOOK Study application (ignoblekeyNookStudy.py)
 
-            try:
-                if iswindows or isosx:
-                    from calibre_plugins.dedrm.ignoblekey import nookkeys
+                try:
+                    if iswindows or isosx:
+                        from calibre_plugins.dedrm.ignoblekeyNookStudy import nookkeys
 
-                    defaultkeys = nookkeys()
-                else: # linux
-                    from .wineutils import WineGetKeys
+                        defaultkeys_study = nookkeys()
+                    else: # linux
+                        from .wineutils import WineGetKeys
 
-                    scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
-                    defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
+                        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()
+                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)
+                ###### Add keys from the NOOK Microsoft Store application (ignoblekeyNookStudy.py)
 
-            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))
+                    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)
 
-                        of = self.temporary_file(".epub")
+                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()
 
-                        # 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()
+                ###### Check if one of the new keys decrypts the book:
 
-                        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))
+                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:
-                                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))
+                                result = ineptepub.decryptBook(userkey, inf.name, of.name)
                             except:
-                                print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
+                                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()
-                            # Return the modified PersistentTemporary file to calibre.
-                            return self.postProcessEPUB(of.name)
+                                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
 
-                        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:
+            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
 
-            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 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))
 
-        # import the Adobe Adept 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 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
 
-            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))
+                        # 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()
 
-            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
+                # 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 UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
+                    print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
                     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)
-                        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
 
+                    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))
 
-            # 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))
+                    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)
 
-            # get the default Adobe keys
-            defaultkeys = []
+                    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))
 
-            try:
-                if iswindows or isosx:
-                    from calibre_plugins.dedrm.adobekey import adeptkeys
+                # 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))
 
-                    defaultkeys, defaultnames = adeptkeys()
-                else: # linux
-                    from .wineutils import WineGetKeys
+                # get the default Adobe keys
+                defaultkeys = []
 
-                    scriptpath = os.path.join(self.alfdir,"adobekey.py")
-                    defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
+                try:
+                    if iswindows or isosx:
+                        from calibre_plugins.dedrm.adobekey import adeptkeys
 
-            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()
+                        defaultkeys, defaultnames = adeptkeys()
+                    else: # linux
+                        from .wineutils import WineGetKeys
 
-            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
+                        scriptpath = os.path.join(self.alfdir,"adobekey.py")
+                        defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
 
-            # Check for DeACSM keys:
-            try: 
-                from calibre_plugins.dedrm.config import checkForDeACSMkeys
+                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()
 
-                newkey, newname = checkForDeACSMkeys()
+                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
 
-                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
+                # Check for DeACSM keys:
+                try: 
+                    from calibre_plugins.dedrm.config import checkForDeACSMkeys
 
-            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")
+                    newkey, newname = checkForDeACSMkeys()
 
-                        # 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
+                    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
 
-                        of.close()
+                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")
 
-                        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))
+                            # Give the user key, ebook and TemporaryPersistent file to the decryption function.
                             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))
+                                result = ineptepub.decryptBook(userkey, inf.name, of.name)
                             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))
+                                print("{0} v{1}: Exception when decrypting 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
+                                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))
+                # 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))
+            # 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
index 6a9b920e6d4585a54d8441ddfc82886a8945df60..c1850e0330c729b8854f516929ad6cbafc9834c9 100755 (executable)
@@ -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. <br/>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(_("<p>Enter an identifying name for this new key.</p>"))
+        ph_key_name_group.addWidget(self.key_ledit)
+
+        andr_nook_group_box_layout.addWidget(QLabel("Hidden in the Android application data is a " +
+            "folder\nnamed '.adobe-digital-editions'. Please enter\nthe full path to that folder.", self))
+
+        ph_path_group = QHBoxLayout()
+        andr_nook_group_box_layout.addLayout(ph_path_group)
+        ph_path_group.addWidget(QLabel("Path:", self))
+        self.cc_ledit = QLineEdit("", self)
+        self.cc_ledit.setToolTip(_("<p>Enter path to .adobe-digital-editions folder.</p>"))
+        ph_path_group.addWidget(self.cc_ledit)
+
+        self.button_box.hide()
+        
+        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+        self.button_box.accepted.connect(self.accept_android_nook)
+        self.button_box.rejected.connect(self.reject)
+        self.layout.addWidget(self.button_box)
+
+        self.resize(self.sizeHint())
+
+    def add_fields_for_windows_nook(self):
+
+        self.win_nook_group_box = QGroupBox("", self)
+        win_nook_group_box_layout = QVBoxLayout()
+        self.win_nook_group_box.setLayout(win_nook_group_box_layout)
+
+        self.layout.addWidget(self.win_nook_group_box)
+
+        ph_key_name_group = QHBoxLayout()
+        win_nook_group_box_layout.addLayout(ph_key_name_group)
+        ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
+        self.key_ledit = QLineEdit("", self)
+        self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>"))
+        ph_key_name_group.addWidget(self.key_ledit)
+
+        self.button_box.hide()
+        
+        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+        self.button_box.accepted.connect(self.accept_win_nook)
+        self.button_box.rejected.connect(self.reject)
+        self.layout.addWidget(self.button_box)
+
+        self.resize(self.sizeHint())
+
+    def add_fields_for_b64_passhash(self):
+
+        self.passhash_group_box = QGroupBox("", self)
+        passhash_group_box_layout = QVBoxLayout()
+        self.passhash_group_box.setLayout(passhash_group_box_layout)
+
+        self.layout.addWidget(self.passhash_group_box)
+
+        ph_key_name_group = QHBoxLayout()
+        passhash_group_box_layout.addLayout(ph_key_name_group)
+        ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
         self.key_ledit = QLineEdit("", self)
         self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
                                 "<p>It should be something that will help you remember " +
                                 "what personal information was used to create it."))
-        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))
+        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(_("<p>Enter the Base64 key string</p>"))
+        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_b64_passhash)
+        self.button_box.rejected.connect(self.reject)
+        self.layout.addWidget(self.button_box)
+
+        self.resize(self.sizeHint())
+
+
+    def add_fields_for_passhash(self):
+
+        self.passhash_group_box = QGroupBox("", self)
+        passhash_group_box_layout = QVBoxLayout()
+        self.passhash_group_box.setLayout(passhash_group_box_layout)
+
+        self.layout.addWidget(self.passhash_group_box)
+
+        ph_key_name_group = QHBoxLayout()
+        passhash_group_box_layout.addLayout(ph_key_name_group)
+        ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
+        self.key_ledit = QLineEdit("", self)
+        self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
+                                "<p>It should be something that will help you remember " +
+                                "what personal information was used to create it."))
+        ph_key_name_group.addWidget(self.key_ledit)
+
+        ph_name_group = QHBoxLayout()
+        passhash_group_box_layout.addLayout(ph_name_group)
+        ph_name_group.addWidget(QLabel("Username:", self))
         self.name_ledit = QLineEdit("", self)
-        self.name_ledit.setToolTip(_("<p>Enter your email address as it appears in your B&N " +
-                                "account.</p>" +
-                                "<p>It will only be used to generate this " +
-                                "key and won\'t be stored anywhere " +
-                                "in calibre or on your computer.</p>" +
-                                "<p>eg: apprenticeharper@gmail.com</p>"))
-        name_group.addWidget(self.name_ledit)
-        name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self)
-        name_disclaimer_label.setAlignment(Qt.AlignHCenter)
-        data_group_box_layout.addWidget(name_disclaimer_label)
+        self.name_ledit.setToolTip(_("<p>Enter the PassHash username</p>"))
+        ph_name_group.addWidget(self.name_ledit)
 
-        ccn_group = QHBoxLayout()
-        data_group_box_layout.addLayout(ccn_group)
-        ccn_group.addWidget(QLabel("B&N/nook account password:", self))
+        ph_pass_group = QHBoxLayout()
+        passhash_group_box_layout.addLayout(ph_pass_group)
+        ph_pass_group.addWidget(QLabel("Password:", self))
         self.cc_ledit = QLineEdit("", self)
-        self.cc_ledit.setToolTip(_("<p>Enter the password " +
-                                "for your B&N account.</p>" +
-                                "<p>The password will only be used to generate this " +
-                                "key and won\'t be stored anywhere in " +
-                                "calibre or on your computer."))
-        ccn_group.addWidget(self.cc_ledit)
-        ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
-        ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
-        data_group_box_layout.addWidget(ccn_disclaimer_label)
-        layout.addSpacing(10)
+        self.cc_ledit.setToolTip(_("<p>Enter the PassHash password</p>"))
+        ph_pass_group.addWidget(self.cc_ledit)
 
-        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)
+        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)
 
-        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.resize(self.sizeHint())
 
-        self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
-        self.button_box.accepted.connect(self.accept)
+
+
+    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)
-        layout.addWidget(self.button_box)
+        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):
-
-        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"))
+    def accept_android_nook(self):
+        
+        if len(self.key_name) < 4:
+            errmsg = "Key name must be at <i>least</i> 4 characters long!"
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+        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 <i>least</i> 4 characters long!"
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+        try: 
+            from calibre_plugins.dedrm.ignoblekeyWindowsStore import dump_keys
+            store_result = dump_keys(False)
+        except:
+            errmsg = "Failed to import from Nook Microsoft Store app."
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+        if len(store_result) == 0:
+            # Nothing found, try the Nook Study app
+            from calibre_plugins.dedrm.ignoblekeyNookStudy import nookkeys
+            store_result = nookkeys()
+
+        # Take the first key we found. In the future it might be a good idea to import them all,
+        # but with how the import dialog is currently structured that's not easily possible.
+        if len(store_result) > 0: 
+            self.result_data = store_result[0]
+            QDialog.accept(self)
+            return
+
+        # Okay, we didn't find anything. How do we get rid of the window?
+        errmsg = "Didn't find any Nook keys in the Windows app."
+        error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+        QDialog.reject(self)
+
+
+    def accept_b64_passhash(self):
+        if len(self.key_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.cc_number.isspace():
+            errmsg = "All fields are required!"
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+        
+        if len(self.key_name) < 4:
+            errmsg = "Key name must be at <i>least</i> 4 characters long!"
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+        try: 
+            x = base64.b64decode(self.cc_number)
+        except: 
+            errmsg = "Key data is no valid base64 string!"
+            return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+
+        self.result_data = self.cc_number
+        QDialog.accept(self)
+    
+    def accept_passhash(self):
         if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
             errmsg = "All fields are required!"
             return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
         if len(self.key_name) < 4:
             errmsg = "Key name must be at <i>least</i> 4 characters long!"
             return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
-        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 (file)
index 0000000..2b3f0ec
--- /dev/null
@@ -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.")
similarity index 99%
rename from DeDRM_plugin/ignoblekeygen.py
rename to DeDRM_plugin/ignoblekeyGenPassHash.py
index 589355361c69fb1666436c83442008a824a37735..cb6d208a39b1b92c48a1f4ab9b945ec6260d1c41 100644 (file)
@@ -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/ignoblekeyWindowsStore.py b/DeDRM_plugin/ignoblekeyWindowsStore.py
new file mode 100644 (file)
index 0000000..919d2e6
--- /dev/null
@@ -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)
index 25c18f6261e176fa5a1b241816488799000b8832..278879b169f317b0c31ad2d917a2ec52ab228ed3 100644 (file)
 #   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 (file)
index 1e6d66a..0000000
+++ /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
-# <http://www.gnu.org/licenses/>
-
-# 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 '<PSBaseParser: %r, bufpos=%d>' % (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 '<PDFObjRef:%d %d>' % (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 '<PDFStream(%r): raw=%d, %r>' % \
-                   (self.objid, len(self.rawdata), self.dic)
-        else:
-            return '<PDFStream(%r): data=%d, %r>' % \
-                   (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 '<PDFXRef: objs=%d>' % 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 '<PDFXRef: objids=%s>' % 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('<l', P)) # 4
-        hash.update(docid[0]) # 5
-        # aes special handling if metadata isn't encrypted
-        if EncMetadata == ('False' or 'false'):
-            hash.update(codecs.decode(b'ffffffff','hex'))
-        if 5 <= R:
-            # 8
-            for _ in range(50):
-                hash = hashlib.md5(hash.digest()[:length//8])
-        key = hash.digest()[:length//8]
-        if R == 2:
-            # Algorithm 3.4
-            u1 = ARC4.new(key).decrypt(password)
-        elif R >= 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('<L', objid)[:3]
-        genno = struct.pack('<L', genno)[:2]
-        key = self.decrypt_key + objid + genno
-        hash = hashlib.md5(key)
-        key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
-        return key
-
-    def genkey_v3(self, objid, genno):
-        objid = struct.pack('<L', objid ^ 0x3569ac)
-        genno = struct.pack('<L', genno ^ 0xca96)
-        key = self.decrypt_key
-        key += objid[0] + genno[0] + objid[1] + genno[1] + objid[2] + b'sAlT'
-        hash = hashlib.md5(key)
-        key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
-        return key
-
-    # aes v2 and v4 algorithm
-    def genkey_v4(self, objid, genno):
-        objid = struct.pack('<L', objid)[:3]
-        genno = struct.pack('<L', genno)[:2]
-        key = self.decrypt_key + objid + genno + b'sAlT'
-        hash = hashlib.md5(key)
-        key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
-        return key
-
-    def decrypt_aes(self, objid, genno, data):
-        key = self.genkey(objid, genno)
-        ivector = data[:16]
-        data = data[16:]
-        plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
-        # remove pkcs#5 aes padding
-        cutter = -1 * plaintext[-1]
-        plaintext = plaintext[:cutter]
-        return plaintext
-
-    def decrypt_aes256(self, objid, genno, data):
-        key = self.genkey(objid, genno)
-        ivector = data[:16]
-        data = data[16:]
-        plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
-        # remove pkcs#5 aes padding
-        cutter = -1 * plaintext[-1]
-        plaintext = plaintext[:cutter]
-        return plaintext
-
-    def decrypt_rc4(self, objid, genno, data):
-        key = self.genkey(objid, genno)
-        return ARC4.new(key).decrypt(data)
-
-
-    KEYWORD_OBJ = PSKeywordTable.intern(b'obj')
-
-    def getobj(self, objid):
-        if not self.ready:
-            raise PDFException('PDFDocument not initialized')
-        #assert self.xrefs
-        if objid in self.objs:
-            genno = 0
-            obj = self.objs[objid]
-        else:
-            for xref in self.xrefs:
-                try:
-                    (stmid, index) = xref.getpos(objid)
-                    break
-                except KeyError:
-                    pass
-            else:
-                #if STRICT:
-                #    raise PDFSyntaxError('Cannot locate objid=%r' % objid)
-                return None
-            if stmid:
-                if gen_xref_stm:
-                    return PDFObjStmRef(objid, stmid, index)
-                # Stuff from pdfminer: extract objects from object stream
-                stream = stream_value(self.getobj(stmid))
-                if stream.dic.get('Type') is not LITERAL_OBJSTM:
-                    if STRICT:
-                        raise PDFSyntaxError('Not a stream object: %r' % stream)
-                try:
-                    n = stream.dic['N']
-                except KeyError:
-                    if STRICT:
-                        raise PDFSyntaxError('N is not defined: %r' % stream)
-                    n = 0
-
-                if stmid in self.parsed_objs:
-                    objs = self.parsed_objs[stmid]
-                else:
-                    parser = PDFObjStrmParser(stream.get_data(), self)
-                    objs = []
-                    try:
-                        while 1:
-                            (_,obj) = parser.nextobject()
-                            objs.append(obj)
-                    except PSEOF:
-                        pass
-                    self.parsed_objs[stmid] = objs
-                genno = 0
-                i = n*2+index
-                try:
-                    obj = objs[i]
-                except IndexError:
-                    raise PDFSyntaxError('Invalid object number: objid=%r' % (objid))
-                if isinstance(obj, PDFStream):
-                    obj.set_objid(objid, 0)
-            else:
-                self.parser.seek(index)
-                (_,objid1) = self.parser.nexttoken() # objid
-                (_,genno) = self.parser.nexttoken() # genno
-                #assert objid1 == objid, (objid, objid1)
-                (_,kwd) = self.parser.nexttoken()
-        # #### hack around malformed pdf files
-        #        assert objid1 == objid, (objid, objid1)
-##                if objid1 != objid:
-##                    x = []
-##                    while kwd is not self.KEYWORD_OBJ:
-##                        (_,kwd) = self.parser.nexttoken()
-##                        x.append(kwd)
-##                    if x:
-##                        objid1 = x[-2]
-##                        genno = x[-1]
-##
-                if kwd is not self.KEYWORD_OBJ:
-                    raise PDFSyntaxError(
-                        'Invalid object spec: offset=%r' % index)
-                (_,obj) = self.parser.nextobject()
-                if isinstance(obj, PDFStream):
-                    obj.set_objid(objid, genno)
-                if self.decipher:
-                    obj = decipher_all(self.decipher, objid, genno, obj)
-            self.objs[objid] = obj
-        return obj
-
-
-class PDFObjStmRef(object):
-    maxindex = 0
-    def __init__(self, objid, stmid, index):
-        self.objid = objid
-        self.stmid = stmid
-        self.index = index
-        if index > 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 '<PDFParser>'
-
-    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} <keyfile.b64> <inbook.pdf> <outbook.pdf>".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())
index 759a60617730659811dbaa3215e8bdaf209f474b..2c6cecac643336ca601b8577d82f0d4cd6c07f60 100644 (file)
@@ -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
 # <http://www.gnu.org/licenses/>
 #   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)
index c6670cfd8ab71c5c068de881ac7765695c718a5e..47d6106bbf727c610ec54c6e6e4f9b8f5f9f901b 100644 (file)
@@ -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'