summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNoDRM <[email protected]>2021-12-23 11:29:58 +0100
committerNoDRM <[email protected]>2021-12-23 11:58:40 +0100
commit3b9c2014214b4d047c7a471edda34ba6a522cc5e (patch)
tree4d1527bfa677c714c621b18775fa0617eea94ac9
parentdb71d35b4030ed07292b78b28085b35f5d8d3afb (diff)
Lots of B&N updates
-rw-r--r--CHANGELOG.md4
-rw-r--r--DeDRM_plugin/__init__.py422
-rwxr-xr-xDeDRM_plugin/config.py340
-rw-r--r--DeDRM_plugin/ignoblekeyAndroid.py65
-rw-r--r--DeDRM_plugin/ignoblekeyGenPassHash.py (renamed from DeDRM_plugin/ignoblekeygen.py)2
-rw-r--r--DeDRM_plugin/ignoblekeyNookStudy.py (renamed from DeDRM_plugin/ignoblekey.py)0
-rw-r--r--DeDRM_plugin/ignoblekeyWindowsStore.py75
-rw-r--r--DeDRM_plugin/ignoblekeyfetch.py7
-rw-r--r--DeDRM_plugin/ignoblepdf.py2199
-rw-r--r--DeDRM_plugin/ineptepub.py70
-rw-r--r--DeDRM_plugin/utilities.py2
11 files changed, 684 insertions, 2502 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3008a2f..462b1ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,3 +41,7 @@ List of changes since the fork of Apprentice Harper's repository:
- Add code to support importing multiple decryption keys from ADE (click the 'plus' button multiple times).
- Improve epubtest.py to also detect Kobo & Apple DRM.
- Small updates to the LCP DRM error messages.
+- Merge ignobleepub into ineptepub so there's no duplicate code.
+- Support extracting the B&N / Nook key from the NOOK Microsoft Store application (based on [this script](https://github.com/noDRM/DeDRM_tools/discussions/9) by fesiwi).
+- Support extracting the B&N / Nook key from a data dump of the NOOK Android application.
+- Support adding an existing B&N key base64 string without having to write it to a file first.
diff --git a/DeDRM_plugin/__init__.py b/DeDRM_plugin/__init__.py
index 8f1c852..2db9acb 100644
--- a/DeDRM_plugin/__init__.py
+++ b/DeDRM_plugin/__init__.py
@@ -302,266 +302,288 @@ class DeDRM(FileTypePlugin):
# Not an LCP book, do the normal EPUB (Adobe) handling.
- # import the Barnes & Noble ePub handler
- import calibre_plugins.dedrm.ignobleepub as ignobleepub
+ # 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
diff --git a/DeDRM_plugin/config.py b/DeDRM_plugin/config.py
index 6a9b920..c1850e0 100755
--- a/DeDRM_plugin/config.py
+++ b/DeDRM_plugin/config.py
@@ -6,12 +6,12 @@ __license__ = 'GPL v3'
# Python 3, September 2020
# Standard Python modules.
-import sys, os, traceback, json, codecs
+import sys, os, traceback, json, codecs, base64
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
- QCheckBox)
+ QCheckBox, QComboBox)
from PyQt5 import Qt as QtGui
from zipfile import ZipFile
@@ -113,8 +113,8 @@ class ConfigWidget(QWidget):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self.bandn_button = QtGui.QPushButton(self)
- self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks"))
- self.bandn_button.setText("Barnes and Noble ebooks")
+ self.bandn_button.setToolTip(_("Click to manage keys for ADE books with PassHash algorithm. <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: [email protected]</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
index 0000000..2b3f0ec
--- /dev/null
+++ b/DeDRM_plugin/ignoblekeyAndroid.py
@@ -0,0 +1,65 @@
+'''
+Extracts the user's ccHash from an .adobe-digital-editions folder
+typically included in the Nook Android app's data folder.
+
+Based on ignoblekeyWindowsStore.py, updated for Android by noDRM.
+'''
+
+import sys
+import os
+import base64
+try:
+ from Cryptodome.Cipher import AES
+except:
+ from Crypto.Cipher import AES
+import hashlib
+from lxml import etree
+
+
+PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
+
+def unpad(data):
+
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+def dump_keys(path_to_adobe_folder):
+
+ activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
+ device_path = os.path.join(path_to_adobe_folder, "device.xml")
+
+ if not os.path.isfile(activation_path):
+ print("Nook activation file is missing: %s\n" % activation_path)
+ return []
+ if not os.path.isfile(device_path):
+ print("Nook device file is missing: %s\n" % device_path)
+ return []
+
+ # Load files:
+ activation_xml = etree.parse(activation_path)
+ device_xml = etree.parse(device_path)
+
+ # Get fingerprint:
+ device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text
+ device_fingerprint = base64.b64decode(device_fingerprint).hex()
+
+ hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16]
+
+ hashes = []
+
+ for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
+ encrypted_cc_hash = base64.b64decode(pass_hash.text)
+ cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
+ hashes.append(base64.b64encode(cc_hash).decode("ascii"))
+ #print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
+
+ return hashes
+
+
+
+if __name__ == "__main__":
+ print("No standalone version available.")
diff --git a/DeDRM_plugin/ignoblekeygen.py b/DeDRM_plugin/ignoblekeyGenPassHash.py
index 5893553..cb6d208 100644
--- a/DeDRM_plugin/ignoblekeygen.py
+++ b/DeDRM_plugin/ignoblekeyGenPassHash.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# ignoblekeygen.py
+# ignoblekeyGenPassHash.py
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
diff --git a/DeDRM_plugin/ignoblekey.py b/DeDRM_plugin/ignoblekeyNookStudy.py
index ea9785a..ea9785a 100644
--- a/DeDRM_plugin/ignoblekey.py
+++ b/DeDRM_plugin/ignoblekeyNookStudy.py
diff --git a/DeDRM_plugin/ignoblekeyWindowsStore.py b/DeDRM_plugin/ignoblekeyWindowsStore.py
new file mode 100644
index 0000000..919d2e6
--- /dev/null
+++ b/DeDRM_plugin/ignoblekeyWindowsStore.py
@@ -0,0 +1,75 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+'''
+Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app.
+https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h
+(Requires a recent Windows version in a supported region (US).)
+This procedure has been tested with Nook app version 1.11.0.4 under Windows 11.
+
+Based on experimental standalone python script created by fesiwi at
+https://github.com/noDRM/DeDRM_tools/discussions/9
+'''
+
+import sys, os
+import apsw
+import base64
+try:
+ from Cryptodome.Cipher import AES
+except:
+ from Crypto.Cipher import AES
+import hashlib
+from lxml import etree
+
+
+NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
+PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
+
+def unpad(data):
+
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+
+def dump_keys(print_result=False):
+ db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
+
+
+ if not os.path.isfile(db_filename):
+ print("Database file not found. Is the Nook Windows Store app installed?")
+ return []
+
+
+ # Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result.
+ # There should only be one result anyways.
+ serial_number = apsw.Connection(db_filename).cursor().execute(
+ "SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0]
+
+
+ hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
+
+ activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml")
+
+ if not os.path.isfile(activation_file_name):
+ print("Activation file not found. Are you logged in to your Nook account?")
+ return []
+
+
+ activation_xml = etree.parse(activation_file_name)
+
+ decrypted_hashes = []
+
+ for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
+ encrypted_cc_hash = base64.b64decode(pass_hash.text)
+ cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
+ decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
+ if print_result:
+ print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
+
+ return decrypted_hashes
+
+if __name__ == "__main__":
+ dump_keys(True)
diff --git a/DeDRM_plugin/ignoblekeyfetch.py b/DeDRM_plugin/ignoblekeyfetch.py
index 25c18f6..278879b 100644
--- a/DeDRM_plugin/ignoblekeyfetch.py
+++ b/DeDRM_plugin/ignoblekeyfetch.py
@@ -25,7 +25,12 @@
# 2.0 - Python 3 for calibre 5.0
"""
-Fetch Barnes & Noble EPUB user key from B&N servers using email and password
+Fetch Barnes & Noble EPUB user key from B&N servers using email and password.
+
+NOTE: This script used to work in the past, but the server it uses is long gone.
+It can no longer be used to download keys from B&N servers, it is no longer
+supported by the Calibre plugin, and it will be removed in the future.
+
"""
__license__ = 'GPL v3'
diff --git a/DeDRM_plugin/ignoblepdf.py b/DeDRM_plugin/ignoblepdf.py
deleted file mode 100644
index 1e6d66a..0000000
--- a/DeDRM_plugin/ignoblepdf.py
+++ /dev/null
@@ -1,2199 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-# ignoblepdf.py
-# Copyright © 2009-2020 by Apprentice Harper et al.
-
-# Released under the terms of the GNU General Public Licence, version 3
-# <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())
diff --git a/DeDRM_plugin/ineptepub.py b/DeDRM_plugin/ineptepub.py
index 759a606..2c6ceca 100644
--- a/DeDRM_plugin/ineptepub.py
+++ b/DeDRM_plugin/ineptepub.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# ineptepub.py
-# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
+# Copyright © 2009-2021 by i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
@@ -30,18 +30,19 @@
# 6.5 - Completely remove erroneous check on DER file sanity
# 6.6 - Import tkFileDialog, don't assume something else will import it.
# 7.0 - Add Python 3 compatibility for calibre 5.0
+# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
-__version__ = "7.0"
+__version__ = "7.1"
-import codecs
import sys
import os
import traceback
+import base64
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
@@ -210,9 +211,14 @@ def _load_crypto_libcrypto():
return (AES, RSA)
def _load_crypto_pycrypto():
- from Crypto.Cipher import AES as _AES
- from Crypto.PublicKey import RSA as _RSA
- from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
+ try:
+ from Cryptodome.Cipher import AES as _AES
+ from Cryptodome.PublicKey import RSA as _RSA
+ from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
+ except:
+ from Crypto.Cipher import AES as _AES
+ from Crypto.PublicKey import RSA as _RSA
+ from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
# ASN.1 parsing code from tlslite
class ASN1Error(Exception):
@@ -417,13 +423,32 @@ def adeptBook(inpath):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) == 172:
+ if len(bookkey) in [192, 172, 64]:
return True
except:
# if we couldn't check, assume it is
return True
return False
+def isPassHashBook(inpath):
+ # If this is an Adobe book, check if it's a PassHash-encrypted book (B&N)
+ with closing(ZipFile(open(inpath, 'rb'))) as inf:
+ namelist = set(inf.namelist())
+ if 'META-INF/rights.xml' not in namelist or \
+ 'META-INF/encryption.xml' not in namelist:
+ return False
+ try:
+ rights = etree.fromstring(inf.read('META-INF/rights.xml'))
+ adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
+ expr = './/%s' % (adept('encryptedKey'),)
+ bookkey = ''.join(rights.findtext(expr))
+ if len(bookkey) == 64:
+ return True
+ except:
+ pass
+
+ return False
+
# Checks the license file and returns the UUID the book is licensed for.
# This is used so that the Calibre plugin can pick the correct decryption key
# first try without having to loop through all possible keys.
@@ -463,7 +488,7 @@ def verify_book_key(bookkey):
def decryptBook(userkey, inpath, outpath):
if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
- rsa = RSA(userkey)
+
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \
@@ -483,10 +508,32 @@ def decryptBook(userkey, inpath, outpath):
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
- if len(bookkey) != 172:
- print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
+
+ if len(bookkey) == 172:
+ print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
+ elif len(bookkey) == 64:
+ print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
+ else:
+ print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath)))
return 1
- bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
+
+ if len(bookkey) != 64:
+ # Normal Adobe ADEPT
+ rsa = RSA(userkey)
+ bookkey = rsa.decrypt(base64.b64decode(bookkey.encode('ascii')))
+ else:
+ # Adobe PassHash / B&N
+ key = base64.b64decode(userkey)[:16]
+ aes = AES(key)
+ bookkey = aes.decrypt(base64.b64decode(bookkey))
+ if type(bookkey[-1]) != int:
+ pad = ord(bookkey[-1])
+ else:
+ pad = bookkey[-1]
+
+ bookkey = bookkey[:-pad]
+
+
# Padded as per RSAES-PKCS1-v1_5
if len(bookkey) > 16:
if verify_book_key(bookkey):
@@ -494,6 +541,7 @@ def decryptBook(userkey, inpath, outpath):
else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2
+
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
diff --git a/DeDRM_plugin/utilities.py b/DeDRM_plugin/utilities.py
index c6670cf..47d6106 100644
--- a/DeDRM_plugin/utilities.py
+++ b/DeDRM_plugin/utilities.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-from calibre_plugins.dedrm.ignoblekeygen import generate_key
+from calibre_plugins.dedrm.ignoblekeyGenPassHash import generate_key
__license__ = 'GPL v3'