From c23b9034209b8c9448dc298bdc3807fa6933779b Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Sat, 19 Jan 2013 14:50:57 +0000 Subject: [PATCH] tools v5.6 --- ...epub ReadMe.txt => Ignobleepub_ReadMe.txt} | 6 +- ...ptepub ReadMe.txt => Ineptepub_ReadMe.txt} | 9 +- ...neptpdf ReadMe.txt => Ineptpdf_ReadMe.txt} | 7 +- ...eDRM ReadMe.txt => K4MobiDeDRM_ReadMe.txt} | 4 +- .../K4MobiDeDRM_plugin/__init__.py | 9 +- .../K4MobiDeDRM_plugin/convert2xml.py | 17 +- .../K4MobiDeDRM_plugin/flatxml2html.py | 12 +- Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py | 2 +- .../K4MobiDeDRM_plugin/k4mobidedrm.py | 6 +- ...L ReadMe.txt => eReaderPDB2PML_ReadMe.txt} | 0 Calibre_Plugins/ignobleepub_plugin.zip | Bin 40545 -> 42116 bytes .../Ignoble Epub DeDRM_Help.htm | Bin 10204 -> 225 bytes .../ignobleepub_plugin/__init__.py | 27 +- .../ignobleepub_plugin/ignobleepub.py | 456 +-- .../ignobleepub_plugin/ignoblekeygen.py | 306 +- .../plugin-import-name-ignobleepub.txt | 319 ++ .../ignobleepub_plugin/utilities.py | Bin 1559 -> 225 bytes .../ignobleepub_plugin/zipfilerugged.py | Bin 52470 -> 225 bytes Calibre_Plugins/ignobleepub_plugin/zipfix.py | 188 +- Calibre_Plugins/ineptepub_plugin.zip | Bin 32315 -> 33009 bytes Calibre_Plugins/ineptepub_plugin/__init__.py | 30 +- Calibre_Plugins/ineptepub_plugin/ineptepub.py | 4 +- Calibre_Plugins/k4mobidedrm_plugin.zip | Bin 230654 -> 230414 bytes DeDRM_Macintosh_Application/DeDRM ReadMe.rtf | 2 +- .../DeDRM.app/Contents/Info.plist | 6 +- .../Contents/Resources/Scripts/main.scpt | Bin 268574 -> 269068 bytes .../DeDRM.app/Contents/Resources/alfcrypto.py | 8 +- .../Contents/Resources/convert2xml.py | 17 +- .../Contents/Resources/flatxml2html.py | 12 +- .../DeDRM.app/Contents/Resources/genbook.py | 2 +- .../Contents/Resources/ignobleepub.py | 39 +- .../DeDRM.app/Contents/Resources/ineptepub.py | 4 +- .../Contents/Resources/k4mobidedrm.py | 7 +- .../DeDRM_App/DeDRM_lib/DeDRM_app.pyw | 236 +- .../DeDRM_App/DeDRM_lib/lib/alfcrypto.py | 8 +- .../DeDRM_App/DeDRM_lib/lib/argv_utils.py | 92 + .../DeDRM_App/DeDRM_lib/lib/convert2xml.py | 17 +- .../DeDRM_App/DeDRM_lib/lib/decryptepub.py | 88 - .../DeDRM_App/DeDRM_lib/lib/decryptpdb.py | 45 - .../DeDRM_App/DeDRM_lib/lib/decryptpdf.py | 54 - .../DeDRM_App/DeDRM_lib/lib/flatxml2html.py | 12 +- .../DeDRM_App/DeDRM_lib/lib/genbook.py | 2 +- .../DeDRM_App/DeDRM_lib/lib/ignobleepub.py | 39 +- .../DeDRM_App/DeDRM_lib/lib/ineptepub.py | 4 +- .../DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py | 7 +- .../DeDRM_lib/lib/scriptinterface.py | 153 + DeDRM_Windows_Application/DeDRM_ReadMe.txt | 8 +- .../BN-Dload.user_ReadMe.txt | 2 +- .../ineptpdf_8.4.51.pyw | 3160 +++++++++++++++++ .../ineptpdf_8.4.51_ReadMe.txt | 8 + ReadMe_First.txt | 6 +- 51 files changed, 4352 insertions(+), 1088 deletions(-) rename Calibre_Plugins/{Ignobleepub ReadMe.txt => Ignobleepub_ReadMe.txt} (96%) rename Calibre_Plugins/{Ineptepub ReadMe.txt => Ineptepub_ReadMe.txt} (94%) rename Calibre_Plugins/{Ineptpdf ReadMe.txt => Ineptpdf_ReadMe.txt} (95%) rename Calibre_Plugins/{K4MobiDeDRM ReadMe.txt => K4MobiDeDRM_ReadMe.txt} (98%) rename Calibre_Plugins/{eReaderPDB2PML ReadMe.txt => eReaderPDB2PML_ReadMe.txt} (100%) create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptepub.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py delete mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdf.py create mode 100644 DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py create mode 100644 Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51.pyw create mode 100644 Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51_ReadMe.txt diff --git a/Calibre_Plugins/Ignobleepub ReadMe.txt b/Calibre_Plugins/Ignobleepub_ReadMe.txt similarity index 96% rename from Calibre_Plugins/Ignobleepub ReadMe.txt rename to Calibre_Plugins/Ignobleepub_ReadMe.txt index dd6a41d3..5cbe6480 100644 --- a/Calibre_Plugins/Ignobleepub ReadMe.txt +++ b/Calibre_Plugins/Ignobleepub_ReadMe.txt @@ -1,4 +1,4 @@ -Ignoble Epub DeDRM - ignobleepub_v02.5_plugin.zip +Ignoble Epub DeDRM - ignobleepub_v02.6_plugin.zip ================================================= All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a calibre plugin. @@ -9,7 +9,7 @@ This plugin is meant to decrypt Barnes & Noble Epubs that are protected with Ado Installation ------------ -Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.5_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. +Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.6_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. Customization @@ -30,7 +30,7 @@ Creating New Keys: On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key. * Unique Key Name: this is a unique name you choose to help you identify the key after it's created. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with. -* Your Name: Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences. +* Your Name: Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences. For some B&N accounts, the name to use is the name used in the default shipping address. For some B&N accounts, the name to use is the name used for the default Credit Card. * Credit Card number: this is the credit card number that was set as default with Barnes & Noble at the time of download. Nothing fancy here; no dashes or spaces ... just the 16 (15?) digits. Again... this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences. Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key. diff --git a/Calibre_Plugins/Ineptepub ReadMe.txt b/Calibre_Plugins/Ineptepub_ReadMe.txt similarity index 94% rename from Calibre_Plugins/Ineptepub ReadMe.txt rename to Calibre_Plugins/Ineptepub_ReadMe.txt index 0620c5fa..dabcb280 100644 --- a/Calibre_Plugins/Ineptepub ReadMe.txt +++ b/Calibre_Plugins/Ineptepub_ReadMe.txt @@ -1,4 +1,4 @@ -Inept Epub DeDRM - ineptepub_v02.0_plugin.zip +Inept Epub DeDRM - ineptepub_v02.1_plugin.zip ============================================= All credit given to i♥cabbages for the original standalone scripts. I had the much easier job of converting them to a Calibre plugin. @@ -9,7 +9,7 @@ This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected w Installation ------------ -Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v02.0_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. +Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v02.1_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. @@ -55,7 +55,7 @@ Paste the information into a comment at my blog, http://apprenticealf.wordpress. Linux and Adobe Digital Editions ePubs -------------------------------------- -Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!) +Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien and Fadel!) 1. download the most recent version of wine from winehq.org (1.3.29 in my case) @@ -81,8 +81,7 @@ again as root use 'apt-get install python-tk’ -4. all programms need to be installed as normal user. All these programm are installed the same way: -‘wine ‘ +4. all programms need to be installed as normal user. The .exe files are installed using ‘wine ’ but .msi files must be installed using ‘wine start ’ we need: a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html) (there is a “can’t install ADE” site, where the setup.exe hides) diff --git a/Calibre_Plugins/Ineptpdf ReadMe.txt b/Calibre_Plugins/Ineptpdf_ReadMe.txt similarity index 95% rename from Calibre_Plugins/Ineptpdf ReadMe.txt rename to Calibre_Plugins/Ineptpdf_ReadMe.txt index 180068cd..c82f6a7b 100644 --- a/Calibre_Plugins/Ineptpdf ReadMe.txt +++ b/Calibre_Plugins/Ineptpdf_ReadMe.txt @@ -54,7 +54,7 @@ Paste the information into a comment at my blog, http://apprenticealf.wordpress. Linux and Adobe Digital Editions PDFs -------------------------------------- -Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien!) +Here are the instructions for using the tools with ePub books and Adobe Digital Editions on Linux under Wine. (Thank you mclien and Fadel!) 1. download the most recent version of wine from winehq.org (1.3.29 in my case) @@ -80,10 +80,9 @@ again as root use 'apt-get install python-tk’ -4. all programms need to be installed as normal user. All these programm are installed the same way: -‘wine ‘ +4. all programms need to be installed as normal user. The .exe files are installed using ‘wine ’ but .msi files must be installed using ‘wine start ’ we need: -a) Adobe Digital Edition 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html) +a) Adobe Digital Editions 1.7.2(from: http://kb2.adobe.com/cps/403/kb403051.html) (there is a “can’t install ADE” site, where the setup.exe hides) b) ActivePython-2.7.2.5-win32-x86.msi (from: http://www.activestate.com/activepython/downloads) diff --git a/Calibre_Plugins/K4MobiDeDRM ReadMe.txt b/Calibre_Plugins/K4MobiDeDRM_ReadMe.txt similarity index 98% rename from Calibre_Plugins/K4MobiDeDRM ReadMe.txt rename to Calibre_Plugins/K4MobiDeDRM_ReadMe.txt index f083b9ff..bacc9b5e 100644 --- a/Calibre_Plugins/K4MobiDeDRM ReadMe.txt +++ b/Calibre_Plugins/K4MobiDeDRM_ReadMe.txt @@ -1,4 +1,4 @@ -Kindle and Mobipocket Plugin - K4MobiDeDRM_v04.10_plugin.zip +Kindle and Mobipocket Plugin - K4MobiDeDRM_v04.18_plugin.zip ============================================================ Credit given to The Dark Reverser for the original standalone script. Credit also to the many people who have updated and expanded that script since then. @@ -13,7 +13,7 @@ This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .az Installation ------------ -Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.10_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. +Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior" to go to Calibre's Preferences page. Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.18_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. Make sure that you delete any old versions of the plugin. They might interfere with the operation of the new one. diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py index 46b57c94..6b3fe2f8 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py @@ -22,13 +22,18 @@ # 0.4.11 - Fixed Linux support of K4PC # 0.4.12 - More Linux Wine fixes # 0.4.13 - Ancient Mobipocket files fix +# 0.4.14 - Error on invalid character in book names fix +# 0.4.15 - Another Topaz fix +# 0.4.16 - Yet another Topaz fix +# 0.4.17 - Manage to include the actual fix. +# 0.4.18 - More Topaz fixes """ Decrypt Amazon Kindle and Mobipocket encrypted ebooks. """ PLUGIN_NAME = u"Kindle and Mobipocket DeDRM" -PLUGIN_VERSION_TUPLE = (0, 4, 13) +PLUGIN_VERSION_TUPLE = (0, 4, 18) PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) import sys, os, re @@ -170,7 +175,7 @@ def run(self, path_to_ebook): print u"{0} v{1}: Successfully decrypted book after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-starttime) - of = self.temporary_file(k4mobidedrm.cleanup_name(k4mobidedrm.unescape(book.getBookTitle()))+book.getBookExtension()) + of = self.temporary_file(u"decrypted_ebook.{0}".format(book.getBookExtension())) book.getFile(of.name) book.cleanup() return of.name diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py index 6c8fa83c..c4e23b76 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py @@ -255,13 +255,15 @@ def __init__(self, filename, dict, debug, flat_xml): 'empty_text_region' : (1, 'snippets', 1, 0), - 'img' : (1, 'snippets', 1, 0), - 'img.x' : (1, 'scalar_number', 0, 0), - 'img.y' : (1, 'scalar_number', 0, 0), - 'img.h' : (1, 'scalar_number', 0, 0), - 'img.w' : (1, 'scalar_number', 0, 0), - 'img.src' : (1, 'scalar_number', 0, 0), - 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img' : (1, 'snippets', 1, 0), + 'img.x' : (1, 'scalar_number', 0, 0), + 'img.y' : (1, 'scalar_number', 0, 0), + 'img.h' : (1, 'scalar_number', 0, 0), + 'img.w' : (1, 'scalar_number', 0, 0), + 'img.src' : (1, 'scalar_number', 0, 0), + 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), + 'img.gridEndCenter' : (1, 'scalar_number', 0, 0), 'paragraph' : (1, 'snippets', 1, 0), 'paragraph.class' : (1, 'scalar_text', 0, 0), @@ -307,6 +309,7 @@ def __init__(self, filename, dict, debug, flat_xml): 'span.gridEndCenter' : (1, 'scalar_number', 0, 0), 'extratokens' : (1, 'snippets', 1, 0), + 'extratokens.class' : (1, 'scalar_text', 0, 0), 'extratokens.type' : (1, 'scalar_text', 0, 0), 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py index e5647f4b..4d83368c 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py @@ -387,10 +387,14 @@ def getParaDescription(self, start, end, regtype): ws_last = int(argres) elif name.endswith('word.class'): - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' + # we only handle spaceafter word class + try: + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + except: + pass elif name.endswith('word.img.src'): result.append(('img' + word_class, int(argres))) diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py b/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py index 97338872..746178f4 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py @@ -117,7 +117,7 @@ def lookup(self,val): self.pos = val return self.stable[self.pos] else: - print "Error - %d outside of string table limits" % val + print "Error: %d outside of string table limits" % val raise TpzDRMError('outside or string table limits') # sys.exit(-1) def getSize(self): diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py index 8adb1071..ca8fdccd 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm.py @@ -50,8 +50,9 @@ # 4.7 - Added timing reports, and changed search for Mac key files # 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts # - Moved back into plugin, __init__ in plugin now only contains plugin code. +# 4.9 - Missed some invalid characters in cleanup_name -__version__ = '4.8' +__version__ = '4.9' import sys, os, re @@ -144,7 +145,7 @@ def unicode_argv(): # and with some (heavily edited) code from Paul Durrant's kindlenamer.py def cleanup_name(name): # substitute filename unfriendly characters - name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'") + name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"") # delete control characters name = u"".join(char for char in name if ord(char)>=32) # white space to single space, delete leading and trailing while space @@ -220,6 +221,7 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids): book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime) except Exception, e: print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) + traceback.print_exc() return 1 # if we're saving to the same folder as the original, use file name_ diff --git a/Calibre_Plugins/eReaderPDB2PML ReadMe.txt b/Calibre_Plugins/eReaderPDB2PML_ReadMe.txt similarity index 100% rename from Calibre_Plugins/eReaderPDB2PML ReadMe.txt rename to Calibre_Plugins/eReaderPDB2PML_ReadMe.txt diff --git a/Calibre_Plugins/ignobleepub_plugin.zip b/Calibre_Plugins/ignobleepub_plugin.zip index 58086686e46768f1e64df5e3c68f95ff2e68d797..1cbdaa1c4ff3b912e59ce530651550685152db19 100644 GIT binary patch delta 15448 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 delta 13936 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 diff --git a/Calibre_Plugins/ignobleepub_plugin/__init__.py b/Calibre_Plugins/ignobleepub_plugin/__init__.py index a967d619..9d17c92c 100644 --- a/Calibre_Plugins/ignobleepub_plugin/__init__.py +++ b/Calibre_Plugins/ignobleepub_plugin/__init__.py @@ -44,13 +44,14 @@ # - added ability to rename existing keys. # 0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and # - ignoblekeygen 2.4 and later. +# 0.2.6 - Tweaked to eliminate issue with both ignoble and inept calibre plugins installed/enabled at once """ Decrypt Barnes & Noble ADEPT encrypted EPUB books. """ PLUGIN_NAME = u"Ignoble Epub DeDRM" -PLUGIN_VERSION_TUPLE = (0, 2, 5) +PLUGIN_VERSION_TUPLE = (0, 2, 6) PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) # Include an html helpfile in the plugin's zipfile with the following name. RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' @@ -138,10 +139,7 @@ def run(self, path_to_ebook): #check the book from calibre_plugins.ignobleepub import ignobleepub if not ignobleepub.ignobleBook(inf.name): - print u"{0} v{1}: {2} is not a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - # return the original file, so that no error message is generated in the GUI - return path_to_ebook - + raise IGNOBLEError(u"{0} v{1}: {2} is not a secure Barnes & Noble ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) # Attempt to decrypt epub with each encryption key (generated or provided). for keyname, userkey in cfg.prefs['keys'].items(): @@ -152,30 +150,21 @@ def run(self, path_to_ebook): # Give the user key, ebook and TemporaryPersistent file to the decryption function. result = ignobleepub.decryptBook(userkey, inf.name, of.name) - # Ebook is not a B&N epub... do nothing and pass it on. - # This allows a non-encrypted epub to be imported without error messages. - if result[0] == 1: - print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1]) - of.close() - return path_to_ebook - break + of.close() # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. - if result[0] == 0: + if result == 0: print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION) - of.close() return of.name break - print u"{0} v{1}: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, result[1]) - of.close() - + print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION) # Something went wrong with decryption. # Import the original unmolested epub. - print(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION)) - return path_to_ebook + raise IGNOBLEError(u"{0} v{1}: Ultimately failed to decrypt".format(PLUGIN_NAME, PLUGIN_VERSION)) + return def is_customizable(self): # return true to allow customization via the Plugin->Preferences. diff --git a/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py b/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py index 2e0bd06d..e58bf1a7 100644 --- a/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py +++ b/Calibre_Plugins/ignobleepub_plugin/ignobleepub.py @@ -1,420 +1,98 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- + + -from __future__ import with_statement + -# ignobleepub.pyw, version 3.6 -# Copyright © 2009-2010 by i♥cabbages + +Ignoble Epub DeDRM Plugin Configuration + -# Released under the terms of the GNU General Public Licence, version 3 -# + -# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf +

Ignoble Epub DeDRM Plugin

+

(version 0.2.6)

+

For additional help read the FAQ on Apprentice Alf's Blog and ask questions in the comments section of the first post.

-# Windows users: Before running this program, you must first install Python 2.6 -# from and PyCrypto from -# (make sure to -# install the version for Python 2.6). Save this script file as -# ineptepub.pyw and double-click on it to run it. -# -# Mac OS X users: Save this script file as ineptepub.pyw. You can run this -# program from the command line (pythonw ineptepub.pyw) or by double-clicking -# it when it has been associated with PythonLauncher. +

All credit given to I ♥ Cabbages for the original standalone scripts (I had the much easier job of converting them to a calibre plugin).

-# Revision history: -# 1 - Initial release -# 2 - Added OS X support by using OpenSSL when available -# 3 - screen out improper key lengths to prevent segfaults on Linux -# 3.1 - Allow Windows versions of libcrypto to be found -# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first and OpenSSL next -# 3.4 - Modify interace to allow use with import -# 3.5 - Fix for potential problem with PyCrypto -# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code +

This plugin is meant to decrypt Barnes & Noble ePubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.

-""" -Decrypt Barnes & Noble encrypted ePub books. -""" +

This help file is always available from within the plugin's customization dialog in calibre (when installed, of course). The "Plugin Help" link can be found in the upper-right portion of the customization dialog.

-__license__ = 'GPL v3' -__version__ = "3.6" +

Installation:

-import sys -import os -import traceback -import zlib -import zipfile -from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from contextlib import closing -import xml.etree.ElementTree as etree +

Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.3_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. Now restart calibre.

-# 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,unicode): - data = data.encode(self.encoding,"replace") - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) -try: - from calibre.constants import iswindows, isosx -except: - iswindows = sys.platform.startswith('win') - isosx = sys.platform.startswith('darwin') +

Configuration:

-def unicode_argv(): - if iswindows: - # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode - # strings. +

Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&N key—or migrate your existing key(s)/data from an earlier version of the plugin—the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.

- # 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 '?'. +

If you are upgrading from an earlier version of this plugin and have provided your name(s) and credit card number(s) as part of the old plugin's customization string, you will be prompted to migrate this data to the plugin's new, more secure, key storage method when you open the customization dialog for the first time. If you choose NOT to migrate that data, you will be prompted to save that data as a text file in a location of your choosing. Either way, this plugin will no longer be storing names and credit card numbers in plain sight (or anywhere for that matter) on your computer or in calibre. If you don't choose to migrate OR save the data, that data will be lost. You have been warned!!

+

Upon configuring for the first time, you may also be asked if you wish to import your existing *.b64 keyfiles (if you use them) to the plugin's new key storage method. The new plugin no longer looks for keyfiles in calibre's configuration directory, so it's highly recommended that you import any existing keyfiles when prompted ... but you always have the ability to import existing keyfiles anytime you might need/want to.

- from ctypes import POINTER, byref, cdll, c_int, windll - from ctypes.wintypes import LPCWSTR, LPWSTR +

If you have upgraded from an earlier version of the plugin, the above instructions may be all you need to do to get the new plugin up and running. Continue reading for new-key generation and existing-key management instructions.

- GetCommandLineW = cdll.kernel32.GetCommandLineW - GetCommandLineW.argtypes = [] - GetCommandLineW.restype = LPCWSTR +

Creating New Keys:

- CommandLineToArgvW = windll.shell32.CommandLineToArgvW - CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)] - CommandLineToArgvW.restype = POINTER(LPWSTR) +

On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.

+
    +
  • Unique Key Name: this is a unique name you choose to help you identify the key after it's created. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with. +
  • Your Name: Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences. +
  • Credit Card#: this is the default credit card number that was on file with Barnes & Noble at the time of download of the ebook to be de-DRMed. Nothing fancy here; no dashes or spaces ... just the 16 (15 for American Express) digits. Again... this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences. +
- 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 - xrange(start, argc.value)] - return [u"ineptepub.py"] - else: - argvencoding = sys.stdin.encoding - if argvencoding == None: - argvencoding = "utf-8" - return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] +

Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.

+

Deleting Keys:

-class IGNOBLEError(Exception): - pass +

On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that's what you truly mean to do. Once gone, it's permanently gone.

-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 +

Exporting Keys:

- if iswindows: - libcrypto = find_library('libeay32') - else: - libcrypto = find_library('crypto') +

On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a computer's hard-drive. Use this button to export the highlighted key to a file (*.b64). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.

- if libcrypto is None: - raise IGNOBLEError('libcrypto not found') - libcrypto = CDLL(libcrypto) +

Importing Existing Keyfiles:

- AES_MAXNR = 14 +

At the bottom-left of the plugin's customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing *.b64 keyfiles. Used for migrating keyfiles from older versions of the plugin (or keys generated with the original I <3 Cabbages script), or moving keyfiles from computer to computer, or restoring a backup. Some very basic validation is done to try to avoid overwriting already configured keys with incoming, imported keyfiles with the same base file name, but I'm sure that could be broken if someone tried hard. Just take care when importing.

- c_char_pp = POINTER(c_char_p) - c_int_p = POINTER(c_int) +

Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).

- class AES_KEY(Structure): - _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), - ('rounds', c_int)] - AES_KEY_p = POINTER(AES_KEY) +

Troubleshooting:

- def F(restype, name, argtypes): - func = getattr(libcrypto, name) - func.restype = restype - func.argtypes = argtypes - return func +

If you find that it's not working for you (imported Barnes & Noble epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)

- AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', - [c_char_p, c_int, AES_KEY_p]) - AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', - [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, - c_int]) +

Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub" **. Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.

- class AES(object): - def __init__(self, userkey): - self._blocksize = len(userkey) - if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : - raise IGNOBLEError('AES improper key used') - return - key = self._key = AES_KEY() - rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) - if rv < 0: - raise IGNOBLEError('Failed to initialize AES key') +

Another way to debug (perhaps easier if you're not all that comfortable with command-line stuff) is to launch calibre in debug mode. Open a command prompt (terminal) and type "calibre-debug -g" (again without the quotes). Calibre will launch, and you can can add the problem book(s) using the normal gui method. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into any online help request you make.

+

 

+

** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.

- def decrypt(self, data): - out = create_string_buffer(len(data)) - iv = ("\x00" * self._blocksize) - rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) - if rv == 0: - raise IGNOBLEError('AES decryption failed') - return out.raw +

 

+

Revision history:

+
+   0.1.0 - Initial release
+   0.1.1 - Allow Windows users to make use of openssl if they have it installed.
+          - Incorporated SomeUpdates zipfix routine.
+   0.1.2 - bug fix for non-ascii file names in encryption.xml
+   0.1.3 - Try PyCrypto on Windows first
+   0.1.4 - update zipfix to deal with mimetype not in correct place
+   0.1.5 - update zipfix to deal with completely missing mimetype files
+   0.1.6 - update to the new calibre plugin interface
+   0.1.7 - Fix for potential problem with PyCrypto
+   0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
+   0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
+   0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
+   0.2.2 - added in potential fixes from 0.1.7 that had been missed.
+   0.2.3 - fixed possible output/unicode problem
+   0.2.4 - ditched nearly hopeless caselessStrCmp method in favor of uStrCmp.
+         - added ability to rename existing keys.
+   0.2.5 - Major code change to use unaltered ignobleepub.py 3.6 and
+         - ignoblekeygen 2.4 and later.
+   0.2.6 - Modified to alleviate the issue with having both the ignoble and inept epub plugins installed/enabled
+
+ - return AES - -def _load_crypto_pycrypto(): - from Crypto.Cipher import AES as _AES - - class AES(object): - def __init__(self, key): - self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) - - def decrypt(self, data): - return self._aes.decrypt(data) - - return AES - -def _load_crypto(): - 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: - AES = loader() - break - except (ImportError, IGNOBLEError): - pass - return AES - -AES = _load_crypto() - -META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') -NSMAP = {'adept': 'http://ns.adobe.com/adept', - 'enc': 'http://www.w3.org/2001/04/xmlenc#'} - -class ZipInfo(zipfile.ZipInfo): - def __init__(self, *args, **kwargs): - if 'compress_type' in kwargs: - compress_type = kwargs.pop('compress_type') - super(ZipInfo, self).__init__(*args, **kwargs) - self.compress_type = compress_type - -class Decryptor(object): - def __init__(self, bookkey, encryption): - enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) - self._aes = AES(bookkey) - encryption = etree.fromstring(encryption) - self._encrypted = encrypted = set() - expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), - enc('CipherReference')) - for elem in encryption.findall(expr): - path = elem.get('URI', None) - if path is not None: - path = path.encode('utf-8') - encrypted.add(path) - - def decompress(self, bytes): - dc = zlib.decompressobj(-15) - bytes = dc.decompress(bytes) - ex = dc.decompress('Z') + dc.flush() - if ex: - bytes = bytes + ex - return bytes - - def decrypt(self, path, data): - if path in self._encrypted: - data = self._aes.decrypt(data)[16:] - data = data[:-ord(data[-1])] - data = self.decompress(data) - return data - -# check file to make check whether it's probably an Adobe Adept encrypted ePub -def ignobleBook(inpath): - 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: - # if we couldn't check, assume it is - return True - return False - -# return error code and error message duple -def decryptBook(keyb64, inpath, outpath): - if AES is None: - # 1 means don't try again - return (1, u"PyCrypto or OpenSSL must be installed.") - key = keyb64.decode('base64')[:16] - aes = AES(key) - 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 (1, u"Not a secure Barnes & Noble ePub.") - for name in META_NAMES: - namelist.remove(name) - 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 (1, u"Not a secure Barnes & Noble ePub.") - bookkey = aes.decrypt(bookkey.decode('base64')) - bookkey = bookkey[:-ord(bookkey[-1])] - encryption = inf.read('META-INF/encryption.xml') - decryptor = Decryptor(bookkey[-16:], encryption) - kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) - with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: - zi = ZipInfo('mimetype', compress_type=ZIP_STORED) - outf.writestr(zi, inf.read('mimetype')) - for path in namelist: - data = inf.read(path) - outf.writestr(path, decryptor.decrypt(path, data)) - except Exception, e: - return (2, u"{0}.".format(e.args[0])) - return (0, u"Success") - - -def cli_main(argv=unicode_argv()): - progname = os.path.basename(argv[0]) - if len(argv) != 4: - print u"usage: {0} ".format(progname) - return 1 - keypath, inpath, outpath = argv[1:] - userkey = open(keypath,'rb').read() - result = decryptBook(userkey, inpath, outpath) - print result[1] - return result[0] - -def gui_main(): - import Tkinter - import Tkconstants - import tkFileDialog - import traceback - - class DecryptionDialog(Tkinter.Frame): - def __init__(self, root): - Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Select files for decryption") - self.status.pack(fill=Tkconstants.X, expand=1) - body = Tkinter.Frame(self) - body.pack(fill=Tkconstants.X, expand=1) - sticky = Tkconstants.E + Tkconstants.W - body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"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(u"bnepubkey.b64"): - self.keypath.insert(0, u"bnepubkey.b64") - button = Tkinter.Button(body, text=u"...", command=self.get_keypath) - button.grid(row=0, column=2) - Tkinter.Label(body, text=u"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=u"...", command=self.get_inpath) - button.grid(row=1, column=2) - Tkinter.Label(body, text=u"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=u"...", command=self.get_outpath) - button.grid(row=2, column=2) - buttons = Tkinter.Frame(self) - buttons.pack() - botton = Tkinter.Button( - buttons, text=u"Decrypt", width=10, command=self.decrypt) - botton.pack(side=Tkconstants.LEFT) - Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) - button = Tkinter.Button( - buttons, text=u"Quit", width=10, command=self.quit) - button.pack(side=Tkconstants.RIGHT) - - def get_keypath(self): - keypath = tkFileDialog.askopenfilename( - parent=None, title=u"Select Barnes & Noble \'.b64\' key file", - defaultextension=u".b64", - filetypes=[('base64-encoded files', '.b64'), - ('All Files', '.*')]) - if keypath: - keypath = os.path.normpath(keypath) - self.keypath.delete(0, Tkconstants.END) - self.keypath.insert(0, keypath) - return - - def get_inpath(self): - inpath = tkFileDialog.askopenfilename( - parent=None, title=u"Select B&N-encrypted ePub file to decrypt", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if inpath: - inpath = os.path.normpath(inpath) - self.inpath.delete(0, Tkconstants.END) - self.inpath.insert(0, inpath) - return - - def get_outpath(self): - outpath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select unencrypted ePub file to produce", - defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) - if outpath: - outpath = os.path.normpath(outpath) - self.outpath.delete(0, Tkconstants.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'] = u"Specified key file does not exist" - return - if not inpath or not os.path.exists(inpath): - self.status['text'] = u"Specified input file does not exist" - return - if not outpath: - self.status['text'] = u"Output file not specified" - return - if inpath == outpath: - self.status['text'] = u"Must have different input and output files" - return - userkey = open(keypath,'rb').read() - self.status['text'] = u"Decrypting..." - try: - decrypt_status = decryptBook(userkey, inpath, outpath) - except Exception, e: - self.status['text'] = u"Error: {0}".format(e.args[0]) - return - if decrypt_status[0] == 0: - self.status['text'] = u"File successfully decrypted" - else: - self.status['text'] = decrypt_status[1] - - root = Tkinter.Tk() - root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) - root.resizable(True, False) - root.minsize(300, 0) - DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) - root.mainloop() - return 0 - -if __name__ == '__main__': - if len(sys.argv) > 1: - sys.stdout=SafeUnbuffered(sys.stdout) - sys.stderr=SafeUnbuffered(sys.stderr) - sys.exit(cli_main()) - sys.exit(gui_main()) + diff --git a/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py b/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py index f25359c9..b7cbdc55 100644 --- a/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py +++ b/Calibre_Plugins/ignobleepub_plugin/ignoblekeygen.py @@ -3,7 +3,7 @@ from __future__ import with_statement -# ignoblekeygen.pyw, version 2.5 +# ignobleepub.pyw, version 3.7 # Copyright © 2009-2010 by i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 @@ -15,31 +15,39 @@ # from and PyCrypto from # (make sure to # install the version for Python 2.6). Save this script file as -# ignoblekeygen.pyw and double-click on it to run it. +# ineptepub.pyw and double-click on it to run it. # -# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this -# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking +# Mac OS X users: Save this script file as ineptepub.pyw. You can run this +# program from the command line (pythonw ineptepub.pyw) or by double-clicking # it when it has been associated with PythonLauncher. # Revision history: # 1 - Initial release -# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) -# 2.1 - Allow Windows versions of libcrypto to be found -# 2.2 - On Windows try PyCrypto first and then OpenSSL next -# 2.3 - Modify interface to allow use of import -# 2.4 - Improvements to UI and now works in plugins -# 2.5 - Additional improvement for unicode and plugin support +# 2 - Added OS X support by using OpenSSL when available +# 3 - screen out improper key lengths to prevent segfaults on Linux +# 3.1 - Allow Windows versions of libcrypto to be found +# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml +# 3.3 - On Windows try PyCrypto first, OpenSSL next +# 3.4 - Modify interface to allow use with import +# 3.5 - Fix for potential problem with PyCrypto +# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 3.7 - Tweaked to match ineptepub more closely """ -Generate Barnes & Noble EPUB user key from name and credit card number. +Decrypt Barnes & Noble encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "2.5" +__version__ = "3.7" import sys import os -import hashlib +import traceback +import zlib +import zipfile +from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED +from contextlib import closing +import xml.etree.ElementTree as etree # Wrap a stream so that output gets flushed immediately # and also make sure that any unicode strings get @@ -58,8 +66,11 @@ def write(self, data): def __getattr__(self, attr): return getattr(self.stream, attr) -iswindows = sys.platform.startswith('win') -isosx = sys.platform.startswith('darwin') +try: + from calibre.constants import iswindows, isosx +except: + iswindows = sys.platform.startswith('win') + isosx = sys.platform.startswith('darwin') def unicode_argv(): if iswindows: @@ -90,9 +101,7 @@ def unicode_argv(): start = argc.value - len(sys.argv) return [argv[i] for i in xrange(start, argc.value)] - # if we don't have any arguments at all, just pass back script name - # this should never happen - return [u"ignoblekeygen.py"] + return [u"ineptepub.py"] else: argvencoding = sys.stdin.encoding if argvencoding == None: @@ -133,26 +142,29 @@ def F(restype, name, argtypes): func.argtypes = argtypes return func - AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', + AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key', [c_char_p, c_int, AES_KEY_p]) AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int]) class AES(object): - def __init__(self, userkey, iv): + def __init__(self, userkey): self._blocksize = len(userkey) - self._iv = iv + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise IGNOBLEError('AES improper key used') + return key = self._key = AES_KEY() - rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) + rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key) if rv < 0: - raise IGNOBLEError('Failed to initialize AES Encrypt key') + raise IGNOBLEError('Failed to initialize AES key') - def encrypt(self, data): + def decrypt(self, data): out = create_string_buffer(len(data)) - rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) + iv = ("\x00" * self._blocksize) + rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) if rv == 0: - raise IGNOBLEError('AES encryption failed') + raise IGNOBLEError('AES decryption failed') return out.raw return AES @@ -161,11 +173,11 @@ def _load_crypto_pycrypto(): from Crypto.Cipher import AES as _AES class AES(object): - def __init__(self, key, iv): - self._aes = _AES.new(key, _AES.MODE_CBC, iv) + def __init__(self, key): + self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) - def encrypt(self, data): - return self._aes.encrypt(data) + def decrypt(self, data): + return self._aes.decrypt(data) return AES @@ -184,78 +196,151 @@ def _load_crypto(): AES = _load_crypto() -def normalize_name(name): - return ''.join(x for x in name.lower() if x != ' ') - - -def generate_key(name, ccn): - # remove spaces and case from name and CC numbers. - if type(name)==unicode: - name = name.encode('utf-8') - if type(ccn)==unicode: - ccn = ccn.encode('utf-8') - - name = normalize_name(name) + '\x00' - ccn = normalize_name(ccn) + '\x00' - - name_sha = hashlib.sha1(name).digest()[:16] - ccn_sha = hashlib.sha1(ccn).digest()[:16] - both_sha = hashlib.sha1(name + ccn).digest() - aes = AES(ccn_sha, name_sha) - crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) - userkey = hashlib.sha1(crypt).digest() - return userkey.encode('base64') - - +META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') +NSMAP = {'adept': 'http://ns.adobe.com/adept', + 'enc': 'http://www.w3.org/2001/04/xmlenc#'} + +class ZipInfo(zipfile.ZipInfo): + def __init__(self, *args, **kwargs): + if 'compress_type' in kwargs: + compress_type = kwargs.pop('compress_type') + super(ZipInfo, self).__init__(*args, **kwargs) + self.compress_type = compress_type + +class Decryptor(object): + def __init__(self, bookkey, encryption): + enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag) + self._aes = AES(bookkey) + encryption = etree.fromstring(encryption) + self._encrypted = encrypted = set() + expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), + enc('CipherReference')) + for elem in encryption.findall(expr): + path = elem.get('URI', None) + if path is not None: + path = path.encode('utf-8') + encrypted.add(path) + + def decompress(self, bytes): + dc = zlib.decompressobj(-15) + bytes = dc.decompress(bytes) + ex = dc.decompress('Z') + dc.flush() + if ex: + bytes = bytes + ex + return bytes + + def decrypt(self, path, data): + if path in self._encrypted: + data = self._aes.decrypt(data)[16:] + data = data[:-ord(data[-1])] + data = self.decompress(data) + return data + +# check file to make check whether it's probably an Adobe Adept encrypted ePub +def ignobleBook(inpath): + 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: + # if we couldn't check, assume it is + return True + return False + +def decryptBook(keyb64, inpath, outpath): + if AES is None: + raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") + key = keyb64.decode('base64')[:16] + aes = AES(key) + 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: + print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) + return 1 + for name in META_NAMES: + namelist.remove(name) + 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: + print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) + return 1 + bookkey = aes.decrypt(bookkey.decode('base64')) + bookkey = bookkey[:-ord(bookkey[-1])] + encryption = inf.read('META-INF/encryption.xml') + decryptor = Decryptor(bookkey[-16:], encryption) + kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) + with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: + zi = ZipInfo('mimetype', compress_type=ZIP_STORED) + outf.writestr(zi, inf.read('mimetype')) + for path in namelist: + data = inf.read(path) + outf.writestr(path, decryptor.decrypt(path, data)) + except: + print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) + return 2 + return 0 def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) - if AES is None: - print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ - "separately. Read the top-of-script comment for details." % \ - (progname,) - return 1 if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 - name, ccn, keypath = argv[1:] - userkey = generate_key(name, ccn) - open(keypath,'wb').write(userkey) - return 0 - + keypath, inpath, outpath = argv[1:] + userkey = open(keypath,'rb').read() + result = decryptBook(userkey, inpath, outpath) + if result == 0: + print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) + return result def gui_main(): import Tkinter import Tkconstants import tkFileDialog - import tkMessageBox + import traceback class DecryptionDialog(Tkinter.Frame): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) - self.status = Tkinter.Label(self, text=u"Enter parameters") + self.status = Tkinter.Label(self, text=u"Select files for decryption") self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) body.pack(fill=Tkconstants.X, expand=1) sticky = Tkconstants.E + Tkconstants.W body.grid_columnconfigure(1, weight=2) - Tkinter.Label(body, text=u"Account Name").grid(row=0) - self.name = Tkinter.Entry(body, width=40) - self.name.grid(row=0, column=1, sticky=sticky) - Tkinter.Label(body, text=u"CC#").grid(row=1) - self.ccn = Tkinter.Entry(body, width=40) - self.ccn.grid(row=1, column=1, sticky=sticky) - Tkinter.Label(body, text=u"Output file").grid(row=2) - self.keypath = Tkinter.Entry(body, width=40) - self.keypath.grid(row=2, column=1, sticky=sticky) - self.keypath.insert(2, u"bnepubkey.b64") + Tkinter.Label(body, text=u"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(u"bnepubkey.b64"): + self.keypath.insert(0, u"bnepubkey.b64") button = Tkinter.Button(body, text=u"...", command=self.get_keypath) + button.grid(row=0, column=2) + Tkinter.Label(body, text=u"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=u"...", command=self.get_inpath) + button.grid(row=1, column=2) + Tkinter.Label(body, text=u"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=u"...", command=self.get_outpath) button.grid(row=2, column=2) buttons = Tkinter.Frame(self) buttons.pack() botton = Tkinter.Button( - buttons, text=u"Generate", width=10, command=self.generate) + buttons, text=u"Decrypt", width=10, command=self.decrypt) botton.pack(side=Tkconstants.LEFT) Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) button = Tkinter.Button( @@ -263,8 +348,8 @@ def __init__(self, root): button.pack(side=Tkconstants.RIGHT) def get_keypath(self): - keypath = tkFileDialog.asksaveasfilename( - parent=None, title=u"Select B&N ePub key file to produce", + keypath = tkFileDialog.askopenfilename( + parent=None, title=u"Select Barnes & Noble \'.b64\' key file", defaultextension=u".b64", filetypes=[('base64-encoded files', '.b64'), ('All Files', '.*')]) @@ -274,37 +359,56 @@ def get_keypath(self): self.keypath.insert(0, keypath) return - def generate(self): - name = self.name.get() - ccn = self.ccn.get() + def get_inpath(self): + inpath = tkFileDialog.askopenfilename( + parent=None, title=u"Select B&N-encrypted ePub file to decrypt", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) + if inpath: + inpath = os.path.normpath(inpath) + self.inpath.delete(0, Tkconstants.END) + self.inpath.insert(0, inpath) + return + + def get_outpath(self): + outpath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select unencrypted ePub file to produce", + defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) + if outpath: + outpath = os.path.normpath(outpath) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def decrypt(self): keypath = self.keypath.get() - if not name: - self.status['text'] = u"Name not specified" + inpath = self.inpath.get() + outpath = self.outpath.get() + if not keypath or not os.path.exists(keypath): + self.status['text'] = u"Specified key file does not exist" return - if not ccn: - self.status['text'] = u"Credit card number not specified" + if not inpath or not os.path.exists(inpath): + self.status['text'] = u"Specified input file does not exist" return - if not keypath: - self.status['text'] = u"Output keyfile path not specified" + if not outpath: + self.status['text'] = u"Output file not specified" return - self.status['text'] = u"Generating..." + if inpath == outpath: + self.status['text'] = u"Must have different input and output files" + return + userkey = open(keypath,'rb').read() + self.status['text'] = u"Decrypting..." try: - userkey = generate_key(name, ccn) + decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error: (0}".format(e.args[0]) + self.status['text'] = u"Error: {0}".format(e.args[0]) return - open(keypath,'wb').write(userkey) - self.status['text'] = u"Keyfile successfully generated" + if decrypt_status == 0: + self.status['text'] = u"File successfully decrypted" + else: + self.status['text'] = u"The was an error decrypting the file." root = Tkinter.Tk() - if AES is None: - root.withdraw() - tkMessageBox.showerror( - "Ignoble EPUB Keyfile Generator", - "This script requires OpenSSL or PyCrypto, which must be installed " - "separately. Read the top-of-script comment for details.") - return 1 - root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) + root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) root.resizable(True, False) root.minsize(300, 0) DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) diff --git a/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt b/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt index e69de29b..f25359c9 100644 --- a/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt +++ b/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignobleepub.txt @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +# ignoblekeygen.pyw, version 2.5 +# Copyright © 2009-2010 by i♥cabbages + +# Released under the terms of the GNU General Public Licence, version 3 +# + +# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf + +# Windows users: Before running this program, you must first install Python 2.6 +# from and PyCrypto from +# (make sure to +# install the version for Python 2.6). Save this script file as +# ignoblekeygen.pyw and double-click on it to run it. +# +# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this +# program from the command line (pythonw ignoblekeygen.pyw) or by double-clicking +# it when it has been associated with PythonLauncher. + +# Revision history: +# 1 - Initial release +# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5) +# 2.1 - Allow Windows versions of libcrypto to be found +# 2.2 - On Windows try PyCrypto first and then OpenSSL next +# 2.3 - Modify interface to allow use of import +# 2.4 - Improvements to UI and now works in plugins +# 2.5 - Additional improvement for unicode and plugin support + +""" +Generate Barnes & Noble EPUB user key from name and credit card number. +""" + +__license__ = 'GPL v3' +__version__ = "2.5" + +import sys +import os +import hashlib + +# 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,unicode): + data = data.encode(self.encoding,"replace") + self.stream.write(data) + self.stream.flush() + 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 + xrange(start, argc.value)] + # if we don't have any arguments at all, just pass back script name + # this should never happen + return [u"ignoblekeygen.py"] + else: + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = "utf-8" + return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv] + + +class IGNOBLEError(Exception): + pass + +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 iswindows: + 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) + + def F(restype, name, argtypes): + func = getattr(libcrypto, name) + func.restype = restype + func.argtypes = argtypes + return func + + AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key', + [c_char_p, c_int, AES_KEY_p]) + AES_cbc_encrypt = F(None, 'AES_cbc_encrypt', + [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, + c_int]) + + class AES(object): + def __init__(self, userkey, iv): + self._blocksize = len(userkey) + self._iv = iv + key = self._key = AES_KEY() + rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key) + if rv < 0: + raise IGNOBLEError('Failed to initialize AES Encrypt key') + + def encrypt(self, data): + out = create_string_buffer(len(data)) + rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1) + if rv == 0: + raise IGNOBLEError('AES encryption failed') + return out.raw + + return AES + +def _load_crypto_pycrypto(): + from Crypto.Cipher import AES as _AES + + class AES(object): + def __init__(self, key, iv): + self._aes = _AES.new(key, _AES.MODE_CBC, iv) + + def encrypt(self, data): + return self._aes.encrypt(data) + + return AES + +def _load_crypto(): + 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: + AES = loader() + break + except (ImportError, IGNOBLEError): + pass + return AES + +AES = _load_crypto() + +def normalize_name(name): + return ''.join(x for x in name.lower() if x != ' ') + + +def generate_key(name, ccn): + # remove spaces and case from name and CC numbers. + if type(name)==unicode: + name = name.encode('utf-8') + if type(ccn)==unicode: + ccn = ccn.encode('utf-8') + + name = normalize_name(name) + '\x00' + ccn = normalize_name(ccn) + '\x00' + + name_sha = hashlib.sha1(name).digest()[:16] + ccn_sha = hashlib.sha1(ccn).digest()[:16] + both_sha = hashlib.sha1(name + ccn).digest() + aes = AES(ccn_sha, name_sha) + crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) + userkey = hashlib.sha1(crypt).digest() + return userkey.encode('base64') + + + + +def cli_main(argv=unicode_argv()): + progname = os.path.basename(argv[0]) + if AES is None: + print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \ + "separately. Read the top-of-script comment for details." % \ + (progname,) + return 1 + if len(argv) != 4: + print u"usage: {0} ".format(progname) + return 1 + name, ccn, keypath = argv[1:] + userkey = generate_key(name, ccn) + open(keypath,'wb').write(userkey) + return 0 + + +def gui_main(): + import Tkinter + import Tkconstants + import tkFileDialog + import tkMessageBox + + class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + self.status = Tkinter.Label(self, text=u"Enter parameters") + self.status.pack(fill=Tkconstants.X, expand=1) + body = Tkinter.Frame(self) + body.pack(fill=Tkconstants.X, expand=1) + sticky = Tkconstants.E + Tkconstants.W + body.grid_columnconfigure(1, weight=2) + Tkinter.Label(body, text=u"Account Name").grid(row=0) + self.name = Tkinter.Entry(body, width=40) + self.name.grid(row=0, column=1, sticky=sticky) + Tkinter.Label(body, text=u"CC#").grid(row=1) + self.ccn = Tkinter.Entry(body, width=40) + self.ccn.grid(row=1, column=1, sticky=sticky) + Tkinter.Label(body, text=u"Output file").grid(row=2) + self.keypath = Tkinter.Entry(body, width=40) + self.keypath.grid(row=2, column=1, sticky=sticky) + self.keypath.insert(2, u"bnepubkey.b64") + button = Tkinter.Button(body, text=u"...", command=self.get_keypath) + button.grid(row=2, column=2) + buttons = Tkinter.Frame(self) + buttons.pack() + botton = Tkinter.Button( + buttons, text=u"Generate", width=10, command=self.generate) + botton.pack(side=Tkconstants.LEFT) + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + button = Tkinter.Button( + buttons, text=u"Quit", width=10, command=self.quit) + button.pack(side=Tkconstants.RIGHT) + + def get_keypath(self): + keypath = tkFileDialog.asksaveasfilename( + parent=None, title=u"Select B&N ePub key file to produce", + defaultextension=u".b64", + filetypes=[('base64-encoded files', '.b64'), + ('All Files', '.*')]) + if keypath: + keypath = os.path.normpath(keypath) + self.keypath.delete(0, Tkconstants.END) + self.keypath.insert(0, keypath) + return + + def generate(self): + name = self.name.get() + ccn = self.ccn.get() + keypath = self.keypath.get() + if not name: + self.status['text'] = u"Name not specified" + return + if not ccn: + self.status['text'] = u"Credit card number not specified" + return + if not keypath: + self.status['text'] = u"Output keyfile path not specified" + return + self.status['text'] = u"Generating..." + try: + userkey = generate_key(name, ccn) + except Exception, e: + self.status['text'] = u"Error: (0}".format(e.args[0]) + return + open(keypath,'wb').write(userkey) + self.status['text'] = u"Keyfile successfully generated" + + root = Tkinter.Tk() + if AES is None: + root.withdraw() + tkMessageBox.showerror( + "Ignoble EPUB Keyfile Generator", + "This script requires OpenSSL or PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) + root.resizable(True, False) + root.minsize(300, 0) + DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) + root.mainloop() + return 0 + +if __name__ == '__main__': + if len(sys.argv) > 1: + sys.stdout=SafeUnbuffered(sys.stdout) + sys.stderr=SafeUnbuffered(sys.stderr) + sys.exit(cli_main()) + sys.exit(gui_main()) diff --git a/Calibre_Plugins/ignobleepub_plugin/utilities.py b/Calibre_Plugins/ignobleepub_plugin/utilities.py index c730607131a9f4a51eac4a9a6a330da8cd823334..6555fed3e44628a21c76c99b2f02d6d9bc35a44c 100644 GIT binary patch literal 225 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@p>Qz@gD*)5x_AdB#1TW_j{mHkd7HZ z?1`q05g{&`oS&u76m=oc(#l$dB2|*It9*Tzl$1CH zT+|1{CON~|*;yXFnYGr;)}&^rw$OC0(keQF>ANZ9x*)YVh1QkRkA5YJN~3GYva)rp z!7Kx+8*Loy$W@tj!eNcdMNyVX%28Q=WC8K@-DlW-h@E5DT`zfa z6~*6m3zgU+G-#|=LISB$n_4*13arxx3tj?Ok%dOK8&a#-Lf-OLO;7!?_`RpH&ah+% zG6ggSx1?JOTPF5gy4)v8S8Ant+=Hi9xO;t)6@}X%7mD3bOyih9xF8UEC@H7m5j!Y^ z6aB8MFp%w`c}YQQ5hlBe@(K!#maA|BiNRkjnF9_m{~8A$W(CLVNGbMcu%}o*aXnA9IQy-I^T@EqVGRemrfJ>ew2y}F3GGS-+%y+boe5?Cw_9hyHM3Icr~{+s`wyh(3& zBaFoheiPVsZrp@N(z>=R2cLQNj9+3?e*6pygyG-gsg$RR+$27Ydl!5J$oE*77nRO* zFMqr(O4P`G%@s4zNO3$+_yhiP1lPzj>4)oF7=;$z!mai>Ti$&;_m6@J??-sFJY$%U zoF6@M+zL~GYU?%UDyZ%`A;7%&R5T5$VzL?T$arz`tSjlO@gV%2W8+@yZ-ljhb?on8 GQuGIueEG=$ diff --git a/Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py b/Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py index adf3c53996aa063764978a757d750d876fd095c9..462cd9410c68aa6f4d21eae274c8c00a4be3ee13 100644 GIT binary patch literal 225 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 0 : - self.ztype = 'epub' - self.inzip = zipfilerugged.ZipFile(zinput,'r') - self.outzip = zipfilerugged.ZipFile(zoutput,'w') - # open the input zip for reading only as a raw file - self.bzf = file(zinput,'rb') - - def getlocalname(self, zi): - local_header_offset = zi.header_offset - self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) - leninfo = self.bzf.read(2) - local_name_length, = unpack(' 0: - if len(cmpdata) > _MAX_SIZE : - newdata = cmpdata[0:_MAX_SIZE] - cmpdata = cmpdata[_MAX_SIZE:] - else: - newdata = cmpdata - cmpdata = '' - newdata = dc.decompress(newdata) - unprocessed = dc.unconsumed_tail - if len(unprocessed) == 0: - newdata += dc.flush() - data += newdata - cmpdata += unprocessed - unprocessed = '' - return data - - def getfiledata(self, zi): - # get file name length and exta data length to find start of file data - local_header_offset = zi.header_offset - - self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET) - leninfo = self.bzf.read(2) - local_name_length, = unpack('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 EKcEzEO8@`> delta 9473 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 diff --git a/Calibre_Plugins/ineptepub_plugin/__init__.py b/Calibre_Plugins/ineptepub_plugin/__init__.py index 2d20be14..e37c0517 100644 --- a/Calibre_Plugins/ineptepub_plugin/__init__.py +++ b/Calibre_Plugins/ineptepub_plugin/__init__.py @@ -6,8 +6,8 @@ __docformat__ = 'restructuredtext en' -# Released under the terms of the GNU General Public Licence, version 3 or -# later. +# Released under the terms of the GNU General Public Licence, version 3 +# # # Requires Calibre version 0.7.55 or higher. # @@ -58,10 +58,11 @@ # 0.1.8 - Fix for potential problem with PyCrypto # 0.1.9 - Fix for potential problem with ADE keys and fix possible output/unicode problem # 0.2.0 - Major code change to use unaltered ineptepub.py file 5.8 or later. +# 0.2.1 - Tweaked to eliminate issue with both ignoble and inept calibre plugins installed/enabled at once PLUGIN_NAME = u"Inept Epub DeDRM" -PLUGIN_VERSION_TUPLE = (0, 2, 0) +PLUGIN_VERSION_TUPLE = (0, 2, 1) PLUGIN_VERSION = u'.'.join([str(x) for x in PLUGIN_VERSION_TUPLE]) import sys, os, re @@ -118,16 +119,14 @@ def run(self, path_to_ebook): fr = zipfix.fixZip(path_to_ebook, inf.name) fr.fix() except Exception, e: - print u"{0} v{1}: Error when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION) + print u"{0} v{1}: Error \'{2}\' when checking zip archive.".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) raise Exception(e) return #check the book from calibre_plugins.ineptepub import ineptepub if not ineptepub.adeptBook(inf.name): - print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) - # return the original file, so that no error message is generated in the GUI - return path_to_ebook + raise ADEPTError(u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))) # Load any keyfiles (*.der) included Calibre's config directory. userkeys = [] @@ -181,30 +180,23 @@ def run(self, path_to_ebook): # Attempt to decrypt epub with each encryption key found. for userkeyinfo in userkeys: - print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, userkeyinfo[1]) + userkey,keyname = userkeyinfo + print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) of = self.temporary_file(u".epub") # Give the user key, ebook and TemporaryPersistent file to the decryption function. - result = ineptepub.decryptBook(userkeyinfo[0], inf.name, of.name) - - # Ebook is not an Adobe Adept epub... do nothing and pass it on. - # This allows a non-encrypted epub to be imported without error messages. - if result == 1: - print u"{0} v{1}: {2} is not a secure Adobe Adept ePub.".format(PLUGIN_NAME, PLUGIN_VERSION,os.path.basename(path_to_ebook)) - of.close() - return path_to_ebook - break + result = ineptepub.decryptBook(userkey, inf.name, of.name) + + of.close() # Decryption was successful return the modified PersistentTemporary # file to Calibre's import process. if result == 0: print u"{0} v{1}: Encryption successfully removed.".format(PLUGIN_NAME, PLUGIN_VERSION) - of.close() return of.name break print u"{0} v{1}: Encryption key incorrect.".format(PLUGIN_NAME, PLUGIN_VERSION) - of.close # Something went wrong with decryption. # Import the original unmolested epub. diff --git a/Calibre_Plugins/ineptepub_plugin/ineptepub.py b/Calibre_Plugins/ineptepub_plugin/ineptepub.py index 4b5a2961..48b7727c 100644 --- a/Calibre_Plugins/ineptepub_plugin/ineptepub.py +++ b/Calibre_Plugins/ineptepub_plugin/ineptepub.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import with_statement @@ -542,7 +542,7 @@ def decrypt(self): try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error; {0}".format(e) + self.status['text'] = u"Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip index 37436f659be8f7074febd5354355d436f7191492..702253e5371ef08076319367590eadc561f8b716 100644 GIT binary patch delta 32572 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 delta 32942 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 z#oocl>y?L#laYzbeCFBundleExecutable droplet CFBundleGetInfoString - DeDRM 5.5.3. AppleScript written 2010–2012 by Apprentice Alf and others. + DeDRM 5.6. AppleScript written 2010–2013 by Apprentice Alf and others. CFBundleIconFile DeDRM CFBundleInfoDictionaryVersion 6.0 CFBundleName - DeDRM 5.5.3 + DeDRM 5.6 CFBundlePackageType APPL CFBundleShortVersionString - 5.5.3 + 5.6 CFBundleSignature dplt LSRequiresCarbon diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt index 1504336a69833e85e9960d9f9354b4ce03f3615f..2108e076d5a207a67e0a765de334f705e3e4d44b 100644 GIT binary patch delta 1636 zcmZuxYfqF{5T4~dMVDm{oZSUhye*Fih^V`;+ypNms30hKMZwFm)JoH;L3^=H3LpH? zCQaIeG$)M>O_Qc#t8KN2;-$4WtMVUA{L=Hurb)jwY0{ZBZIm`A?`586=9x1y=Y1!x z^&{u;uqHvhM#-Qw_CgmC+)?jB`pEaGlpF_vH{VkiOJFYd1U+5O~xbU(Q7-5vLx`&MJI1l`6m&)p^- z$}$;BDVC#5qf|1M8qZzAEjo1&Qz&1%#jsZlL{wFb3RY>iRHQH>gb>I~{6G$}^CLB0JrPfe7@MCk?^ zJal%L zb+f2$!B#M}MdCUQI;oIH72~rXqc$Gc2(J_!r_&l+#bukpwkMf7#OyNYqR%ojdM*=Y zmxffO-5yf)$!qBL+%>7OoA_RGMzT+1yXU4Q`*z8`13l=~*dbaw4R+d@kSg`}h`GyP zm;F`DUNLtY?6%>sTG4bBdpviQl*S%8pnI_oec11&+&AuuyX?MpU%4;cC3n$%;XZd4 zH1H~PK@S=nr1zhM{#BqB z*oB3vHso^`q^LK=az)-3fq%~s!JUaazsH3HBeR9BN^o){OUQI>tYT5h|xs znYa<=(~Laj?LBTI@*Z!JTPtOGk6Yxnk!qw=k0V<4FoB5Dm_9z3jX=<;ibO;6zgwQ delta 1307 zcmZvb+iz4=6vo$?y(r9WcXm7NOs6e$zLqI1(9ZNWZKvFNfpQtzQXhui8ODYF(8f4s`-w#&?C|7%4` zFBYQKtKhd+%@s;d4~1=fZJGVUs-T>04VEpYU`IpzURhKz%pa6lha=ow8_-xs0YVTV zjUago2pfd$`3N^oMGPVoqZ2en$EiruG)c3R)#;NIV7H8@a?9jtM9Cv)4K4P4R4R9$ zd~Bs)V=x*q!B!bm*`6pr5i}yJ4XW)&QBE|a-;lg}h%0xG)@a22lBhu~5=f%X-L*$F z*Jf%Y=nhiK-65rsB1Nc20~$5zg}2^dJtggrRh(?sXb@|I!3Lj|r5p|TPZ#z0-&u`D znQSs>qIo-Ic-H<@#VLEDk`pbpQsp+%%H5VKX{iD z&?1wq2CYOI31QyCCgpAkbCWRJ(2mU-Z4$S|U<;+}&oNG=G}^`5YOqyOH;d>n=%7z! zr6v1YjDwk^0B@pGxtjv)6krxPbZKNIq1&LF8U$EhH=~ggug9QAh+QIj4SMZ_;mk%I<<|8%uqtb#$|(Vnw@N3+!XsIz?<12Q>eF!ZmkcCPW< zT_PKe^FYsaY*X&KEMgn^V|rFVwc7aVGSD$+f#9F3GNg*VlZOQ zi`*r0)L_*966dzU62_EUBGwp_ld~IpFpj(%3EO z`waHcxyQeMp@v(7d&J#uu-`tc;V;AEA`ciG_-i-81n0tg#Vr~X>BGmV-zT_EUG;bP zYl8DpoqkgS4jLS!CvDE->Qd~F3j0}-2V=+RIU3aIwX&2de|Pzz;$bNc diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py index b1b06068..036ba10e 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/alfcrypto.py @@ -34,10 +34,14 @@ def _load_libalfcrypto(): else: name_of_lib = 'libalfcrypto64.so' + # hard code to local location for libalfcrypto libalfcrypto = os.path.join(sys.path[0],name_of_lib) - if not os.path.isfile(libalfcrypto): - raise Exception('libalfcrypto not found') + libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib) + if not os.path.isfile(libalfcrypto): + libalfcrypto = os.path.join('.',name_of_lib) + if not os.path.isfile(libalfcrypto): + raise Exception('libalfcrypto not found at %s' % libalfcrypto) libalfcrypto = CDLL(libalfcrypto) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py index 6c8fa83c..c4e23b76 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py @@ -255,13 +255,15 @@ def __init__(self, filename, dict, debug, flat_xml): 'empty_text_region' : (1, 'snippets', 1, 0), - 'img' : (1, 'snippets', 1, 0), - 'img.x' : (1, 'scalar_number', 0, 0), - 'img.y' : (1, 'scalar_number', 0, 0), - 'img.h' : (1, 'scalar_number', 0, 0), - 'img.w' : (1, 'scalar_number', 0, 0), - 'img.src' : (1, 'scalar_number', 0, 0), - 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img' : (1, 'snippets', 1, 0), + 'img.x' : (1, 'scalar_number', 0, 0), + 'img.y' : (1, 'scalar_number', 0, 0), + 'img.h' : (1, 'scalar_number', 0, 0), + 'img.w' : (1, 'scalar_number', 0, 0), + 'img.src' : (1, 'scalar_number', 0, 0), + 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), + 'img.gridEndCenter' : (1, 'scalar_number', 0, 0), 'paragraph' : (1, 'snippets', 1, 0), 'paragraph.class' : (1, 'scalar_text', 0, 0), @@ -307,6 +309,7 @@ def __init__(self, filename, dict, debug, flat_xml): 'span.gridEndCenter' : (1, 'scalar_number', 0, 0), 'extratokens' : (1, 'snippets', 1, 0), + 'extratokens.class' : (1, 'scalar_text', 0, 0), 'extratokens.type' : (1, 'scalar_text', 0, 0), 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py index e5647f4b..4d83368c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/flatxml2html.py @@ -387,10 +387,14 @@ def getParaDescription(self, start, end, regtype): ws_last = int(argres) elif name.endswith('word.class'): - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' + # we only handle spaceafter word class + try: + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + except: + pass elif name.endswith('word.img.src'): result.append(('img' + word_class, int(argres))) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py index 97338872..746178f4 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/genbook.py @@ -117,7 +117,7 @@ def lookup(self,val): self.pos = val return self.stable[self.pos] else: - print "Error - %d outside of string table limits" % val + print "Error: %d outside of string table limits" % val raise TpzDRMError('outside or string table limits') # sys.exit(-1) def getSize(self): diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py index 2e0bd06d..b7cbdc55 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py @@ -3,7 +3,7 @@ from __future__ import with_statement -# ignobleepub.pyw, version 3.6 +# ignobleepub.pyw, version 3.7 # Copyright © 2009-2010 by i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 @@ -26,18 +26,19 @@ # 2 - Added OS X support by using OpenSSL when available # 3 - screen out improper key lengths to prevent segfaults on Linux # 3.1 - Allow Windows versions of libcrypto to be found -# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first and OpenSSL next -# 3.4 - Modify interace to allow use with import +# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml +# 3.3 - On Windows try PyCrypto first, OpenSSL next +# 3.4 - Modify interface to allow use with import # 3.5 - Fix for potential problem with PyCrypto # 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 3.7 - Tweaked to match ineptepub more closely """ Decrypt Barnes & Noble encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "3.6" +__version__ = "3.7" import sys import os @@ -254,18 +255,17 @@ def ignobleBook(inpath): return True return False -# return error code and error message duple def decryptBook(keyb64, inpath, outpath): if AES is None: - # 1 means don't try again - return (1, u"PyCrypto or OpenSSL must be installed.") + raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") key = keyb64.decode('base64')[:16] aes = AES(key) 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 (1, u"Not a secure Barnes & Noble ePub.") + print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) + return 1 for name in META_NAMES: namelist.remove(name) try: @@ -274,7 +274,8 @@ def decryptBook(keyb64, inpath, outpath): expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) != 64: - return (1, u"Not a secure Barnes & Noble ePub.") + print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) + return 1 bookkey = aes.decrypt(bookkey.decode('base64')) bookkey = bookkey[:-ord(bookkey[-1])] encryption = inf.read('META-INF/encryption.xml') @@ -286,21 +287,23 @@ def decryptBook(keyb64, inpath, outpath): for path in namelist: data = inf.read(path) outf.writestr(path, decryptor.decrypt(path, data)) - except Exception, e: - return (2, u"{0}.".format(e.args[0])) - return (0, u"Success") + except: + print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) + return 2 + return 0 def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) - print result[1] - return result[0] + if result == 0: + print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) + return result def gui_main(): import Tkinter @@ -399,10 +402,10 @@ def decrypt(self): except Exception, e: self.status['text'] = u"Error: {0}".format(e.args[0]) return - if decrypt_status[0] == 0: + if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" else: - self.status['text'] = decrypt_status[1] + self.status['text'] = u"The was an error decrypting the file." root = Tkinter.Tk() root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py index 4b5a2961..48b7727c 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import with_statement @@ -542,7 +542,7 @@ def decrypt(self): try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error; {0}".format(e) + self.status['text'] = u"Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index 8adb1071..70ed8985 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -50,8 +50,9 @@ # 4.7 - Added timing reports, and changed search for Mac key files # 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts # - Moved back into plugin, __init__ in plugin now only contains plugin code. +# 4.9 - Missed some invalid characters in cleanup_name -__version__ = '4.8' +__version__ = '4.9' import sys, os, re @@ -144,7 +145,7 @@ def unicode_argv(): # and with some (heavily edited) code from Paul Durrant's kindlenamer.py def cleanup_name(name): # substitute filename unfriendly characters - name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'") + name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"") # delete control characters name = u"".join(char for char in name if ord(char)>=32) # white space to single space, delete leading and trailing while space @@ -220,6 +221,7 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids): book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime) except Exception, e: print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) + traceback.print_exc() return 1 # if we're saving to the same folder as the original, use file name_ @@ -246,6 +248,7 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids): # remove internal temporary directory of Topaz pieces book.cleanup() + return 0 def usage(progname): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw index 23cc30a1..8e9290e3 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw @@ -1,13 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# DeDRM.pyw, version 5.5.3 +# DeDRM.pyw, version 5.6 # By some_updates and Apprentice Alf import sys import os, os.path sys.path.append(os.path.join(sys.path[0],"lib")) -os.environ['PYTHONIOENCODING'] = "utf-8" +import sys, os +import codecs + +from argv_utils import add_cp65001_codec, set_utf8_default_encoding, utf8_argv +add_cp65001_codec() +set_utf8_default_encoding() + import shutil import Tkinter @@ -16,15 +22,35 @@ import Tkconstants import tkFileDialog from scrolltextwidget import ScrolledText from activitybar import ActivityBar -import subprocess -from subprocess import Popen, PIPE, STDOUT -import subasyncio -from subasyncio import Process import re import simpleprefs - -__version__ = '5.5.3' +from Queue import Full +from Queue import Empty +from multiprocessing import Process, Queue + +from scriptinterface import decryptepub, decryptpdb, decryptpdf, decryptk4mobi + + +# Wrap a stream so that output gets flushed immediately +# and appended to shared queue +class QueuedStream: + def __init__(self, stream, q): + self.stream = stream + self.encoding = stream.encoding + self.q = q + if self.encoding == None: + self.encoding = "utf-8" + def write(self, data): + if isinstance(data,unicode): + data = data.encode(self.encoding,"replace") + self.q.put(data) + # self.stream.write(data) + # self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +__version__ = '5.6' class DrmException(Exception): pass @@ -35,6 +61,7 @@ class MainApp(Tk): self.withdraw() self.dnd = dnd self.apphome = apphome + # preference settings # [dictionary key, file in preferences directory where info is stored] description = [ ['pids' , 'pidlist.txt' ], @@ -152,7 +179,7 @@ class PrefsDialog(Toplevel): self.pidnums.set(self.prefs_array['pids']) self.pidinfo.grid(row=3, column=1, sticky=sticky) - Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, first character B, comma separated)').grid(row=4, sticky=Tkconstants.E) + Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, comma separated)').grid(row=4, sticky=Tkconstants.E) self.sernums = Tkinter.StringVar() self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums) if 'serials' in self.prefs_array: @@ -327,10 +354,11 @@ class ConvDialog(Toplevel): self.filenames = filenames self.interval = 50 self.p2 = None + self.q = Queue() self.running = 'inactive' self.numgood = 0 self.numbad = 0 - self.log = u"" + self.log = '' self.status = Tkinter.Label(self, text='DeDRM processing...') self.status.pack(fill=Tkconstants.X, expand=1) body = Tkinter.Frame(self) @@ -378,16 +406,18 @@ class ConvDialog(Toplevel): if len(self.filenames) > 0: filename = self.filenames.pop(0) if filename == None: - msg = u"\nComplete: Successes: {0}, Failures: {1}\n".format(self.numgood,self.numbad) + msg = '\nComplete: ' + msg += 'Successes: %d, ' % self.numgood + msg += 'Failures: %d\n' % self.numbad self.showCmdOutput(msg) if self.numbad == 0: self.after(2000,self.conversion_done()) logfile = os.path.join(rscpath,'dedrm.log') - file(logfile,'w').write(self.log.encode('utf8')) + file(logfile,'wb').write(self.log) return infile = filename bname = os.path.basename(infile) - msg = u"Processing: {0} ... ".format(bname) + msg = 'Processing: ' + bname + ' ... ' self.log += msg self.showCmdOutput(msg) outdir = os.path.dirname(filename) @@ -399,9 +429,9 @@ class ConvDialog(Toplevel): if rv == 0: self.bar.start() self.running = 'active' - self.processPipe() + self.processQueue() else: - msg = u"Unknown File: {0}\n".format(bname) + msg = 'Unknown File: ' + bname + '\n' self.log += msg self.showCmdOutput(msg) self.numbad += 1 @@ -410,7 +440,7 @@ class ConvDialog(Toplevel): # kill any still running subprocess self.running = 'stopped' if self.p2 != None: - if (self.p2.wait('nowait') == None): + if (self.p2.exitcode == None): self.p2.terminate() self.conversion_done() @@ -426,130 +456,127 @@ class ConvDialog(Toplevel): # read from subprocess pipe without blocking # invoked every interval via the widget "after" # option being used, so need to reset it for the next time - def processPipe(self): + def processQueue(self): if self.p2 == None: # nothing to wait for so just return return - poll = self.p2.wait('nowait') + poll = self.p2.exitcode if poll != None: self.bar.stop() if poll == 0: - msg = u"\nSuccess\n" + msg = 'Success\n' self.numgood += 1 - text = self.p2.read().decode('utf8') - text += self.p2.readerr().decode('utf8') + done = False + text = '' + while not done: + try: + data = self.q.get_nowait() + text += data + except Empty: + done = True + pass self.log += text self.log += msg - else: - msg = u"\nFailed\n" - text = self.p2.read().decode('utf8') - text += self.p2.readerr().decode('utf8') - msg += text - self.numbad += 1 + if poll != 0: + msg = 'Failed\n' + done = False + text = '' + while not done: + try: + data = self.q.get_nowait() + text += data + except Empty: + done = True + pass + msg += '\n' + self.log += text self.log += msg + self.numbad += 1 + self.p2.join() self.showCmdOutput(msg) self.p2 = None self.running = 'inactive' self.after(50,self.processBooks) return + try: + text = self.q.get_nowait() + except Empty: + text = '' + pass + if text != '': + self.log += text # make sure we get invoked again by event loop after interval - self.stext.after(self.interval,self.processPipe) + self.stext.after(self.interval,self.processQueue) return def decrypt_ebook(self, infile, outdir, rscpath): - apphome = self.apphome + q = self.q rv = 1 name, ext = os.path.splitext(os.path.basename(infile)) ext = ext.lower() if ext == '.epub': - self.p2 = processEPUB(apphome, infile, outdir, rscpath) + self.p2 = Process(target=processEPUB, args=(q, infile, outdir, rscpath)) + self.p2.start() return 0 if ext == '.pdb': - self.p2 = processPDB(apphome, infile, outdir, rscpath) + self.p2 = Process(target=processPDB, args=(q, infile, outdir, rscpath)) + self.p2.start() return 0 if ext in ['.azw', '.azw1', '.azw3', '.azw4', '.prc', '.mobi', '.tpz']: - self.p2 = processK4MOBI(apphome, infile, outdir, rscpath) + self.p2 = Process(target=processK4MOBI,args=(q, infile, outdir, rscpath)) + self.p2.start() return 0 if ext == '.pdf': - self.p2 = processPDF(apphome, infile, outdir, rscpath) + self.p2 = Process(target=processPDF, args=(q, infile, outdir, rscpath)) + self.p2.start() return 0 return rv -# run as a subprocess via pipes and collect stdout, stderr, and return value -def runit(apphome, ncmd, nparms): - pengine = sys.executable - if pengine is None or pengine == '': - pengine = 'python' - pengine = os.path.normpath(pengine) - cmdline = pengine + ' "' + os.path.join(apphome, ncmd) + '" ' - # if sys.platform.startswith('win'): - # search_path = os.environ['PATH'] - # search_path = search_path.lower() - # if search_path.find('python') < 0: - # # if no python hope that win registry finds what is associated with py extension - # cmdline = pengine + ' "' + os.path.join(apphome, ncmd) + '" ' - cmdline += nparms - cmdline = cmdline.encode(sys.getfilesystemencoding()) - p2 = subasyncio.Process(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False, env = os.environ) - return p2 - -def processK4MOBI(apphome, infile, outdir, rscpath): - cmd = os.path.join('lib','k4mobidedrm.py') - parms = '' - pidnums = '' - pidspath = os.path.join(rscpath,'pidlist.txt') - if os.path.exists(pidspath): - pidnums = file(pidspath,'r').read() - pidnums = pidnums.rstrip(os.linesep) - if pidnums != '': - parms += '-p "' + pidnums + '" ' - serialnums = '' - serialnumspath = os.path.join(rscpath,'seriallist.txt') - if os.path.exists(serialnumspath): - serialnums = file(serialnumspath,'r').read() - serialnums = serialnums.rstrip(os.linesep) - if serialnums != '': - parms += '-s "' + serialnums + '" ' - - files = os.listdir(rscpath) - filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - dpath = os.path.join(rscpath,filename) - parms += '-k "' + dpath + '" ' - parms += '"' + infile +'" "' + outdir + '"' - p2 = runit(apphome, cmd, parms) - return p2 - -def processPDF(apphome, infile, outdir, rscpath): - cmd = os.path.join('lib','decryptpdf.py') - parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"' - p2 = runit(apphome, cmd, parms) - return p2 - -def processEPUB(apphome, infile, outdir, rscpath): - # invoke routine to check both Adept and Barnes and Noble - cmd = os.path.join('lib','decryptepub.py') - parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"' - p2 = runit(apphome, cmd, parms) - return p2 - -def processPDB(apphome, infile, outdir, rscpath): - cmd = os.path.join('lib','decryptpdb.py') - parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"' - p2 = runit(apphome, cmd, parms) - return p2 - - -def main(argv=sys.argv): - apphome = os.path.dirname(sys.argv[0]) +# child process starts here +def processK4MOBI(q, infile, outdir, rscpath): + add_cp65001_codec() + set_utf8_default_encoding() + sys.stdout = QueuedStream(sys.stdout, q) + sys.stderr = QueuedStream(sys.stderr, q) + rv = decryptk4mobi(infile, outdir, rscpath) + sys.exit(rv) + +# child process starts here +def processPDF(q, infile, outdir, rscpath): + add_cp65001_codec() + set_utf8_default_encoding() + sys.stdout = QueuedStream(sys.stdout, q) + sys.stderr = QueuedStream(sys.stderr, q) + rv = decryptpdf(infile, outdir, rscpath) + sys.exit(rv) + +# child process starts here +def processEPUB(q, infile, outdir, rscpath): + add_cp65001_codec() + set_utf8_default_encoding() + sys.stdout = QueuedStream(sys.stdout, q) + sys.stderr = QueuedStream(sys.stderr, q) + rv = decryptepub(infile, outdir, rscpath) + sys.exit(rv) + +# child process starts here +def processPDB(q, infile, outdir, rscpath): + add_cp65001_codec() + set_utf8_default_encoding() + sys.stdout = QueuedStream(sys.stdout, q) + sys.stderr = QueuedStream(sys.stderr, q) + rv = decryptpdb(infile, outdir, rscpath) + sys.exit(rv) + + +def main(argv=utf8_argv()): + apphome = os.path.dirname(argv[0]) apphome = os.path.abspath(apphome) # windows may pass a spurious quoted null string as argv[1] from bat file # simply work around this until we can figure out a better way to handle things - if len(argv) == 2: + if sys.platform.startswith('win') and len(argv) == 2: temp = argv[1] temp = temp.strip('"') temp = temp.strip() @@ -563,11 +590,10 @@ def main(argv=sys.argv): else : # processing books via drag and drop dnd = True # build a list of the files to be processed + # note all filenames and paths have been utf-8 encoded infilelst = argv[1:] filenames = [] for infile in infilelst: - infile = infile.decode(sys.getfilesystemencoding()) - print infile infile = infile.replace('"','') infile = os.path.abspath(infile) if os.path.isdir(infile): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py index b1b06068..036ba10e 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.py @@ -34,10 +34,14 @@ def _load_libalfcrypto(): else: name_of_lib = 'libalfcrypto64.so' + # hard code to local location for libalfcrypto libalfcrypto = os.path.join(sys.path[0],name_of_lib) - if not os.path.isfile(libalfcrypto): - raise Exception('libalfcrypto not found') + libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib) + if not os.path.isfile(libalfcrypto): + libalfcrypto = os.path.join('.',name_of_lib) + if not os.path.isfile(libalfcrypto): + raise Exception('libalfcrypto not found at %s' % libalfcrypto) libalfcrypto = CDLL(libalfcrypto) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py new file mode 100644 index 00000000..717387a7 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/argv_utils.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys, os +import locale +import codecs + +# get sys.argv arguments and encode them into utf-8 +def utf8_argv(): + if sys.platform.startswith('win'): + # 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 '?'. So use shell32.GetCommandLineArgvW to get sys.argv + # as a list of Unicode strings and encode them as utf-8 + + 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].encode('utf-8') for i in + xrange(start, argc.value)] + # this should never happen + return None + else: + argv = [] + argvencoding = sys.stdin.encoding + if argvencoding == None: + argvencoding = sys.getfilesystemencoding() + if argvencoding == None: + argvencoding = 'utf-8' + for arg in sys.argv: + if type(arg) == unicode: + argv.append(arg.encode('utf-8')) + else: + argv.append(arg.decode(argvencoding).encode('utf-8')) + return argv + + +def add_cp65001_codec(): + try: + codecs.lookup('cp65001') + except LookupError: + codecs.register( + lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) + return + + +def set_utf8_default_encoding(): + if sys.getdefaultencoding() == 'utf-8': + return + + # Regenerate setdefaultencoding. + reload(sys) + sys.setdefaultencoding('utf-8') + + for attr in dir(locale): + if attr[0:3] != 'LC_': + continue + aref = getattr(locale, attr) + try: + locale.setlocale(aref, '') + except locale.Error: + continue + try: + lang = locale.getlocale(aref)[0] + except (TypeError, ValueError): + continue + if lang: + try: + locale.setlocale(aref, (lang, 'UTF-8')) + except locale.Error: + os.environ[attr] = lang + '.UTF-8' + try: + locale.setlocale(locale.LC_ALL, '') + except locale.Error: + pass + return + + diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py index 6c8fa83c..c4e23b76 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/convert2xml.py @@ -255,13 +255,15 @@ def __init__(self, filename, dict, debug, flat_xml): 'empty_text_region' : (1, 'snippets', 1, 0), - 'img' : (1, 'snippets', 1, 0), - 'img.x' : (1, 'scalar_number', 0, 0), - 'img.y' : (1, 'scalar_number', 0, 0), - 'img.h' : (1, 'scalar_number', 0, 0), - 'img.w' : (1, 'scalar_number', 0, 0), - 'img.src' : (1, 'scalar_number', 0, 0), - 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img' : (1, 'snippets', 1, 0), + 'img.x' : (1, 'scalar_number', 0, 0), + 'img.y' : (1, 'scalar_number', 0, 0), + 'img.h' : (1, 'scalar_number', 0, 0), + 'img.w' : (1, 'scalar_number', 0, 0), + 'img.src' : (1, 'scalar_number', 0, 0), + 'img.color_src' : (1, 'scalar_number', 0, 0), + 'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), + 'img.gridEndCenter' : (1, 'scalar_number', 0, 0), 'paragraph' : (1, 'snippets', 1, 0), 'paragraph.class' : (1, 'scalar_text', 0, 0), @@ -307,6 +309,7 @@ def __init__(self, filename, dict, debug, flat_xml): 'span.gridEndCenter' : (1, 'scalar_number', 0, 0), 'extratokens' : (1, 'snippets', 1, 0), + 'extratokens.class' : (1, 'scalar_text', 0, 0), 'extratokens.type' : (1, 'scalar_text', 0, 0), 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptepub.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptepub.py deleted file mode 100644 index e64c8606..00000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptepub.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -class Unbuffered: - def __init__(self, stream): - self.stream = stream - def write(self, data): - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -import sys -sys.stdout=Unbuffered(sys.stdout) -import os - -import ineptepub -import ignobleepub -import zipfix -import re - -def main(argv=sys.argv): - args = argv[1:] - if len(args) != 3: - return -1 - infile = args[0] - outdir = args[1] - rscpath = args[2] - errlog = '' - - # first fix the epub to make sure we do not get errors - name, ext = os.path.splitext(os.path.basename(infile)) - bpath = os.path.dirname(infile) - zippath = os.path.join(bpath,name + '_temp.zip') - rv = zipfix.repairBook(infile, zippath) - if rv != 0: - print "Error while trying to fix epub" - return rv - - # determine a good name for the output file - outfile = os.path.join(outdir, name + '_nodrm.epub') - - rv = 1 - # first try with the Adobe adept epub - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - try: - rv = ineptepub.decryptBook(keypath, zippath, outfile) - if rv == 0: - break - except Exception, e: - errlog += str(e) - rv = 1 - pass - if rv == 0: - os.remove(zippath) - return 0 - - # still no luck - # now try with ignoble epub - # try with any keyfiles (*.b64) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.b64$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - try: - rv = ignobleepub.decryptBook(keypath, zippath, outfile) - if rv == 0: - break - except Exception, e: - errlog += str(e) - rv = 1 - pass - os.remove(zippath) - if rv != 0: - print errlog - return rv - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py deleted file mode 100644 index f0775c14..00000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdb.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -class Unbuffered: - def __init__(self, stream): - self.stream = stream - def write(self, data): - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -import sys -sys.stdout=Unbuffered(sys.stdout) -import os - -import erdr2pml - -def main(argv=sys.argv): - args = argv[1:] - if len(args) != 3: - return -1 - infile = args[0] - outdir = args[1] - rscpath = args[2] - rv = 1 - socialpath = os.path.join(rscpath,'sdrmlist.txt') - if os.path.exists(socialpath): - keydata = file(socialpath,'r').read() - keydata = keydata.rstrip(os.linesep) - ar = keydata.split(',') - for i in ar: - try: - name, cc8 = i.split(':') - except ValueError: - print ' Error parsing user supplied social drm data.' - return 1 - rv = erdr2pml.decryptBook(infile, outdir, True, erdr2pml.getuser_key(name, cc8) ) - if rv == 0: - break - return rv - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdf.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdf.py deleted file mode 100644 index ddaeacdc..00000000 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/decryptpdf.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python -# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab - -class Unbuffered: - def __init__(self, stream): - self.stream = stream - def write(self, data): - self.stream.write(data) - self.stream.flush() - def __getattr__(self, attr): - return getattr(self.stream, attr) - -import sys -sys.stdout=Unbuffered(sys.stdout) -import os -import re -import ineptpdf - -def main(argv=sys.argv): - args = argv[1:] - if len(args) != 3: - return -1 - infile = args[0] - outdir = args[1] - rscpath = args[2] - errlog = '' - rv = 1 - - # determine a good name for the output file - name, ext = os.path.splitext(os.path.basename(infile)) - outfile = os.path.join(outdir, name + '_nodrm.pdf') - - # try with any keyfiles (*.der) in the rscpath - files = os.listdir(rscpath) - filefilter = re.compile("\.der$", re.IGNORECASE) - files = filter(filefilter.search, files) - if files: - for filename in files: - keypath = os.path.join(rscpath, filename) - try: - rv = ineptpdf.decryptBook(keypath, infile, outfile) - if rv == 0: - break - except Exception, e: - errlog += str(e) - rv = 1 - pass - if rv != 0: - print errlog - return rv - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py index e5647f4b..4d83368c 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/flatxml2html.py @@ -387,10 +387,14 @@ def getParaDescription(self, start, end, regtype): ws_last = int(argres) elif name.endswith('word.class'): - (cname, space) = argres.split('-',1) - if space == '' : space = '0' - if (cname == 'spaceafter') and (int(space) > 0) : - word_class = 'sa' + # we only handle spaceafter word class + try: + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + except: + pass elif name.endswith('word.img.src'): result.append(('img' + word_class, int(argres))) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py index 97338872..746178f4 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/genbook.py @@ -117,7 +117,7 @@ def lookup(self,val): self.pos = val return self.stable[self.pos] else: - print "Error - %d outside of string table limits" % val + print "Error: %d outside of string table limits" % val raise TpzDRMError('outside or string table limits') # sys.exit(-1) def getSize(self): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py index 2e0bd06d..b7cbdc55 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py @@ -3,7 +3,7 @@ from __future__ import with_statement -# ignobleepub.pyw, version 3.6 +# ignobleepub.pyw, version 3.7 # Copyright © 2009-2010 by i♥cabbages # Released under the terms of the GNU General Public Licence, version 3 @@ -26,18 +26,19 @@ # 2 - Added OS X support by using OpenSSL when available # 3 - screen out improper key lengths to prevent segfaults on Linux # 3.1 - Allow Windows versions of libcrypto to be found -# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml -# 3.3 - On Windows try PyCrypto first and OpenSSL next -# 3.4 - Modify interace to allow use with import +# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml +# 3.3 - On Windows try PyCrypto first, OpenSSL next +# 3.4 - Modify interface to allow use with import # 3.5 - Fix for potential problem with PyCrypto # 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code +# 3.7 - Tweaked to match ineptepub more closely """ Decrypt Barnes & Noble encrypted ePub books. """ __license__ = 'GPL v3' -__version__ = "3.6" +__version__ = "3.7" import sys import os @@ -254,18 +255,17 @@ def ignobleBook(inpath): return True return False -# return error code and error message duple def decryptBook(keyb64, inpath, outpath): if AES is None: - # 1 means don't try again - return (1, u"PyCrypto or OpenSSL must be installed.") + raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") key = keyb64.decode('base64')[:16] aes = AES(key) 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 (1, u"Not a secure Barnes & Noble ePub.") + print u"{0:s} is DRM-free.".format(os.path.basename(inpath)) + return 1 for name in META_NAMES: namelist.remove(name) try: @@ -274,7 +274,8 @@ def decryptBook(keyb64, inpath, outpath): expr = './/%s' % (adept('encryptedKey'),) bookkey = ''.join(rights.findtext(expr)) if len(bookkey) != 64: - return (1, u"Not a secure Barnes & Noble ePub.") + print u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)) + return 1 bookkey = aes.decrypt(bookkey.decode('base64')) bookkey = bookkey[:-ord(bookkey[-1])] encryption = inf.read('META-INF/encryption.xml') @@ -286,21 +287,23 @@ def decryptBook(keyb64, inpath, outpath): for path in namelist: data = inf.read(path) outf.writestr(path, decryptor.decrypt(path, data)) - except Exception, e: - return (2, u"{0}.".format(e.args[0])) - return (0, u"Success") + except: + print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()) + return 2 + return 0 def cli_main(argv=unicode_argv()): progname = os.path.basename(argv[0]) if len(argv) != 4: - print u"usage: {0} ".format(progname) + print u"usage: {0} ".format(progname) return 1 keypath, inpath, outpath = argv[1:] userkey = open(keypath,'rb').read() result = decryptBook(userkey, inpath, outpath) - print result[1] - return result[0] + if result == 0: + print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)) + return result def gui_main(): import Tkinter @@ -399,10 +402,10 @@ def decrypt(self): except Exception, e: self.status['text'] = u"Error: {0}".format(e.args[0]) return - if decrypt_status[0] == 0: + if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" else: - self.status['text'] = decrypt_status[1] + self.status['text'] = u"The was an error decrypting the file." root = Tkinter.Tk() root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py index 4b5a2961..48b7727c 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py @@ -1,4 +1,4 @@ -#! /usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import with_statement @@ -542,7 +542,7 @@ def decrypt(self): try: decrypt_status = decryptBook(userkey, inpath, outpath) except Exception, e: - self.status['text'] = u"Error; {0}".format(e) + self.status['text'] = u"Error: {0}".format(e.args[0]) return if decrypt_status == 0: self.status['text'] = u"File successfully decrypted" diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py index 8adb1071..70ed8985 100644 --- a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mobidedrm.py @@ -50,8 +50,9 @@ # 4.7 - Added timing reports, and changed search for Mac key files # 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts # - Moved back into plugin, __init__ in plugin now only contains plugin code. +# 4.9 - Missed some invalid characters in cleanup_name -__version__ = '4.8' +__version__ = '4.9' import sys, os, re @@ -144,7 +145,7 @@ def unicode_argv(): # and with some (heavily edited) code from Paul Durrant's kindlenamer.py def cleanup_name(name): # substitute filename unfriendly characters - name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'") + name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"") # delete control characters name = u"".join(char for char in name if ord(char)>=32) # white space to single space, delete leading and trailing while space @@ -220,6 +221,7 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids): book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime) except Exception, e: print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) + traceback.print_exc() return 1 # if we're saving to the same folder as the original, use file name_ @@ -246,6 +248,7 @@ def decryptBook(infile, outdir, kInfoFiles, serials, pids): # remove internal temporary directory of Topaz pieces book.cleanup() + return 0 def usage(progname): diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py new file mode 100644 index 00000000..b8f1cff5 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/scriptinterface.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import sys +import os +import re +import ineptepub +import ignobleepub +import zipfix +import ineptpdf +import erdr2pml +import k4mobidedrm + +def decryptepub(infile, outdir, rscpath): + errlog = '' + + # first fix the epub to make sure we do not get errors + name, ext = os.path.splitext(os.path.basename(infile)) + bpath = os.path.dirname(infile) + zippath = os.path.join(bpath,name + '_temp.zip') + rv = zipfix.repairBook(infile, zippath) + if rv != 0: + print "Error while trying to fix epub" + return rv + + # determine a good name for the output file + outfile = os.path.join(outdir, name + '_nodrm.epub') + + rv = 1 + # first try with the Adobe adept epub + # try with any keyfiles (*.der) in the rscpath + files = os.listdir(rscpath) + filefilter = re.compile("\.der$", re.IGNORECASE) + files = filter(filefilter.search, files) + if files: + for filename in files: + keypath = os.path.join(rscpath, filename) + userkey = open(keypath,'rb').read() + try: + rv = ineptepub.decryptBook(userkey, zippath, outfile) + if rv == 0: + break + except Exception, e: + errlog += str(e) + rv = 1 + pass + if rv == 0: + os.remove(zippath) + return 0 + + # still no luck + # now try with ignoble epub + # try with any keyfiles (*.b64) in the rscpath + files = os.listdir(rscpath) + filefilter = re.compile("\.b64$", re.IGNORECASE) + files = filter(filefilter.search, files) + if files: + for filename in files: + keypath = os.path.join(rscpath, filename) + userkey = open(keypath,'rb').read() + try: + rv = ignobleepub.decryptBook(userkey, zippath, outfile) + if rv == 0: + break + except Exception, e: + errlog += str(e) + rv = 1 + pass + os.remove(zippath) + if rv != 0: + print errlog + return rv + + +def decryptpdf(infile, outdir, rscpath): + errlog = '' + rv = 1 + + # determine a good name for the output file + name, ext = os.path.splitext(os.path.basename(infile)) + outfile = os.path.join(outdir, name + '_nodrm.pdf') + + # try with any keyfiles (*.der) in the rscpath + files = os.listdir(rscpath) + filefilter = re.compile("\.der$", re.IGNORECASE) + files = filter(filefilter.search, files) + if files: + for filename in files: + keypath = os.path.join(rscpath, filename) + userkey = open(keypath,'rb').read() + try: + rv = ineptpdf.decryptBook(userkey, infile, outfile) + if rv == 0: + break + except Exception, e: + errlog += str(e) + rv = 1 + pass + if rv != 0: + print errlog + return rv + + +def decryptpdb(infile, outdir, rscpath): + outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz" + outpath = os.path.join(outdir, outname) + rv = 1 + socialpath = os.path.join(rscpath,'sdrmlist.txt') + if os.path.exists(socialpath): + keydata = file(socialpath,'r').read() + keydata = keydata.rstrip(os.linesep) + ar = keydata.split(',') + for i in ar: + try: + name, cc8 = i.split(':') + except ValueError: + print ' Error parsing user supplied social drm data.' + return 1 + rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) + if rv == 0: + break + return rv + + +def decryptk4mobi(infile, outdir, rscpath): + rv = 1 + pidnums = [] + pidspath = os.path.join(rscpath,'pidlist.txt') + if os.path.exists(pidspath): + pidstr = file(pidspath,'r').read() + pidstr = pidstr.rstrip(os.linesep) + pidstr = pidstr.strip() + if pidstr != '': + pidnums = pidstr.split(',') + serialnums = [] + serialnumspath = os.path.join(rscpath,'seriallist.txt') + if os.path.exists(serialnumspath): + serialstr = file(serialnumspath,'r').read() + serialstr = serialstr.rstrip(os.linesep) + serialstr = serialstr.strip() + if serialstr != '': + serialnums = serialstr.split(',') + kInfoFiles = [] + files = os.listdir(rscpath) + filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE) + files = filter(filefilter.search, files) + if files: + for filename in files: + dpath = os.path.join(rscpath,filename) + kInfoFiles.append(dpath) + rv = k4mobidedrm.decryptBook(infile, outdir, kInfoFiles, serialnums, pidnums) + return rv diff --git a/DeDRM_Windows_Application/DeDRM_ReadMe.txt b/DeDRM_Windows_Application/DeDRM_ReadMe.txt index ad52d330..2650a190 100644 --- a/DeDRM_Windows_Application/DeDRM_ReadMe.txt +++ b/DeDRM_Windows_Application/DeDRM_ReadMe.txt @@ -1,7 +1,7 @@ -ReadMe_DeDRM_v5.5.3_WinApp +ReadMe_DeDRM_v5.6_WinApp ======================== -DeDRM_v5.5.3_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed. It repackages all the "tools" python software in one easy to use program that remembers preferences and settings. +DeDRM_v5.6_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed. It repackages all the "tools" python software in one easy to use program that remembers preferences and settings. It will work without manual configuration for Kindle for PC ebooks and Adobe Adept epub and pdf ebooks. @@ -23,9 +23,9 @@ Installation 0. If you don't already have a correct version of Python and PyCrypto installed, follow the "Installing Python on Windows" and "Installing PyCrypto on Windows" sections below before continuing. -1. Drag the DeDRM_5.5.3 folder from tools_v5.5.3/DeDRM_Applications/Windows to your "My Documents" folder. +1. Drag the DeDRM_5.6 folder from tools_v5.6/DeDRM_Applications/Windows to your "My Documents" folder. -2. Open the DeDRM_5.5.3 folder you've just dragged, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop. +2. Open the DeDRM_5.6 folder you've just dragged, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop. 3. To set the preferences simply double-click on your just created short-cut. diff --git a/Other_Tools/B&N_Download_Helper/BN-Dload.user_ReadMe.txt b/Other_Tools/B&N_Download_Helper/BN-Dload.user_ReadMe.txt index 6c41ed52..bf0390be 100644 --- a/Other_Tools/B&N_Download_Helper/BN-Dload.user_ReadMe.txt +++ b/Other_Tools/B&N_Download_Helper/BN-Dload.user_ReadMe.txt @@ -9,7 +9,7 @@ If the downloaded file is encrypted, install and configure the ignoble plugin in DOWNLOAD HIDDEN FILES FROM B&N ------------------------------ -Some content is not downloadable from the B&N website, notably magazines. The Greasemonkey script included in the tools modifies the myNook page of the Barnes and Noble website to show a download button for normally non-downloadable content. This will work until Barnes & Noble changes their website. +Some content is not downloadable from the B&N website, notably magazines. A Greasemonkey script (link below) modifies the myNook page of the Barnes and Noble website to show a download button for normally non-downloadable content. This will work until Barnes & Noble changes their website. Prerequisites ------------- diff --git a/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51.pyw b/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51.pyw new file mode 100644 index 00000000..6277c502 --- /dev/null +++ b/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51.pyw @@ -0,0 +1,3160 @@ +#! /usr/bin/python + +# ineptpdf8.4.51.pyw +# ineptpdf, version 8.4.51 + +# To run this program install Python 2.7 from http://www.python.org/download/ +# +# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# +# and PyWin Extension (Win32API module) from +# http://sourceforge.net/projects/pywin32/files/ +# +# Make sure to install the dedicated versions for Python 2.7. +# +# It's recommended to use the 32-Bit Python Windows versions (even with a 64-bit +# Windows system). +# +# Save this script file as +# ineptpdf8.4.51.pyw and double-click on it to run it. + +# Revision history: +# 1 - Initial release +# 2 - Improved determination of key-generation algorithm +# 3 - Correctly handle PDF >=1.5 cross-reference streams +# 4 - Removal of ciando's personal ID (anon) +# 5 - removing small bug with V3 ebooks (anon) +# 6 - changed to adeptkey4.der format for 1.7.2 support (anon) +# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der (anon) +# 7 - Get cross reference streams and object streams working for input. +# Not yet supported on output but this only effects file size, +# not functionality. (anon2) +# 7.1 - Correct a problem when an old trailer is not followed by startxref (anon2) +# 7.2 - Correct malformed Mac OS resource forks for Stanza +# - Support for cross ref streams on output (decreases file size) (anon2) +# 7.3 - Correct bug in trailer with cross ref stream that caused the error (anon2) +# "The root object is missing or invalid" in Adobe Reader. +# 7.4 - Force all generation numbers in output file to be 0, like in v6. +# Fallback code for wrong xref improved (search till last trailer +# instead of first) (anon2) +# 8 - fileopen user machine identifier support (Tetrachroma) +# 8.1 - fileopen user cookies support (Tetrachroma) +# 8.2 - fileopen user name/password support (Tetrachroma) +# 8.3 - fileopen session cookie support (Tetrachroma) +# 8.3.1 - fix for the "specified key file does not exist" error (Tetrachroma) +# 8.3.2 - improved server result parsing (Tetrachroma) +# 8.4 - Ident4D and encrypted Uuid support (Tetrachroma) +# 8.4.1 - improved MAC address processing (Tetrachroma) +# 8.4.2 - FowP3Uuid fallback file processing (Tetrachroma) +# 8.4.3 - improved user/password pdf file detection (Tetrachroma) +# 8.4.4 - small bugfix (Tetrachroma) +# 8.4.5 - improved cookie host searching (Tetrachroma) +# 8.4.6 - STRICT parsing disabled (non-standard pdf processing) (Tetrachroma) +# 8.4.7 - UTF-8 input file conversion (Tetrachroma) +# 8.4.8 - fix for more rare utf8 problems (Tetrachroma) +# 8.4.9 - solution for utf8 in comination with +# ident4id method (Tetrachroma) +# 8.4.10 - line feed processing, non c system drive patch, nrbook support (Tetrachroma) +# 8.4.11 - alternative ident4id calculation (Tetrachroma) +# 8.4.12 - fix for capital username characters and +# other unusual user login names (Tetrachroma & ZeroPoint) +# 8.4.13 - small bug fixes (Tetrachroma) +# 8.4.14 - fix for non-standard-conform fileopen pdfs (Tetrachroma) +# 8.4.15 - 'bad file descriptor'-fix (Tetrachroma) +# 8.4.16 - improves user/pass detection (Tetrachroma) +# 8.4.17 - fix for several '=' chars in a DPRM entity (Tetrachroma) +# 8.4.18 - follow up bug fix for the DPRM problem, +# more readable error messages (Tetrachroma) +# 8.4.19 - 2nd fix for 'bad file descriptor' problem (Tetrachroma) +# 8.4.20 - follow up patch (Tetrachroma) +# 8.4.21 - 3rd patch for 'bad file descriptor' (Tetrachroma) +# 8.4.22 - disable prints for exception prevention (Tetrachroma) +# 8.4.23 - check for additional security attributes (Tetrachroma) +# 8.4.24 - improved cookie session support (Tetrachroma) +# 8.4.25 - more compatibility with unicode files (Tetrachroma) +# 8.4.26 - automated session/user cookie request function (works +# only with Firefox 3.x+) (Tetrachroma) +# 8.4.27 - user/password fallback +# 8.4.28 - AES decryption, improved misconfigured pdf handling, +# limited experimental APS support (Tetrachroma & Neisklar) +# 8.4.29 - backport for bad formatted rc4 encrypted pdfs (Tetrachroma) +# 8.4.30 - extended authorization attributes support (Tetrachroma) +# 8.4.31 - improved session cookie and better server response error +# handling (Tetrachroma) +# 8.4.33 - small cookie optimizations (Tetrachroma) +# 8.4.33 - debug output option (Tetrachroma) +# 8.4.34 - better user/password management +# handles the 'AskUnp' response) (Tetrachroma) +# 8.4.35 - special handling for non-standard systems (Tetrachroma) +# 8.4.36 - previous machine/disk handling [PrevMach/PrevDisk] (Tetrachroma) +# 8.4.36 - FOPN_flock support (Tetrachroma) +# 8.4.37 - patch for unicode paths/filenames (Tetrachroma) +# 8.4.38 - small fix for user/password dialog (Tetrachroma) +# 8.4.39 - sophisticated request mode differentiation, forced +# uuid calculation (Tetrachroma) +# 8.4.40 - fix for non standard server responses (Tetrachroma) +# 8.4.41 - improved user/password request windows, +# better server response tolerance (Tetrachroma) +# 8.4.42 - improved nl/cr server response parsing (Tetrachroma) +# 8.4.43 - fix for user names longer than 13 characters and special +# uuid encryption (Tetrachroma) +# 8.4.44 - another fix for ident4d problem (Tetrachroma) +# 8.4.45 - 2nd fix for ident4d problem (Tetrachroma) +# 8.4.46 - script cleanup and optimizations (Tetrachroma) +# 8.4.47 - script identification change to Adobe Reader (Tetrachroma) +# 8.4.48 - improved tolerance for false file/registry entries (Tetrachroma) +# 8.4.49 - improved username encryption (Tetrachroma) +# 8.4.50 - improved (experimental) APS support (Tetrachroma & Neisklar) +# 8.4.51 - automatic APS offline key retrieval (works only for +# Onleihe right now) (80ka80 & Tetrachroma) + +""" +Decrypts Adobe ADEPT-encrypted and Fileopen PDF files. +""" + +from __future__ import with_statement + +__license__ = 'GPL v3' + +import sys +import os +import re +import zlib +import struct +import hashlib +from itertools import chain, islice +import xml.etree.ElementTree as etree +import Tkinter +import Tkconstants +import tkFileDialog +import tkMessageBox +# added for fileopen support +import urllib +import urlparse +import time +import socket +import string +import uuid +import subprocess +import time +import getpass +from ctypes import * +import traceback +import inspect +import tempfile +import sqlite3 +import httplib +try: + from Crypto.Cipher import ARC4 + # needed for newer pdfs + from Crypto.Cipher import AES + from Crypto.Hash import SHA256 + from Crypto.PublicKey import RSA + +except ImportError: + ARC4 = None + RSA = None +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +class ADEPTError(Exception): + pass + +# global variable (needed for fileopen and password decryption) +INPUTFILEPATH = '' +KEYFILEPATH = '' +PASSWORD = '' +DEBUG_MODE = False +IVERSION = '8.4.51' + +# 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 + +### +### ASN.1 parsing code from tlslite + +def bytesToNumber(bytes): + total = 0L + for byte in bytes: + total = (total << 8) + byte + return total + +class ASN1Error(Exception): + pass + +class ASN1Parser(object): + class Parser(object): + def __init__(self, bytes): + self.bytes = bytes + self.index = 0 + + def get(self, length): + if self.index + length > len(self.bytes): + raise ASN1Error("Error decoding ASN.1") + x = 0 + for count in range(length): + x <<= 8 + x |= self.bytes[self.index] + self.index += 1 + return x + + def getFixBytes(self, lengthBytes): + bytes = self.bytes[self.index : self.index+lengthBytes] + self.index += lengthBytes + return bytes + + def getVarBytes(self, lengthLength): + lengthBytes = self.get(lengthLength) + return self.getFixBytes(lengthBytes) + + def getFixList(self, length, lengthList): + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def getVarList(self, length, lengthLength): + lengthList = self.get(lengthLength) + if lengthList % length != 0: + raise ASN1Error("Error decoding ASN.1") + lengthList = int(lengthList/length) + l = [0] * lengthList + for x in range(lengthList): + l[x] = self.get(length) + return l + + def startLengthCheck(self, lengthLength): + self.lengthCheck = self.get(lengthLength) + self.indexCheck = self.index + + def setLengthCheck(self, length): + self.lengthCheck = length + self.indexCheck = self.index + + def stopLengthCheck(self): + if (self.index - self.indexCheck) != self.lengthCheck: + raise ASN1Error("Error decoding ASN.1") + + def atLengthCheck(self): + if (self.index - self.indexCheck) < self.lengthCheck: + return False + elif (self.index - self.indexCheck) == self.lengthCheck: + return True + else: + raise ASN1Error("Error decoding ASN.1") + + def __init__(self, bytes): + p = self.Parser(bytes) + p.get(1) + self.length = self._getASN1Length(p) + self.value = p.getFixBytes(self.length) + + def getChild(self, which): + p = self.Parser(self.value) + for x in range(which+1): + markIndex = p.index + p.get(1) + length = self._getASN1Length(p) + p.getFixBytes(length) + return ASN1Parser(p.bytes[markIndex:p.index]) + + def _getASN1Length(self, p): + firstLength = p.get(1) + if firstLength<=127: + return firstLength + else: + lengthLength = firstLength & 0x7F + return p.get(lengthLength) + +### +### 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', '\x00'+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 + 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 + 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('{') +KEYWORD_BRACE_END = KWD('}') +KEYWORD_ARRAY_BEGIN = KWD('[') +KEYWORD_ARRAY_END = KWD(']') +KEYWORD_DICT_BEGIN = KWD('<<') +KEYWORD_DICT_END = KWD('>>') + + +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(r'[\r\n]') +SPC = re.compile(r'\s') +NONSPC = re.compile(r'\S') +HEX = re.compile(r'[0-9a-fA-F]') +END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') +END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') +HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') +END_NUMBER = re.compile(r'[^0-9]') +END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') +END_STRING = re.compile(r'[()\134]') +OCT_STRING = re.compile(r'[0-7]') +ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } + +class PSBaseParser(object): + + ''' + Most basic PostScript parser that performs only basic tokenization. + ''' + BUFSIZ = 4096 + + def __init__(self, fp): + self.fp = fp + self.seek(0) + return + + def __repr__(self): + return '' % (self.fp, self.bufpos) + + def flush(self): + return + + def close(self): + self.flush() + return + + def tell(self): + return self.bufpos+self.charpos + + def poll(self, pos=None, n=80): + pos0 = self.fp.tell() + if not pos: + pos = self.bufpos+self.charpos + self.fp.seek(pos) + ##print >>sys.stderr, 'poll(%d): %r' % (pos, self.fp.read(n)) + 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 = '' + 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) + c = s[j] + self.tokenstart = self.bufpos+j + if c == '%': + self.token = '%' + return (self.parse_comment, j+1) + if c == '/': + self.token = '' + return (self.parse_literal, j+1) + if c in '-+' or c.isdigit(): + self.token = c + return (self.parse_number, j+1) + if c == '.': + self.token = c + return (self.parse_float, j+1) + if c.isalpha(): + self.token = c + return (self.parse_keyword, j+1) + if c == '(': + self.token = '' + self.paren = 1 + return (self.parse_string, j+1) + if c == '<': + self.token = '' + return (self.parse_wopen, j+1) + if c == '>': + self.token = '' + 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] + c = s[j] + if c == '#': + self.hex = '' + return (self.parse_literal_hex, j+1) + self.add_token(LIT(self.token)) + return (self.parse_main, j) + + def parse_literal_hex(self, s, i): + c = 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 += chr(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] + c = s[j] + if c == '.': + self.token += c + return (self.parse_float, j+1) + try: + self.add_token(int(self.token)) + except ValueError: + pass + return (self.parse_main, j) + def parse_float(self, s, i): + m = END_NUMBER.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_float, len(s)) + j = m.start(0) + self.token += s[i:j] + self.add_token(float(self.token)) + 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] + c = s[j] + if c == '\\': + self.oct = '' + return (self.parse_string_1, j+1) + if c == '(': + self.paren += 1 + self.token += c + return (self.parse_string, j+1) + if c == ')': + 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): + c = 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 += chr(int(self.oct, 8)) + return (self.parse_string, i) + if c in ESC_STRING: + self.token += chr(ESC_STRING[c]) + return (self.parse_string, i+1) + + def parse_wopen(self, s, i): + c = s[i] + if c.isspace() or HEX.match(c): + return (self.parse_hexstring, i) + if c == '<': + self.add_token(KEYWORD_DICT_BEGIN) + i += 1 + return (self.parse_main, i) + + def parse_wclose(self, s, i): + c = s[i] + if c == '>': + self.add_token(KEYWORD_DICT_END) + i += 1 + return (self.parse_main, i) + + def parse_hexstring(self, s, i): + m = END_HEX_STRING.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_hexstring, len(s)) + j = m.start(0) + self.token += s[i:j] + token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), + SPC.sub('', 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 = '' + linepos = self.bufpos + self.charpos + eol = False + while 1: + self.fillbuf() + if eol: + c = self.buf[self.charpos] + # handle '\r\n' + if c == '\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 linebuf[-1] == '\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 = '' + 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('\r'), s.rfind('\n')) + if n == -1: + buf = s + buf + break + yield s[n:]+buf + s = s[:n] + buf = '' + 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, + 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, float) or + isinstance(token, bool) 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: + 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('Crypt') +LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) +LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) +LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) + + +## PDF Objects +## +class PDFObject(PSObject): pass + +class PDFException(PSException): pass +class PDFTypeError(PDFException): pass +class PDFValueError(PDFException): pass +class PDFNotImplementedError(PSException): pass + + +## PDFObjRef +## +class PDFObjRef(PDFObject): + + def __init__(self, doc, objid, genno): + if objid == 0: + if STRICT: + raise PDFValueError('PDF object id cannot be 0.') + self.doc = doc + self.objid = objid + self.genno = genno + return + + def __repr__(self): + return '' % (self.objid, self.genno) + + def resolve(self): + return self.doc.getobj(self.objid) + + +# resolve +def resolve1(x): + ''' + Resolve an object. If this is an array or dictionary, + it may still contains some indirect objects inside. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + return x + +def resolve_all(x): + ''' + Recursively resolve X and all the internals. + Make sure there is no indirect reference within the nested object. + This procedure might be slow. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + if isinstance(x, list): + x = [ resolve_all(v) for v in x ] + elif isinstance(x, dict): + for (k,v) in x.iteritems(): + x[k] = resolve_all(v) + return x + +def decipher_all(decipher, objid, genno, x): + ''' + Recursively decipher X. + ''' + if 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 x.iteritems()) + 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 float_value(x): + x = resolve1(x) + if not isinstance(x, float): + if STRICT: + raise PDFTypeError('Float required: %r' % x) + return 0.0 + return x + +def num_value(x): + x = resolve1(x) + if not (isinstance(x, int) or isinstance(x, float)): + 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, 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 = '' + for c in data: + if '!' <= c and c <= 'u': + n += 1 + b = b*85+(ord(c)-33) + if n == 5: + out += struct.pack('>L',b) + n = b = 0 + elif c == 'z': + assert n == 0 + out += '\0\0\0\0' + elif c == '~': + 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 ('\r', '\n', '\r\n'): + rawdata = rawdata[:length] + + self.dic = dic + self.rawdata = rawdata + self.decipher = decipher + self.data = None + self.decdata = None + self.objid = None + self.genno = None + return + + def set_objid(self, objid, genno): + self.objid = objid + self.genno = genno + return + + def __repr__(self): + if self.rawdata: + return '' % \ + (self.objid, len(self.rawdata), self.dic) + else: + return '' % \ + (self.objid, len(self.data), self.dic) + + def decode(self): + assert self.data is None and self.rawdata is not None + data = self.rawdata + if self.decipher: + # Handle encryption + data = self.decipher(self.objid, self.genno, data) + if gen_xref_stm: + self.decdata = data # keep decrypted data + if 'Filter' not in self.dic: + self.data = data + self.rawdata = None + ##print self.dict + return + filters = self.dic['Filter'] + if not isinstance(filters, list): + filters = [ filters ] + for f in filters: + if f in LITERALS_FLATE_DECODE: + # will get errors if the document is encrypted. + data = zlib.decompress(data) + elif f in LITERALS_LZW_DECODE: + data = ''.join(LZWDecoder(StringIO(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 = '' + ent0 = '\x00' * columns + for i in xrange(0, len(data), columns+1): + pred = data[i] + ent1 = data[i+1:i+1+columns] + if pred == '\x02': + ent1 = ''.join(chr((ord(a)+ord(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('ObjStm') +LITERAL_XREF = PSLiteralTable.intern('XRef') +LITERAL_PAGE = PSLiteralTable.intern('Page') +LITERAL_PAGES = PSLiteralTable.intern('Pages') +LITERAL_CATALOG = PSLiteralTable.intern('Catalog') + + +## XRefs +## + +## PDFXRef +## +class PDFXRef(object): + + def __init__(self): + self.offsets = None + return + + def __repr__(self): + return '' % len(self.offsets) + + def objids(self): + return self.offsets.iterkeys() + + 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('trailer'): + parser.seek(pos) + break + f = line.strip().split(' ') + 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 xrange(start, start+nobjs): + try: + (_, line) = parser.nextline() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF - file corrupted?') + f = line.strip().split(' ') + if len(f) != 3: + raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) + (pos, genno, use) = f + if use != 'n': continue + self.offsets[objid] = (int(genno), int(pos)) + self.load_trailer(parser) + return + + KEYWORD_TRAILER = PSKeywordTable.intern('trailer') + def load_trailer(self, parser): + try: + (_,kwd) = parser.nexttoken() + assert kwd is self.KEYWORD_TRAILER + (_,dic) = parser.nextobject(direct=True) + except PSEOF: + x = parser.pop(1) + if not x: + raise PDFNoValidXRef('Unexpected EOF - file corrupted') + (_,dic) = x[0] + self.trailer = dict_value(dic) + return + + def getpos(self, objid): + try: + (genno, pos) = self.offsets[objid] + except KeyError: + raise + return (None, pos) + + +## PDFXRefStream +## +class PDFXRefStream(object): + + def __init__(self): + self.index = None + self.data = None + self.entlen = None + self.fl1 = self.fl2 = self.fl3 = None + return + + def __repr__(self): + return '' % self.index + + def objids(self): + for first, size in self.index: + for objid in xrange(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 = zip(islice(index, 0, None, 2), + 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 + # dictionaries for fileopen + self.fileopen = {} + self.urlresult = {} + self.ready = False + 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 = ('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=''): + if not self.encryption: + self.is_printable = self.is_modifiable = self.is_extractable = True + self.ready = True + 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) + if type == 'FOPN_fLock': + # remove of unnecessairy password attribute + return self.initialize_fopn_flock(docid, param) + if type == 'FOPN_foweb': + # remove of unnecessairy password attribute + return self.initialize_fopn(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 getPrincipalKey(self, k=None, url=None, referer=None): + if url == None: + url="ssl://edc.bibliothek-digital.de/edcws/services/urn:EDCLicenseService" + data1='<wsse:Security '+\ + 'xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-'+\ + '1.0.xsd"><wsse:UsernameToken><wsse:Username>edc_anonymous</wsse:Username&'+\ + 'gt;<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-'+\ + 'token-profile-1.0#PasswordText">edc_anonymous</wsse:Password></wsse:UsernameToken&'+\ + 'gt;</wsse:Security>7de-de'+\ + '1010<'+\ + 'watermarkTemplateSeqNum>0' + if k not in url[:40]: + return None + #~ extract host and path: + host=re.compile(r'[a-zA-Z]://([^/]+)/.+', re.I).search(url).group(1) + urlpath=re.compile(r'[a-zA-Z]://[^/]+(/.+)', re.I).search(url).group(1) + + # open a socket connection on port 80 + + conn = httplib.HTTPSConnection(host, 443) + + #~ Headers for request + headers={"Accept": "*/*", "Host": host, "User-Agent": "Mozilla/3.0 (compatible; Acrobat EDC SOAP 1.0)", + "Content-Type": "text/xml; charset=utf-8", "Cache-Control": "no-cache", "SOAPAction": ""} + + # send data1 and headers + try: + conn.request("POST", urlpath, data1, headers) + except: + raise ADEPTError("Could not post request to '"+host+"'.") + + # read respose + try: + response = conn.getresponse() + responsedata=response.read() + except: + raise ADEPTError("Could not read response from '"+host+"'.") + + # close connection + conn.close() + + try: + key=re.compile(r'PricipalKey"((?!).)*]*>(((?!).)*)', re.I).search(responsedata).group(2) + + except : + key=None + return key + + def genkey_adobe_ps(self, param): + # nice little offline principal keys dictionary + principalkeys = { 'bibliothek-digital.de': 'Dzqx8McQUNd2CDzBVmtnweUxVWlqJTMqyYtiDIc4dZI='.decode('base64')} + for k, v in principalkeys.iteritems(): + result = self.getPrincipalKey(k) + #print result + if result != None: + principalkeys[k] = result.decode('base64') + else: + raise ADEPTError("No (Online) PrincipalKey found.") + + self.is_printable = self.is_modifiable = self.is_extractable = True +## print 'keyvalue' +## print len(keyvalue) +## print keyvalue.encode('hex') + 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') + #print 'ecd rights' + edclist = [] + for pair in edcdata.split('\n'): + edclist.append(pair) +## print edclist +## print 'edcdata decrypted' +## print edclist[0].decode('base64').encode('hex') +## print edclist[1].decode('base64').encode('hex') +## print edclist[2].decode('base64').encode('hex') +## print edclist[3].decode('base64').encode('hex') +## print 'offlinekey' +## print len(edclist[9].decode('base64')) +## print pdrllic + # principal key request + for key in principalkeys: + if key in pdrllic: + principalkey = principalkeys[key] + else: + raise ADEPTError('Cannot find principal key for this pdf') +## print 'minorversion' +## print int(edclist[8]) + # fix for minor version +## minorversion = int(edclist[8]) - 100 +## if minorversion < 1: +## minorversion = 1 +## print int(minorversion) + shakey = SHA256.new() + shakey.update(principalkey) +## for i in range(0,minorversion): +## shakey.update(principalkey) + shakey = shakey.digest() +## shakey = SHA256.new(principalkey).digest() + ivector = 16 * chr(0) + #print shakey + plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) + if plaintext[-16:] != 16 * chr(16): + raise ADEPTError('Offlinekey cannot be decrypted, aborting (hint: redownload pdf) ...') + pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) + if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16: + raise ADEPTError('Could not decrypt PDRLPol, aborting ...') + else: + cutter = -1 * ord(pdrlpol[-1]) + #print cutter + pdrlpol = pdrlpol[:cutter] + #print plaintext.encode('hex') + #print 'pdrlpol' + #print pdrlpol + return plaintext[:16] + + PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ + '\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 = 'True' + self.is_printable = bool(P & 4) + self.is_modifiable = bool(P & 8) + self.is_extractable = bool(P & 16) + self.is_annotationable = bool(P & 32) + self.is_formsenabled = bool(P & 256) + self.is_textextractable = bool(P & 512) + self.is_assemblable = bool(P & 1024) + self.is_formprintable = bool(P & 2048) + # Algorithm 3.2 + password = (password+self.PASSWORD_PADDING)[:32] # 1 + hash = hashlib.md5(password) # 2 + hash.update(O) # 3 + hash.update(struct.pack('= 3: + # Algorithm 3.5 + hash = hashlib.md5(self.PASSWORD_PADDING) # 2 + hash.update(docid[0]) # 3 + x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 + for i in xrange(1,19+1): + k = ''.join( chr(ord(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 ADEPTError('Password is not correct.') +## raise PDFPasswordIncorrect + 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: + elf.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, password, docid, param): + global KEYFILEPATH + self.is_printable = self.is_modifiable = self.is_extractable = True + # keyfile path is wrong + if KEYFILEPATH == False: + errortext = 'Cannot find adeptkey.der keyfile. Use ineptkey to generate it.' + raise ADEPTError(errortext) + with open(password, 'rb') as f: + keyder = f.read() + # KEYFILEPATH = '' + key = ASN1Parser([ord(x) for x in keyder]) + key = [bytesToNumber(key.getChild(x).value) for x in xrange(1, 4)] + rsa = RSA.construct(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 = rsa.decrypt(bookkey) + if bookkey[0] != '\x02': + raise ADEPTError('error decrypting book session key') + index = bookkey.index('\0') + 1 + bookkey = bookkey[index:] + ebx_V = int_value(param.get('V', 4)) + ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) + # added because of the booktype / decryption book session key error + if ebx_V == 3: + V = 3 + elif ebx_V < 4 or ebx_type < 6: + V = ord(bookkey[0]) + bookkey = bookkey[1:] + else: + V = 2 + if length and len(bookkey) != length: + raise ADEPTError('error decrypting book session key') + 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 + + # fileopen support + def initialize_fopn_flock(self, docid, param): + raise ADEPTError('FOPN_fLock not supported, yet ...') + # debug mode processing + global DEBUG_MODE + global IVERSION + if DEBUG_MODE == True: + if os.access('.',os.W_OK) == True: + debugfile = open('ineptpdf-'+IVERSION+'-debug.txt','w') + else: + raise ADEPTError('Cannot write debug file, current directory is not writable') + self.is_printable = self.is_modifiable = self.is_extractable = True + # get parameters and add it to the fo dictionary + self.fileopen['V'] = int_value(param.get('V',2)) + # crypt base + (docid, param) = self.encryption + #rights = dict_value(param['Info']) + rights = param['Info'] + #print rights + if DEBUG_MODE == True: debugfile.write(rights + '\n\n') +## for pair in rights.split(';'): +## try: +## key, value = pair.split('=',1) +## self.fileopen[key] = value +## # fix for some misconfigured INFO variables +## except: +## pass +## kattr = { 'SVID': 'ServiceID', 'DUID': 'DocumentID', 'I3ID': 'Ident3ID', \ +## 'I4ID': 'Ident4ID', 'VERS': 'EncrVer', 'PRID': 'USR'} +## for keys in kattr: +## try: +## self.fileopen[kattr[keys]] = self.fileopen[keys] +## del self.fileopen[keys] +## except: +## continue + # differentiate OS types +## sysplatform = sys.platform +## # if ostype is Windows +## if sysplatform=='win32': +## self.osuseragent = 'Windows NT 6.0' +## self.get_macaddress = self.get_win_macaddress +## self.fo_sethwids = self.fo_win_sethwids +## self.BrowserCookie = WinBrowserCookie +## elif sysplatform=='linux2': +## adeptout = 'Linux is not supported, yet.\n' +## raise ADEPTError(adeptout) +## self.osuseragent = 'Linux i686' +## self.get_macaddress = self.get_linux_macaddress +## self.fo_sethwids = self.fo_linux_sethwids +## else: +## adeptout = '' +## adeptout = adeptout + 'Due to various privacy violations from Apple\n' +## adeptout = adeptout + 'Mac OS X support is disabled by default.' +## raise ADEPTError(adeptout) +## # add static arguments for http/https request +## self.fo_setattributes() +## # add hardware specific arguments for http/https request +## self.fo_sethwids() +## +## if 'Code' in self.urlresult: +## if self.fileopen['Length'] == len(self.urlresult['Code']): +## self.decrypt_key = self.urlresult['Code'] +## else: +## self.decrypt_key = self.urlresult['Code'].decode('hex') +## else: +## raise ADEPTError('Cannot find decryption key.') + self.decrypt_key = 'stuff' + self.genkey = self.genkey_v2 + self.decipher = self.decrypt_rc4 + self.ready = True + return + + def initialize_fopn(self, docid, param): + # debug mode processing + global DEBUG_MODE + global IVERSION + if DEBUG_MODE == True: + if os.access('.',os.W_OK) == True: + debugfile = open('ineptpdf-'+IVERSION+'-debug.txt','w') + else: + raise ADEPTError('Cannot write debug file, current directory is not writable') + self.is_printable = self.is_modifiable = self.is_extractable = True + # get parameters and add it to the fo dictionary + self.fileopen['Length'] = int_value(param.get('Length', 0)) / 8 + self.fileopen['VEID'] = str_value(param.get('VEID')) + self.fileopen['BUILD'] = str_value(param.get('BUILD')) + self.fileopen['SVID'] = str_value(param.get('SVID')) + self.fileopen['DUID'] = str_value(param.get('DUID')) + self.fileopen['V'] = int_value(param.get('V',2)) + # crypt base + rights = str_value(param.get('INFO')).decode('base64') + rights = self.genkey_fileopeninfo(rights) + if DEBUG_MODE == True: debugfile.write(rights + '\n\n') + for pair in rights.split(';'): + try: + key, value = pair.split('=',1) + self.fileopen[key] = value + # fix for some misconfigured INFO variables + except: + pass + kattr = { 'SVID': 'ServiceID', 'DUID': 'DocumentID', 'I3ID': 'Ident3ID', \ + 'I4ID': 'Ident4ID', 'VERS': 'EncrVer', 'PRID': 'USR'} + for keys in kattr: + # fishing some misconfigured slashs out of it + try: + self.fileopen[kattr[keys]] = urllib.quote(self.fileopen[keys],safe='') + del self.fileopen[keys] + except: + continue + # differentiate OS types + sysplatform = sys.platform + # if ostype is Windows + if sysplatform=='win32': + self.osuseragent = 'Windows NT 6.0' + self.get_macaddress = self.get_win_macaddress + self.fo_sethwids = self.fo_win_sethwids + self.BrowserCookie = WinBrowserCookie + elif sysplatform=='linux2': + adeptout = 'Linux is not supported, yet.\n' + raise ADEPTError(adeptout) + self.osuseragent = 'Linux i686' + self.get_macaddress = self.get_linux_macaddress + self.fo_sethwids = self.fo_linux_sethwids + else: + adeptout = '' + adeptout = adeptout + 'Mac OS X is not supported, yet.' + adeptout = adeptout + 'Read the blogs FAQs for more information' + raise ADEPTError(adeptout) + # add static arguments for http/https request + self.fo_setattributes() + # add hardware specific arguments for http/https request + self.fo_sethwids() + #if DEBUG_MODE == True: debugfile.write(self.fileopen) + if 'UURL' in self.fileopen: + buildurl = self.fileopen['UURL'] + else: + buildurl = self.fileopen['PURL'] + # fix for bad DPRM structure + if self.fileopen['DPRM'][0] != r'/': + self.fileopen['DPRM'] = r'/' + self.fileopen['DPRM'] + # genius fix for bad server urls (IMHO) + if '?' in self.fileopen['DPRM']: + buildurl = buildurl + self.fileopen['DPRM'] + '&' + else: + buildurl = buildurl + self.fileopen['DPRM'] + '?' + + # debug customization + #self.fileopen['Machine'] = '' + #self.fileopen['Disk'] = '' + + + surl = ( 'Stamp', 'Mode', 'USR', 'ServiceID', 'DocumentID',\ + 'Ident3ID', 'Ident4ID','DocStrFmt', 'OSType', 'OSName', 'OSData', 'Language',\ + 'LngLCID', 'LngRFC1766', 'LngISO4Char', 'Build', 'ProdVer', 'EncrVer',\ + 'Machine', 'Disk', 'Uuid', 'PrevMach', 'PrevDisk',\ + 'FormHFT',\ + 'SelServer', 'AcroVersion', 'AcroProduct', 'AcroReader',\ + 'AcroCanEdit', 'AcroPrefIDib', 'InBrowser', 'CliAppName',\ + 'DocIsLocal', 'DocPathUrl', 'VolName', 'VolType', 'VolSN',\ + 'FSName', 'FowpKbd', 'OSBuild',\ + 'RequestSchema') + + #settings request and special modes + if 'EVER' in self.fileopen and float(self.fileopen['EVER']) < 3.8: + self.fileopen['Mode'] = 'ICx' + + origurl = buildurl + buildurl = buildurl + 'Request=Setting' + for keys in surl: + try: + buildurl = buildurl + '&' + keys + '=' + self.fileopen[keys] + except: + continue + if DEBUG_MODE == True: debugfile.write( 'settings url:\n') + if DEBUG_MODE == True: debugfile.write( buildurl+'\n\n') + # custom user agent identification? + if 'AGEN' in self.fileopen: + useragent = self.fileopen['AGEN'] + urllib.URLopener.version = useragent + # attribute doesn't exist - take the default user agent + else: + urllib.URLopener.version = self.osuseragent + # try to open the url + try: + u = urllib.urlopen(buildurl) + u.geturl() + result = u.read() + except: + raise ADEPTError('No internet connection or a blocking firewall!') +## finally: +## u.close() + # getting rid of the line feed + if DEBUG_MODE == True: debugfile.write('Settings'+'\n') + if DEBUG_MODE == True: debugfile.write(result+'\n\n') + #get rid of unnecessary characters + result = result.rstrip('\n') + result = result.rstrip(chr(13)) + result = result.lstrip('\n') + result = result.lstrip(chr(13)) + self.surlresult = {} + for pair in result.split('&'): + try: + key, value = pair.split('=',1) + # fix for bad server response + if key not in self.surlresult: + self.surlresult[key] = value + except: + pass + if 'RequestSchema' in self.surlresult: + self.fileopen['RequestSchema'] = self.surlresult['RequestSchema'] + if 'ServerSessionData' in self.surlresult: + self.fileopen['ServerSessionData'] = self.surlresult['ServerSessionData'] + if 'SetScope' in self.surlresult: + self.fileopen['RequestSchema'] = self.surlresult['SetScope'] + #print self.surlresult + if 'RetVal' in self.surlresult and 'SEMO' not in self.fileopen and(('Reason' in self.surlresult and \ + self.surlresult['Reason'] == 'AskUnp') or ('SetTarget' in self.surlresult and\ + self.surlresult['SetTarget'] == 'UnpDlg')): + # get user and password dialog + try: + self.gen_pw_dialog(self.surlresult['UnpUiName'], self.surlresult['UnpUiPass'],\ + self.surlresult['UnpUiTitle'], self.surlresult['UnpUiOk'],\ + self.surlresult['UnpUiSunk'], self.surlresult['UnpUiComm']) + except: + self.gen_pw_dialog() + + # the fileopen check might not be always right because of strange server responses + if 'SEMO' in self.fileopen and (self.fileopen['SEMO'] == '1'\ + or self.fileopen['SEMO'] == '2') and ('CSES' in self.fileopen and\ + self.fileopen['CSES'] != 'fileopen'): + # get the url name for the cookie(s) + if 'CURL' in self.fileopen: + self.surl = self.fileopen['CURL'] + if 'CSES' in self.fileopen: + self.cses = self.fileopen['CSES'] + elif 'PHOS' in self.fileopen: + self.surl = self.fileopen['PHOS'] + elif 'LHOS' in self.fileopen: + self.surl = self.fileopen['LHOS'] + else: + raise ADEPTError('unknown Cookie name.\n Check ineptpdf forum for further assistance') + self.pwfieldreq = 1 + # session cookie processing + if self.fileopen['SEMO'] == '1': + cookies = self.BrowserCookie() + #print self.cses + #print self.surl + csession = cookies.getcookie(self.cses,self.surl) + if csession != None: + self.fileopen['Session'] = csession + self.gui = False + # fallback + else: + self.pwtk = Tkinter.Tk() + self.pwtk.title('Ineptpdf8') + self.pwtk.minsize(150, 0) + infotxt1 = 'Get the session cookie key manually (Firefox step-by-step:\n'+\ + 'Start Firefox -> Tools -> Options -> Privacy -> Show Cookies\n'+\ + '-> Search for a cookie from ' + self.surl +' with the\n'+\ + 'name ' + self.cses +' and copy paste the content field in the\n'+\ + 'Session Content field. Remove possible spaces or new lines at the '+\ + 'end\n (cursor must be blinking right behind the last character)' + self.label0 = Tkinter.Label(self.pwtk, text=infotxt1) + self.label0.pack() + self.label1 = Tkinter.Label(self.pwtk, text="Session Content") + self.pwfieldreq = 0 + self.gui = True + # user cookie processing + elif self.fileopen['SEMO'] == '2': + cookies = self.BrowserCookie() + #print self.cses + #print self.surl + name = cookies.getcookie('name',self.surl) + passw = cookies.getcookie('pass',self.surl) + if name != None or passw != None: + self.fileopen['UserName'] = urllib.quote(name) + self.fileopen['UserPass'] = urllib.quote(passw) + self.gui = False + # fallback + else: + self.pwtk = Tkinter.Tk() + self.pwtk.title('Ineptpdf8') + self.pwtk.minsize(150, 0) + self.label1 = Tkinter.Label(self.pwtk, text="Username") + infotxt1 = 'Get the user cookie keys manually (Firefox step-by-step:\n'+\ + 'Start Firefox -> Tools -> Options -> Privacy -> Show Cookies\n'+\ + '-> Search for cookies from ' + self.surl +' with the\n'+\ + 'name name in the user field and copy paste the content field in the\n'+\ + 'username field. Do the same with the name pass in the password field).' + self.label0 = Tkinter.Label(self.pwtk, text=infotxt1) + self.label0.pack() + self.pwfieldreq = 1 + self.gui = True +## else: +## self.pwtk = Tkinter.Tk() +## self.pwtk.title('Ineptpdf8') +## self.pwtk.minsize(150, 0) +## self.pwfieldreq = 0 +## self.label1 = Tkinter.Label(self.pwtk, text="Username") +## self.pwfieldreq = 1 +## self.gui = True + if self.gui == True: + self.un_entry = Tkinter.Entry(self.pwtk) + # cursor here + self.un_entry.focus() + self.label2 = Tkinter.Label(self.pwtk, text="Password") + self.pw_entry = Tkinter.Entry(self.pwtk, show="*") + self.button = Tkinter.Button(self.pwtk, text='Go for it!', command=self.fo_save_values) + # widget layout, stack vertical + self.label1.pack() + self.un_entry.pack() + # create a password label and field + if self.pwfieldreq == 1: + self.label2.pack() + self.pw_entry.pack() + self.button.pack() + self.pwtk.update() + # start the event loop + self.pwtk.mainloop() + + # original request + # drive through tupple for building the permission url + burl = ( 'Stamp', 'Mode', 'USR', 'ServiceID', 'DocumentID',\ + 'Ident3ID', 'Ident4ID','DocStrFmt', 'OSType', 'Language',\ + 'LngLCID', 'LngRFC1766', 'LngISO4Char', 'Build', 'ProdVer', 'EncrVer',\ + 'Machine', 'Disk', 'Uuid', 'PrevMach', 'PrevDisk', 'User', 'SaUser', 'SaSID',\ + # special security measures + 'HostIsDomain', 'PhysHostname', 'LogiHostname', 'SaRefDomain',\ + 'FormHFT', 'UserName', 'UserPass', 'Session', \ + 'SelServer', 'AcroVersion', 'AcroProduct', 'AcroReader',\ + 'AcroCanEdit', 'AcroPrefIDib', 'InBrowser', 'CliAppName',\ + 'DocIsLocal', 'DocPathUrl', 'VolName', 'VolType', 'VolSN',\ + 'FSName', 'ServerSessionData', 'FowpKbd', 'OSBuild', \ + 'DocumentSessionData', 'RequestSchema') + + buildurl = origurl + buildurl = buildurl + 'Request=DocPerm' + for keys in burl: + try: + buildurl = buildurl + '&' + keys + '=' + self.fileopen[keys] + except: + continue + if DEBUG_MODE == True: debugfile.write('1st url:'+'\n') + if DEBUG_MODE == True: debugfile.write(buildurl+'\n\n') + # custom user agent identification? + if 'AGEN' in self.fileopen: + useragent = self.fileopen['AGEN'] + urllib.URLopener.version = useragent + # attribute doesn't exist - take the default user agent + else: + urllib.URLopener.version = self.osuseragent + # try to open the url + try: + u = urllib.urlopen(buildurl) + u.geturl() + result = u.read() + except: + raise ADEPTError('No internet connection or a blocking firewall!') +## finally: +## u.close() + # getting rid of the line feed + if DEBUG_MODE == True: debugfile.write('1st preresult'+'\n') + if DEBUG_MODE == True: debugfile.write(result+'\n\n') + #get rid of unnecessary characters + result = result.rstrip('\n') + result = result.rstrip(chr(13)) + result = result.lstrip('\n') + result = result.lstrip(chr(13)) + self.urlresult = {} + for pair in result.split('&'): + try: + key, value = pair.split('=',1) + self.urlresult[key] = value + except: + pass +## if 'RequestSchema' in self.surlresult: +## self.fileopen['RequestSchema'] = self.urlresult['RequestSchema'] + #self.urlresult + #result[0:8] == 'RetVal=1') or (result[0:8] == 'RetVal=2'): + if ('RetVal' in self.urlresult and (self.urlresult['RetVal'] != '1' and \ + self.urlresult['RetVal'] != '2' and \ + self.urlresult['RetVal'] != 'Update' and \ + self.urlresult['RetVal'] != 'Answer')): + + if ('Reason' in self.urlresult and (self.urlresult['Reason'] == 'BadUserPwd'\ + or self.urlresult['Reason'] == 'AskUnp')) or ('SwitchTo' in self.urlresult\ + and (self.urlresult['SwitchTo'] == 'Dialog')): + if 'ServerSessionData' in self.urlresult: + self.fileopen['ServerSessionData'] = self.urlresult['ServerSessionData'] + if 'DocumentSessionData' in self.urlresult: + self.fileopen['DocumentSessionData'] = self.urlresult['DocumentSessionData'] + buildurl = origurl + buildurl = buildurl + 'Request=DocPerm' + self.gen_pw_dialog() + # password not found - fallback + for keys in burl: + try: + buildurl = buildurl + '&' + keys + '=' + self.fileopen[keys] + except: + continue + if DEBUG_MODE == True: debugfile.write( '2ndurl:') + if DEBUG_MODE == True: debugfile.write( buildurl+'\n\n') + # try to open the url + try: + u = urllib.urlopen(buildurl) + u.geturl() + result = u.read() + except: + raise ADEPTError('No internet connection or a blocking firewall!') + # getting rid of the line feed + if DEBUG_MODE == True: debugfile.write( '2nd preresult') + if DEBUG_MODE == True: debugfile.write( result+'\n\n') + #get rid of unnecessary characters + result = result.rstrip('\n') + result = result.rstrip(chr(13)) + result = result.lstrip('\n') + result = result.lstrip(chr(13)) + self.urlresult = {} + for pair in result.split('&'): + try: + key, value = pair.split('=',1) + self.urlresult[key] = value + except: + pass + # did it work? + if ('RetVal' in self.urlresult and (self.urlresult['RetVal'] != '1' and \ + self.urlresult['RetVal'] != '2' and + self.urlresult['RetVal'] != 'Update' and \ + self.urlresult['RetVal'] != 'Answer')): + raise ADEPTError('Decryption was not successfull.\nReason: ' + self.urlresult['Error']) + # fix for non-standard-conform fileopen pdfs +## if self.fileopen['Length'] != 5 and self.fileopen['Length'] != 16: +## if self.fileopen['V'] == 1: +## self.fileopen['Length'] = 5 +## else: +## self.fileopen['Length'] = 16 + # patch for malformed pdfs + #print len(self.urlresult['Code']) + #print self.urlresult['Code'].encode('hex') + if 'code' in self.urlresult: + self.urlresult['Code'] = self.urlresult['code'] + if 'Code' in self.urlresult: + if len(self.urlresult['Code']) == 5 or len(self.urlresult['Code']) == 16: + self.decrypt_key = self.urlresult['Code'] + else: + self.decrypt_key = self.urlresult['Code'].decode('hex') + else: + raise ADEPTError('Cannot find decryption key.') + self.genkey = self.genkey_v2 + self.decipher = self.decrypt_rc4 + self.ready = True + return + + def gen_pw_dialog(self, Username='Username', Password='Password', Title='User/Password Authentication',\ + OK='Proceed', Text1='Authorization', Text2='Enter Required Data'): + self.pwtk = Tkinter.Tk() + self.pwtk.title(Title) + self.pwtk.minsize(150, 0) + self.label1 = Tkinter.Label(self.pwtk, text=Text1) + self.label2 = Tkinter.Label(self.pwtk, text=Text2) + self.label3 = Tkinter.Label(self.pwtk, text=Username) + self.pwfieldreq = 1 + self.gui = True + self.un_entry = Tkinter.Entry(self.pwtk) + # cursor here + self.un_entry.focus() + self.label4 = Tkinter.Label(self.pwtk, text=Password) + self.pw_entry = Tkinter.Entry(self.pwtk, show="*") + self.button = Tkinter.Button(self.pwtk, text=OK, command=self.fo_save_values) + # widget layout, stack vertical + self.label1.pack() + self.label2.pack() + self.label3.pack() + self.un_entry.pack() + # create a password label and field + if self.pwfieldreq == 1: + self.label4.pack() + self.pw_entry.pack() + self.button.pack() + self.pwtk.update() + # start the event loop + self.pwtk.mainloop() + + # genkey functions + def genkey_v2(self, objid, genno): + objid = struct.pack(' -1: + mac = line.split()[4] + break + return mac.replace(':','') + except: + raise ADEPTError('Cannot find MAC address. Get forum help.') + + def get_win_macaddress(self): + try: + gasize = c_ulong(5000) + p = create_string_buffer(5000) + GetAdaptersInfo = windll.iphlpapi.GetAdaptersInfo + GetAdaptersInfo(byref(p),byref(gasize)) + return p[0x194:0x19a].encode('hex') + except: + raise ADEPTError('Cannot find MAC address. Get forum help.') + + # custom conversion 5 bytes to 8 chars method + def fo_convert5to8(self, edisk): + # byte to number/char mapping table + darray=[0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x41,0x42,0x43,0x44,0x45,\ + 0x46,0x47,0x48,0x4A,0x4B,0x4C,0x4D,0x4E,0x50,0x51,0x52,0x53,0x54,\ + 0x55,0x56,0x57,0x58,0x59,0x5A] + pdid = struct.pack('> 5 + outputhw = outputhw + chr(darray[index]) + pdid = (ord(edisk[4]) << 2)|pdid + # get the last 2 bits from the hwid + low part of the cpuid + for i in range(0,2): + index = pdid & 0x1f + # shift the disk id 5 bits to the right + pdid = pdid >> 5 + outputhw = outputhw + chr(darray[index]) + return outputhw + + # Linux processing + def fo_linux_sethwids(self): + # linux specific attributes + self.fileopen['OSType']='Linux' + self.fileopen['AcroProduct']='AcroReader' + self.fileopen['AcroReader']='Yes' + self.fileopen['AcroVersion']='9.101' + self.fileopen['FSName']='ext3' + self.fileopen['Build']='878' + self.fileopen['ProdVer']='1.8.5.1' + self.fileopen['OSBuild']='2.6.33' + # write hardware keys + hwkey = 0 + pmac = self.get_macaddress().decode("hex"); + self.fileopen['Disk'] = self.fo_convert5to8(pmac[1:]) + # get primary used default mac address + self.fileopen['Machine'] = self.fo_convert5to8(pmac[1:]) + # get uuid + # check for reversed offline handler 6AB83F4Ah + AFh 6AB83F4Ah + if 'LILA' in self.fileopen: + pass + if 'Ident4ID' in self.fileopen: + self.fileopen['User'] = getpass.getuser() + self.fileopen['SaUser'] = getpass.getuser() + try: + cuser = winreg.HKEY_CURRENT_USER + FOW3_UUID = 'Software\\Fileopen' + regkey = winreg.OpenKey(cuser, FOW3_UUID) + userkey = winreg.QueryValueEx(regkey, 'Fowp3Uuid')[0] +# if self.genkey_cryptmach(userkey)[0:4] != 'ec20': + self.fileopen['Uuid'] = self.genkey_cryptmach(userkey)[4:] +## elif self.genkey_cryptmach(userkey)[0:4] != 'ec20': +## self.fileopen['Uuid'] = self.genkey_cryptmach(userkey,1)[4:] +## else: + except: + raise ADEPTError('Cannot find FowP3Uuid file - reason might be Adobe (Reader) X.'\ + 'Read the FAQs for more information how to solve the problem.') + else: + self.fileopen['Uuid'] = str(uuid.uuid1()) + # get time stamp + self.fileopen['Stamp'] = str(time.time())[:-3] + # get fileopen input pdf name + path + self.fileopen['DocPathUrl'] = 'file%3a%2f%2f%2f'\ + + urllib.quote(os.path.normpath(INPUTFILEPATH)) + # clear the link + #INPUTFILEPATH = '' +## # get volume name (urllib quote necessairy?) urllib.quote( +## self.fileopen['VolName'] = win32api.GetVolumeInformation("C:\\")[0] +## # get volume serial number +## self.fileopen['VolSN'] = str(win32api.GetVolumeInformation("C:\\")[1]) + return + + # Windows processing + def fo_win_sethwids(self): + # Windows specific attributes + self.fileopen['OSType']='Windows' + self.fileopen['OSName']='Vista' + self.fileopen['OSData']='Service%20Pack%204' + self.fileopen['AcroProduct']='Reader' + self.fileopen['AcroReader']='Yes' + self.fileopen['OSBuild']='7600' + self.fileopen['AcroVersion']='9.1024' + self.fileopen['Build']='879' + # write hardware keys + hwkey = 0 + # get the os type and save it in ostype + try: + import win32api + import win32security + import win32file + import _winreg as winreg + except: + raise ADEPTError('PyWin Extension (Win32API module) needed.\n'+\ + 'Download from http://sourceforge.net/projects/pywin32/files/ ') + try: + v0 = win32api.GetVolumeInformation('C:\\') + v1 = win32api.GetSystemInfo()[6] + # fix for possible negative integer (Python problem) + volserial = v0[1] & 0xffffffff + lowcpu = v1 & 255 + highcpu = (v1 >> 8) & 255 + # changed to int + volserial = struct.pack(' 0 and mode == True: + m.update(key_string[:(13-len(uname))]) + md5sum = m.digest()[0:16] + # print md5sum.encode('hex') + # normal ident4id calculation + retval = [] + for sdata in data: + retval.append(ARC4.new(md5sum).decrypt(sdata)) + for rval in retval: + if rval[:4] == 'ec20': + return rval[4:] + return False + # start normal execution + # list for username variants + unamevars = [] + # fill username variants list + unamevars.append(self.user) + unamevars.append(self.user + chr(0)) + unamevars.append(self.user.lower()) + unamevars.append(self.user.lower() + chr(0)) + unamevars.append(self.user.upper()) + unamevars.append(self.user.upper() + chr(0)) + # go through it + for uname in unamevars: + result = genkeysub(uname, True) + if result != False: + return result + result = genkeysub(uname) + if result != False: + return result + # didn't find it, return false + return False +## raise ADEPTError('Unsupported Ident4D Decryption,\n'+\ +## 'report the bug to the ineptpdf script forum') + + KEYWORD_OBJ = PSKeywordTable.intern('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 + +# helper class for cookie retrival +class WinBrowserCookie(): + def __init__(self): + pass + def getcookie(self, cname, chost): + # check firefox db + fprofile = os.environ['AppData']+r'\Mozilla\Firefox' + pinifile = 'profiles.ini' + fini = os.path.normpath(fprofile + '\\' + pinifile) + try: + with open(fini,'r') as ffini: + firefoxini = ffini.read() + # Firefox not installed or on an USB stick + except: + return None + for pair in firefoxini.split('\n'): + try: + key, value = pair.split('=',1) + if key == 'Path': + fprofile = os.path.normpath(fprofile+'//'+value+'//'+'cookies.sqlite') + break + # asdf + except: + continue + if os.path.isfile(fprofile): + try: + con = sqlite3.connect(fprofile,1) + except: + raise ADEPTError('Firefox Cookie data base locked. Close Firefox and try again') + cur = con.cursor() + try: + cur.execute("select value from moz_cookies where name=? and host=?", (cname, chost)) + except Exception: + raise ADEPTError('Firefox Cookie database is locked. Close Firefox and try again') + try: + return cur.fetchone()[0] + except Exception: + # sometimes is a dot in front of the host + chost = '.'+chost + cur.execute("select value from moz_cookies where name=? and host=?", (cname, chost)) + try: + return cur.fetchone()[0] + except: + return None + +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 '' + + KEYWORD_R = PSKeywordTable.intern('R') + KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') + KEYWORD_STREAM = PSKeywordTable.intern('stream') + KEYWORD_XREF = PSKeywordTable.intern('xref') + KEYWORD_STARTXREF = PSKeywordTable.intern('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 'endstream' in line: + i = line.index('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 == '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(r'^(\d+)\s+(\d+)\s+obj\b') + offsets = {} + xref = PDFXRef() + while 1: + try: + (pos, line) = self.nextline() + except PSEOF: + break + if line.startswith('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, StringIO(data)) + self.doc = doc + return + + def flush(self): + self.add_results(*self.popall()) + return + + KEYWORD_R = KWD('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, keypath): + 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(keypath) + 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('\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('xref\n') + self.write('0 %d\n' % (maxobj + 1,)) + for objid in xrange(0, maxobj + 1): + if objid in xrefs: + # force the genno to be 0 + self.write("%010d 00000 n \n" % xrefs[objid][0]) + else: + self.write("%010d %05d f \n" % (0, 65535)) + + self.write('trailer\n') + self.serialize_object(trailer) + self.write('\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(''.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('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('\\', '\\\\') + string = string.replace('\n', r'\n') + string = string.replace('(', r'\(') + string = string.replace(')', r'\)') + # get rid of ciando id + regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}') + if regularexp.match(string): return ('http://www.ciando.com') + 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('<<') + for key, val in obj.items(): + self.write('/%s' % key) + self.serialize_object(val) + self.write('>>') + elif isinstance(obj, list): + self.write('[') + for val in obj: + self.serialize_object(val) + self.write(']') + elif isinstance(obj, str): + self.write('(%s)' % self.escape_string(obj)) + elif isinstance(obj, bool): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj).lower()) + elif isinstance(obj, (int, long, float)): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj)) + elif isinstance(obj, PDFObjRef): + if self.last.isalnum(): + self.write(' ') + self.write('%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('stream\n') + self.write(data) + self.write('\nendstream') + else: + data = str(obj) + if data[0].isalnum() and self.last.isalnum(): + self.write(' ') + self.write(data) + + def serialize_indirect(self, objid, obj): + self.write('%d 0 obj' % (objid,)) + self.serialize_object(obj) + if self.last.isalnum(): + self.write('\n') + self.write('endobj\n') + +def cli_main(argv=sys.argv): + progname = os.path.basename(argv[0]) + if RSA is None: + print "%s: This script requires PyCrypto, which must be installed " \ + "separately. Read the top-of-script comment for details." % \ + (progname,) + return 1 + if len(argv) != 4: + print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,) + return 1 + keypath, inpath, outpath = argv[1:] + with open(inpath, 'rb') as inf: + serializer = PDFSerializer(inf, keypath) + # hope this will fix the 'bad file descriptor' problem + with open(outpath, 'wb') as outf: + # help construct to make sure the method runs to the end + serializer.dump(outf) + return 0 + + +class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + # debug mode debugging + global DEBUG_MODE + Tkinter.Frame.__init__(self, root, border=5) + ltext='Select file for decryption\n(Ignore Password / Key file option for Fileopen/APS PDFs)' + self.status = Tkinter.Label(self, text=ltext) + self.status.pack(fill=Tkconstants.X, expand=1) + body = Tkinter.Frame(self) + body.pack(fill=Tkconstants.X, expand=1) + sticky = Tkconstants.E + Tkconstants.W + body.grid_columnconfigure(1, weight=2) + Tkinter.Label(body, text='Password\nor 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('adeptkey.der'): + self.keypath.insert(0, 'adeptkey.der') + 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) + debugmode = Tkinter.Checkbutton(self, text = "Debug Mode (writable directory required)", command=self.debug_toggle, height=2, \ + width = 40) + debugmode.pack() + 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=Tkconstants.LEFT) + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + button = Tkinter.Button( + buttons, text="Quit", width=10, command=self.quit) + button.pack(side=Tkconstants.RIGHT) + + + def get_keypath(self): + keypath = tkFileDialog.askopenfilename( + parent=None, title='Select ADEPT key file', + defaultextension='.der', filetypes=[('DER-encoded files', '.der'), + ('All Files', '.*')]) + if keypath: + keypath = os.path.normpath(os.path.realpath(keypath)) + self.keypath.delete(0, Tkconstants.END) + self.keypath.insert(0, keypath) + return + + def get_inpath(self): + inpath = tkFileDialog.askopenfilename( + parent=None, title='Select ADEPT or FileOpen-encrypted PDF file to decrypt', + defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), + ('All files', '.*')]) + if inpath: + inpath = os.path.normpath(os.path.realpath(inpath)) + self.inpath.delete(0, Tkconstants.END) + self.inpath.insert(0, inpath) + return + + def debug_toggle(self): + global DEBUG_MODE + if DEBUG_MODE == False: + DEBUG_MODE = True + else: + DEBUG_MODE = False + + def get_outpath(self): + outpath = tkFileDialog.asksaveasfilename( + parent=None, title='Select unencrypted PDF file to produce', + defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), + ('All files', '.*')]) + if outpath: + outpath = os.path.normpath(os.path.realpath(outpath)) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def decrypt(self): + global INPUTFILEPATH + global KEYFILEPATH + global PASSWORD + keypath = self.keypath.get() + inpath = self.inpath.get() + outpath = self.outpath.get() + if not keypath or not os.path.exists(keypath): + # keyfile doesn't exist + KEYFILEPATH = False + PASSWORD = keypath + 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 + # patch for non-ascii characters + INPUTFILEPATH = inpath.encode('utf-8') + argv = [sys.argv[0], keypath, inpath, outpath] + self.status['text'] = 'Processing ...' + try: + cli_main(argv) + except Exception, a: + self.status['text'] = 'Error: ' + str(a) + return + self.status['text'] = 'File successfully decrypted.\n'+\ + 'Close this window or decrypt another pdf file.' + return + +def gui_main(): + root = Tkinter.Tk() + if RSA is None: + root.withdraw() + tkMessageBox.showerror( + "INEPT PDF and FileOpen Decrypter", + "This script requires PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title('INEPT PDF Decrypter 8.4.51 (FileOpen/APS-Support)') + root.resizable(True, False) + root.minsize(370, 0) + DecryptionDialog(root).pack(fill=Tkconstants.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/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51_ReadMe.txt b/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51_ReadMe.txt new file mode 100644 index 00000000..0d2e401b --- /dev/null +++ b/Other_Tools/Tetrachroma_FileOpen_ineptpdf/ineptpdf_8.4.51_ReadMe.txt @@ -0,0 +1,8 @@ +ineptpdf 8.4.51 +--------------- + +This is a version of the ineptpdf script produced by TetraChroma that can remove, on Windows, "FileOpen" DRM. + +No support for this script is offered at Apprentice Alf's blog. + +Trtrachroma's blog is http://tetrachroma.wordpress.com/ diff --git a/ReadMe_First.txt b/ReadMe_First.txt index ffb5f32c..cb3dd5cd 100644 --- a/ReadMe_First.txt +++ b/ReadMe_First.txt @@ -1,7 +1,7 @@ Welcome to the tools! ===================== -This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v5.5.3 archive. +This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v5.6 archive. The is archive includes tools to remove DRM from: @@ -51,7 +51,7 @@ DeDRM application for Mac OS X users: (Mac OS X 10.4 and above) ---------------------------------------------------------------------- This application combines all the tools into one easy-to-use tool for Mac OS X users. -Drag the "DeDRM 5.5.3.app" application from the DeDRM_Applications/Macintosh folder to your Desktop (or your Applications Folder, or anywhere else you find convenient). Double-click on the application to run it and it will guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe. +Drag the "DeDRM 5.6.app" application from the DeDRM_Applications/Macintosh folder to your Desktop (or your Applications Folder, or anywhere else you find convenient). Double-click on the application to run it and it will guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe. To use the DeDRM application, simply drag ebooks, or folders containing ebooks, onto the DeDRM application and it will remove the DRM of the kinds listed above. @@ -67,7 +67,7 @@ DeDRM application for Windows users: (Windows XP through Windows 8) This application combines all the tools into one easy-to-use tool for Windows users. -Drag the DeDRM_5.5.3 folder that's in the DeDRM_Applications/Windows folder, to your "My Documents" folder (or anywhere else you find convenient). Make a short-cut on your Desktop of the DeDRM_Drop_Target.bat file that's in the DeDRM_5.5.3 folder. Double-click on the shortcut and the DeDRM application will run and guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe. +Drag the DeDRM_5.6 folder that's in the DeDRM_Applications/Windows folder, to your "My Documents" folder (or anywhere else you find convenient). Make a short-cut on your Desktop of the DeDRM_Drop_Target.bat file that's in the DeDRM_5.6 folder. Double-click on the shortcut and the DeDRM application will run and guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe. To use the DeDRM application, simply drag ebooks, or folders containing ebooks, onto the DeDRM_Drop_Target.bat shortcut and it will remove the DRM of the kinds listed above.